Sleazy Fork is available in English.
Jav-Pro版:收藏、屏蔽、标记已下载、演员黑名单、收藏演员同步、新作品检测、热播/Top250/Fc2ppv/评论增强、WebDAV数据备份、以图识图、字幕搜索;支持 JavDB / JavBus。
// ==UserScript== // @name JHS-Pro // @namespace https://greasyfork.org/zh-CN/scripts/590210-jhs-pro // @version 1.1.5 // @author lesspem // @description Jav-Pro版:收藏、屏蔽、标记已下载、演员黑名单、收藏演员同步、新作品检测、热播/Top250/Fc2ppv/评论增强、WebDAV数据备份、以图识图、字幕搜索;支持 JavDB / JavBus。 // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=javdb.com // @homepageURL https://greasyfork.org/zh-CN/scripts/590210-jhs-pro // @match https://javdb.com/* // @match https://www.javbus.com/* // @include https://javdb*.com/* // @include https://*javbus*/* // @include https://*javsee*/* // @include https://*seejav*/* // @include https://javtrailers.com/* // @include https://subtitlecat.com/* // @exclude https://*javbus*/forum/* // @exclude https://*javbus*/*actresses // @exclude https://*javsee*/forum/* // @exclude https://*javsee*/*actresses // @exclude https://*seejav*/forum/* // @exclude https://*seejav*/*actresses // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tabulator.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/layer.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/js/md5.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/localforage.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/viewer.min.js // @connect xunlei.com // @connect geilijiasu.com // @connect ja.wikipedia.org // @connect beta.magnet.pics // @connect jdforrepam.com // @connect cc3001.dmm.co.jp // @connect cc3001.dmm.com // @connect www.dmm.co.jp // @connect www.dmm.com // @connect api.dmm.com // @connect special.dmm.co.jp // @connect adult.contents.fc2.com // @connect fc2ppvdb.com // @connect 123av.com // @connect u3c3.com // @connect u9a9.com // @connect btsow.lol // @connect sukebei.nyaa.si // @connect javstore.net // @connect 3xplanet.com // @connect javbest.net // @connect missav.live // @connect jable.tv // @connect www.av.gl // @connect jav.rs // @connect javtrailers.com // @connect javdb.com // @connect javbus.com // @connect supjav.com // @connect translate-pa.googleapis.com // @connect 127.0.0.1 // @connect 115.com // @connect * // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_openInTab // @grant GM_setClipboard // @grant unsafeWindow // @run-at document-idle // ==/UserScript== var e, t, n = Object.defineProperty, a = e => { throw TypeError(e); }, i = (e, t, a) => ((e, t, a) => t in e ? n(e, t, { enumerable: !0, configurable: !0, writable: !0, value: a }) : e[t] = a)(e, "symbol" != typeof t ? t + "" : t, a), s = (e, t, n) => (((e, t, n) => { t.has(e) || a("Cannot " + n); })(e, t, "access private method"), n); const o = window.location.href, r = o.includes("javdb"), l = o.includes("javbus") || o.includes("seejav") || o.includes("bus") || o.includes("javsee") || $("title").text().trim().startsWith("JavBus - AV"), c = o.includes("/search?q") || o.includes("/search/") || o.includes("/users/"), d = "filter", h = "favorite", g = "hasDown", p = "hasWatch", m = "屏蔽", u = "已屏蔽", f = "#de3333", v = "收藏", b = "已收藏", w = "#25b1dc", y = "已下载", x = "#7bc73b", k = "已观看", S = "#d7a80c", C = "no", _ = "yes", T = "javdb", I = "javbus", B = "actor", P = "actress", D = "censored", A = "uncensored", L = [ { id: "video-mhb", quality: "dmb_w", text: "旧视频源-中画质宽版 (404p)", canSelect: !1 }, { id: "video-mhb", quality: "sm_s", text: "旧视频源-低画质 (240p)", canSelect: !1 }, { id: "video-mhb", quality: "dm_s", text: "旧视频源-中画质 (360p)", canSelect: !1 }, { id: "video-mhb", quality: "dmb_s", text: "旧视频源-中画质 (480p)", canSelect: !1 }, { id: "video-mhb", quality: "mhb_w", text: "旧视频源-高画质宽版 (404p)", canSelect: !1 }, { id: "video-mmb", quality: "mmb", text: "中画质 (432p)", canSelect: !0 }, { id: "video-mhb", quality: "mhb", text: "高画质 (576p)", canSelect: !0 }, { id: "video-hmb", quality: "hmb", text: "HD (720p)", canSelect: !0 }, { id: "video-hhb", quality: "hhb", text: "FullHD (1080p)", canSelect: !0 }, { id: "video-hhbs", quality: "hhbs", text: "FullHD (1080p60fps)", canSelect: !0 }, { id: "video-4k", quality: "4k", text: "4K (2160p)", canSelect: !0 }, { id: "video-4ks", quality: "4ks", text: "4K (2160p60fps)", canSelect: !0 } ]; function escapeHtml(e) { const t = document.createElement("span"); return t.textContent = e, t.innerHTML; } const CURRENT_DATA_VERSION = 1; let M = ""; window.location.href.includes("hideNav=1") && (M = "\n .navbar-default {\n display: none !important;\n }\n body {\n padding-top:0px!important;\n }\n "); /* N (JavBus CSS) moved to css-injection.js */ let j = ""; window.location.href.includes("hideNav=1") && (j = "\n .main-nav,#search-bar-container {\n display: none !important;\n }\n \n html {\n padding-top:0px!important;\n }\n "); /* E (JavDB CSS) moved to css-injection.js */ /* F (global UI CSS) moved to css-injection.js */ function H(e) { if (e) if (e.includes("<style>")) document.head.insertAdjacentHTML("beforeend", e); else { const t = document.createElement("style"); t.textContent = e, document.head.appendChild(t); } } /** CSS injection for site-specific layouts, global UI, button styles, and mobile responsive design. */ const N = `\n<style>\n .top-bar {\n z-index: 12345689 !important;\n }\n \n ${M}\n\n .masonry {\n height: 100% !important;\n width: 100% !important;\n padding: 0 15px !important;\n }\n .masonry {\n display: grid;\n column-gap: 10px; /* 列间距*/\n row-gap: 10px; /* 行间距 */\n grid-template-columns: repeat(4, minmax(0, 1fr));\n align-items: start;\n }\n .masonry .item {\n /*position: initial !important;*/\n top: initial !important;\n left: initial !important;\n float: none !important;\n background-color:#c4b1b1;\n position: relative !important;\n }\n \n .masonry .item:hover {\n box-shadow: 0 .5em 1em -.125em rgba(10, 10, 10, .1), 0 0 0 1px #485fc7;\n }\n .masonry .movie-box{\n width: 100% !important;\n height: 100% !important;\n margin: 0 !important;\n overflow: inherit !important;\n }\n .masonry .movie-box .photo-frame {\n /*height: 70% !important;*/\n height:auto !important;\n margin: 0 !important;\n position:relative; /* 方便预览视频定位*/\n }\n .masonry .movie-box img {\n max-height: 500px;\n height: 100% !important;\n object-fit: contain;\n object-position: top;\n }\n .masonry .movie-box img:hover {\n transform: scale(1.04);\n transition: transform 0.3s;\n }\n .masonry .photo-info{\n /*height: 30% !important;*/\n }\n .masonry .photo-info span {\n display: inline-block; /* 或者 block */\n max-width: 100%; /* 根据父容器限制宽度 */\n white-space: nowrap; /* 禁止换行 */\n overflow: hidden; /* 隐藏溢出内容 */\n text-overflow: ellipsis; /* 显示省略号 */\n }\n \n /* 无码页面的样式 */\n .photo-frame .mheyzo,\n .photo-frame .mcaribbeancom2{\n margin-left: 0 !important;\n }\n .avatar-box{\n width: calc(100% - 30px) !important;\n display: flex !important;\n margin: 0 15px !important;\n padding: 0 !important;\n box-sizing: border-box !important;\n }\n .avatar-box img{\n max-height: 150px !important;\n width: auto !important;\n object-fit: contain;\n }\n .avatar-box .photo-info{\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 30px;\n flex-direction: row;\n background-color:#fff !important;\n }\n \n footer{\n display: none!important;\n }\n \n \n .video-title {\n white-space: normal !important;\n line-height: 24px !important; /* 显式行高, 让高度正好是整行数 */\n height: 72px; /* 3 行 × 24px, 切口落在行间, 不留第4行的缝 */\n overflow: hidden !important; /* 裁掉溢出的第4行文字 */\n \n display: -webkit-box !important; /* 必须设置,使接下来的属性生效 */\n -webkit-box-orient: vertical; /* 垂直方向堆叠行 */\n -webkit-line-clamp: 3; /* 设置文本最多显示的行数*/\n }\n\n \n</style>\n`; const E = `\n<style>\n ${j}\n \n .navbar {\n z-index: 12345679 !important;\n padding: 0 0;\n }\n \n .navbar-link:not(.is-arrowless) {\n padding-right: 33px;\n }\n \n .sub-header,\n /*#search-bar-container, !*搜索框*!*/\n #footer,\n /*.search-recent-keywords, !*搜索框底部热搜词条*!*/\n .app-desktop-banner,\n h3.main-title,\n .top-meta,\n .float-buttons {\n display: none !important;\n }\n \n div.tabs.no-bottom,\n .tabs ul {\n border-bottom: none !important;\n }\n \n \n /* 视频列表项 相对相对 方便标签绝对定位*/\n .movie-list .item {\n position: relative !important;\n }\n \n .video-title {\n white-space: normal !important;\n height: 80px; /* 固定高度 容器就不会出现高低不一*/\n \n display: -webkit-box; /* 必须设置,使接下来的属性生效 */\n -webkit-box-orient: vertical; /* 垂直方向堆叠行 */\n -webkit-line-clamp: 3; /* 设置文本最多显示的行数*/\n }\n \n /* 列表页顶部分类自适应 */\n .main-tabs, .tabs {\n overflow-x:hidden;\n flex-wrap: wrap;\n justify-content: flex-start;\n }\n \n .main-tabs ul, .tabs ul {\n flex-wrap: wrap;\n flex-grow: 0;\n }\n \n \n /* 二级工具栏 大小封面,可播放,含磁链...*/\n .toolbar {\n display: flex;\n }\n\n</style>\n`; const F = `\n<style>\n /* 全局通用样式 */\n .fr-btn {\n float: right;\n margin-left: 4px !important;\n }\n \n .menu-box {\n position: fixed;\n right: 10px;\n top: 50%;\n transform: translateY(-50%);\n display: flex;\n flex-direction: column;\n z-index: 1000;\n gap: 6px;\n }\n \n .menu-btn {\n display: inline-block;\n min-width: 80px;\n padding: 7px 12px;\n border-radius: 4px;\n color: white !important;\n text-decoration: none;\n font-weight: bold;\n font-size: 12px;\n text-align: center;\n cursor: pointer;\n transition: all 0.3s ease;\n box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);\n border: none;\n line-height: 1.3;\n margin: 0;\n }\n \n .menu-btn:hover {\n transform: translateY(-1px);\n box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);\n opacity: 0.9;\n }\n \n .menu-btn:active {\n transform: translateY(0);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n }\n \n .do-hide {\n display: none !important;\n }\n \n .main-tab-btn {\n border-bottom:none !important; \n border-radius:3px !important; \n height: 30px; \n margin-left: 5px !important; \n }\n\n .jhs-icon {\n width: 16px;\n height: 16px;\n }\n \n .tool-box .jhs-icon {\n width: 1.5rem;\n height: 1.5rem; \n }\n \n \n /*表格内按钮溢出,防止被隐藏*/\n .tabulator .tabulator-row .action-cell-dropdown {\n overflow: visible !important;\n }\n /* 去除行内鼠标小手*/\n .tabulator .tabulator-row.tabulator-selectable:hover {\n cursor: default !important;\n }\n \n /* 排序小箭头颜色 */\n .tabulator .tabulator-col.tabulator-sortable[aria-sort="ascending"] .tabulator-arrow {\n border-bottom-color: #337ab7 !important;\n }\n .tabulator .tabulator-col.tabulator-sortable[aria-sort="descending"] .tabulator-arrow {\n border-top-color: #337ab7 !important;\n }\n \n /* 针对折叠行的容器或内容进行样式修改 */\n .tabulator-responsive-collapse {\n border-top: none !important;\n }\n \n .tabulator-responsive-collapse table{\n margin-left: 50px !important;\n }\n \n .tabulator-cell {\n height:auto !important;\n }\n \n /* 列允许换行,去除省略号 */\n .tabulator .tabulator-cell {\n white-space: normal !important; \n text-overflow: clip !important; \n }\n \n .tabulator-tableholder {\n overflow-x: hidden !important;\n }\n\n ${function() { const e = [ ".jhs-scrollbar", ".content-panel", ".tabulator-tableholder", ".has-navbar-fixed-top", ".layui-layer-content" ], t = (e, t) => e.map((e => `${e}${t}`)).join(","), n = "::-webkit-scrollbar-track", a = "::-webkit-scrollbar-thumb", i = "::-webkit-scrollbar-thumb:hover"; return `\n ${t(e, "::-webkit-scrollbar")}{width:6px;height:6px;}\n ${t(e, n)}{background:#f1f1f1;border-radius:10px;}\n ${t(e, a)}{background:#888;border-radius:10px;}\n ${t(e, i)}{background:#555;}\n `.trim().replace(/\n/g, ""); }()}\n</style>\n`; /** Inject site-specific CSS based on current hostname. */ l && H(N), r && H(E); /** Inject global button styles. */ H("\n<style>\n .a-normal, /* 白色 */\n .a-primary, /* 浅蓝色 */\n .a-success, /* 浅绿色 */\n .a-danger, /* 浅粉色 */\n .a-warning, /* 浅橙色 */\n .a-info /* 灰色 */\n {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 6px 14px;\n margin-right: 10px;\n border-radius: 6px;\n text-decoration: none;\n font-size: 13px;\n font-weight: 500;\n transition: all 0.2s ease;\n cursor: pointer;\n border: 1px solid rgba(0, 0, 0, 0.08);\n white-space: nowrap;\n }\n \n .a-primary {\n background: #e0f2fe;\n color: #0369a1;\n border-color: #bae6fd;\n }\n \n .a-primary:hover {\n background: #bae6fd;\n }\n \n .a-success {\n background: #dcfce7;\n color: #166534;\n border-color: #bbf7d0;\n }\n \n .a-success:hover {\n background: #bbf7d0;\n }\n \n .a-danger {\n background: #fee2e2;\n color: #b91c1c;\n border-color: #fecaca;\n }\n \n .a-danger:hover {\n background: #fecaca;\n }\n \n .a-warning {\n background: #ffedd5;\n color: #9a3412;\n border-color: #fed7aa;\n }\n \n .a-warning:hover {\n background: #fed7aa;\n }\n \n .a-info {\n background: #e2e8f0;\n color: #334155;\n border-color: #cbd5e1;\n }\n \n .a-info:hover {\n background: #cbd5e1;\n }\n \n .a-normal {\n background: transparent;\n color: #64748b;\n border-color: #cbd5e1;\n }\n \n .a-normal:hover {\n background: #f8fafc;\n }\n</style>\n"); /** Inject global UI styles (menu, tabulator overrides, scrollbar). */ H(F); /** 按字段为数组数据构建运行时索引。 */ function createIndexedMap(items, key) { return new Map(items.filter((item => item && item[key])).map((item => [ item[key], item ]))); } /** 为番号状态构建运行时 Set 索引。 */ function createStatusMap(items) { const statusMap = { [d]: new Set, [h]: new Set, [g]: new Set, [p]: new Set }; items.forEach((item => { item && Object.prototype.hasOwnProperty.call(statusMap, item.status) && statusMap[item.status].add(item.carNum); })); return statusMap; } /** 统计指定字段的重复值。 */ function groupDuplicateItems(items, key) { const counts = new Map; items.forEach((item => { const value = item && item[key]; value && counts.set(value, (counts.get(value) || 0) + 1); })); return Array.from(counts.entries()).filter((item => item[1] > 1)); } /** 按字段去重,保留第一条记录并合并后续字段。 */ function dedupeByKey(items, key) { const seen = new Map, list = []; let changed = !1; for (const item of items) { const value = item && item[key]; if (!value) { list.push(item); continue; } if (seen.has(value)) { Object.assign(seen.get(value), item), changed = !0; } else seen.set(value, item), list.push(item); } return { list, changed }; } e = new WeakSet, t = async function(e, t, n) { let a; if (Array.isArray(e)) a = [ ...e ]; else { if (a = await this.forage.getItem(t) || [], a.includes(e)) { const t = `${e} ${n}已存在`; throw show.error(t), new Error(t); } a.push(e); } return await this._setItemAndInvalidate(t, a), a; }; let z = class n { constructor() { var t, s, o; if (t = this, (s = e).has(t) ? a("Cannot add the same private member more than once") : s instanceof WeakSet ? s.add(t) : s.set(t, o), i(this, "car_list_key", "car_list"), i(this, "filter_keyword_title_key", "filter_keyword_title"), i(this, "filter_keyword_review_key", "filter_keyword_review"), i(this, "setting_key", "setting"), i(this, "blacklist_key", "blacklist"), i(this, "blacklist_car_list_key", "blacklist_car_list"), i(this, "third_party_cache_key", "third_party_ttl_cache"), i(this, "favorite_actresses_key", "favorite_actresses"), i(this, "highlighted_tags_key", "highlighted_tags"), i(this, "_actressLock", Promise.resolve()), i(this, "forage", localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JHS", version: 1, storeName: "appData" })), i(this, "cacheCarList", null), i(this, "cacheBlacklist", null), i(this, "cacheTitleFilterKeyword", null), i(this, "cacheFavoriteActresses", null), i(this, "cache_filter_actor_actress_car_list", null), i(this, "cacheSettingObj", null), i(this, "cacheCarMap", null), i(this, "cacheStatusMap", null), i(this, "cacheBlacklistMap", null), i(this, "cacheFavoriteActressMap", null), i(this, "_cacheStats", { hits: 0, misses: 0 }), n.instance) throw new Error("StorageManager已被实例化过了!"); n.instance = this; } async getDataVersion() { return await this.forage.getItem("data_version") || 0; } async setDataVersion(e) { await this.forage.setItem("data_version", e); } _invalidateCache(e = null) { (!e || e === this.car_list_key) && (this.cacheCarList = null, this.cacheCarMap = null, this.cacheStatusMap = null); (!e || e === this.blacklist_key) && (this.cacheBlacklist = null, this.cacheBlacklistMap = null); (!e || e === this.filter_keyword_title_key) && (this.cacheTitleFilterKeyword = null); (!e || e === this.favorite_actresses_key) && (this.cacheFavoriteActresses = null, this.cacheFavoriteActressMap = null); (!e || e === this.blacklist_car_list_key) && (this.cache_filter_actor_actress_car_list = null); (!e || e === this.setting_key) && (this.cacheSettingObj = null); } async _setItemAndInvalidate(e, t) { await this.forage.setItem(e, t); this._invalidateCache(e); } async withActressLock(fn) { let release; const lock = new Promise(r => release = r); const prev = this._actressLock; this._actressLock = this._actressLock.then(() => lock); await prev; try { return await fn(); } finally { release(); } } async _rawUpdateFavoriteActress(e) { const t = await this.getFavoriteActressList(); const {starId: n, name: a, allName: i, avatar: s, lastCheckTime: o, newVideoList: r, lastPublishTime: l, actressType: c, remark: d} = e; if (!n) throw new Error("缺失starId"); let h = t.find((e => e.starId === n)); if (!h) return clog.error("未找到演员信息", n, a), !1; a && (h.name = a), i && (h.allName = i), s && (h.avatar = s), null != c && (h.actressType = c), o && (h.lastCheckTime = o), r && (h.newVideoList = r), l && (h.lastPublishTime = l), d && (h.remark = d), h.updateDate = utils.getNowStr(), await this._setItemAndInvalidate(this.favorite_actresses_key, t); return true; } async getCarList() { return null === this.cacheCarList && (this.cacheCarList = await this.forage.getItem(this.car_list_key) || []), this.cacheCarList; } async getCarMap() { if (null === this.cacheCarMap) { const e = await this.getCarList(); this.cacheCarMap = createIndexedMap(e, "carNum"); } return this.cacheCarMap; } async getStatusMap() { if (null === this.cacheStatusMap) { const e = await this.getCarList(); this.cacheStatusMap = createStatusMap(e); } return this.cacheStatusMap; } async getCar(e) { return (await this.getCarMap()).get(e); } _saveSingleCar(e, t) { let {carNum: n, url: a, names: i, actionType: s, publishTime: o, starId: r} = e; if (!n) throw show.error("番号为空!"), new Error("番号为空!"); if (!a) throw show.error("url为空!"), new Error("url为空!"); a.includes("http") || (a = window.location.origin + a), i && (i = i.trim()); let l = t.find((e => e.carNum === n)); if (l) i && (l.names = i), a && (l.url = a), o && (l.publishTime = o), l.updateDate = utils.getNowStr(); else { let e = utils.getNowStr(); l = { carNum: n, url: a, names: i, status: "", createDate: e, updateDate: e, publishTime: o }, r && (l.starId = r), t.push(l); } switch (s) { case d: if (l.status === d) { const e = `${n} 已在屏蔽列表中`; throw show.error(e), new Error(e); } l.status = d; break; case h: if (l.status === h) { const e = `${n} 已在收藏列表中`; throw show.error(e), new Error(e); } l.status = h; break; case g: if (l.status === g) { const e = `${n} 已标记为已下载`; throw show.error(e), new Error(e); } l.status = g; break; case p: if (l.status === p) { const e = `${n} 已标记为已观看`; throw show.error(e), new Error(e); } l.status = p; break; default: const e = "actionType错误, 请联系作者更正: " + s; throw show.error(e), new Error(e); } } async saveCar(e) { const t = await this.getCarList(); this._saveSingleCar(e, t), await this._setItemAndInvalidate(this.car_list_key, t), await this.removeNewVideoList([ e.carNum ]); } async updateCarInfo(e) { let {carNum: t, url: n, names: a, actionType: i, publishTime: s, remark: o} = e; if (!t) throw show.error("番号为空!"), new Error("番号为空!"); if (!n) throw show.error("url为空!"), new Error("url为空!"); a && (a = a.trim()); const r = await this.getCarList(); let l = r.find((e => e.carNum === t)); if (!l) { const e = "数据不存在: " + t; throw show.error(e), new Error(e); } switch (l.names = a, l.url = n, l.remark = o, l.updateDate = utils.getNowStr(), i) { case d: l.status = d; break; case h: l.status = h; break; case g: l.status = g; break; case p: l.status = p; break; default: const e = "actionType错误, 请联系作者更正: " + i; throw show.error(e), new Error(e); } await this._setItemAndInvalidate(this.car_list_key, r), await this.removeNewVideoList([ t ]); } async saveCarList(e) { if (!e || !Array.isArray(e) || 0 === e.length) throw show.error("记录列表为空!"), new Error("记录列表为空!"); const t = await this.getCarList(); for (const a of e) try { this._saveSingleCar(a, t); } catch (n) { throw n; } await this._setItemAndInvalidate(this.car_list_key, t), await this.removeNewVideoList(e.map((e => e.carNum))); } async removeNewVideoList(e) { return this.withActressLock(async () => { const t = await this.getFavoriteActressList(); let n = !1; const a = t.map((t => { if (!t.newVideoList || !Array.isArray(t.newVideoList)) return t; const a = t.newVideoList.filter((t => { const a = "string" == typeof t ? t : t.carNum, i = e.includes(a); return i && (clog.log("移除关联女优新作品", t.name, a), n = !0), !i; })); const result = { ...t, newVideoList: a }; if (a.length === 0 && t.lastPublishTime) result.lastPublishTime = null; return result; })); n && await this._setItemAndInvalidate(this.favorite_actresses_key, a); }); } async removeCar(e) { const t = await this.getCarList(), n = t.length, a = t.filter((t => t.carNum !== e)); return a.length === n ? (show.error(`${e} 不存在`), !1) : (await this._setItemAndInvalidate(this.car_list_key, a), !0); } async batchRemoveCars(e) { const t = await this.getCarList(), n = t.length, a = new Set(e), i = t.filter((e => !a.has(e.carNum))), s = n - i.length; return 0 !== s && (await this._setItemAndInvalidate(this.car_list_key, i), s); } async getBlacklist() { return null === this.cacheBlacklist && (this.cacheBlacklist = await this.forage.getItem(this.blacklist_key) || []), this.cacheBlacklist; } async getBlacklistMap() { if (null === this.cacheBlacklistMap) { const e = await this.getBlacklist(); this.cacheBlacklistMap = createIndexedMap(e, "starId"); } return this.cacheBlacklistMap; } async addBlacklistItem(e) { let {starId: t, name: n, allName: a, role: i, movieType: s, url: o} = e; if (!t) throw new Error("缺失starId"); if (!n) throw new Error("缺失name"); if (!i) throw new Error("缺失role"); const r = await this.getBlacklist(), l = r.find((e => e.starId === t)); if (l) l.url = o, l.role = i, l.movieType = s, clog.log("更新黑名单演员信息", l); else { const e = { starId: t, name: n, allName: a || [ n ], createTime: utils.getNowStr(), role: i, movieType: s, url: o }; r.push(e), clog.log("增加黑名单演员信息", e); } await this._setItemAndInvalidate(this.blacklist_key, r); } async updateBlacklistItem(e) { if (!e || !e.starId) throw new Error("参数不全"); const t = await this.getBlacklist(), n = t.find((t => t.starId === e.starId)); if (!n) throw new Error(`未找到黑名单演员信息:${e.name} ${e.starId}`); e.checkTime && (n.checkTime = e.checkTime), e.lastPublishTime && (n.lastPublishTime = e.lastPublishTime), await this._setItemAndInvalidate(this.blacklist_key, t); } async deleteBlacklistItem(e) { const t = await this.getBlacklist(), n = t.filter((t => t.starId !== e)); t.length !== n.length && await this._setItemAndInvalidate(this.blacklist_key, n); } async getBlacklistCarList() { return null !== this.cache_filter_actor_actress_car_list || (this.cache_filter_actor_actress_car_list = await this.forage.getItem(this.blacklist_car_list_key) || []), this.cache_filter_actor_actress_car_list; } async batchSaveBlacklistCarList(e) { const t = await this.getBlacklistCarList(), n = JSON.parse(JSON.stringify(t)); let a = !1, i = []; for (const s of e) { n.find((e => e.carNum === s.carNum)) || (this._saveSingleCar(s, n), clog.log(`屏蔽演员番号: <span style="color: #f40">${escapeHtml(s.names)} ${escapeHtml(s.carNum)}</span>`), a = !0, i.push(s.carNum)); } a && (await this._setItemAndInvalidate(this.blacklist_car_list_key, n), await this.removeNewVideoList(i), window.cleanCache_filter_actor_actress_car_list()); } async removeBlacklistCarList(e) { const t = await this.getBlacklistCarList(), n = t.filter((t => t.starId !== e)); n.length !== t.length && (await this._setItemAndInvalidate(this.blacklist_car_list_key, n), window.cleanCache_filter_actor_actress_car_list()); } async getFavoriteActressList() { return null === this.cacheFavoriteActresses && (this.cacheFavoriteActresses = await this.forage.getItem(this.favorite_actresses_key) || []), this.cacheFavoriteActresses; } async getActressMap() { if (null === this.cacheFavoriteActressMap) { const e = await this.getFavoriteActressList(); this.cacheFavoriteActressMap = createIndexedMap(e, "starId"); } return this.cacheFavoriteActressMap; } async addFavoriteActressList(e) { return this.withActressLock(async () => { const t = await this.getFavoriteActressList(); let n = 0; for (const a of e) { let {starId: e, name: i, allName: s, avatar: o, lastCheckTime: r, lastPublishTime: l, actressType: c} = a; if (!e) throw new Error("缺失starId"); if (!i) throw new Error("缺失name"); s || (s = [ i ]); const d = "(無碼)"; if (!c) { c = i.includes(d) || s.some((e => e.includes(d))) ? A : D; } i = i.replace(d, ""), s = s.map((e => e.replace(d, ""))); let h = t.find((t => t.starId === e)); if (h) { h.avatar && h.avatar.includes("https") || o && (clog.log(o), h.avatar = o, clog.log(`<span style="color: #f40">补全女优头像: ${escapeHtml(i)}</span>`), n++), !h.actressType && c && (h.actressType = c, clog.log(`<span style="color: #f40">补全女优类别: ${escapeHtml(i)} ${escapeHtml(c)}</span>`), n++), h.name.includes(d) && (h.name = i, h.allName = s, clog.log(`<span style="color: #f40">更正女优名字: ${escapeHtml(i)} ${escapeHtml(s)}</span>`), n++); continue; } const g = utils.getNowStr(); t.push({ starId: e, name: i, allName: s, avatar: o, lastCheckTime: r, lastPublishTime: l, createDate: g, updateDate: g, actressType: c }), clog.log(`<span style="color: #f40">同步JavDB已收藏的演员: ${escapeHtml(i)}</span>`), n++; } return n > 0 ? await this._setItemAndInvalidate(this.favorite_actresses_key, t) : clog.log("信息已记录, 无需要进行同步收藏的演员"), n; }); } async removeFavoriteActress(e) { return this.withActressLock(async () => { const t = await this.getFavoriteActressList(), n = t.length, a = t.filter((t => t.starId !== e)); return a.length === n ? (clog.error(`移除演员失败, ${e} 不存在`), !1) : (await this._setItemAndInvalidate(this.favorite_actresses_key, a), !0); }); } async updateFavoriteActress(e) { return this.withActressLock(() => this._rawUpdateFavoriteActress(e)); } async getHighlightedTags() { return await this.forage.getItem(this.highlighted_tags_key) || []; } async setHighlightedTags(e) { return await this.forage.setItem(this.highlighted_tags_key, e); } async saveTitleFilterKeyword(n) { if (await s(this, e, t).call(this, n, this.filter_keyword_title_key, "标题关键词"), Array.isArray(n)) return null; return this.withActressLock(async () => { const a = await this.getFavoriteActressList(); let i = !1; const o = a.map((e => { if (!e.newVideoList || !Array.isArray(e.newVideoList)) return e; const t = e.newVideoList.filter((t => { const s = "string" == typeof t ? t : t.carNum, a = s.startsWith(n); return a && (clog.log("移除关联女优新作品", e.name, s), i = !0), !a; })); return { ...e, newVideoList: t }; })); i && await this._setItemAndInvalidate(this.favorite_actresses_key, o); }); } async getTitleFilterKeyword() { return null === this.cacheTitleFilterKeyword && (this.cacheTitleFilterKeyword = await this.forage.getItem(this.filter_keyword_title_key) || []), this.cacheTitleFilterKeyword; } async getReviewFilterKeywordList() { return await this.forage.getItem(this.filter_keyword_review_key) || []; } async saveReviewFilterKeyword(n) { return s(this, e, t).call(this, n, this.filter_keyword_review_key, "评论关键词"); } async getSetting(e = null, t) { this.cacheSettingObj || (this.cacheSettingObj = await this.forage.getItem(this.setting_key) || {}); let n = this.cacheSettingObj; if (null === e) return n; const a = n[e]; return a ? "true" === a || "false" === a ? "true" === a.toLowerCase() : "string" != typeof a || "" === a.trim() || isNaN(Number(a)) ? a : Number(a) : t; } getSettingSync(e, t) { if (!this.cacheSettingObj) return t; const n = this.cacheSettingObj[e]; return n ? "true" === n || "false" === n ? "true" === n.toLowerCase() : "string" != typeof n || "" === n.trim() || isNaN(Number(n)) ? n : Number(n) : t; } async saveSetting(e) { e ? (await this._setItemAndInvalidate(this.setting_key, e), window.clean_cacheSettingObj()) : show.error("设置对象为空"); } async saveSettingItem(e, t) { if (!e) return void show.error("key 不能为空"); await navigator.locks.request("jhs_setting_lock", async () => { let n = await this.getSetting(); n[e] = t, await this.saveSetting(n); }), window.clean_cacheSettingObj(); } async importData(e) { const VALID_KEYS = new Set(["car_list", "filter_keyword_title", "filter_keyword_review", "setting", "blacklist", "blacklist_car_list", "third_party_ttl_cache", "favorite_actresses", "highlighted_tags"]); const t = []; for (const n in e) { if (!VALID_KEYS.has(n)) { clog.warn(`[导入] 跳过未知数据键: ${n}`); continue; } t.push(this._setItemAndInvalidate(n, e[n])); } await Promise.all(t); const n = []; await this.forage.iterate(((t, a) => { a in e || n.push(this.forage.removeItem(a)); })); await Promise.all(n); this._invalidateCache(); } async exportData() { const e = {}; if (await this.forage.iterate(((t, n) => { e[n] = t; })), 0 === Object.keys(e).length) throw new Error("没有可导出的数据"); return e; } async getThirdPartyCache() { return await this.forage.getItem(this.third_party_cache_key) || {}; } async setThirdPartyCache(e) { await this.forage.setItem(this.third_party_cache_key, e || {}); } async clearThirdPartyCache() { await this.forage.removeItem(this.third_party_cache_key); } async getThirdPartyCacheStats() { const e = await this.getThirdPartyCache(), t = Date.now(); let n = 0, a = 0; return Object.values(e).forEach((e => { n++, e && e.time && e.ttl && t - e.time < e.ttl && a++; })), { total: n, valid: a, expired: n - a }; } async cachedRequest(e, t, n) { const a = Date.now(), i = await this.getThirdPartyCache(), s = i[e]; if (s && s.time && a - s.time < (s.ttl || t)) return this._cacheStats.hits++, s.data; this._cacheStats.misses++; const o = await n(); if (void 0 === o || null === o) return o; return i[e] = { time: a, ttl: t, data: o }, await this.setThirdPartyCache(i), o; } getCacheHitStats() { const e = this._cacheStats.hits + this._cacheStats.misses; return { hits: this._cacheStats.hits, misses: this._cacheStats.misses, total: e, rate: e > 0 ? (this._cacheStats.hits / e * 100).toFixed(1) + "%" : "N/A" }; } resetCacheHitStats() { this._cacheStats = { hits: 0, misses: 0 }; } _groupDuplicateItems(e, t) { return groupDuplicateItems(e, t); } _dedupeByKey(e, t) { return dedupeByKey(e, t); } async inspectDataHealth() { const [e, t, n, a] = await Promise.all([ this.getCarList(), this.getFavoriteActressList(), this.getBlacklist(), this.getBlacklistCarList() ]), i = await this.getCarMap(), s = { checkedAt: utils.getNowStr(), totals: { carList: e.length, favoriteActresses: t.length, blacklist: n.length, blacklistCarList: a.length }, fixable: [], readonly: [] }, o = (e, t, n) => s.fixable.push({ type: e, message: t, count: n }), r = (e, t, n) => s.readonly.push({ type: e, message: t, count: n }); const l = this._groupDuplicateItems(e, "carNum"), c = this._groupDuplicateItems(t, "starId"), d = this._groupDuplicateItems(n, "starId"); l.length && o("duplicate-car", "重复番号记录", l.length), c.length && o("duplicate-actress", "重复收藏演员", c.length), d.length && o("duplicate-blacklist", "重复黑名单演员", d.length); const h = e.filter((e => e && e.actress)).length, g = a.filter((e => e && e.actress)).length, p = t.filter((e => e && Object.prototype.hasOwnProperty.call(e, "dbId"))).length, m = n.filter((e => e && ("key" in e || "recordTime" in e || "isActor" in e))).length; h + g + p + m > 0 && o("legacy-fields", "旧字段残留", h + g + p + m); const u = t.filter((e => e && e.name && !Array.isArray(e.allName))).length + n.filter((e => e && e.name && !Array.isArray(e.allName))).length; u && o("invalid-all-name", "演员别名不是数组", u); let f = 0; t.forEach((e => { Array.isArray(e?.newVideoList) && (f += e.newVideoList.filter((e => { const t = "string" == typeof e ? e : e.carNum; return i.has(t); })).length); })), f && o("stored-new-video", "新作品列表中已有鉴定记录", f); const v = e.filter((e => e && e.carNum && !e.url)).length; v && r("missing-url", "番号记录缺失 url,需要人工确认来源", v); const b = t.filter((e => e && !e.starId)).length + n.filter((e => e && !e.starId)).length; b && r("missing-star-id", "演员缺失 starId,需要人工确认身份", b); const w = new Set(n.map((e => e && e.starId)).filter(Boolean)), y = a.filter((e => e && e.starId && !w.has(e.starId))).length; return y && r("orphan-blacklist-car", "黑名单作品找不到关联演员", y), s; } async repairDataHealth() { let e = 0, t = await this.getCarList(), n = await this.getFavoriteActressList(), a = await this.getBlacklist(), i = await this.getBlacklistCarList(); const s = this._dedupeByKey(t, "carNum"); s.changed && (t = s.list, e++); const o = this._dedupeByKey(n, "starId"); o.changed && (n = o.list, e++); const r = this._dedupeByKey(a, "starId"); r.changed && (a = r.list, e++); t = t.map((t => { if (!t) return t; let n = !1; return void 0 !== t.actress && (t.names = t.actress, delete t.actress, n = !0), n && e++, t; })), i = i.map((t => { if (!t) return t; let n = !1; return void 0 !== t.actress && (t.names = t.actress, delete t.actress, n = !0), Object.prototype.hasOwnProperty.call(t, "type") && (delete t.type, n = !0), n && e++, t; })), n = n.map((t => { if (!t) return t; let n = !1; return Object.prototype.hasOwnProperty.call(t, "dbId") && (t.starId || (t.starId = t.dbId), delete t.dbId, n = !0), t.name && !Array.isArray(t.allName) && (t.allName = t.allName ? [ t.allName ] : [ t.name ], n = !0), n && e++, t; })), a = a.map((t => { if (!t) return t; let n = !1; return Object.prototype.hasOwnProperty.call(t, "isActor") && (t.role || (t.role = t.isActor ? B : P), delete t.isActor, n = !0), Object.prototype.hasOwnProperty.call(t, "recordTime") && (t.createTime || (t.createTime = t.recordTime), delete t.recordTime, n = !0), Object.prototype.hasOwnProperty.call(t, "key") && (delete t.key, n = !0), t.name && !Array.isArray(t.allName) && (t.allName = t.allName ? [ t.allName ] : [ t.name ], n = !0), n && e++, t; })); const l = new Set(t.filter((e => e && e.carNum)).map((e => e.carNum))); n = n.map((t => { if (!Array.isArray(t?.newVideoList)) return t; const n = t.newVideoList.filter((e => { const n = "string" == typeof e ? e : e.carNum; return !l.has(n); })); return n.length !== t.newVideoList.length && (t = { ...t, newVideoList: n }, 0 === n.length && t.lastPublishTime && (t.lastPublishTime = null), e++), t; })), await this._setItemAndInvalidate(this.car_list_key, t), await this._setItemAndInvalidate(this.favorite_actresses_key, n), await this._setItemAndInvalidate(this.blacklist_key, a), await this._setItemAndInvalidate(this.blacklist_car_list_key, i); return { fixedGroups: e, report: await this.inspectDataHealth() }; } /** 数据迁移: 将旧版扁平键名重命名为新格式 */ async merge_table_name() { let e = "filter_actor_actress_info_list", t = await this.forage.getItem(e) || []; t && t.length > 0 && (clog.debug("更正", e), await this._setItemAndInvalidate(this.blacklist_key, t)), await this.forage.removeItem(e), e = "favorite_actresses_info_list", t = await this.forage.getItem(e) || [], t && t.length > 0 && (clog.debug("更正", e), await this._setItemAndInvalidate(this.favorite_actresses_key, t)), await this.forage.removeItem(e), e = "car_list_filter_actor_actress", t = await this.forage.getItem(e) || [], t && t.length > 0 && (clog.debug("更正", e), await this._setItemAndInvalidate(this.blacklist_car_list_key, t)), await this.forage.removeItem(e), e = "title_filter_keyword", t = await this.forage.getItem(e) || [], t && t.length > 0 && (clog.debug("更正", e), await this._setItemAndInvalidate(this.filter_keyword_title_key, t)), await this.forage.removeItem(e), e = "review_filter_keyword", t = await this.forage.getItem(e) || [], t && t.length > 0 && (clog.debug("更正", e), await this._setItemAndInvalidate(this.filter_keyword_review_key, t)), await this.forage.removeItem(e), e = "highlightedTags", t = await this.forage.getItem(e) || [], t && t.length > 0 && (clog.debug("更正", e), await this._setItemAndInvalidate(this.highlighted_tags_key, t)), await this.forage.removeItem(e); } async clean_no_url_blacklist() { const [e, t] = await Promise.all([ this.getBlacklistCarList(), this.getBlacklist() ]); if (e.length && !e[0].actress) return; const n = new Set(t.map((e => e.name))), a = e.filter((e => !e.actress || n.has(e.actress))); e.length !== a.length && (clog.debug("清理 blacklistCarList 前", e.length), clog.debug("清理 blacklistCarList 后", a.length), await this._setItemAndInvalidate(this.blacklist_car_list_key, a)); const i = new Set(a.map((e => e.actress))); let s = t.filter((e => i.has(e.name))); s = s.map((e => { const {key: t, recordTime: n, ...a} = e, i = a; return void 0 !== n && (i.createTime = n), i; })), (t.length !== s.length || t.some((e => "key" in e || "recordTime" in e))) && (clog.debug("清理 Blacklist 前", t.length), clog.debug("清理 Blacklist 后", s.length), await this._setItemAndInvalidate(this.blacklist_key, s)); } async async_merge_other() { const e = await this.getSetting(); let t = !1; const n = { enableCheckFilterActorActress: "enableCheckBlacklist", checkIntervalTime_filterActorActress: "checkBlacklist_intervalTime", checkIntervalTime_ruleTime: "checkNewVideo_ruleTime", checkIntervalTime_newVideo: "checkNewVideo_intervalTime", checkIntervalTime_favoriteActress: "checkFavoriteActress_IntervalTime" }; for (const a in n) { const i = n[a]; Object.prototype.hasOwnProperty.call(e, a) && (e[i] = e[a], delete e[a], t = !0); } e.checkFilterTime && (delete e.checkFilterTime, t = !0), e.checkFilterConcurrencyCount && (delete e.checkFilterConcurrencyCount, t = !0), e.checkFilterSleep && (delete e.checkFilterSleep, t = !0), t && (await this.saveSetting(e), clog.debug("配置数据已更正")); } /** 数据迁移: 补全黑名单条目缺失的 role/starId/allName/movieType 字段 */ async merge_blacklist() { const e = await this.getBlacklist(); if (!e || 0 === e.length) return; let t = !1; const n = e.map((e => { let n = !1; if (Object.prototype.hasOwnProperty.call(e, "isActor") && !e.role && (e.role = e.isActor ? B : P, delete e.isActor, n = !0), !e.starId && e.url) try { const t = new URL(e.url).pathname, a = t.split("/").filter((e => "" !== e.trim())).pop(); e.starId !== a && (e.starId = a, n = !0); } catch (a) { clog.error("提取url-starId发生错误", e.url, a); } if (e.allName || (e.allName = e.name ? [ e.name ] : [], n = !0), e.movieType || (e.movieType = D, n = !0), e.url && e.url.includes("sort_type")) { const t = new URL(e.url); t.searchParams.delete("sort_type"), e.url = t.toString(), clog.debug("去除黑名单地址sort_type参数"); } return n && (t = !0), e; })); t && (clog.debug("更正 Blacklist 数据结构"), await this._setItemAndInvalidate(this.blacklist_key, n)); const a = await this.getBlacklistCarList(); t = !1; const i = a.map((n => { if (!n.starId) { let a = e.find((e => e.name === n.actress)); a && (n.starId = a.starId), t = !0; } return n.type && (delete n.type, t = !0), n; })); t && (clog.debug("更正 blacklistCarList 数据结构"), await this._setItemAndInvalidate(this.blacklist_car_list_key, i)); } async merge_favoriteActress() { const e = await this.getFavoriteActressList(); if (!e || 0 === e.length) return; let t = !1; const n = e.map((e => { let n = !1; return e.dbId && (e.starId = e.dbId, delete e.dbId, n = !0), n && (t = !0), e; })); t && (clog.debug("更正 favoriteActressesInfoList 数据结构"), await this._setItemAndInvalidate(this.favorite_actresses_key, n)); } async merge_tow_car_list_table() { const e = await this.getBlacklistCarList(), t = await this.getCarList(); let n = !1; const a = e.map((e => { let t = !1; return void 0 !== e.actress && (e.names = e.actress, delete e.actress, t = !0), t && (n = !0), e; })); n && (clog.debug("更正 blacklistCarList 数据结构 actress->names"), await this._setItemAndInvalidate(this.blacklist_car_list_key, a)), n = !1; const i = t.map((e => { let t = !1; return void 0 !== e.actress && (e.names = e.actress, delete e.actress, t = !0), t && (n = !0), e; })); n && (clog.debug("更正 carList 数据结构 actress->names"), await this._setItemAndInvalidate(this.car_list_key, i)); } /* ───── 快照管理 ───── */ _snapshotKey() { return "snapshots"; } _snapshotMetaKeys() { return [ "snapshots", "data_version" ]; } async _getSnapshots() { return await this.forage.getItem(this._snapshotKey()) || []; } async _saveSnapshots(e) { await this._setItemAndInvalidate(this._snapshotKey(), e); } async _withSnapshotLock(e) { return await navigator.locks.request("jhs_snapshot_lock", async () => await e()); } async createSnapshot(e = "", t = "manual") { return this._withSnapshotLock(async () => { const n = await this._getSnapshots(), a = await this.exportData(); for (const i of this._snapshotMetaKeys()) delete a[i]; let i = 0; for (const s of Object.values(a)) Array.isArray(s) ? i += s.length : "object" == typeof s && s && i++; const s = { id: "snap_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8), name: e || utils.getNowStr(), source: t, time: utils.getNowStr(), itemCount: i, data: a }; n.push(s), n.length > 10 && n.splice(0, n.length - 10); return await this._saveSnapshots(n), clog.log(`创建快照: ${s.name} (${t})`), s; }); } async getSnapshotList() { return (await this._getSnapshots()).map((e => ({ id: e.id, name: e.name, source: e.source, time: e.time, itemCount: e.itemCount }))); } async getSnapshot(e) { return (await this._getSnapshots()).find((t => t.id === e)) || null; } async deleteSnapshot(e) { return this._withSnapshotLock(async () => { const t = await this._getSnapshots(), n = t.filter((t => t.id !== e)); if (n.length === t.length) return void clog.warn("删除快照失败, ID不存在: " + e); await this._saveSnapshots(n), clog.log("删除快照: " + e); }); } async restoreSnapshot(e) { const t = await this.getSnapshot(e); if (!t) throw new Error("快照不存在: " + e); if (!t.data || "object" != typeof t.data) throw new Error("快照数据损坏"); await this.createSnapshot("恢复前自动备份", "auto-restore"); const n = { ...t.data }; for (const a of this._snapshotMetaKeys()) delete n[a]; await this.importData(n); clog.log("已恢复快照: " + t.name); return t; } /* ───── 差异对比引擎 ───── */ _stableStringify(e) { if (null === e || "object" != typeof e) return JSON.stringify(e); if (Array.isArray(e)) return "[" + e.map((e => this._stableStringify(e))).join(",") + "]"; return "{" + Object.keys(e).sort().map((t => JSON.stringify(t) + ":" + this._stableStringify(e[t]))).join(",") + "}"; } diffData(e, t) { const n = new Set([ ...Object.keys(e), ...Object.keys(t) ]), a = {}, i = { added: 0, removed: 0, modified: 0, unchanged: 0 }; for (const o of n) { const r = e[o], l = t[o]; if (void 0 === r && void 0 === l) continue; if (void 0 === r) { const e = Array.isArray(l) ? l.length : 1; a[o] = { status: "added", oldCount: 0, newCount: e, added: Array.isArray(l) ? l : [], removed: [], modified: [] }, i.added++; } else if (void 0 === l) { const t = Array.isArray(r) ? r.length : 1; a[o] = { status: "removed", oldCount: t, newCount: 0, added: [], removed: Array.isArray(r) ? r : [], modified: [] }, i.removed++; } else if (Array.isArray(r) && Array.isArray(l)) { const e = this._diffArrays(r, l, o); a[o] = { status: e.status, oldCount: r.length, newCount: l.length, ...e }, i[e.status]++; } else if ("object" == typeof r && "object" == typeof l && !Array.isArray(r) && !Array.isArray(l)) { const e = this._diffObjects(r, l); a[o] = { status: e.status, oldCount: Object.keys(r).length, newCount: Object.keys(l).length, added: [], removed: [], modified: e.changes }, i[e.status]++; } else r === l ? (a[o] = { status: "unchanged", oldCount: 1, newCount: 1, added: [], removed: [], modified: [] }, i.unchanged++) : (a[o] = { status: "modified", oldCount: 1, newCount: 1, added: [], removed: [], modified: [{ key: o, changes: { _value: [r, l] } }] }, i.modified++); } return { summary: i, stores: a }; } _getArrayKey(e) { return "car_list" === e || "blacklist_car_list" === e ? "carNum" : "blacklist" === e || "favorite_actresses" === e ? "starId" : null; } _diffArrays(e, t, n) { const a = this._getArrayKey(n); if (!a) { const n = this._stableStringify(e), i = this._stableStringify(t); if (n === i) return { status: "unchanged", added: [], removed: [], modified: [] }; const s = new Set(e.map((e => this._stableStringify(e)))), o = new Set(t.map((e => this._stableStringify(e)))); return { status: "modified", added: t.filter((e => !s.has(this._stableStringify(e)))), removed: e.filter((e => !o.has(this._stableStringify(e)))), modified: [] }; } const i = new Map(e.map((e => [ e[a], e ]))), s = new Map(t.map((e => [ e[a], e ]))), o = [], r = [], l = []; for (const [c, d] of s) { const t = i.get(c); if (!t) o.push(d); else { const e = this._diffObjects(t, d); "unchanged" !== e.status && l.push({ key: c, changes: e.changes }); } } for (const [c] of i) s.has(c) || r.push(i.get(c)); const c = o.length + r.length + l.length; return { status: 0 === c ? "unchanged" : "modified", added: o, removed: r, modified: l }; } _diffObjects(e, t) { const n = new Set([ ...Object.keys(e), ...Object.keys(t) ]), a = {}; let i = 0; for (const s of n) { const o = e[s], r = t[s]; this._stableStringify(o) !== this._stableStringify(r) && (a[s] = [ o, r ], i++); } return { status: 0 === i ? "unchanged" : "modified", changes: a }; } }; const U = "https://jdforrepam.com/api"; /** 生成 JavDB API 请求签名 (HMAC-时间戳+盐+MD5, 20秒缓存) */ function O() { const e = "jhs_review_ts", t = "jhs_review_sign", n = Math.floor(Date.now() / 1e3); if (n - (localStorage.getItem(e) || 0) <= 20) return localStorage.getItem(t); const a = `${n}.lpw6vgqzsp.${md5(`${n}71cf27bb3c0bcdf207b64abecddc970098c7421ee7203b9cdae54478478a199e7d5a6e1a57691123c1a931c057842fb73ba3b3c83bcd69c17ccf174081e3d8aa`)}`; return localStorage.setItem(e, n), localStorage.setItem(t, a), a; } const R = async (e, t = 1, n = 20) => { let a = `${U}/v1/movies/${e}/reviews`, i = { jdSignature: await O() }; return await storageManager.cachedRequest(`reviews:${e}:${t}:${n}`, 864e5, (async () => { const e = await gmHttp.get(a, { page: t, sort_by: "hotly", limit: n }, i); if (!e?.data?.reviews) throw new Error(e?.message || "获取评论失败"); return e.data.reviews; })); }, V = async e => { let t = `${U}/v4/movies/${e}`, n = { jdSignature: await O() }; const a = await storageManager.cachedRequest(`movie-detail:${e}`, 6048e5, (async () => { const e = await gmHttp.get(t, null, n); if (!e.data) throw new Error(e.message || "获取视频详情失败"); return e; })); if (!a.data) throw show.error("获取视频详情失败: " + a.message), new Error(a.message); const i = a.data.movie, s = i.preview_images, o = []; return s.forEach((e => { o.push(e.large_url.replace(/https:\/\/[^/]+\/rhe951l4q/, "https://c0.jdbstatic.com")); })), { movieId: i.id, actors: i.actors, duration: i.duration, title: i.origin_title, carNum: i.number, score: i.score, releaseDate: i.release_date, watchedCount: i.watched_count, imgList: o }; }, K = async (e, t = 1, n = 20) => { let a = `${U}/v1/lists/related?movie_id=${e}&page=${t}&limit=${n}`, i = { jdSignature: await O() }; const s = await storageManager.cachedRequest(`related:${e}:${t}:${n}`, 864e5, (async () => { const e = await gmHttp.get(a, null, i); if (!e?.data?.lists) throw new Error(e?.message || "获取相关清单失败"); return e; })), o = []; return s.data.lists.forEach((e => { o.push({ relatedId: e.id, name: e.name, movieCount: e.movies_count, collectionCount: e.collections_count, viewCount: e.views_count, createTime: utils.formatDate(e.created_at) }); })), o; }, W = async (e = "daily", t = "high_score") => { let n = `${U}/v1/rankings/playback?period=${e}&filter_by=${t}`, a = { jdSignature: await O() }; return (await gmHttp.get(n, null, a)).data.movies; }, q = async (e = "all", t = "", n = 1, a = 40) => { let i = `${U}/v1/movies/top?start_rank=1&type=${e}&type_value=${t}&ignore_watched=false&page=${n}&limit=${a}`; const l = localStorage.getItem("jhs_appAuthorization"), c = l ? await decryptData(l) : ""; let s = { "user-agent": "Dart/3.5 (dart:io)", "accept-language": "zh-TW", host: "jdforrepam.com", authorization: "Bearer " + c, jdsignature: await O() }; return await gmHttp.get(i, null, s); }; class J { constructor() { return i(this, "intervalContainer", {}), i(this, "mimeTypes", { txt: "text/plain", html: "text/html", css: "text/css", csv: "text/csv", json: "application/json", xml: "application/xml", jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", pdf: "application/pdf", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", zip: "application/zip", rar: "application/x-rar-compressed", "7z": "application/x-7z-compressed", mp3: "audio/mpeg", wav: "audio/wav", mp4: "video/mp4", webm: "video/webm", ogg: "audio/ogg" }), i(this, "timers", new Map), i(this, "insertStyle", (e => { e && (-1 === e.indexOf("<style>") && (e = "<style>" + e + "</style>"), $("head").append(e)); })), i(this, "layerIndexStack", []), J.instance || (J.instance = this), J.instance; } importResource(e) { let t; e.indexOf("css") >= 0 ? (t = document.createElement("link"), t.setAttribute("rel", "stylesheet"), t.href = e) : (t = document.createElement("script"), t.setAttribute("type", "text/javascript"), t.src = e), document.documentElement.appendChild(t); } openPage(e, t, n, a) { if (n = n ?? !0, a && (a.ctrlKey || a.metaKey)) return void GM_openInTab(e.includes("http") ? e : window.location.origin + e, { insert: 0 }); let i = e; e.includes("/actors/") || e.includes("/star/") || (i = e.includes("?") ? `${e}&hideNav=1` : `${e}?hideNav=1`), layer.open({ type: 2, title: t, content: i, scrollbar: !1, shadeClose: n, area: this.getResponsiveArea([ "85%", "90%" ]), isOutAnim: !1, anim: -1, success: (e, t) => { this.setupEscClose(t); } }); } _handleGlobalEscKey(e) { if ("Escape" !== e.key && 27 !== e.keyCode) return; if (0 === this.layerIndexStack.length) return; const t = this.layerIndexStack[this.layerIndexStack.length - 1], n = $(`#layui-layer${t}`); let a = !1; if (n.find(".viewer-container").length > 0) a = !0; else { const e = n.find(`#layui-layer-iframe${t}`)[0]; if (e && e.contentDocument) try { $(e.contentDocument).find(".viewer-container").length > 0 && (a = !0); } catch (i) { clog.warn("无法检查跨域 iframe 内的 .viewer-container"); } } a || (this.layerIndexStack.pop(), layer.close(t)); } setupEscClose(e) { var t; this._boundHandler || (this._boundHandler = this._handleGlobalEscKey.bind(this), $(document).off("keydown.globalLayerEsc"), $(document).on("keydown.globalLayerEsc", this._boundHandler)), -1 === this.layerIndexStack.indexOf(e) && this.layerIndexStack.push(e); const n = $(`#layui-layer-iframe${e}`), a = `keydown.layerEsc${e}`; try { const e = null == (t = n[0]) ? void 0 : t.contentDocument; if (e) { if ("yes" === n.attr("data-esc-bound")) return; $(e).off(a), $(e).on(a, this._boundHandler), n.attr("data-esc-bound", "yes"); } } catch (i) { clog.error("iframe监听失败 (跨域或未加载完毕):", i); } } closePage() { storageManager.getSetting("needClosePage", "yes").then((e => { if ("yes" !== e) return; parent.document.documentElement.style.overflow = "auto"; [ ".layui-layer-shade", ".layui-layer-move", ".layui-layer" ].forEach((function(e) { const t = parent.document.querySelectorAll(e); if (t.length > 0) { const e = t.length > 1 ? t[t.length - 1] : t[0]; e.parentNode.removeChild(e); } })), window.close(); })); } loopDetector(e, t, n = 100, a = 1e4, i = !0) { const s = Math.random(), o = (new Date).getTime(), r = e => { clearInterval(this.intervalContainer[s]), e && t && t(), delete this.intervalContainer[s]; }; this.intervalContainer[s] = setInterval((() => { const t = (new Date).getTime() - o; e() ? r(!0) : t >= a && r(i); }), n); } rightClick(e, t, n) { let a; "string" == typeof e ? a = document.querySelector(e) : e instanceof HTMLElement && (a = e), a || (console.warn("rightClick(), 容器无效或未提供,将使用 document.body 进行全局委托。"), a = document.body), "string" == typeof t && "" !== t.trim() ? a.addEventListener("contextmenu", (e => { const a = e.target.closest(t); a && n(e, a); })) : console.error("rightClick(), 必须提供有效的 targetSelector。"); } q(e, t, n, a) { let o; if (this.isMobileMode()) { o = layer.confirm(t, { title: "提示", btn: [ "确定", "取消" ], shade: 0, zIndex: 999999991 }, (function() { n && n(), layer.close(o); }), (function() { a && a(); })); } else { let i, s; e ? (i = e.clientX - 130, s = e.clientY - 120) : (i = window.innerWidth / 2 - 120, s = window.innerHeight / 2 - 120); o = layer.confirm(t, { offset: [ s, i ], title: "提示", btn: [ "确定", "取消" ], shade: 0, zIndex: 999999991 }, (function() { n && n(), layer.close(o); }), (function() { a && a(); })); } } getNowStr(e = "-", t = ":", n = null) { let a; a = n ? new Date(n) : new Date; const i = a.getFullYear(), s = String(a.getMonth() + 1).padStart(2, "0"), o = String(a.getDate()).padStart(2, "0"), r = String(a.getHours()).padStart(2, "0"), l = String(a.getMinutes()).padStart(2, "0"), c = String(a.getSeconds()).padStart(2, "0"); return `${[ i, s, o ].join(e)} ${[ r, l, c ].join(t)}`; } formatDate(e, t = "-", n = ":") { let a; if (e instanceof Date) a = e; else { if ("string" != typeof e) throw new Error("Invalid date input: must be Date object or date string"); if (a = new Date(e), isNaN(a.getTime())) throw new Error("Invalid date string"); } const i = a.getFullYear(), s = String(a.getMonth() + 1).padStart(2, "0"), o = String(a.getDate()).padStart(2, "0"), r = String(a.getHours()).padStart(2, "0"), l = String(a.getMinutes()).padStart(2, "0"), c = String(a.getSeconds()).padStart(2, "0"); return `${[ i, s, o ].join(t)} ${[ r, l, c ].join(n)}`; } getHourDifference(e, t) { const n = e.getTime(), a = t.getTime(), i = Math.abs(a - n) / 36e5; return Math.floor(i); } download(e, t) { show.info("开始请求下载..."); const n = t.split(".").pop().toLowerCase(); let a, i = this.mimeTypes[n] || "application/octet-stream"; if (e instanceof Blob) a = e; else if (e instanceof ArrayBuffer || ArrayBuffer.isView(e)) a = new Blob([ e ], { type: i }); else if ("string" == typeof e && e.startsWith("data:")) { const t = atob(e.split(",")[1]), n = new ArrayBuffer(t.length), s = new Uint8Array(n); for (let e = 0; e < t.length; e++) s[e] = t.charCodeAt(e); a = new Blob([ s ], { type: i }); } else a = new Blob([ e ], { type: i }); const s = URL.createObjectURL(a), o = document.createElement("a"); o.href = s, o.download = t, document.body.appendChild(o), o.click(), setTimeout((() => { document.body.removeChild(o), URL.revokeObjectURL(s); }), 100); } smoothScrollToTop(e = 500) { return new Promise((t => { const n = performance.now(), a = window.pageYOffset; window.requestAnimationFrame((function i(s) { const o = s - n, r = Math.min(o / e, 1), l = r < .5 ? 4 * r * r * r : 1 - Math.pow(-2 * r + 2, 3) / 2; window.scrollTo(0, a * (1 - l)), r < 1 ? window.requestAnimationFrame(i) : t(); })); })); } simpleId() { return crypto.randomUUID().replace("-", ""); } isUrl(e) { try { return new URL(e), !0; } catch (t) { return !1; } } setHrefParam(e, t) { const n = new URL(window.location.href); n.searchParams.set(e, t), window.history.pushState({}, "", n.toString()); } getUrlParam(e, t) { const n = e.split("?")[1]; if (!n) return null; const a = new RegExp(`(?:^|&)${t}=([^&]*)`), i = n.match(a); let s = ""; return i && i[1] && (s = decodeURIComponent(i[1].replace(/\+/g, " "))), s ? "true" === s || "false" === s ? "true" === s.toLowerCase() : "string" != typeof s || "" === s.trim() || isNaN(Number(s)) ? s : Number(s) : s; } reBuildSignature() { return O(); } getResponsiveArea(e) { const t = window.innerWidth; return this.isMobileMode() ? [ "100%", "90%" ] : t >= 1200 ? e || this.getDefaultArea() : [ "70%", "90%" ]; } getDefaultArea() { return [ "85%", "90%" ]; } isMobile() { const e = navigator.userAgent.toLowerCase(); return [ "iphone", "ipod", "ipad", "android", "blackberry", "windows phone", "nokia", "webos", "opera mini", "mobile", "mobi", "tablet" ].some((t => e.includes(t))); } isMobileMode() { const e = storageManager.getSettingSync("mobileMode", "auto"); return "on" === e || ("off" !== e && (this.isMobile() || window.innerWidth < 768)); } copyToClipboard(e, t) { navigator.clipboard.writeText(t).then((() => show.info(`${e}已复制到剪切板, ${t}`))).catch((e => console.error("复制失败: ", e))); } htmlTo$dom(e) { const t = new DOMParser; return $(t.parseFromString(e, "text/html")); } addCookie(e, t = {}) { const {maxAge: n = 604800, path: a = "/", domain: i = "", secure: s = !1, sameSite: o = "Lax"} = t; e.split(";").forEach((e => { const t = e.trim(); if (t) { const e = t.split("="); if (e.length >= 2 && e[0].trim()) { let t = [ `${e[0].trim()}=${e.slice(1).join("=")}` ]; n > 0 && t.push(`max-age=${n}`), t.push(`path=${a}`), i && t.push(`domain=${i}`), s && t.push("Secure"), o && t.push(`SameSite=${o}`), clog.debug("document.cookie = '" + t.join("; ") + "'"), document.cookie = t.join("; "); } } })); } isHidden(e) { const t = e.jquery ? e[0] : e; return !t || (t.offsetWidth <= 0 && t.offsetHeight <= 0 || "none" === window.getComputedStyle(t).display); } time(e = "default", t = "s", n = 2) { if (this.timers.has(e)) { const t = this.timers.get(e), n = performance.now() - t.startTime; let a, i; return "s" === t.unit ? (a = (n / 1e3).toFixed(t.precision), i = "秒") : (a = n.toFixed(t.precision), i = "毫秒"), this.timers.delete(e), `${e}: ${a}${i}`; } this.timers.set(e, { startTime: performance.now(), unit: t, precision: n }); } sleep(e = 1e3) { return new Promise((t => setTimeout(t, e))); } genericSort(e, t, n = !0) { if (!Array.isArray(e) || 0 === e.length) return []; if (!Array.isArray(t) || 0 === t.length) return [ ...e ]; const i = e => { if (e instanceof Date) return e; if ("string" == typeof e) { const t = new Date(e); if (!isNaN(t.getTime())) return t; } return e; }; const getVal = (e, t) => null != t ? "function" == typeof t ? t(e) : e && "object" == typeof e ? e[t] : void 0 : e; /* Separate nulls and non-nulls, sort non-nulls, then reassemble */ const nulls = [], nonNulls = []; for (const item of e) { let hasNull = !1; for (const s of t) { const val = getVal(item, s.key); if (null == val || void 0 === val) { hasNull = !0; break; } } hasNull ? nulls.push(item) : nonNulls.push(item); } return nonNulls.sort(((e, a) => { for (const s of t) { const {key: t, order: o = "asc"} = s; let r = getVal(e, t), l = getVal(a, t); const c = i(r), d = i(l); let h = c instanceof Date && d instanceof Date ? c.getTime() - d.getTime() : "number" == typeof r && "number" == typeof l ? r - l : "string" == typeof r && "string" == typeof l ? r.localeCompare(l) : String(r).localeCompare(String(l)); "desc" === o && (h *= -1); if (0 !== h) return h; } return 0; })), n ? [ ...nonNulls, ...nulls ] : [ ...nulls, ...nonNulls ]; } async retry(e, t = 3) { let n = 0; for (;n < t; ) try { const t = await e(); return n > 0 && clog.debug(`[重试] 请求成功,共发起 ${n + 1} 次。`), t; } catch (a) { const e = a instanceof Error ? a.message : "object" == typeof a ? JSON.stringify(a) : String(a); if (e.includes("Just a moment") || e.includes("重定向") || e.toLowerCase().includes("404 not found")) throw a; if (n++, n === t) throw clog.debug(`[重试] 达到最大重试次数 (${t}),最终失败:`, a), a; await this.sleep(500 * n), clog.debug(`[重试] 请求失败,准备第 ${n + 1} 次重试, 错误信息: ${e}`); } } async pingLocalService(e, t = 3e4, timeout = 2e3) { const n = "jhs_ping_" + e, a = sessionStorage.getItem(n); if (a) { const {result: e, time: n} = JSON.parse(a); if (Date.now() - n < t) return e; } try { const t = new AbortController, a = setTimeout((() => t.abort()), timeout), i = await fetch(e, { signal: t.signal, mode: "no-cors" }); clearTimeout(a); const s = {ok: !0, url: e}; return sessionStorage.setItem(n, JSON.stringify({result: s, time: Date.now()})), s; } catch (i) { const t = {ok: !1, url: e, error: i.message}; return sessionStorage.setItem(n, JSON.stringify({result: t, time: Date.now()})), t; } } } unsafeWindow.utils = window.utils = new J, unsafeWindow.gmHttp = window.gmHttp = new class { constructor() { this._circuitBreakers = new Map(); this._domainStats = new Map(); } _getDomain(e) { try { return new URL(e).hostname; } catch { return "unknown"; } } _checkCircuitBreaker(e) { const t = this._circuitBreakers.get(e); if (!t) return null; if ("open" === t.state) { const n = Date.now() - t.openTime; return n < t.cooldownMs ? { state: "open", remaining: Math.ceil((t.cooldownMs - n) / 1e3) } : (t.state = "half-open", t.failCount = 0, t.probing = !1, null); } if ("half-open" === t.state && t.probing) return { state: "half-open", remaining: 0 }; return null; } isDomainCircuitBroken(e) { return this._checkCircuitBreaker(this._getDomain(e)); } /** 判断目标 URL 是否与当前页面同源 */ _isSameOrigin(e) { try { return new URL(e, location.href).origin === location.origin; } catch { return !1; } } /** * 同源请求走原生 fetch。 * GM_xmlhttpRequest 由扩展后台发起, 缺少页面的完整浏览器指纹(UA/TLS/Sec-Fetch-* 等), * 会被 Cloudflare 判定为机器人直接返回 403 挑战页; 原生 fetch 继承当前页面会话, * 与用户正常浏览一致, 因此可正常通过 Cloudflare 校验。 */ async _fetchSameOrigin(method, url, body, headers, timeout, domain, checkRedirect) { const ctrl = new AbortController, timer = setTimeout((() => ctrl.abort()), timeout); let resp, text; try { resp = await fetch(url, { method: method, headers: headers || {}, body: body || void 0, credentials: "same-origin", signal: ctrl.signal }); text = await resp.text(); } catch (err) { this._recordFailure(domain); throw "AbortError" === err.name ? new Error("请求超时: " + url) : new Error(err.message || "网络错误"); } finally { clearTimeout(timer); } if (checkRedirect && resp.url && resp.url !== url) { this._recordFailure(domain); throw new Error("请求被重定向了,URL是:" + resp.url); } if (resp.ok) { this._recordSuccess(domain); if (!text) return text; try { return JSON.parse(text); } catch { return text; } } clog.error("请求失败,状态码:", resp.status, url), this._recordFailure(domain); let e2; try { e2 = JSON.parse(text), e2.status = resp.status; } catch { e2 = new Error(text || `请求发生错误 ${resp.status}`), e2.status = resp.status; } throw e2; } /* domainStats.count 统计请求数(含重试),非独立请求数 */ _recordSuccess(e) { let t = this._circuitBreakers.get(e); t && (t.state = "closed", t.failCount = 0, t.probing = !1); let n = this._domainStats.get(e); n || (n = { count: 0, errors: 0, lastUsed: 0 }, this._domainStats.set(e, n)), n.count++, n.lastUsed = Date.now(); } _recordFailure(e) { let t = this._circuitBreakers.get(e); t || (t = { state: "closed", failCount: 0, openTime: 0, cooldownMs: 6e4, threshold: 3 }, this._circuitBreakers.set(e, t)), t.failCount++, t.failCount >= (t.threshold || 3) && (t.state = "open", t.openTime = Date.now(), clog.warn(`[熔断] ${e} 连续失败 ${t.failCount} 次,已熔断 ${t.cooldownMs / 1e3} 秒`)); let n = this._domainStats.get(e); n || (n = { count: 0, errors: 0, lastUsed: 0 }, this._domainStats.set(e, n)), n.count++, n.errors++, n.lastUsed = Date.now(); } getCircuitBreakerStatus() { const e = {}; return this._circuitBreakers.forEach(((t, n) => { e[n] = { ...t }; })), e; } resetCircuitBreaker(e) { this._circuitBreakers.delete(e); } resetAllCircuitBreakers() { this._circuitBreakers.clear(); } getDomainStats() { const e = {}; return this._domainStats.forEach(((t, n) => { e[n] = { ...t }; })), e; } clearDomainStats() { this._domainStats.clear(); } getUsedDomains() { return [...this._domainStats.keys()]; } async get(e, t = {}, n = {}, a) { return this.gmRequest("GET", e, null, t, n, a); } post(e, t = {}, n = {}) { n = { "Content-Type": "application/json", ...n }; let a = JSON.stringify(t); return this.gmRequest("POST", e, a, null, n); } postForm(e, t = {}, n = {}) { n || (n = {}), n["Content-Type"] || (n["Content-Type"] = "application/x-www-form-urlencoded"); let a = ""; return t && Object.keys(t).length > 0 && (a = Object.entries(t).map((([e, t]) => `${e}=${encodeURIComponent(t)}`)).join("&")), this.gmRequest("POST", e, a, null, n); } postFileFormData(e, t = {}, n = {}) { n || (n = {}); const a = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; n["Content-Type"] = `multipart/form-data; boundary=${a}`; let i = ""; return t && Object.keys(t).length > 0 && (i = Object.entries(t).map((([e, t]) => `--${a}\r\nContent-Disposition: form-data; name="${e}"\r\n\r\n${t}\r\n`)).join("")), i += `--${a}--`, this.gmRequest("POST", e, i, null, n); } /* 注意: 分块下载未使用 AbortController, 某块失败时其他飞行中的请求不会被取消 */ async downloadFileInChunks(e, t = {}, n, a) { if (!n) throw new Error("请提供文件名 (filename) 用于保存。"); const i = await storageManager.getSetting("httpTimeout", 5e3), s = await storageManager.getSetting("httpRetryCount", 3); let o, r; clog.log(`[${n}] 正在获取文件大小...`); try { const a = await utils.retry((() => new Promise(((n, a) => { GM_xmlhttpRequest({ method: "GET", url: e, headers: { ...t, Range: "bytes=0-0" }, timeout: i, onload: n, onerror: e => a(new Error("网络错误:无法获取文件大小")), ontimeout: () => a(new Error("超时:获取文件大小")) }); }))), s); if (206 !== a.status && 200 !== a.status) throw new Error(`请求文件大小失败,状态码: ${a.status}`); { const e = a.responseHeaders.match(/content-range:\s*bytes\s*\d+-\d+\/(\d+)/i), t = a.responseHeaders.match(/content-type:\s*([^\s;]+)/i); if (e && e[1]) o = parseInt(e[1], 10); else { if (!a.responseHeaders.match(/content-length:\s*(\d+)/i) || 200 !== a.status) throw new Error("无法从响应头中获取文件总大小,服务器可能不支持 Range 请求。"); { const e = a.responseHeaders.match(/content-length:\s*(\d+)/i); o = parseInt(e[1], 10), clog.warn(`[${n}] 服务器返回 200 状态码,可能不支持 Range 请求。将尝试完整下载。`); } } t && t[1] && (r = t[1]), clog.log(`[${n}] 文件总大小:${(o / 1024 / 1024).toFixed(2)} MB, MIME 类型: ${r || "未知"}`); } } catch (u) { throw clog.error(`[${n}] 获取文件大小失败:`, u.message), u; } if (!o || o <= 0) throw new Error("获取到的文件大小无效或服务器拒绝提供大小信息。"); const l = 1048576, c = Math.ceil(o / l), d = [], h = new Array(c); clog.log(`[${n}] 文件将被分为 ${c} 块进行下载 (每块约 1.00 MB)`); for (let f = 0; f < c; f++) { const a = f * l, r = `bytes=${a}-${Math.min(a + l - 1, o - 1)}`, g = await utils.retry((() => new Promise(((a, s) => { const o = { ...t, Range: r, Accept: "application/octet-stream" }; GM_xmlhttpRequest({ method: "GET", url: e, headers: o, timeout: i, responseType: "arraybuffer", onload: e => { 206 === e.status || 200 === e.status ? e.response instanceof ArrayBuffer ? (h[f] = e.response, clog.log(`[${n}] 成功下载第 ${f + 1}/${c} 块 (${r})`), a()) : s(new Error(`第 ${f + 1} 块响应不是 ArrayBuffer。`)) : s(new Error(`第 ${f + 1} 块请求失败,状态码: ${e.status}`)); }, onerror: e => s(new Error(`第 ${f + 1} 块网络错误: ${e.error}`)), ontimeout: () => s(new Error(`第 ${f + 1} 块超时。`)) }); }))), s); d.push(g); } try { await Promise.all(d), clog.log(`[${n}] 所有分块下载完成,开始合并...`); } catch (u) { throw clog.error(`[${n}] 分块下载过程中发生错误:`, u.message), u; } const g = new Blob(h); g.size !== o && clog.warn(`[${n}] 警告:合并后的 Blob 大小 (${g.size}) 与预期文件大小 (${o}) 不匹配!`); const p = await g.text(); let m; m = a ? a(p) : p, utils.download(m, n), clog.log(`[${n}] 文件合并完成,已触发浏览器下载。`); } async gmRequest(e, t, n = {}, a = {}, i = {}, s = !1) { if (a && Object.keys(a).length) { const e = new URLSearchParams(a).toString(); t += (t.includes("?") ? "&" : "?") + e; } // 缓存网络配置(单次会话内不变), 避免每个请求都 await IndexedDB if (!this._httpCfg) { const [m, r, b, k] = await Promise.all([storageManager.getSetting("httpTimeout", 5e3), storageManager.getSetting("httpRetryCount", 3), storageManager.getSetting("circuitBreakerThreshold", 3), storageManager.getSetting("circuitBreakerCooldown", 6e4)]); this._httpCfg = { timeout: m, retry: r, threshold: b, cooldown: k }; } const { timeout: m, retry: r, threshold: b, cooldown: k } = this._httpCfg; const o = this._getDomain(t); let u = this._circuitBreakers.get(o); u || (u = { state: "closed", failCount: 0, openTime: 0, cooldownMs: k, threshold: b, probing: !1 }, this._circuitBreakers.set(o, u)); const w = this._checkCircuitBreaker(o); if (w) { const e = new Error(`站点 ${o} 已熔断,${w.remaining}秒后重试`); throw e._circuitBroken = !0, e; } "half-open" === u.state && (u.probing = !0); // 同源请求优先走原生 fetch,避免 GM_xmlhttpRequest 被 Cloudflare 拦截 if (this._isSameOrigin(t)) { return await utils.retry((() => this._fetchSameOrigin(e, t, n, i, m, o, s)), r); } return n || (n = void 0), await utils.retry((() => new Promise(((a, r) => { GM_xmlhttpRequest({ method: e, url: t, headers: i, timeout: m, data: n, onload: e => { try { if (s && e.finalUrl !== t && r("请求被重定向了,URL是:" + e.finalUrl), e.status >= 200 && e.status < 300) { this._recordSuccess(o); if (e.responseText) try { a(JSON.parse(e.responseText)); } catch (n) { a(e.responseText); } else a(e.responseText || e); } else { clog.error("请求失败,状态码:", e.status, t), this._recordFailure(o); if (e.responseText) { try { const t = JSON.parse(e.responseText); t.status = e.status, r(t); } catch { const t = new Error(e.responseText || `请求发生错误 ${e.status}`); t.status = e.status, r(t); } } else { const t = new Error(`请求发生错误 ${e.status}`); t.status = e.status, r(t); } } } catch (n) { this._recordFailure(o), r(n); } }, onerror: e => { clog.error("网络错误:", t), this._recordFailure(o), r(new Error(e.error || "网络错误")); }, ontimeout: () => { this._recordFailure(o), r(new Error("请求超时: " + t)); } }); }))), r); } }, unsafeWindow.storageManager = window.storageManager = new z; const G = new BroadcastChannel("channel-refresh"); window.refresh = function() { G.postMessage({ type: "refresh" }); }; window.cleanCache_filter_actor_actress_car_list = function() { G.postMessage({ type: "cleanCache_filter_actor_actress_car_list" }); }; window.clean_cacheSettingObj = function() { G.postMessage({ type: "clean_cacheSettingObj" }); }; document.head.insertAdjacentHTML("beforeend", '\n <style>\n .loading-container {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.1);\n z-index: 99999999;\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 }\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 </style>\n '); unsafeWindow.loading = window.loading = function() { const e = document.createElement("div"); e.className = "loading-container"; const t = document.createElement("div"); return t.className = "loading-animation", e.appendChild(t), document.body.appendChild(e), { close: () => { e && e.parentNode && e.parentNode.removeChild(e); } }; }, function() { const e = (e, t, n, a, i) => { let s; "object" == typeof n ? s = n : (s = "object" == typeof a ? a : i || {}, s.gravity = n || "top", s.position = "string" == typeof a ? a : "center"), s.gravity && "center" !== s.gravity || (s.offset = { y: "calc(50vh - 150px)" }); const o = "#60A5FA", r = "#93C5FD", l = "#10B981", c = "#6EE7B7", d = "#EF4444", h = "#FCA5A5", g = { borderRadius: "12px", color: "white", padding: "12px 16px", boxShadow: "0 4px 6px rgba(0,0,0,0.1)", minWidth: "150px", textAlign: "center", zIndex: 999999999 }, p = { text: e, duration: 2500, close: !1, gravity: "top", position: "center", style: { info: { ...g, background: `linear-gradient(to right, ${o}, ${r})` }, success: { ...g, background: `linear-gradient(to right, ${l}, ${c})` }, error: { ...g, background: `linear-gradient(to right, ${d}, ${h})` } }[t], stopOnFocus: !0, oldestFirst: !1, ...s }; -1 === p.duration && (p.close = !0); const m = Toastify(p); return m.showToast(), m.closeShow = () => { m.toastElement.remove(); }, m; }; unsafeWindow.show = window.show = { ok: (t, n = "center", a, i) => e(t, "success", n, a, i), error: (t, n = "center", a, i) => e(t, "error", n, a, i), info: (t, n = "center", a, i) => e(t, "info", n, a, i) }; }(), function() { function e(e = 10) { setTimeout((() => { const e = document.querySelectorAll(".layui-layer-shade").length; document.documentElement.style.overflow = e > 0 ? "hidden" : ""; }), e); } document.head.insertAdjacentHTML("beforeend", "\n <style>\n .viewer-canvas {\n overflow: auto !important;\n }\n \n .viewer-close {\n background: rgba(255,0,0,0.6) !important;\n }\n .viewer-close:hover {\n background: rgba(255,0,0,0.8) !important;\n }\n </style>\n "), window.showImageViewer = function(t, n = "") { let a = null, i = !1; "string" == typeof t || t instanceof String ? (a = $('<div class="temporary-container" style="display:none;">').append(`<img src="${t}" alt="${n}">`).appendTo("body"), i = !0) : a = $(t); const s = { zIndex: 999999990, navbar: !1, zoomOnWheel: !1, zoomRatio: .1, toggleOnDblclick: !1, toolbar: { zoomIn: 1, zoomOut: 1, reset: 1, rotateLeft: 0, rotateRight: 0, flipHorizontal: 0, flipVertical: 0 }, title: !1, keyboard: !1, viewed() { o.zoomTo(1.4); let e = (o.viewerData.width - o.imageData.width) / 2; o.moveTo(e, 0); }, shown() { i && a.remove(), document.documentElement.style.overflow = "hidden", document.body.style.overflow = "hidden", o.handleKeydown = function(t) { "Escape" !== t.key && " " !== t.key || (t.preventDefault(), t.stopPropagation(), o.destroy(), document.removeEventListener("keydown", o.handleKeydown), document.documentElement.style.overflow = "", document.body.style.overflow = "", e()); }, document.addEventListener("keydown", o.handleKeydown); }, hidden() { o && o.handleKeydown && document.removeEventListener("keydown", o.handleKeydown), o.destroy(), document.documentElement.style.overflow = "", document.body.style.overflow = "", e(); } }, o = new Viewer(a[0], s); o.show(); }; }(), window.ImageHoverPreview = class { constructor(e = {}) { this.config = { selector: ".hover-preview", dataAttribute: "data-full", maxWidth: 1e3, maxHeight: 1e3, offsetX: 20, offsetY: 20, zIndex: 9999999999, transition: .2, autoAdjustPosition: !0, ...e }, this.preview = null, this.currentTarget = null, this.timer = null, this.imgElement = null, this.boundElements = new WeakSet, this.init(); } init() { if (utils.isMobileMode()) return; this.injectStyles(), this.createPreviewElement(); // 延迟绑定: 确保 DOM 中已有目标元素时选择器能正确匹配 if ("loading" === document.readyState) { document.addEventListener("DOMContentLoaded", () => this.bindEvents()); } else { this.bindEvents(); } } injectStyles() { const e = `\n <style>\n .image-hover-preview {\n position: fixed;\n display: none;\n z-index: ${this.config.zIndex};\n border-radius: 4px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n overflow: hidden;\n pointer-events: none;\n opacity: 0;\n transition: opacity ${this.config.transition}s ease;\n background-color: #fff;\n }\n \n .image-hover-preview.active {\n opacity: 1;\n }\n \n .image-hover-preview img {\n max-width: ${this.config.maxWidth}px;\n max-height: ${this.config.maxHeight}px;\n display: block;\n object-fit: contain;\n }\n \n .image-hover-preview::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.03);\n pointer-events: none;\n }\n \n .image-hover-preview.loading::before {\n content: '加载中...';\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n color: #666;\n font-size: 14px;\n }\n </style>\n `; document.head.insertAdjacentHTML("beforeend", e); } createPreviewElement() { this.preview = document.createElement("div"), this.preview.className = "image-hover-preview", document.body.appendChild(this.preview); } bindEvents() { if (this._delegated) return; this._delegated = !0; const matchTarget = e => { const t = e.target; if (!t || !t.closest) return null; return t.matches && t.matches(this.config.selector) ? t : t.closest(this.config.selector); }; this._onOver = e => { const t = matchTarget(e); if (!t || this.currentTarget === t) return; this.handleMouseEnter(e, t); }; this._onOut = e => { if (!this.currentTarget) return; const rt = e.relatedTarget; if (rt && this.currentTarget.contains && this.currentTarget.contains(rt)) return; this.handleMouseLeave(e); }; this._onMove = e => { this.currentTarget && this.handleMouseMove(e); }; document.addEventListener("mouseover", this._onOver, !0); document.addEventListener("mouseout", this._onOut, !0); document.addEventListener("mousemove", this._onMove, !0); } handleMouseEnter(e, target) { clearTimeout(this.timer), this.currentTarget = target || e.currentTarget || e.target; const t = this.currentTarget.getAttribute(this.config.dataAttribute) || this.currentTarget.src; if (!t) return; this.preview.innerHTML = "", this.preview.classList.add("loading"), this.preview.style.display = "block", this.preview.classList.remove("active"); const n = new Image; n.onload = () => { this.preview.classList.remove("loading"), this.preview.innerHTML = `<img src="${escapeHtml(t)}" alt="预览图">`, this.imgElement = this.preview.querySelector("img"); const {width: a, height: i} = this.calculateImageSize(n); this.preview.style.width = `${a}px`, this.preview.style.height = `${i}px`, this.preview.offsetHeight, this.preview.classList.add("active"), this.handleMouseMove(e); }, n.onerror = () => { this.preview.classList.remove("loading"), this.preview.innerHTML = '<div style="padding:10px;color:#f00;">图片加载失败</div>'; }, n.src = t; } calculateImageSize(e) { let t = e.naturalWidth, n = e.naturalHeight; if (t > this.config.maxWidth || n > this.config.maxHeight) { const e = Math.min(this.config.maxWidth / t, this.config.maxHeight / n); t *= e, n *= e; } return { width: t, height: n }; } handleMouseMove(e) { if (!this.currentTarget || !this.preview.classList.contains("active")) return; let {offsetX: t, offsetY: n} = this.config, a = e.clientX + t, i = e.clientY + n; if (this.config.autoAdjustPosition) { const s = this.preview.offsetWidth, o = this.preview.offsetHeight; a + s > window.innerWidth && (a = e.clientX - s - t), i + o > window.innerHeight && (i = e.clientY - o - n), a = Math.max(0, a), i = Math.max(0, i); } this.preview.style.left = `${a}px`, this.preview.style.top = `${i}px`; } handleMouseLeave() { this.preview.classList.remove("active"), this.preview.style.display = "none", this.currentTarget = null, this.imgElement = null, this._lastSrc = null; } /** 保存当前悬浮预览图片 */ saveCurrentImage() { if (!this.currentTarget) return; const src = this.currentTarget.getAttribute(this.config.dataAttribute) || this.currentTarget.src; if (!src) return; // 用当前番号命名(大写), 若取不到则用 URL 尾段 let carNum = ""; try { if (window.isDetailPage) { // JavDB 详情页: data-clipboard-text 即番号 const $clip = $("a[data-clipboard-text]").first(); if ($clip.length) carNum = ($clip.attr("data-clipboard-text") || "").trim(); // JavBus 详情页: 番号是 URL 最后一段 if (!carNum) { const seg = location.href.split("?")[0].split("#")[0].split("/").filter(Boolean).pop() || ""; if (/^[A-Za-z0-9]+-?\d+/.test(seg) || /^[A-Za-z]{2,}/.test(seg)) carNum = seg; } // JavBus 识别码 label 兜底 if (!carNum) { const $id = $("span.header:contains('識別碼'), span.header:contains('识别码')").first().parent(); if ($id.length) carNum = $id.text().replace(/.*[::]/, "").trim(); } } else { // 列表页: 从 .item / .movie-box 提取 const $item = $(this.currentTarget).closest(".item, .movie-box"); if ($item.length) { const $title = $item.find(".video-title strong, date"); if ($title.length) carNum = $title.first().text().trim(); if (!carNum) { const m = $item.text().match(/[A-Za-z]{2,6}-?\d{2,6}/); if (m) carNum = m[0]; } } } } catch (e) {} carNum = carNum ? carNum.toUpperCase().replace(/[\\/:*?"<>|]/g, "_") : ""; // 使用 GM_xmlhttpRequest 获取 blob 并触发下载 if (typeof GM_xmlhttpRequest !== "undefined") { GM_xmlhttpRequest({ method: "GET", url: src, responseType: "arraybuffer", headers: { Referer: window.location.href }, onload: (res) => { if (!res.response || res.response.byteLength < 100) { show.error && show.error("图片下载失败: 响应为空或被拒绝"); return; } const arr = new Uint8Array(res.response); // 根据文件头魔数判断真实格式 let ext = "jpg", mime = "image/jpeg"; if (arr[0] === 0x89 && arr[1] === 0x50) { ext = "png"; mime = "image/png"; } else if (arr[0] === 0x47 && arr[1] === 0x49) { ext = "gif"; mime = "image/gif"; } else if (arr[0] === 0x52 && arr[1] === 0x49 && arr[8] === 0x57 && arr[9] === 0x45) { ext = "webp"; mime = "image/webp"; } else if (arr[0] === 0x3C || arr[0] === 0x0A) { // 很可能是 HTML 错误页 show.error && show.error("图片下载失败: 服务器返回了非图片内容"); return; } const name = (carNum || src.split("/").pop().split("?")[0].replace(/\.[^.]+$/, "") || "image").toUpperCase() + "." + ext; const blob = new Blob([res.response], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = name; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1e3); show.ok && show.ok("图片已保存: " + name); }, onerror: () => { show.error && show.error("图片下载失败: 网络错误"); } }); } else { this._fallbackSave(src, (carNum || "image") + ".jpg"); } } _fallbackSave(src, name) { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; canvas.getContext("2d").drawImage(img, 0, 0); canvas.toBlob((blob) => { if (!blob) return; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = name; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1e3); }); }; img.onerror = () => show.error && show.error("图片下载失败"); img.src = src; } destroy() { document.removeEventListener("mouseover", this._onOver, !0); document.removeEventListener("mouseout", this._onOut, !0); document.removeEventListener("mousemove", this._onMove, !0); this._delegated = !1; this.preview && this.preview.parentNode && this.preview.parentNode.removeChild(this.preview); } }, async function() { document.head.insertAdjacentHTML("beforeend", "\n <style>\n .console-logger-container {\n position: fixed;\n bottom: 0;\n right: 0;\n z-index: 99999999;\n font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n display: flex;\n flex-direction: column; \n align-items: flex-end;\n width: fit-content;\n }\n\n .console-logger-toggle {\n width: 40px;\n height: 30px;\n background: #2c3e50;\n border-radius: 120px 10px 0 0;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);\n transition: all 0.3s ease;\n color: white;\n font-size: 16px;\n }\n\n .console-logger-toggle:hover {\n background: #34495e;\n }\n\n .console-logger-toggle::after {\n content: '▼';\n transition: transform 0.3s ease;\n }\n\n .console-logger-toggle.collapsed::after {\n content: '▲';\n }\n\n .console-logger-window {\n width: 400px;\n height: 400px;\n background: white;\n border-radius: 10px 0 10px 10px;\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n transform: translateY(0);\n opacity: 1;\n /* 简化过渡属性 */\n transition: width 0.3s ease, height 0.3s ease, opacity 0.3s ease, transform 0.3s ease;\n }\n\n .console-logger-window.maximized {\n width: 600px !important;\n height: 85vh !important;\n border-radius: 10px 0 0 10px; /* 调整圆角以匹配右下角 */\n }\n\n .console-logger-window.collapsed {\n height: 0 !important;\n min-height: 0 !important; \n opacity: 0;\n }\n\n .console-logger-header {\n background: #2c3e50;\n color: white;\n padding: 12px 15px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex-shrink: 0;\n }\n\n .console-logger-title {\n font-weight: 600;\n font-size: 16px;\n }\n\n .console-logger-controls {\n display: flex;\n gap: 10px;\n }\n\n .console-logger-controls button {\n background: transparent;\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 5px 10px;\n font-size: 12px;\n color: white;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.3s;\n }\n\n .console-logger-controls button:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n\n /* 新增的按钮样式 */\n .console-logger-maximize-toggle {\n line-height: 1;\n font-size: 14px !important; /* 使箭头看起来更大 */\n padding: 5px 8px !important;\n }\n .console-logger-maximize-toggle::before {\n content: '⇱'; /* Unicode symbol for maximized */\n }\n .console-logger-maximize-toggle.active::before {\n content: '⇲'; /* Unicode symbol for minimized */\n }\n\n\n .console-logger-filters {\n display: flex;\n align-items: center;\n gap: 5px;\n padding: 10px;\n background: #f8f9fa;\n border-bottom: 1px solid #e9ecef;\n flex-shrink: 0;\n overflow-x: hidden; \n }\n\n /* 新增: 过滤器按钮组的容器,负责滚动 */\n .console-logger-filter-group {\n display: flex;\n gap: 5px;\n overflow-x: auto; /* 允许过滤器按钮滚动 */\n flex-grow: 1; /* 占据剩余空间 */\n padding-right: 10px; /* 避免滚动条影响按钮 */\n }\n\n .console-logger-filter {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 15px;\n background: #ecf0f1;\n color: #7f8c8d;\n border: 1px solid #ddd;\n cursor: pointer;\n transition: all 0.3s;\n white-space: nowrap;\n flex-shrink: 0; /* 确保不被压缩 */\n }\n\n .console-logger-filter.active {\n background: #3498db;\n color: white;\n border-color: #3498db;\n }\n\n /* 新增: 滚动到底部按钮的样式 (位于 filtersContainer 内部右侧) */\n .console-logger-scroll-to-bottom {\n background: #3498db;\n border: none;\n padding: 5px 10px;\n font-size: 12px;\n color: white;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.3s;\n line-height: 1;\n height: fit-content;\n white-space: nowrap;\n margin-left: auto; /* 将按钮推到最右侧 */\n flex-shrink: 0; /* 确保不被压缩 */\n }\n\n .console-logger-scroll-to-bottom:hover {\n background: #2980b9;\n }\n\n\n .console-logger-content {\n flex: 1;\n overflow-y: auto;\n padding: 10px;\n background: #ffffff;\n word-wrap: break-word;\n text-align: left;\n }\n\n .console-logger-entry {\n padding: 8px 10px;\n margin-bottom: 3px;\n border-radius: 4px;\n font-size: 12px;\n line-height: 1.4;\n /*animation: consoleFadeIn 0.3s ease;*/\n border-left: 3px solid transparent;\n }\n\n @keyframes consoleFadeIn {\n from { opacity: 0; transform: translateY(5px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .console-logger-timestamp {\n color: #7f8c8d;\n font-size: 11px;\n margin-right: 2px;\n }\n\n @media (max-width: 768px) {\n .console-logger-container {\n right: 10px;\n bottom: 10px;\n }\n\n .console-logger-window {\n width: calc(100vw - 20px);\n height: 300px;\n }\n }\n \n .console-logger-message[data-type=\"json\"] {\n white-space: pre-wrap; \n }\n </style>\n "); const e = { base: { label: "信息", background: "#e8f4fd", borderLeftColor: "#3498db" }, warn: { label: "警告", background: "#fef9e7", borderLeftColor: "#f39c12" }, error: { label: "错误", background: "#fdedec", borderLeftColor: "#e74c3c" }, debug: { label: "调试", background: "#f4f6f6", borderLeftColor: "#95a5a6" } }, t = { base: [ "base", "warn", "error" ], warn: [ "warn" ], error: [ "error" ], debug: [ "base", "warn", "error", "debug" ] }, n = await storageManager.getSetting("clogMsgCount", 2e3), a = "jhs_clog_maximize", i = "jhs_clog_expand", s = "jhs_clog_filter"; class o { constructor() { const t = localStorage.getItem(s); this.currentFilter = t && e[t] ? t : "base", this.logs = [], this.isInitialized = !1, this.userScrolledUp = !1; } tryInitialize() { return "loading" !== document.readyState && (this.isInitialized || (this.init(), this.isInitialized = !0), !0); } init() { this.createContainer(), this.bindEvents(), this.checkInitialMaximizeState(), this.checkInitialCollapseState(); } createContainer() { this.container = document.createElement("div"), this.container.className = "console-logger-container", this.container.style.display = "none", this.toggleBtn = document.createElement("div"), this.toggleBtn.className = "console-logger-toggle collapsed", this.container.appendChild(this.toggleBtn), this.window = document.createElement("div"), this.window.className = "console-logger-window collapsed"; const t = document.createElement("div"); t.className = "console-logger-header"; const n = document.createElement("div"); n.className = "console-logger-title", n.textContent = "JHS V3.6.0"; const a = document.createElement("div"); a.className = "console-logger-controls", this.maximizeBtn = document.createElement("button"), this.maximizeBtn.textContent = "", this.maximizeBtn.classList.add("console-logger-maximize-toggle"), a.appendChild(this.maximizeBtn); const i = document.createElement("button"); i.textContent = "清空", i.addEventListener("click", (() => this.clear())), a.appendChild(i), t.appendChild(n), t.appendChild(a), this.filtersContainer = document.createElement("div"), this.filtersContainer.className = "console-logger-filters", this.filterButtonGroup = document.createElement("div"), this.filterButtonGroup.className = "console-logger-filter-group", this.filtersContainer.appendChild(this.filterButtonGroup), this.scrollToBottomBtn = document.createElement("button"), this.scrollToBottomBtn.className = "console-logger-scroll-to-bottom", this.scrollToBottomBtn.textContent = "到底部", this.filtersContainer.appendChild(this.scrollToBottomBtn), this.content = document.createElement("div"), this.content.className = "console-logger-content jhs-scrollbar", this.window.appendChild(t), this.window.appendChild(this.filtersContainer), this.window.appendChild(this.content), this.container.appendChild(this.window), document.body.appendChild(this.container), Object.keys(e).forEach((t => { const n = document.createElement("div"); n.className = "console-logger-filter", t === this.currentFilter && n.classList.add("active"), n.textContent = e[t].label, n.dataset.type = t, n.addEventListener("click", (() => this.setFilter(t))), this.filterButtonGroup.appendChild(n); })); } bindEvents() { this.toggleBtn.addEventListener("click", (() => { this.toggleExpandCollapsed(); })), this.maximizeBtn.addEventListener("click", (() => this.toggleMaximize())), this.scrollToBottomBtn.addEventListener("click", (() => { this.content.scrollTop = this.content.scrollHeight, this.userScrolledUp = !1; })), this.content.addEventListener("scroll", (() => { const e = this.content.scrollHeight - this.content.clientHeight <= this.content.scrollTop + 5; this.userScrolledUp = !e; })), this.content.addEventListener("wheel", (e => { const t = 0 === this.content.scrollTop, n = this.content.scrollHeight - this.content.clientHeight <= this.content.scrollTop + 1; (t && e.deltaY < 0 || n && e.deltaY > 0) && (e.preventDefault(), e.stopPropagation()); }), { passive: !1 }); } toggleExpandCollapsed() { const e = this.window.classList.toggle("collapsed"); this.toggleBtn.classList.toggle("collapsed"), e ? localStorage.setItem(i, "no") : (localStorage.setItem(i, "yes"), this.reRenderAllLogs()); } checkInitialCollapseState() { const e = localStorage.getItem(i); e && "no" !== e ? (this.window.classList.toggle("collapsed"), this.toggleBtn.classList.toggle("collapsed"), setTimeout((() => { this.content.scrollTop = this.content.scrollHeight; }), 0)) : (this.window.classList.add("collapsed"), this.toggleBtn.classList.add("collapsed")); } checkInitialMaximizeState() { "maximized" === localStorage.getItem(a) && (this.window.classList.add("maximized"), this.maximizeBtn.classList.add("active")); } toggleMaximize() { const e = this.window.classList.toggle("maximized"); this.maximizeBtn.classList.toggle("active", e), e ? localStorage.setItem(a, "maximized") : localStorage.setItem(a, "minimized"), this.window.classList.contains("collapsed") || (this.content.scrollTop = this.content.scrollHeight); } addLog(t, a = "base", ...i) { const s = this.tryInitialize(); let o, r = []; e[a] ? (o = a, r = i) : (o = "base", r = [ a, ...i ]), o = e[o] ? o : "base"; const l = [ t, ...r ]; let c = "msg"; const d = []; l.forEach((e => { if ("[object Error]" === Object.prototype.toString.call(e)) d.push(String(e)); else if ("object" == typeof e && null !== e) try { d.push("<br/>" + JSON.stringify(e, null, 2)), c = "json"; } catch (t) { d.push(String(e)), c = "msg"; } else d.push(String(e)); })); let h = d.join(" "); h = h.replace(/(?:(?:https?|ftp):\/\/|www\.|(?:\/\/))[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|]/gi, (e => { const t = e.startsWith("http") || e.startsWith("ftp"), n = e.startsWith("//"), a = e.startsWith("www."); let i = e; return n ? i = `http:${e}` : !t && a && (i = `http://${e}`), `<a href="${escapeHtml(i)}" target="_blank">${escapeHtml(e)}</a>`; })); const g = { message: h, messageType: c, type: o, timestamp: new Date, id: Date.now() + Math.random() }; if (this.logs.push(g), this.logs.length > n) { const e = this.logs[0]; if (s) { const t = this.content.querySelector(`.console-logger-entry[data-id="${e.id}"]`); t && (this.logs.shift(), this.content.removeChild(t)); } } s && this.renderLog(g); } log(...e) { const [t, ...n] = e; setTimeout((() => { this.addLog(t, "base", ...n); }), 0); } error(...e) { const [t, ...n] = e; console.error(...e), setTimeout((() => { this.addLog(t, "error", ...n); }), 0); } warn(...e) { const [t, ...n] = e; setTimeout((() => { this.addLog(t, "warn", ...n); }), 0); } debug(...e) { const [t, ...n] = e; setTimeout((() => { this.addLog(t, "debug", ...n); }), 0); } renderLog(e) { if ("none" === this.container.style.display) return; if (this.window.classList.contains("collapsed")) return; if (!(t[this.currentFilter] || []).includes(e.type)) return; const n = this._createLogElement(e); this.content.appendChild(n), this.window.classList.contains("collapsed") || this.userScrolledUp || (this.content.scrollTop = this.content.scrollHeight); } reRenderAllLogs() { "none" !== this.container.style.display && (this.window.classList.contains("collapsed") || setTimeout((() => { if (this.content.innerHTML = "", 0 === this.logs.length) return; const e = t[this.currentFilter] || [], n = document.createDocumentFragment(); this.logs.forEach((t => { if (e.includes(t.type)) { const e = this._createLogElement(t); n.appendChild(e); } })), this.content.appendChild(n), this.content.scrollTop = this.content.scrollHeight; }), 0)); } _createLogElement(t) { const n = document.createElement("div"); n.className = "console-logger-entry", n.dataset.type = t.type, n.dataset.id = t.id; const a = e[t.type] || e.base; n.style.borderLeft = "3px solid " + a.borderLeftColor, n.style.background = a.background; const i = (t.timestamp instanceof Date ? t.timestamp : new Date(t.timestamp)).toTimeString().split(" ")[0]; return n.innerHTML = `\n <span class="console-logger-timestamp">[${i}]</span>\n <span class="console-logger-message" data-type="${t.messageType}">${t.message}</span>\n `, n; } setFilter(e) { if (this.currentFilter === e) return; this.currentFilter = e, localStorage.setItem(s, e); this.filterButtonGroup.querySelectorAll(".console-logger-filter").forEach((t => { t.dataset.type === e ? t.classList.add("active") : t.classList.remove("active"); })), this.reRenderAllLogs(); } clear() { this.logs = [], this.content.innerHTML = ""; } show() { (this.isInitialized && this.container || this.tryInitialize() && this.container) && (this.container.style.display = "", this.reRenderAllLogs()); } hide() { this.isInitialized && this.container && (this.container.style.display = "none"); } lowZIndex() { this.isInitialized && this.container && (this.container.style.zIndex = "12345678"); } highZIndex() { this.isInitialized && this.container && (this.container.style.zIndex = "999999999"); } } try { unsafeWindow.parent.clog && "function" == typeof unsafeWindow.parent.clog.log ? window.clog = unsafeWindow.clog = unsafeWindow.parent.clog : window.clog = unsafeWindow.clog = new o; } catch (r) { console.error("创建日志控制台出现异常", r), window.clog = unsafeWindow.clog = new o; } !function() { const e = window.clog || console; window.addEventListener("error", (function(t) { const n = t.filename, a = t.message; n.includes("javdb") || n.includes("javbus") || e.error(`[全局 Error 异常捕获] ${a} 来源: ${n}`); })), window.addEventListener("unhandledrejection", (function(t) { const n = t.reason, a = (null == n ? void 0 : n.message) ?? ""; if (a.includes("play()")) return; if (a.includes("The element has no supported sources")) return show.error("播放失败, 请检查是否已对节点分流?"), void e.error("播放失败, 请检查是否已对节点分流?"); if (a.includes("<span>1005</span>") && a.includes("fc2ppvdb")) return; const i = `[全局 Promise 异常捕获] ${n.message || n}`; e.error(i, n), t.preventDefault(); })); }(), document.addEventListener("mousedown", (e => { const t = window.clog; if (!t.isInitialized || !t.container) return; const n = e.target, a = [ ".console-logger-container", ".layui-layer-shade", ".loading-container" ].join(","); n.closest(a) ? t.highZIndex() : t.lowZIndex(); })); }(), function() { function e(e, t, n) { const a = function(e) { const t = document.createElement("div"); t.classList.add("js-tooltip"); const n = document.createElement("div"); return n.innerHTML = escapeHtml(e), t.appendChild(n), document.body.appendChild(t), t; }(t); a.style.display = "block"; const i = e.getBoundingClientRect(), s = a.getBoundingClientRect(); a.style.display = "none"; const o = window.innerWidth, r = window.innerHeight; let l, c, d = n; const h = e => e >= 8 && e + s.height <= r - 8, g = e => e >= 8 && e + s.width <= o - 8, p = i.left + i.width / 2 - s.width / 2, m = i.top + i.height / 2 - s.height / 2; switch (n) { case "top": c = i.top - s.height - 0, c < 8 && h(i.bottom + 0) && (c = i.bottom + 0, d = "bottom"); break; case "bottom": c = i.bottom + 0, c + s.height > r - 8 && h(i.top - s.height - 0) && (c = i.top - s.height - 0, d = "top"); break; case "left": l = i.left - s.width - 0, l < 8 && g(i.right + 0) && (l = i.right + 0, d = "right"); break; case "right": l = i.right + 0, l + s.width > o - 8 && g(i.left - s.width - 0) && (l = i.left - s.width - 0, d = "left"); } const u = "left" === d || "right" === d; "top" === d || "bottom" === d ? (l = p, l < 8 ? l = 8 : l + s.width > o - 8 && (l = o - s.width - 8)) : u && (c = m, c < 8 ? c = 8 : c + s.height > r - 8 && (c = r - s.height - 8)), a.style.left = `${l}px`, a.style.top = `${c}px`, a.classList.add("is-active"), e.tooltipElement = a; } document.head.insertAdjacentHTML("beforeend", "\n <style>\n .js-tooltip {\n /* 通用样式 */\n position: fixed;\n padding: 8px 12px; \n border-radius: 6px; \n white-space: normal;\n max-width: 600px; \n \n pointer-events: none;\n font-size: 14px;\n line-height: 1.5;\n z-index: 9999999999;\n \n background: #F0FDF4; \n color: #166534; \n border: none; \n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); \n \n display: none; \n }\n .js-tooltip.is-active {\n display: block !important;\n }\n\n </style>\n "); const t = "[data-tip-top], [data-tip-bottom], [data-tip-left], [data-tip-right], [data-tip]"; document.addEventListener("mouseover", (n => { const a = n.target.closest(t); if (a && !a.tooltipElement) { let t, n = "top"; if (a.hasAttribute("data-tip-bottom") ? (t = a.getAttribute("data-tip-bottom"), n = "bottom") : a.hasAttribute("data-tip-left") ? (t = a.getAttribute("data-tip-left"), n = "left") : a.hasAttribute("data-tip-right") ? (t = a.getAttribute("data-tip-right"), n = "right") : a.hasAttribute("data-tip-top") ? (t = a.getAttribute("data-tip-top"), n = "top") : a.hasAttribute("data-tip") && (t = a.getAttribute("data-tip"), n = "top"), !t) return; a.hoverTimeout = setTimeout((() => { a.matches(":hover") && !a.tooltipElement && e(a, t, n); }), 50); } })), document.addEventListener("mouseout", (e => { const n = e.target.closest(t); var a; n && (n.hoverTimeout && (clearTimeout(n.hoverTimeout), n.hoverTimeout = null), n.contains(e.relatedTarget) || n.tooltipElement && ((a = n.tooltipElement) && a.parentNode && a.remove(), n.tooltipElement = null)); })); }(); class Y { constructor() { this.plugins = new Map; this._errorLog = []; this._lastTimings = []; } register(e) { if ("function" != typeof e) throw new Error("插件必须是一个类"); const t = new e; t.pluginManager = this; const n = t.getName(); if (this.plugins.has(n)) throw new Error(`插件"${n}"已注册`); this.plugins.set(n, t); } getBean(e) { return this.plugins.get(e); } _addError(e, t, n) { this._errorLog.push({ time: new Date().toISOString(), plugin: e, phase: t, message: n?.message || String(n), stack: n?.stack || "" }); this._errorLog.length > 200 && this._errorLog.shift(); } getErrorLog() { return [...this._errorLog]; } clearErrorLog() { this._errorLog = []; } getTimings() { return [...this._lastTimings]; } getPluginNames() { return Array.from(this.plugins.keys()); } async _getDisabledPlugins() { try { const e = await storageManager.getSetting("disabledPlugins", "[]"); return JSON.parse(e); } catch (e) { return []; } } async processCss() { const t = await this._getDisabledPlugins(); const m = utils.isMobileMode(); const e = (await Promise.allSettled(Array.from(this.plugins).map((async ([e, n]) => { try { if (t.includes(e)) return { name: e, status: "disabled" }; if (m && "function" == typeof n.shouldSkipOnMobile && n.shouldSkipOnMobile()) return { name: e, status: "skipped" }; if ("function" == typeof n.initCss) { const t = await n.initCss(); return t && utils.insertStyle(t), { name: e, status: "fulfilled" }; } return { name: e, status: "skipped" }; } catch (a) { return console.error(`插件 ${e} 加载 CSS 失败`, a), this._addError(e, "initCss", a), { name: e, status: "rejected", error: a }; } })))).filter((e => "rejected" === e.status)); e.length && console.error("以下插件的 CSS 加载失败:", e.map((e => e.reason.name))); } async processPlugins() { const t = await this._getDisabledPlugins(); const a = []; const m = utils.isMobileMode(); const e = (await Promise.allSettled(Array.from(this.plugins).map((async ([e, n]) => { if (t.includes(e)) return a.push({ name: e, elapsed: 0, status: "disabled" }), { name: e, status: "disabled" }; if (m && "function" == typeof n.shouldSkipOnMobile && n.shouldSkipOnMobile()) return a.push({ name: e, elapsed: 0, status: "skipped-mobile" }), { name: e, status: "skipped" }; const i = performance.now(); try { if ("function" == typeof n.handle) await n.handle(); const s = performance.now() - i; return a.push({ name: e, elapsed: s, status: "ok" }), { name: e, status: "fulfilled" }; } catch (s) { const o = performance.now() - i; return clog.error(`插件 ${e} 执行失败`, s), this._addError(e, "handle", s), a.push({ name: e, elapsed: o, status: "error", error: s.message }), { name: e, status: "rejected", error: s }; } })))).filter((e => "rejected" === e.status)); this._lastTimings = a; e.length && console.error("以下插件执行失败:", e.map((e => e.reason.name))); } } /** 快捷键管理器: 纯静态类, 全局 keydown/keyup 分发 */ const _HotkeyManager = class _HotkeyManager { constructor() { if (new.target === _HotkeyManager) throw new Error("HotkeyManager cannot be instantiated."); } 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 && 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())), ctrl = keyList.includes("ctrl"), shift = keyList.includes("shift"), alt = keyList.includes("alt"), key = keyList.find((k => "ctrl" !== k && "shift" !== k && "alt" !== k)); return (this.isMac ? event.metaKey : event.ctrlKey) === ctrl && event.shiftKey === shift && event.altKey === alt && event.key.toLowerCase() === key; } }; i(_HotkeyManager, "isMac", 0 === navigator.platform.indexOf("Mac")); i(_HotkeyManager, "registerHotKeyMap", new Map); i(_HotkeyManager, "handleKeydown", (event => { for (const [id, data] of _HotkeyManager.registerHotKeyMap) { let hotkeyString = data.hotkeyString, callback = data.callback; _HotkeyManager.judgeHotkey(hotkeyString, event) && callback(event); } })); i(_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 X { constructor() { i(this, "pluginManager", null), i(this, "settingSvg", '<svg t="1760926954860" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4947" width="200" height="200"><path d="M511.099222 365.825763c-80.7786 0-146.26579 65.482515-146.26579 146.259556 0 80.7786 65.48719 146.259556 146.26579 146.259556 80.777041 0 146.259556-65.480957 146.259556-146.259556C657.358779 431.308278 591.876263 365.825763 511.099222 365.825763L511.099222 365.825763zM511.099222 585.215097c-40.391637 0-73.136012-32.742816-73.136012-73.129778 0-40.391637 32.742816-73.129778 73.136012-73.129778 40.386962 0 73.129778 32.738141 73.129778 73.129778C584.229 552.472281 551.486184 585.215097 511.099222 585.215097L511.099222 585.215097zM511.099222 585.215097M900.893017 568.24369l-26.451395-15.268032c3.065451-27.021784 3.138697-54.472139 0.077922-81.822754l26.373473-15.225955c69.953678-40.391637 93.920921-129.844512 53.533959-199.799749-40.390079-69.95212-129.839837-93.925596-199.799749-53.533959l-26.373473 15.225955c-22.153219-16.330888-45.963059-29.99217-70.896534-40.843585l0-30.545416c0-80.777041-65.48719-146.259556-146.26579-146.259556-80.7786 0-146.259556 65.482515-146.259556 146.259556l0 30.515806c-12.377127 5.421811-24.587501 11.55583-36.562551 18.473743-11.97505 6.917913-23.396854 14.420242-34.277879 22.432179l-26.431136-15.258682c-69.958353-40.391637-159.406553-16.424395-199.79819 53.533959C27.378272 326.082437 51.343956 415.535311 121.299193 455.922273l26.449837 15.275825c-3.063892 27.020226-3.137139 54.465905-0.077922 81.822754l-26.373473 15.224397c-69.953678 40.391637-93.920921 129.841395-53.533959 199.799749 40.391637 69.95212 129.839837 93.920921 199.79819 53.533959l26.375032-15.224397c22.153219 16.32933 45.963059 29.984378 70.896534 40.843585l0 30.537624c0 80.7786 65.48719 146.26579 146.26579 146.26579 80.777041 0 146.259556-65.48719 146.259556-146.26579l0-30.515806c12.377127-5.415577 24.587501-11.55583 36.567226-18.467509 11.97505-6.917913 23.398412-14.420242 34.277879-22.432179l26.423343 15.258682c69.959912 40.391637 159.408111 16.418162 199.799749-53.533959C994.813938 698.085085 970.848254 608.635327 900.893017 568.24369L900.893017 568.24369zM891.096666 731.474653c-20.198936 34.982294-64.923035 46.962019-99.900654 26.770875l-63.331869-36.567226 0 0 0 0-7.988562-4.611422c-18.134004 18.450366-39.024886 34.787489-62.516805 48.353705-23.49971 13.559983-48.091888 23.482568-73.129778 29.964118l0 9.222846 0 0 0 65.828489 0 7.301289c0 40.391637-32.742816 73.136012-73.136012 73.136012-40.386962 0-73.129778-32.742816-73.129778-73.136012l0-7.402588 0-65.72719 0 0 0-9.300768c-50.682014-13.090892-97.855981-39.682547-135.652816-78.232109l-7.983886 4.606747 0 0-63.331869 36.567226c-34.977618 20.191144-79.706394 8.206743-99.900654-26.770875-20.192702-34.977618-8.206743-79.701718 26.770875-99.899095l6.341291-3.657657 0 0 64.972905-37.516316c-14.487254-52.005129-13.929333-106.151555 0.073247-156.593569l-8.057133-4.650384 0 0-63.331869-36.567226c-34.982294-20.192702-46.963578-64.923035-26.770875-99.900654 20.192702-34.97606 64.923035-46.962019 99.900654-26.763083l6.324148 3.649866 0 0 64.996282 37.528784c18.132445-18.450366 39.024886-34.790606 62.516805-48.353705 23.493477-13.559983 48.085654-23.485685 73.129778-29.964118l0-9.229079L437.960093 153.739276l0-7.309082c0-40.385404 32.742816-73.129778 73.129778-73.129778 40.391637 0 73.129778 32.744375 73.129778 73.129778l0 7.404147 0 65.72719 0 9.307001c50.686689 13.086217 97.862215 39.684106 135.657491 78.232109l48.487732-27.997368 22.828023-13.176607c34.977618-20.192702 79.701718-8.212977 99.89442 26.763083 20.198936 34.982294 8.212977 79.706394-26.764641 99.900654l-30.822819 17.79738-32.50905 18.769847 0 0 0 0-7.983886 4.605189c14.488813 52.009805 13.929333 106.159347-0.077922 156.599803l64.979139 37.511641 0 0 6.414537 3.701294C899.303409 651.772936 911.289368 696.498594 891.096666 731.474653L891.096666 731.474653zM891.096666 731.474653M197.330785 324.240361c-1.932465 3.232203-3.824411 6.497135-5.649343 9.785442L197.330785 324.240361 197.330785 324.240361zM197.330785 324.240361M830.515443 690.133926l-5.655577 9.804144C826.793889 696.699632 828.685835 693.433143 830.515443 690.133926L830.515443 690.133926zM830.515443 690.133926M505.297151 146.430195l11.304921 0C512.835324 146.369416 509.067017 146.374091 505.297151 146.430195L505.297151 146.430195zM505.297151 146.430195M516.898176 877.740444l-11.31583 0C509.350653 877.796547 513.125193 877.796547 516.898176 877.740444L516.898176 877.740444zM516.898176 877.740444" fill="#272636" p-id="4948"></path></svg>'), i(this, "editSvg", '<svg t="1760920692801" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3545" width="200" height="200"><path d="M1013.929675 128.26571a143.759824 143.759824 0 0 1 10.44409 53.858738 84.576649 84.576649 0 0 1-5.836403 30.308339 92.870485 92.870485 0 0 1-18.635533 29.284408 1314.726599 1314.726599 0 0 1-24.983901 24.574329c-7.372299 7.06512-13.82306 13.311095-19.249891 18.737926-6.143582 6.143582-12.082378 11.672806-17.406817 16.382886L720.266444 82.598415c9.317766-8.601015 20.478607-18.942712 33.277737-31.02509s23.448006-21.604931 31.946628-28.67005a102.085858 102.085858 0 0 1 68.193763-22.731255c11.263234 0.307179 22.116896 2.047861 32.560985 5.222045 10.546483 3.071791 19.659463 6.655547 27.441334 10.546483 16.280493 8.601015 34.301667 23.550399 54.063524 45.052936 19.864249 21.502538 35.120812 43.82422 46.076867 67.272226z m-907.20231 570.943576l32.560986-33.38013c17.099637-17.509209 38.397389-39.216533 64.098041-64.917186l84.986221-85.395793 94.303987-94.815953 250.350976-251.477299L850.817567 389.163169 600.46659 640.640468l-93.177663 94.815953c-31.02509 30.410732-58.978389 58.364031-83.859898 83.655111-24.779115 25.29108-45.360116 46.17926-61.743001 62.562146a504.797674 504.797674 0 0 1-55.804206 50.274981c-10.239304 7.884264-20.581 14.130239-31.537055 18.737926a507.152714 507.152714 0 0 1-47.715156 19.86425 1609.311367 1609.311367 0 0 1-131.063087 42.185931c-20.478607 5.426831-35.837563 8.908194-45.974474 10.546483-20.88818 2.35504-34.813633-0.819144-41.981145-9.42016-6.860333-8.601015-8.805801-22.93604-5.73401-43.312254a396.261054 396.261054 0 0 1 11.058448-47.305584c5.836403-20.683394 12.082378-42.185931 18.635532-64.40522 6.553154-22.219289 13.003916-42.697897 19.249891-61.435822 6.143582-18.635533 11.263234-31.537055 15.15417-38.602176 4.607687-10.853662 9.829732-20.785787 15.666135-29.796373a192.49891 192.49891 0 0 1 25.086294-29.796374z" fill="#FF9500" p-id="3546"></path></svg>'), i(this, "deleteSvg", '<svg t="1760921450746" class="jhs-icon icon" viewBox="0 0 1194 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4530" width="200" height="200"><path d="M761.086847 36.028779s309.754321-147.538628 424.952209 231.50509c2.047962 6.570546 71.337359 253.862013-220.838618 415.139055-12.970429 7.167869-267.515096 145.746661-370.339877 341.327076 0 0-90.963666-205.649563-393.379455-351.566888-6.399883-3.071944-304.549083-156.583796-163.751664-487.2444 3.669266-8.533177 163.666333-336.20717 466.423449-99.411511l24.575549 27.391498L387.931021 324.279495l237.648977 159.570408-109.139333 145.746661L625.579998 849.069874l-30.719437-205.820227 166.226286-169.81022-216.486698-168.103585L761.086847 36.028779z" fill="#F4382E" p-id="4531"></path></svg>'), i(this, "checkSvg", '<svg t="1760921633527" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5603" width="200" height="200"><path d="M924.928 544A413.76 413.76 0 0 1 544 924.736v3.264h-64v-3.2A413.696 413.696 0 0 1 99.072 544H96v-64h3.072A413.696 413.696 0 0 1 480 99.2V96h64v3.2a413.76 413.76 0 0 1 380.928 380.8h3.072v64h-3.072z m-64-64A350.016 350.016 0 0 0 544 163.2V288h-64V163.2A350.016 350.016 0 0 0 163.072 480H288v64H163.072A350.016 350.016 0 0 0 480 860.8V736h64v124.8a350.016 350.016 0 0 0 316.928-316.8H736v-64h124.928zM512 544a32 32 0 1 1 32-32 32 32 0 0 1-32 32z" fill="#333333" p-id="5604"></path></svg>'), i(this, "actressSvg", '<svg t="1760926744637" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1948" width="200" height="200"><path d="M265.950168 668.467036V209.809493A209.809493 209.809493 0 0 1 475.759661 0h40.949536A209.809493 209.809493 0 0 1 726.564189 209.809493v440.435" p-id="1949"></path><path d="M916.558657 825.861124a193.463804 193.463804 0 0 0-137.442564-155.83573l-186.001889-45.795231-10.487631-124.293214H424.106373L412.231008 624.025416l-170.623063 44.44162a193.452429 193.452429 0 0 0-133.666108 154.698244L76.410695 1023.192384h871.189985z" fill="#FFE7D9" p-id="1950"></path><path d="M668.472724 265.682859c68.431223-29.187919 96.140409 100.349111 5.20969 151.774902z" fill="#FFCFB5" p-id="1951"></path><path d="M676.378259 334.421203c1.137487-99.814492-38.674561-172.158671-38.674561-172.15867l-59.740822 11.920865a493.805894 493.805894 0 0 1-80.761583 9.099896 493.669396 493.669396 0 0 1-80.761583-9.099896l-59.683948-11.88674s-39.812048 72.344179-38.776934 172.15867l-1.080613 92.05683c5.209691 56.271486 92.4777 121.381247 195.022161 119.163147 61.196805 0.034125 165.59537-51.573665 165.59537-119.197272z" fill="#FFE7D9" p-id="1952"></path><path d="M322.198905 274.703131c-68.419848-29.187919-96.140409 100.349111-5.209691 151.774902z" fill="#FFCFB5" p-id="1953"></path><path d="M297.390311 812.461526H742.034014a38.458438 38.458438 0 0 1 38.458438 38.458439V1020.325917H258.931873V850.90859a38.458438 38.458438 0 0 1 38.458438-38.447064z" fill="#FFD527" p-id="1954"></path><path d="M690.539973 92.284327c-20.645391 84.287793-275.613121 235.323328-424.589805 117.525166l104.955934-95.548915 139.399042-64.529643z" p-id="1955"></path><path d="M285.321573 383.708519h33.624119v177.118114h-33.624119zM675.855015 383.708519h33.624118v177.118114h-33.624118z" fill="#FFD527" p-id="1956"></path></svg>'), i(this, "newSvg", '<svg t="1760926857487" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3954" width="200" height="200"><path d="M508.330667 733.994667c-11.008-7.338667-13.44-17.109333-7.338667-29.333334 28.117333-37.888 41.557333-98.986667 40.341333-183.317333v-165.013333c0-14.656 7.338667-23.210667 21.994667-25.664 37.888-1.216 82.496-5.504 133.845333-12.842667 13.44-2.432 21.376 3.072 23.829334 16.512 1.216 12.224-4.266667 19.562667-16.512 21.994667a1787.093333 1787.093333 0 0 1-113.664 11.008c-6.101333 0-9.173333 3.669333-9.173334 10.986666v84.330667h135.68c12.224 1.237333 18.944 7.957333 20.16 20.181333-1.216 10.986667-7.936 17.109333-20.16 18.346667h-36.672v223.658667c-1.216 12.202667-7.936 18.944-20.16 20.16-11.008-1.216-17.109333-7.957333-18.346666-20.16V501.162667h-60.48v18.346666c1.216 92.885333-13.44 161.92-44.010667 207.146667-6.101333 12.224-15.893333 14.677333-29.333333 7.338667z m-131.989334-282.325334c-1.237333 0-2.453333 0.618667-3.669333 1.834667h45.824a522.666667 522.666667 0 0 0 16.512-31.168c7.317333-12.224 12.224-20.778667 14.656-25.664 6.122667-11.008 15.274667-14.677333 27.52-11.008 9.770667 6.122667 12.202667 14.058667 7.317333 23.829333-4.906667 9.792-13.44 24.448-25.664 44.010667h49.493334c9.770667 1.216 15.274667 6.72 16.512 16.490667-1.237333 11.008-6.741333 17.109333-16.512 18.346666h-82.496a12.437333 12.437333 0 0 1 3.669333 9.173334v38.485333h69.653333c9.792 1.216 15.296 6.72 16.512 16.490667-1.216 11.008-6.72 17.130667-16.512 18.346666h-69.653333v108.16c0 34.218667-15.274667 51.946667-45.845333 53.162667h-16.490667a195.157333 195.157333 0 0 1-20.16 1.834667c-12.224 0-19.562667-6.72-22.016-20.16 1.237333-12.224 7.338667-18.944 18.346667-20.16 2.432 0 6.101333 0.597333 10.986666 1.834666h11.008c15.893333 0 23.829333-8.554667 23.829334-25.685333v-98.986667H314.026667c-11.008-1.216-17.109333-7.338667-18.346667-18.346666 1.237333-9.770667 7.338667-15.274667 18.346667-16.490667h75.157333V497.493333c0-3.669333 1.216-6.72 3.669333-9.173333h-89.813333c-11.029333-1.216-17.130667-7.317333-18.346667-18.325333 1.216-9.770667 7.317333-15.274667 18.346667-16.490667h56.810667c-3.669333-1.216-6.72-4.266667-9.173334-9.173333-1.216-1.216-3.050667-4.266667-5.482666-9.173334a758.336 758.336 0 0 0-14.677334-23.829333c-4.885333-9.770667-3.050667-17.706667 5.504-23.829333 11.008-3.669333 19.562667-1.216 25.664 7.338666 2.453333 2.432 6.122667 7.338667 11.008 14.656 6.101333 8.554667 9.770667 14.08 10.986667 16.512 4.906667 9.770667 2.453333 18.346667-7.317333 25.664z m-60.501333-71.509333c-9.792-1.216-15.274667-7.317333-16.512-18.346667 1.237333-9.749333 6.72-15.253333 16.512-16.490666h75.157333c-3.669333-12.202667-7.338667-21.973333-10.986666-29.333334-1.237333-12.202667 3.648-19.541333 14.656-21.973333 12.224-2.453333 21.397333 1.216 27.52 10.986667 0 1.216 0.597333 3.669333 1.813333 7.338666 4.906667 15.872 9.173333 26.88 12.842667 32.981334h60.48c11.008 1.237333 17.130667 6.741333 18.346666 16.512-1.216 11.008-7.338667 17.109333-18.346666 18.346666h-181.482667z m-14.677333 311.68c-8.533333-6.122667-10.986667-14.08-7.338667-23.829333a1659.648 1659.648 0 0 0 33.002667-66.005334c4.906667-9.792 12.224-12.842667 22.016-9.173333 9.770667 4.906667 13.44 12.224 10.986666 21.994667-3.669333 6.122667-9.173333 17.728-16.490666 34.837333-8.554667 15.893333-14.677333 27.52-18.346667 34.837333-4.885333 8.554667-12.821333 11.008-23.829333 7.338667z m201.664-25.664c-9.770667 4.885333-18.346667 2.432-25.664-7.338667a1138.56 1138.56 0 0 1-27.498667-44.010666c-4.885333-8.533333-3.050667-16.490667 5.504-23.829334 9.770667-3.669333 18.346667-1.216 25.664 7.338667l14.677333 21.994667c6.101333 9.770667 10.389333 17.109333 12.821334 21.994666 4.906667 8.554667 3.050667 16.512-5.504 23.850667z" fill="#333333" p-id="3955"></path><path d="M675.328 117.717333A425.429333 425.429333 0 0 0 512 85.333333C276.352 85.333333 85.333333 276.352 85.333333 512s191.018667 426.666667 426.666667 426.666667 426.666667-191.018667 426.666667-426.666667c0-56.746667-11.093333-112-32.384-163.328a21.333333 21.333333 0 0 0-39.402667 16.341333A382.762667 382.762667 0 0 1 896 512c0 212.074667-171.925333 384-384 384S128 724.074667 128 512 299.925333 128 512 128c51.114667 0 100.8 9.984 146.986667 29.12a21.333333 21.333333 0 0 0 16.341333-39.402667z" fill="#333333" p-id="3956"></path></svg>'), i(this, "refreshSvg", '<svg t="1760926993643" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5942" width="200" height="200"><path d="M511.966722 0a511.966722 511.966722 0 1 0 179.828311 32.445891l-22.46254 59.964102A447.970882 447.970882 0 1 1 511.966722 63.99584a31.99792 31.99792 0 0 0 0-63.99584z" fill="#333333" p-id="5943"></path><path d="M649.2378 9.151405A30.909991 30.909991 0 0 1 671.316364 0h193.267438a31.99792 31.99792 0 0 1 31.357962 31.99792c0 17.662852-13.759106 31.99792-31.357962 31.99792H703.954243v160.629559a31.99792 31.99792 0 0 1-31.99792 31.357962 31.485953 31.485953 0 0 1-31.99792-31.357962V31.357962c0-8.511447 3.647763-16.318939 9.343392-21.950573z" fill="#333333" p-id="5944"></path></svg>'), i(this, "blacklistSvg", '<svg t="1761386375897" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1936" width="200" height="200"><path d="M513.199827 65.667605c-246.537999 0-446.399933 199.861934-446.399933 446.399933 0 246.553349 199.861934 446.399933 446.399933 446.399933 246.553349 0 446.399933-199.846584 446.399933-446.399933C959.599759 265.529539 759.753175 65.667605 513.199827 65.667605zM513.199827 894.697075c-211.320916 0-382.629537-171.322947-382.629537-382.628514 0-94.183056 34.029024-180.417069 90.461291-247.080352l165.389818 165.389818c4.320399 39.651069 26.816762 73.840752 58.981323 94.068446-72.189136 27.369348-123.517151 97.156784-123.517151 178.936345l337.541643 0 100.846826 100.846826C693.608709 860.664981 607.375719 894.697075 513.199827 894.697075zM805.362956 759.14175 697.264982 651.0448c-16.556071-58.332547-60.10082-105.306394-116.275213-126.601396 35.888372-22.570042 59.752896-62.511729 59.752896-108.032482 0-70.436212-57.108672-127.542838-127.542838-127.542838-48.218188 0-90.184999 26.765597-111.865787 66.245773L266.120498 219.900316c66.663282-56.432267 152.897296-90.461291 247.079328-90.461291 211.304544 0 382.628514 171.308621 382.628514 382.629537C895.82834 606.244454 861.796246 692.476421 805.362956 759.14175z" fill="#272636" p-id="1937"></path></svg>'), i(this, "copySvg", '<svg t="1749017229420" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9184" width="200" height="200"><path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667z m0 810.666666c-204.8 0-373.333333-168.533333-373.333333-373.333333S307.2 138.666667 512 138.666667 885.333333 307.2 885.333333 512 716.8 885.333333 512 885.333333z" fill="#666666" p-id="9185"></path><path d="M512 512m-42.666667 0a42.666667 42.666667 0 1 0 85.333334 0 42.666667 42.666667 0 1 0-85.333334 0Z" fill="#666666" p-id="9186"></path><path d="M341.333333 512m-42.666666 0a42.666667 42.666667 0 1 0 85.333333 0 42.666667 42.666667 0 1 0-85.333333 0Z" fill="#666666" p-id="9187"></path><path d="M682.666667 512m-42.666667 0a42.666667 42.666667 0 1 0 85.333333 0 42.666667 42.666667 0 1 0-85.333333 0Z" fill="#666666" p-id="9188"></path></svg>'), i(this, "titleSvg", '<svg t="1747553289744" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7507" width="200" height="200"><path d="M959.8 150.8c0-2.3-1.9-4.2-4.2-4.2H253.3c-2.3 0-4.2 1.9-4.2 4.2v115.9c0 2.3 1.9 4.2 4.2 4.2h702.3c2.3 0 4.2-1.9 4.2-4.2V150.8z" fill="" p-id="7508"></path><path d="M126.4 208.8m-62.2 0a62.2 62.2 0 1 0 124.4 0 62.2 62.2 0 1 0-124.4 0Z" fill="" p-id="7509"></path><path d="M851.5 453.7c0-2.1-1.8-3.9-3.9-3.9H252.9c-2.1 0-3.9 1.7-3.9 3.9v116.6c0 2.1 1.7 3.9 3.9 3.9h594.7c2.1 0 3.9-1.7 3.9-3.9V453.7z" fill="" p-id="7510"></path><path d="M126.4 512m-62.2 0a62.2 62.2 0 1 0 124.4 0 62.2 62.2 0 1 0-124.4 0Z" fill="" p-id="7511"></path><path d="M851.5 756.9c0-2.1-1.8-3.9-3.9-3.9H252.9c-2.1 0-3.9 1.8-3.9 3.9v116.6c0 2.1 1.7 3.9 3.9 3.9h594.7c2.1 0 3.9-1.7 3.9-3.9V756.9z" fill="" p-id="7512"></path><path d="M126.4 815.2m-62.2 0a62.2 62.2 0 1 0 124.4 0 62.2 62.2 0 1 0-124.4 0Z" fill="" p-id="7513"></path></svg>'), i(this, "carNumSvg", '<svg t="1747552574854" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3539" width="200" height="200"><path d="M920.337035 447.804932c-6.067182-6.067182-10.918677-11.643178-16.985859-17.71036l48.536436-30.334889-42.469254-109.207238-121.340579 12.134365c-6.067182-6.067182-6.067182-12.134365-12.134365-18.201547-12.134365-12.134365-18.201547-24.267706-24.267706-30.334889-24.26873-36.402071-30.334889-42.469254-54.603619-42.469254H339.116511c-18.201547 0-24.267706 6.067182-54.603619 42.469254-6.067182 6.067182-12.134365 18.201547-24.267706 30.334889 0 0-6.067182 6.067182-12.134365 18.201547l-115.27442-12.134365-48.536436 109.207238 51.090608 24.378223c-6.067182 6.067182-30.334889 34.660404-30.334889 34.660405l-15.542998 22.280446-12.282744 17.018605c-6.067182 12.134365-5.064342 10.868535-5.064342 29.070082v224.480635c0 36.402071 18.201547 60.670801 54.603618 60.670801h115.273397c36.402071 0 54.603619-24.267706 54.603619-54.603619v-18.201547h424.693562v18.201547c0 30.334889 18.201547 54.603619 54.603618 54.603619h115.273397c36.402071 0 60.670801-24.267706 60.670801-60.670801V539.300786c0-42.469254 0.685615-46.662763-11.44875-64.863287-4.731768-6.744611-11.94403-16.196891-20.101827-26.632567z m-35.186383-78.381161l-30.334889 18.201547-12.134365-12.134365c-6.067182-8.899694-12.134365-12.134365-12.134365-18.201547l42.469254-6.067183 12.134365 18.201548z m-533.899776-97.072873h339.755054l78.871325 103.140055H272.378527l78.872349-103.140055zM175.305655 357.290429h36.402071c-6.067182 6.067182-6.067182 12.134365-12.134365 18.201547l-18.201547 6.067183-18.201547-12.134365 12.135388-12.134365z m667.375743 394.35765h-54.603619V678.843936H242.043638v72.804143H132.837424V527.167444c0-12.134365-0.041956-20.662599 1.216711-23.556508 1.258667-2.89391 9.955746-16.924461 21.193695-29.173437l35.722596-38.276768h639.576607l21.917172 20.938891c6.067182 6.067182 21.847587 21.366633 25.712615 28.732392 7.621585 9.996678 6.973832 10.999518 13.041014 23.133883v242.682182h-48.536436zM242.043638 533.234627h133.474944v60.670801H242.043638v-60.670801z m412.559197 0h133.474944v60.670801H654.602835v-60.670801z" p-id="3540"></path></svg>'), i(this, "downSvg", '<svg t="1747552626242" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4551" width="200" height="200"><path d="M641.6 660l-8.64-64 32-4.32a211.2 211.2 0 0 0-26.72-420.32 215.36 215.36 0 0 0-213.12 192 94.56 94.56 0 0 0 0 11.52v41.28h-64V384v-7.04a153.12 153.12 0 0 1 0-19.52A279.84 279.84 0 0 1 636.16 108H640A275.2 275.2 0 0 1 673.28 656z" fill="#333333" p-id="4552"></path><path d="M490.4 446.24l-7.52-39.84a182.4 182.4 0 0 1 107.52-162.88l29.12-13.28L646.08 288l-29.12 13.28a117.92 117.92 0 0 0-70.08 101.28l6.24 30.4zM392.96 652.32h-78.72A202.24 202.24 0 0 1 256 256l30.72-9.12 18.24 61.28-30.72 9.12a138.24 138.24 0 0 0 39.68 270.72h78.72zM479.2 512h64v320h-64z" fill="#333333" p-id="4553"></path><path d="M510.4 908l-156.32-147.68 43.84-46.4 112.48 106.08 112.8-106.08 43.84 46.56-156.64 147.52z" fill="#333333" p-id="4554"></path></svg>'), i(this, "handleSvg", '<svg t="1749106236917" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2628" width="200" height="200"><path d="M838 989.48a32 32 0 0 1-22.5-9.22L519.3 687.6 207.48 980.8a32 32 0 0 1-54-23.32V136.52A98.54 98.54 0 0 1 252 38.1h519.6A98.52 98.52 0 0 1 870 136.52v820.96a32 32 0 0 1-32 32zM252 102.1a34.46 34.46 0 0 0-34.42 34.42v746.96L498 619.84a32 32 0 0 1 44.42 0.56L806 880.88V136.52a34.46 34.46 0 0 0-34.4-34.42z" p-id="2629"></path><path d="M648 604.92a28 28 0 0 1-16.46-5.34l-112.84-82-112.84 82a28 28 0 0 1-43.08-31.32l43.1-132.64-112.84-82a28 28 0 0 1 16.46-50.66h139.48L492 170.34a28 28 0 0 1 53.26 0l43.1 132.64h139.48a28 28 0 0 1 16.46 50.66l-112.84 82 43.1 132.64A28 28 0 0 1 648 604.92z m-129.3-150a27.86 27.86 0 0 1 16.46 5.36l59.58 43.28-22.76-70a28 28 0 0 1 10.02-31.28l59.58-43.3H568a28 28 0 0 1-26.64-19.34l-22.76-70-22.76 70a28 28 0 0 1-26.62 19.34h-73.64l59.58 43.3a28 28 0 0 1 10.16 31.3l-22.76 70 59.58-43.28a28 28 0 0 1 16.46-5.32z" p-id="2630"></path></svg>'), i(this, "siteSvg", '<svg t="1749107903569" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12439" width="200" height="200"><path d="M882.758621 133.674884C882.758621 59.84828 822.91034 0 749.083736 0 675.25715 0 615.40887 59.84828 615.40887 133.674884 615.40887 163.358402 625.152318 191.656395 642.813352 214.773283L670.872117 193.336726 648.314739 166.170836 253.911693 493.666092 276.469054 520.831982 302.371681 496.834595C277.256669 469.725608 241.995388 453.990153 204.295574 453.990153 130.46897 453.990153 70.62069 513.838433 70.62069 587.66502 70.62069 661.491624 130.46897 721.339904 204.295574 721.339904 255.555319 721.339904 301.619094 692.208675 324.036714 647.136344L276.646223 663.002394 706.082022 877.440106 721.856794 845.849335 690.37312 829.861888C680.932829 848.452414 675.940882 869.068818 675.940882 890.325116 675.940882 964.15172 735.789162 1024 809.615766 1024 883.442353 1024 943.290633 964.15172 943.290633 890.325116 943.290633 874.050807 940.36533 858.125365 934.723584 843.16446L868.645076 868.0826C871.294817 875.109252 872.669943 882.595452 872.669943 890.325116 872.669943 925.14899 844.439623 953.37931 809.615766 953.37931 774.791892 953.37931 746.561571 925.14899 746.561571 890.325116 746.561571 880.245089 748.902894 870.575616 753.340487 861.836782L769.436089 830.140063 737.631567 814.258564 308.195769 599.820853 276.554929 584.02108 260.805279 615.686903C250.212352 636.984797 228.494795 650.719214 204.295574 650.719214 169.4717 650.719214 141.241379 622.488894 141.241379 587.66502 141.241379 552.841163 169.4717 524.610842 204.295574 524.610842 222.12269 524.610842 238.680594 531.99985 250.566444 544.829369L273.29589 569.363385 299.026432 547.997855 693.429478 220.502616 719.514606 198.84265 698.930882 171.900169C690.596687 160.991373 686.029559 147.727007 686.029559 133.674884 686.029559 98.85101 714.25988 70.62069 749.083736 70.62069 783.90761 70.62069 812.137931 98.85101 812.137931 133.674884 812.137931 148.208022 807.249885 161.899255 798.379608 172.996785L853.543883 217.089695C872.331935 193.584128 882.758621 164.379366 882.758621 133.674884ZM749.083736 196.729062C729.149334 196.729062 710.818745 187.460449 698.930882 171.900169L642.813352 214.773283C667.922573 247.639305 706.904064 267.349751 749.083736 267.349751 790.225902 267.349751 828.357809 248.599782 853.543883 217.089695L798.379608 172.996785C786.455411 187.915034 768.530291 196.729062 749.083736 196.729062ZM337.970441 587.66502C337.970441 553.551854 325.093782 521.360666 302.371681 496.834595L250.566444 544.829369C261.309069 556.424898 267.349751 571.526356 267.349751 587.66502 267.349751 597.565263 265.091478 607.069184 260.805279 615.686903L324.036714 647.136344C333.156105 628.801148 337.970441 608.540036 337.970441 587.66502ZM809.615766 756.650249C758.753986 756.650249 712.986006 785.330865 690.37312 829.861888L753.340487 861.836782C764.027215 840.791658 785.603302 827.270938 809.615766 827.270938 836.08553 827.270938 859.461862 843.730308 868.645076 868.0826L934.723584 843.16446C915.252259 791.529949 865.714547 756.650249 809.615766 756.650249Z" fill="#389BFF" p-id="12440"></path></svg>'), i(this, "videoSvg", '<svg t="1749003664455" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1952" width="200" height="200"><path d="M825.6 153.6H198.4C124.5 153.6 64 214.1 64 288v448c0 73.9 60.5 134.4 134.4 134.4h627.2c73.9 0 134.4-60.5 134.4-134.4V288c0-73.9-60.5-134.4-134.4-134.4z m-138.2 44.8l112 112H706l-112-112h93.4z m-156.8 0l112 112H526.7l-112-112h115.9z m-179.2 0l112 112H347.5l-112-112h115.9zM108.8 288c0-41.4 28.4-76.1 66.7-86.3l108.7 108.7H108.8V288z m806.4 448c0 49.4-40.2 89.6-89.6 89.6H198.4c-49.4 0-89.6-40.2-89.6-89.6V355.2h806.4V736z m0-425.6h-52.5l-112-112h74.9c49.4 0 89.6 40.2 89.6 89.6v22.4z" p-id="1953"></path><path d="M454 687.2l149.3-77.6c27.5-13.8 27.5-53 0-66.8L468 472.2c-31.2-15.6-68 7.1-68 42v139.6c0 27.8 29.2 45.8 54 33.4zM444.8 512l134.4 67.2-134.4 67.2V512z" p-id="1954"></path></svg>'), i(this, "screenSvg", '<svg t="1750691468062" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2693" width="200" height="200"><path d="M288 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64v-576a64 64 0 0 0-64-64h-448m0-64h448a128 128 0 0 1 128 128v576a128 128 0 0 1-128 128h-448a128 128 0 0 1-128-128v-576a128 128 0 0 1 128-128z" fill="#4078FD" p-id="2694"></path><path d="M416 352m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#FE9C23" p-id="2695"></path><path d="M352 732.448a32 32 0 0 1-32-32v-160a32 32 0 0 1 44.224-29.568l130.112 53.632 153.952-169.984a32 32 0 0 1 55.712 21.472v284.448a32 32 0 0 1-32 32z m0-32h320z" fill="#4078FD" opacity=".2" p-id="2696"></path><path d="M672 416l-169.088 186.656-150.912-62.208v160h320V416m0-32a32 32 0 0 1 32 32v284.448a32 32 0 0 1-32 32h-320a32 32 0 0 1-32-32v-160a32 32 0 0 1 44.192-29.6l130.112 53.632 153.984-169.984a32 32 0 0 1 23.712-10.496z" fill="#4078FD" p-id="2697"></path></svg>'), i(this, "recoveryVideoSvg", '<svg t="1749003779161" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8204" width="200" height="200"><path d="M938.666667 553.92V768c0 64.8-52.533333 117.333333-117.333334 117.333333H202.666667c-64.8 0-117.333333-52.533333-117.333334-117.333333V256c0-64.8 52.533333-117.333333 117.333334-117.333333h618.666666c64.8 0 117.333333 52.533333 117.333334 117.333333v297.92z m-64-74.624V256a53.333333 53.333333 0 0 0-53.333334-53.333333H202.666667a53.333333 53.333333 0 0 0-53.333334 53.333333v344.48A290.090667 290.090667 0 0 1 192 597.333333a286.88 286.88 0 0 1 183.296 65.845334C427.029333 528.384 556.906667 437.333333 704 437.333333c65.706667 0 126.997333 16.778667 170.666667 41.962667z m0 82.24c-5.333333-8.32-21.130667-21.653333-43.648-32.917333C796.768 511.488 753.045333 501.333333 704 501.333333c-121.770667 0-229.130667 76.266667-270.432 188.693334-2.730667 7.445333-7.402667 20.32-13.994667 38.581333-7.68 21.301333-34.453333 28.106667-51.370666 13.056-16.437333-14.634667-28.554667-25.066667-36.138667-31.146667A222.890667 222.890667 0 0 0 192 661.333333c-14.464 0-28.725333 1.365333-42.666667 4.053334V768a53.333333 53.333333 0 0 0 53.333334 53.333333h618.666666a53.333333 53.333333 0 0 0 53.333334-53.333333V561.525333zM320 480a96 96 0 1 1 0-192 96 96 0 0 1 0 192z m0-64a32 32 0 1 0 0-64 32 32 0 0 0 0 64z" fill="#000000" p-id="8205"></path></svg>'); } getName() { throw new Error(`${this.constructor.name} 未显示getName()`); } getBean(e) { return this.pluginManager.getBean(e); } async initCss() { return ""; } async handle() {} shouldSkipOnMobile() { return false; } /** 提取 JavDB 详情页番号: 兼容 繁体(複製番號)/简体(复制番号)/英文 界面 */ findJavDbCarNum() { let e = $("a[data-clipboard-text]").filter((function() { const t = ($(this).attr("title") || "").toLowerCase(); return t.includes("番") || t.includes("copy") || t.includes("code"); })).first().attr("data-clipboard-text"); if (e && e.trim()) return e.trim(); e = $("a[data-clipboard-text]").first().attr("data-clipboard-text"); if (e && e.trim()) return e.trim(); e = $(".title.is-4 strong").not(".current-title").first().text().trim(); if (e) return e.split(/\s+/)[0]; const t = window.location.href.split("?")[0].split("#")[0].match(/\/v\/([^/]+)$/); return t ? "" : ""; } getPageInfo() { let e, t, n, a, i, s = window.location.href; return r && (e = this.findJavDbCarNum(), t = s.split("?")[0].split("#")[0], n = $(".female").prev().map(((e, t) => $(t).text())).get().join(" "), a = $(".male").prev().map(((e, t) => $(t).text())).get().join(" "), i = $('strong:contains("日期:")').parent(".panel-block").find(".value").text().trim()), l && (t = s.split("?")[0], e = t.split("/").filter(Boolean).pop().replace(/_\d{4}-\d{2}-\d{2}$/, ""), n = $('span[onmouseover*="star_"] a').map(((e, t) => $(t).text())).get().join(" "), a = "", i = ($('span.header:contains("發行日期:")').parent("p").text().trim() || $('span.header:contains("发行日期:")').parent("p").text().trim()).replace("發行日期:", "").replace("发行日期:", "").trim()), { carNum: e, url: t, actress: n, actors: a, publishTime: i }; } getActressId() { const e = o.match(/\/actors\/([^/?]+)/); return e && e.length > 1 ? e[1] : null; } getActressPageInfo() { let e = window.location.href; if (!e.includes("/actors/") && !e.includes("/star/")) throw new Error("接口调用错误, 非演员详情页"); let t = [], n = r ? $(".actor-section-name") : $(".avatar-box .photo-info .pb10"); n.length && n.text().trim().split(",").forEach((e => { t.push(e.trim()); })); let a = $(".section-meta:not(:contains('影片'))"); a.length && a.text().trim().split(",").forEach((e => { t.push(e.trim()); })); let i = $(".section-meta:contains('男優')").length > 0 ? B : P, s = D; t.some((e => e.includes("無碼"))) && (s = A), e.includes("uncensored") && (s = A); let o = null, c = null; const d = new URL(e); if (r) { c = d.pathname.split("/").filter((e => "" !== e.trim())).pop(); const e = d.searchParams; e.delete("sort_type"), e.delete("page"), o = d.toString(); } else if (l) { const t = "/star/", n = e.split(t); if (n.length < 2) throw new Error("提取演员url失败"); const a = n[0]; c = n[1].split("/")[0], o = a + t + c; } return { starId: c, name: t[0], allName: t, role: i, movieType: s, blacklistUrl: o }; } getSelector(e) { const t = e || (r ? T : l ? I : null), n = { javdb: { boxSelector: ".movie-list", itemSelector: ".movie-list .item", coverImgSelector: ".cover img", requestDomItemSelector: ".movie-list .item", nextPageSelector: ".pagination-next" }, javbus: { boxSelector: ".masonry", itemSelector: ".masonry .item", coverImgSelector: ".masonry .movie-box .photo-frame img", requestDomItemSelector: "#waterfall .item", nextPageSelector: "#next" } }; if (!t || !n[t]) throw new Error("类型错误: 无法确定选择器类型 (JavDb 或 JavBus)"); return n[t]; } parseMovieId(e) { return e.split("/").pop().split(/[?#]/)[0]; } getBoxCarInfo(e) { var t, n, a; const i = e.find("a"), s = i.attr("href"); let o = null, r = null, l = null; const c = e.find(".video-title"); if (c.length > 0) { const n = c.find("strong"); if (n.length > 0 && (o = n.text().trim()), r = null == (t = i.attr("title")) ? void 0 : t.trim(), !r) { const e = c.text().trim(); r = o && e.includes(o) ? e.replace(o, "").trim() : e; } l = e.find(".meta").text().trim(); } if (!o) { const t = e.find("img"); t.length > 0 && (r = (null == (n = t.attr("title")) ? void 0 : n.trim()) || (null == (a = t.attr("data-title")) ? void 0 : a.trim())); const i = e.find("date").map(((e, t) => $(t).text().trim())).get(), s = e => /^\d{4}-\d{1,2}-\d{1,2}$/.test(e); l = i.find(s) || null, o = i.find((e => !s(e))) || null; } if (!o) { const t = "提取番号信息失败: carNum 为空"; throw console.error("Error in getBoxCarInfo:", t, "Box Element:", e.get(0)), show.error(t), new Error(t); } return { carNum: o, url: s || "", title: r || "", publishTime: l || "" }; } getBoxCarInfoList(e = null) { if (e || (e = $(this.getSelector().itemSelector)), 0 === e.length) return clog.error("获取当前列表页所有item的番号信息失败!"), []; const t = []; return e.each(((e, n) => { const a = $(n); try { const e = this.getBoxCarInfo(a); t.push(e); } catch (i) { clog.error("[getBoxCarInfoList] 提取单个 boxCar 信息失败:", i.message, "元素索引:", e); } })), t; } checkDuplicateCarNumbers(e, t) { if (!e || 0 === e.length || !t || 0 === t.length) return !1; const n = new Set(e.map((e => e.carNum)).filter((e => e))); if (0 === n.size) return !1; let a = 0; for (let i = 0; i < t.length; i++) { const e = t[i] ? t[i].carNum : null; if (e && n.has(e)) { if (a++, a >= 2) return clog.warn("警告: 检测到连续番号信息重复, 该类别可能已被限制页码。"), !0; } else a = 0; } return !1; } } class Q extends X { getName() { return "DetailPagePlugin"; } constructor() { super(); } handle() { if (!window.isDetailPage) return; $(".video-meta-panel a").each((function() { const e = $(this).attr("href"); e && (e.startsWith("http://") || e.startsWith("https://") || e.startsWith("/")) && $(this).attr("target", "_blank"); })); this.addCopyCarNumBtn(); } /** JavDB 详情页: 番号后追加"复制番号"按钮 (样式移植自老司机脚本) */ addCopyCarNumBtn() { if ($(".jav-avid-copy").length) return; const anchor = $("a[data-clipboard-text]").filter((function() { const t = ($(this).attr("title") || "").toLowerCase(); return t.includes("番") || t.includes("copy") || t.includes("code"); })).first(); if (!anchor.length) return; const code = String(anchor.attr("data-clipboard-text") || "").trim(); if (!code) return; anchor.hide(); const btn = document.createElement("button"); btn.type = "button"; btn.className = "jav-avid-copy"; btn.textContent = "复制番号"; btn.title = `复制番号:${code}`; btn.style.cssText = 'display:inline-block;min-width:62px;margin-left:8px;padding:2px 8px;font-size:12px;font-family:-apple-system,BlinkMacSystemFont,"Microsoft YaHei","PingFang SC","Noto Sans CJK SC","Segoe UI",sans-serif;font-weight:600;text-align:center;background:#e8f4fd;border:1px solid #90c5e8;border-radius:4px;cursor:pointer;color:#1a6fa8;vertical-align:middle;white-space:nowrap;box-sizing:border-box;'; btn.addEventListener("click", (e => { e.preventDefault(), e.stopPropagation(); const done = () => { btn.textContent = "已复制"; setTimeout((() => { btn.textContent = "复制番号"; }), 900); }; navigator.clipboard && navigator.clipboard.writeText ? navigator.clipboard.writeText(code).then(done).catch((() => { alert("复制失败,请手动复制: " + code); })) : done(); })); anchor[0].parentNode.insertBefore(btn, anchor[0].nextSibling); } } const Z = (e, t) => { if (!e || 0 === e.length) return null; const n = new Set(e); if (n.has(t)) return t; const a = L.map((e => e.quality)).reverse(); for (const i of a) if (n.has(i)) return i; return e[0]; }, ee = "jhs_dmm_video"; class te { constructor(e, t = !0) { this.carNum = e, this.showErrorMessages = t; } _checkCache() { const e = localStorage.getItem(ee) ? JSON.parse(localStorage.getItem(ee)) : {}; return e[this.carNum] ? (clog.debug("缓存中存在预览视频信息", e[this.carNum]), e[this.carNum]) : null; } _updateCache(e) { const t = localStorage.getItem(ee) ? JSON.parse(localStorage.getItem(ee)) : {}; t[this.carNum] = e, clog.debug("成功解析出预览视频并已缓存:", e), localStorage.setItem(ee, JSON.stringify(t)); } async _searchContentIds() { const e = this.carNum, t = e.replace(/-/g, ""), n = [ { keyword: e.replace("-", "00"), name: "00-替换关键词" }, { keyword: e, name: "原始番号关键词" }, { keyword: t, name: "无连字符关键词" } ], a = e.toLowerCase(); for (const o of n) { const {keyword: e, name: n} = o, i = e.toLowerCase(); clog.debug(`--- 尝试使用 ${n} (${e}) 进行 API 搜索 ---`); const r = `https://api.dmm.com/affiliate/v3/ItemList?${new URLSearchParams({ api_id: "UrwskPfkqQ0DuVry2gYL", affiliate_id: "10278-996", output: "json", site: "FANZA", sort: "match", keyword: e }).toString()}`; let l; try { l = await gmHttp.get(r); } catch (s) { clog.error(`API 请求失败,跳过 ${n}:`, s); continue; } if (!l || !l.result || !l.result.result_count) { clog.debug("API 返回无结果,尝试下一个关键词。"); continue; } const c = []; for (const s of l.result.items) { if (c.length >= 2) break; const e = s.content_id || "", o = s.maker_product || ""; (e.includes(i.replace("-", "")) || a === o.toLowerCase() || e.includes(t.toLowerCase())) && (c.push({ serviceCode: s.service_code, floorCode: s.floor_code, contentId: e, pageUrl: s.URL }), clog.debug(`[${n}] cid|makerProduct 匹配成功:`, e, o)); } if (c.length > 0) { clog.debug(`--- 成功通过 ${n} 找到 Content IDs ---`); const t = $("#fanzaBtn"); let a = `https://www.dmm.co.jp/search/=/searchstr=${e}`, i = "single"; c.length > 1 ? (t.attr("href", a), t.css("backgroundColor", "#7bc73b"), i = "multiple") : (a = c[0].pageUrl, t.attr("href", a), t.css("backgroundColor", "#7bc73b")); const s = "jhs_other_site_dmm", o = localStorage.getItem(s) ? JSON.parse(localStorage.getItem(s)) : {}; return o[this.carNum] = { type: i, url: a }, localStorage.setItem(s, JSON.stringify(o)), c; } clog.debug(`[${n}] API 返回结果数 ${l.result.result_count},但无精确匹配的 Content ID。`); } clog.warn("所有关键词尝试均未找到匹配的Content ID, 解析Dmm视频失败"); const i = $("#fanzaBtn"); return i.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`), i.attr("title", "未查询到, 点击前往搜索页"), i.css("backgroundColor", "#de3333"), null; } async _extractTrailerLinks({contentId: e, serviceCode: t, floorCode: n}) { const a = `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${e}/mtype=AhRVShI_/service=${t}/floor=${n}/mode=/`, i = await gmHttp.get(a, null, { "accept-language": "ja-JP,ja;q=0.9", Cookie: "age_check_done=1" }); if ("string" != typeof i) throw clog.error(i), new Error("解析播放页内容失败, 非文本内容"); if (i.includes("このサービスはお住まいの地域からは")) throw new Error("节点不可用,请将DMM域名分流到日本ip"); const s = i.match(/const\s+args\s+=\s+(.*);/); if (!s) throw new Error("未在脚本中找到 const args = ... 变量"); let o; try { ({bitrates: o} = JSON.parse(s[1])); } catch (d) { throw new Error(`解析播放器脚本 JSON 失败: ${d.message}`); } const r = {}, l = L.map((e => e.quality)).join("|"), c = new RegExp(`(${l})\\.mp4$`); if (!Array.isArray(o)) throw clog.error("解析画质链接失败: bitrates 字段不是一个数组或不存在"), new Error("解析画质链接失败: bitrates 字段不是一个数组或不存在"); clog.debug("原始数据返回:", o); for (const h of o) { const e = null == h ? void 0 : h.src; if (!e || "string" != typeof e || !e.endsWith(".mp4")) continue; const t = e.match(c); let n = ""; t && t[1] && (n = t[1]), n && !r[n] && (r[n] = e); } if (0 === Object.keys(r).length) throw new Error("未找到匹配要求的预览画质视频"); return r; } async fetchVideo() { const e = this._checkCache(); if (e) return e; let t; try { const e = this.carNum.toLowerCase(); if (e.startsWith("heyzo") || /^(n\d+|\d+(-\d+)*)$/.test(e) || /^n\d+$/.test(e)) throw new Error("无码番号类型, 取消dmm解析"); if (this.carNum.includes("VR-")) throw new Error("VR类型, 取消dmm解析"); t = await this._searchContentIds(); } catch (n) { clog.error("DMM API 搜索失败:", n); const e = $("#fanzaBtn"); return e.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`), e.attr("title", "未查询到, 点击前往搜索页"), e.css("backgroundColor", "#de3333"), null; } if (!t || 0 === t.length) return null; try { const e = await Promise.any(t.map((e => this._extractTrailerLinks(e)))); return this._updateCache(e), e; } catch (a) { const e = a.errors || [ a ]; if (e.some((e => e.message.includes("节点不可用")))) this.showErrorMessages && show.error("节点不可用,请将DMM域名分流到日本ip"); else { const t = e[0].message || e[0]; clog.error(`解析失败: ${t}`, e), this.showErrorMessages && show.error(`解析失败: ${t}`); } const t = $("#fanzaBtn"); return t.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`), t.attr("title", "未查询到, 点击前往搜索页"), t.css("backgroundColor", "#de3333"), null; } } } const ne = async (e, t = !0) => new te(e, t).fetchVideo(); class ae extends X { getName() { return "PreviewVideoPlugin"; } async initCss() { return "\n .video-control-btn {\n min-width:120px;\n padding: 7px 12px;\n font-size: 12px;\n background: rgba(0,0,0,0.7);\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n }\n .video-control-btn.active {\n background-color: #1890ff;\n color: white;\n font-weight: bold;\n border: 2px solid #096dd9;\n }\n "; } async handle() { if (!isDetailPage) return; let e = await storageManager.getSetting(); let t = $(".preview-video-container"); t.on("click", (e => { utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (() => { this.handleVideo().then(); })); })); await storageManager.getSetting("enableLoadPreviewVideo", _) !== _ || o.includes("autoPlay=1") || this.initDmm().then(); let n = window.location.href; (n.includes("gallery-1") || n.includes("gallery-2")) && utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (() => { $(".fancybox-content #preview-video").length > 0 && this.handleVideo().then(); })), n.includes("autoPlay=1") && t.length > 0 && t[0].click(); } async initDmm() { try { const e = await ne(this.getPageInfo().carNum, !1); if (!e) return; let t = await storageManager.getSetting("videoQuality"); clog.debug("解析其它画质预览视频", "设置-期望画质", t); const n = e[Z(Object.keys(e), t)]; clog.log("切换其它画质预览视频: ", n); const a = $("#preview-video"), i = a.length ? a[0] : null, s = !i || utils.isHidden(a); if (a.length) { if (i) { const e = i.currentTime; a.attr("src", n), s || (clog.debug("播放器已手动打开, 变更进度条"), i.currentTime = e, i.play()); } } else { clog.debug("JavDB没有视频播放元素, 开始创建..."); const e = $(".column-video-cover img").attr("src"); $(".preview-images").prepend(`\n <a class="preview-video-container" data-fancybox="gallery" href="#preview-video">\n <span>預告片</span>\n <img src="${e}" class="video-cover" style="width: 150px; height: auto;" alt="">\n </a>\n `); $(".preview-video-container").on("click", (e => { utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (async () => { await this.handleVideo(); })); })); } } catch (e) { clog.error("预加载dmm失败:", e); } } async handleVideo() { if (await storageManager.getSetting("enableLoadPreviewVideo", _) === C) return; const e = $("#preview-video"); if (!e.length) return; const t = e.parent(); t.css("position", "relative"); const n = e[0], a = localStorage.getItem("jhs_videoMuted"); a && (n.muted = "yes" === a), n.addEventListener("volumechange", (function() { localStorage.setItem("jhs_videoMuted", n.muted ? "yes" : "no"); })), n.play(); let i = this.getPageInfo().carNum; const s = await ne(i); let o = $("<div></div>").attr("id", "video-bottom-toolbar").css({ display: "flex", gap: "5px", "align-items": "center", "flex-wrap": "wrap" }), r = $("<div></div>").css({ display: "flex", gap: "5px", "align-items": "center" }), l = null; if (s) { let t = await storageManager.getSetting("videoQuality"); l = Z(Object.keys(s), t); let a = s[l]; e.attr("src") !== a && (e.attr("src", a), n.load(), n.play()), L.forEach((e => { let t = s[e.quality]; if (t) { const n = l === e.quality; let a = $(`\n <button class="video-control-btn${n ? " active" : ""}" \n id="${e.id}" \n data-quality="${e.quality}"\n data-video-src="${t}"\n style="min-width: 40px; border: 1px solid #ccc; background-color: ${n ? "#007bff" : "#fff"}; color: ${n ? "white" : "black"};">\n ${e.text}\n </button>\n `); r.append(a); } })); } o.append(r); let c = $("<div></div>").css({ display: "flex", gap: "5px", "align-items": "center", "margin-left": "auto" }), d = $(`<button class="menu-btn" id="video-filterBtn" style="min-width: 120px; background-color:#de3333;">屏蔽</button>`); c.append(d); let h = $(`<button class="menu-btn" id="video-favoriteBtn" style="min-width: 120px; background-color:#25b1dc;">收藏</button>`); c.append(h); let g = $(`<button class="menu-btn" id="speed-btn" style="min-width: 120px; background-color:#76b45d;">快进</button>`); c.append(g), o.append(c), t.append(o), o.on("click", ".video-control-btn", (async t => { const a = $(t.currentTarget), i = a.data("video-src"); if (!a.hasClass("active")) try { const t = n.currentTime; e.attr("src", i), n.load(), n.currentTime = t, await n.play(), o.find(".video-control-btn").removeClass("active").css({ "background-color": "#fff", color: "black" }), a.addClass("active").css({ "background-color": "#007bff", color: "white" }); } catch (s) { console.error("切换画质失败:", s); } })), $("#speed-btn").on("click", (() => { this.getBean("DetailPageButtonPlugin").speedVideo(); })), utils.rightClick(document.body, "#speed-btn", (e => { this.getBean("DetailPageButtonPlugin").filterOne(e); })), $("#video-filterBtn").on("click", (e => { this.getBean("DetailPageButtonPlugin").filterOne(e); })), $("#video-favoriteBtn").on("click", (e => { this.getBean("DetailPageButtonPlugin").favoriteOne(e); })); } } class oe extends X { getName() { return "JavTrailersPlugin"; } constructor() { super(), this.hasBand = !1; } handle() { let e = window.location.href; if (!e.includes("handle=1")) return; if ($("h1:contains('Page not found')").length) { clog.log("番号无法匹配, 跳搜索"); let t = e.split("?")[0].split("video/")[1].toLowerCase().replace("00", "-"); return void (window.location.href = "/search/" + encodeURIComponent(t) + window.location.search); } let t = $(".videos-list .video-link").toArray(); if (t.length) { const n = e.split("?")[0].split("search/")[1].toLowerCase(), a = t.find((e => $(e).find(".vid-title").text().toLowerCase().includes(n))); if (a) return void (window.location.href = $(a).attr("href") + window.location.search); } this.handlePlayJavTrailers(), $("#videoPlayerContainer").on("click", (() => { this.handlePlayJavTrailers(); })), window.addEventListener("message", (e => { let t = document.getElementById("vjs_video_3_html5_api"); t && (t.currentTime += 5); })); } handlePlayJavTrailers() { this.hasBand || (utils.loopDetector((() => 0 !== $("#vjs_video_3_html5_api").length), (() => { setTimeout((() => { this.hasBand = !0; let e = document.getElementById("vjs_video_3_html5_api"); clog.debug(e), e.play(), e.currentTime = 5, e.addEventListener("timeupdate", (function() { e.currentTime >= 14 && e.currentTime < 16 && (e.currentTime += 2); })), $("#vjs_video_3_html5_api").css({ position: "fixed", width: "100vw", height: "100vh", objectFit: "cover", zIndex: "999999999" }), $(".vjs-control-bar").css({ position: "fixed", bottom: "20px", zIndex: "999999999" }); }), 100); })), utils.loopDetector((() => $("#vjs_video_3 canvas").length > 0), (() => { 0 !== $("#vjs_video_3 canvas").length && $("#vjs_video_3 canvas").css({ position: "fixed", width: "100vw", height: "100vh", objectFit: "cover", top: "0", right: "0", zIndex: "999999998" }); }))); } } class re extends X { getName() { return "SubTitleCatPlugin"; } handle() { $(".t-banner-inner").hide(), $("#navbar").hide(); let e = new URLSearchParams(window.location.search).get("search").toLowerCase(), t = $(".sub-table tr td a").toArray(), n = 0; t.forEach((t => { let a = $(t); a.text().toLowerCase().includes(e) ? n++ : a.parent().parent().hide(); })), 0 === n && show.error("该番号无字幕!"); const a = $(".sec-title"), i = a.html().replace(/^\d+/, n); a.html(i); } } class le extends X { getName() { return "Fc2Plugin"; } async initCss() { return "\n <style>\n /* 弹层样式 */\n .movie-detail-layer .layui-layer-title {\n font-size: 18px;\n color: #333;\n background: #f8f8f8;\n }\n \n \n /* 容器样式 */\n .movie-detail-container {\n margin: 40px;\n height: 100%;\n background: #fff;\n }\n \n .movie-poster-container {\n flex: 0 0 60%;\n padding: 15px;\n }\n \n .right-box {\n flex: 1;\n padding: 20px;\n overflow-y: auto;\n }\n \n /* 预告片iframe */\n .movie-trailer {\n width: 100%;\n height: 100%;\n min-height: 400px;\n background: #000;\n border-radius: 4px;\n }\n \n /* 电影信息样式 */\n .movie-title {\n font-size: 24px;\n margin-bottom: 15px;\n color: #333;\n }\n \n .movie-meta {\n margin-bottom: 20px;\n color: #666;\n }\n \n .movie-meta span {\n margin-right: 15px;\n }\n \n /* 演员列表 */\n .actor-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 10px;\n }\n \n .actor-tag {\n padding: 4px 12px;\n background: #f0f0f0;\n border-radius: 15px;\n font-size: 12px;\n color: #555;\n }\n \n /* 图片列表 */\n .image-list {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-top: 10px;\n }\n \n .movie-image-thumb {\n width: 120px;\n height: 80px;\n object-fit: cover;\n border-radius: 4px;\n cursor: pointer;\n transition: transform 0.3s;\n }\n \n .movie-image-thumb:hover {\n transform: scale(1.05);\n }\n \n /* 加载中和错误状态 */\n .search-loading, .movie-error {\n padding: 40px;\n text-align: center;\n color: #999;\n }\n \n .movie-error {\n color: #f56c6c;\n }\n \n .fancybox-container{\n z-index:99999999\n }\n \n \n /* 错误提示样式 */\n .movie-not-found, .movie-error {\n text-align: center;\n padding: 30px;\n color: #666;\n }\n \n .movie-not-found h3, .movie-error h3 {\n color: #f56c6c;\n margin: 15px 0;\n }\n \n .icon-warning, .icon-error {\n font-size: 50px;\n color: #e6a23c;\n }\n \n .icon-error {\n color: #f56c6c;\n }\n \n .fc2-movie-panel-info .panel-block {\n padding: 0 !important;\n }\n </style>\n "; } handle() { let e = "/advanced_search?type=3&score_min=0&d=1"; if ($('.navbar-item:contains("FC2")').attr("href", e), $('.tabs a:contains("FC2")').attr("href", e), o.includes("advanced_search?type=3")) { $("h2.section-title").contents().first().replaceWith("Fc2PPV"), $(".section .container > .box").remove(); } if (o.includes("collection_codes?movieId")) { $("section").html(""); const e = new URLSearchParams(window.location.search); let t = e.get("movieId"), n = e.get("carNum"), a = e.get("url"); t && n && a && this.openFc2Dialog(t, n, a); } } openFc2Dialog(e, t, n) { let a = t.replace("FC2-", ""); if (n.includes("123av")) return void this.getBean("Fc2By123AvPlugin").open123AvFc2Dialog(t, n); let i = `\n <div class="movie-detail-container">\n \x3c!--<div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>--\x3e\n \x3c!-- <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n <div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方资源: </strong></div>\n \n <div style="margin: 30px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:${f}"><span>${m}</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:${w}"><span>${v}</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:${x}"><span>${y}</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:${S};"><span>${k}</span></a>\n \n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn fr-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links" style="margin: 0 0.75rem">\n <div class="search-loading">加载中...</div>\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!--</div>--\x3e\n </div>\n `; layer.open({ type: 1, title: t, content: i, area: utils.getResponsiveArea([ "70%", "90%" ]), skin: "movie-detail-layer", scrollbar: !1, success: (i, s) => { this.loadData(e, t), $("#favoriteBtn").on("click", (async e => { const a = $("#data-actress").text(), i = $("#data-releaseDate").text(); await storageManager.saveCar({ carNum: t, url: n, names: a, actionType: h, publishTime: i }), window.refresh(), layer.closeAll(); })), $("#filterBtn").on("click", (e => { utils.q(e, `是否屏蔽${t}?`, (async () => { const e = $("#data-actress").text(), a = $("#data-releaseDate").text(); await storageManager.saveCar({ carNum: t, url: n, names: e, actionType: d, publishTime: a }), window.refresh(), layer.closeAll(), window.location.href.includes("collection_codes?movieId") && utils.closePage(); })); })), $("#hasDownBtn").on("click", (async e => { const a = $("#data-actress").text(), i = $("#data-releaseDate").text(); await storageManager.saveCar({ carNum: t, url: n, names: a, actionType: g, publishTime: i }), window.refresh(), layer.closeAll(); })), $("#hasWatchBtn").on("click", (async e => { const a = $("#data-actress").text(), i = $("#data-releaseDate").text(); await storageManager.saveCar({ carNum: t, url: n, names: a, actionType: p, publishTime: i }), window.refresh(), layer.closeAll(); })), $("#search-subtitle-btn").on("click", (e => utils.openPage(`https://subtitlecat.com/index.php?search=${t}`, t, !1, e))), $("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(t))), $("#magnetSearchBtn").on("click", (() => { let e = this.getBean("MagnetHubPlugin").createMagnetHub(t); layer.open({ type: 1, title: "磁力搜索", content: '<div id="magnetHubBox"></div>', area: utils.getResponsiveArea([ "60%", "80%" ]), scrollbar: !1, success: () => { $("#magnetHubBox").append(e); } }); })), this.getBean("OtherSitePlugin").loadOtherSite(a, t).then(), utils.setupEscClose(s); }, end() { window.location.href.includes("collection_codes?movieId") && utils.closePage(); } }); } loadData(e, t) { let n = t.replace("FC2-", ""); this.handleMovieDetail(e), this.handleLongImg(n), this.handleMagnets(e); } handleMovieDetail(e) { V(e).then((e => { const t = e.actors || [], n = e.imgList || []; let a = ""; if (t.length > 0) { let e = ""; for (let n = 0; n < t.length; n++) { let i = t[n]; a += `<span class="actor-tag"><a href="/actors/${escapeHtml(i.id)}" target="_blank">${escapeHtml(i.name)}</a></span>`, 0 === i.gender && (e += i.name + " "); } $("#data-actress").text(e); } else a = '<span class="no-data">暂无演员信息</span>'; let i = ""; i = Array.isArray(n) && n.length > 0 ? n.map(((e, t) => `\n <a href="${e}" data-fancybox="movie-gallery" data-caption="剧照 ${t + 1}">\n <img src="${e}" class="movie-image-thumb" loading="lazy" alt=""/>\n </a>\n `)).join("") : '<div class="no-data">暂无剧照</div>', $(".movie-info-container").html(`\n <h3 class="movie-title"><strong class="current-title">${e.title || "无标题"}</strong></h3>\n <div class="movie-meta">\n <span><strong>番号: </strong>${e.carNum || "未知"}</span>\n <span><strong>年份: </strong>${e.releaseDate || "未知"}</span>\n <span><strong>评分: </strong>${e.score || "无"}</span>\n <span><strong>时长: </strong>${e.duration + " m" || "无"}</span>\n </div>\n <div class="movie-meta">\n <span>\n <strong>站点: </strong>\n <a href="https://fc2ppvdb.com/articles/${e.carNum.replace("FC2-", "")}" target="_blank">fc2ppvdb</a>\n <a style="margin-left: 5px;" href="https://adult.contents.fc2.com/article/${e.carNum.replace("FC2-", "")}/" target="_blank">fc2电子市场</a>\n </span>\n </div>\n <div class="movie-actors">\n <div class="actor-list"><strong>主演: </strong>${a}</div>\n </div>\n <div class="movie-gallery" style="margin-top:10px">\n <strong>剧照: </strong>\n <div class="image-list">${i}</div>\n </div>\n <div id="data-releaseDate" style="display: none">${e.releaseDate || ""}</div>\n `), this.getBean("TranslatePlugin").translate(e.carNum, !1).then(); })).catch((e => { console.error(e), $(".movie-info-container").html(`\n <div class="movie-error">加载失败: ${escapeHtml(e.message)}</div>\n `); })); } handleLongImg(e) { utils.loopDetector((() => $(".movie-gallery .image-list").length > 0), (async () => { $(".movie-gallery .image-list").prepend(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 150px;max-width:150px; text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> '); const t = this.getBean("ScreenShotPlugin"), n = await t.getScreenshot(e); n && await t.addImg("缩略图", n); })); } handleMagnets(e) { (async e => { let t = `${U}/v1/movies/${e}/magnets`, n = { jdSignature: await O() }; return (await gmHttp.get(t, null, n)).data.magnets; })(e).then((e => { let t = ""; if (e.length > 0) for (let n = 0; n < e.length; n++) { let a = e[n], i = ""; n % 2 == 0 && (i = "odd"), t += `\n <div class="item columns is-desktop ${i}">\n <div class="magnet-name column is-four-fifths">\n <a href="magnet:?xt=urn:btih:${a.hash}" title="右鍵點擊並選擇「複製鏈接地址」">\n <span class="name">${a.name}</span>\n <br>\n <span class="meta">\n ${(a.size / 1024).toFixed(2)}GB, ${a.files_count}個文件 \n </span>\n <br>\n <div class="tags">\n ${a.hd ? '<span class="tag is-primary is-small is-light">高清</span>' : ""}\n ${a.cnsub ? '<span class="tag is-warning is-small is-light">字幕</span>' : ""}\n </div>\n </a>\n </div>\n <div class="buttons column">\n <button class="button is-info is-small copy-to-clipboard" data-clipboard-text="magnet:?xt=urn:btih:${a.hash}" type="button"> 複製 </button>\n </div>\n <div class="date column"><span class="time">${a.created_at}</span></div>\n </div>\n `; } else t = '<span class="no-data">暂无磁力信息</span>'; $("#magnets-content").html(t) })).catch((e => { console.error(e), $("#magnets-content").html(`\n <div class="movie-error">加载失败: ${escapeHtml(e.message)}</div>\n `); })); } async openFc2Page(e, t, n) { const a = this.getBean("OtherSitePlugin"); let i = await a.getJavDbUrl(); window.open(`${i}/users/collection_codes?movieId=${e}&carNum=${t}&url=${n}`); } } class ce extends X { getName() { return "HighlightMagnetPlugin"; } async doFilterMagnet() { // 新的磁力面板启用时接管原生区, 老的高亮逻辑让步 if (await storageManager.getSetting(NMP_SETTING_KEY, C) === _) return; this.handleDb(), this.handleBus(); } /** 给磁力行注入评分徽章(幂等:已有则跳过) */ injectScoreBadge(el, title) { try { if (el.find(".jhs-magnet-score").length > 0) return; const score = calcMagnetScore({ title: title || "", seeders: 0 }); const total = score.total; const label = total >= 70 ? "高" : total >= 40 ? "中" : "低"; const color = total >= 70 ? "#22c55e" : total >= 40 ? "#f59e0b" : "#9ca3af"; const tip = `分辨率:${score.resolution}/25 字幕:${score.subtitle}/20 做种:${score.seeders}/35 新鲜度:${score.freshness}/15`; el.append(`<span class="jhs-magnet-score" title="${tip}" style="display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:10px;font-size:11px;font-weight:600;color:#fff;background:${color};margin-left:6px;cursor:help;vertical-align:middle;">${label} ${total}</span>`); } catch (e) {} } handleDb() { if (!r) return; let e = $("#magnets-content .name"); if (0 === e.length) return; const t = [ "4k", "-c", "-u", "-uc" ]; let n = !1; e.each(((e, a) => { const i = $(a), s = i.text().toLowerCase(), o = t.some((e => s.includes(e))); const row = i.parent().parent().parent(); row.addClass("magnet-row"), s.includes("4k") && i.css("color", "#f40"), o && (n = !0, row.addClass("high-quality")); this.injectScoreBadge(i, i.text()); })), n && $("#magnets-content .magnet-row").not(".high-quality").hide(); } handleBus() { l && isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => { const e = $("#magnet-table tr"), t = [ "4k", "-c", "-u", "-uc" ]; let n = !1; e.each(((e, a) => { const i = $(a), s = i.find("td:first-child"), o = s.find("a:first-child"), r = s.find("a:nth-child(2)"), l = o.text().toLowerCase(); l.includes("4k") && o.css("color", "#f40"); (t.some((e => l.includes(e))) || r.length && r.text().includes("字幕")) && (n = !0, i.addClass("high-quality")); this.injectScoreBadge(o, o.text()); })), n && e.each(((e, t) => { const n = $(t); n.hasClass("high-quality") || n.hide(); })); })); } showAll() { if (r) { $("#magnets-content .item").toArray().forEach((e => $(e).show())); } l && $("#magnet-table tr").toArray().forEach((e => $(e).show())); } } const NMP_PANEL_CLASS = "jhs-nmp-panel"; const NMP_SETTING_KEY = "enableNativeMagnetPanel"; const NMP_SORT_KEY = "nmpMagnetSort"; /** 解析磁链体积字符串 -> 字节数, 支持 TiB/GiB/MiB/KiB 与 TB/GB/MB/KB/B */ function nmpParseSize(value) { const m = String(value || "").replace(/,/g, "").match(/([\d.]+)\s*(TiB|GiB|MiB|KiB|TB|GB|MB|KB|B)?/i); if (!m) return 0; const num = parseFloat(m[1]); const unit = (m[2] || "B").toUpperCase(); const mult = { TIB: 1099511627776, TB: 1099511627776, GIB: 1073741824, GB: 1073741824, MIB: 1048576, MB: 1048576, KIB: 1024, KB: 1024, B: 1 }; return Number.isFinite(num) ? num * (mult[unit] || 1) : 0; } /** 解析日期字符串 -> 时间戳, 兼容 10/13 位秒级毫秒级数字 */ function nmpParseTimestamp(value) { const text = String(value ?? "").trim(); if (!text) return 0; if (/^\d{10,13}$/.test(text)) { const n = Number(text); return 10 === text.length ? 1e3 * n : n; } const normalized = text.replace(/\//g, "-").replace(/(\d)\s+(\d{1,2}:\d{2})/, "$1T$2"); const ts = Date.parse(normalized); return Number.isFinite(ts) ? ts : 0; } function nmpItemTimestamp(item) { return nmpParseTimestamp(item && item.timestamp) || nmpParseTimestamp(item && item.date); } /** 破解番号识别: UN/UC 独立出现 (非紧邻数字或"字幕") 才算破解 */ function nmpHasCrackedCode(text) { const re = /\b(?:UN|UC)\b/g; let m; while (m = re.exec(text)) { const before = text.slice(0, m.index), after = text.slice(m.index + m[0].length); const adjNum = /\d$/.test(before) || /^\d/.test(after); const adjSub = /字幕$/.test(before) || /^字幕/.test(after); if (!adjNum && !adjSub) return !0; } return !1; } /** 质量分类: 返回 { isChinese, is4K, isCracked } */ function nmpClassifyQuality(title) { const text = String(title || ""); const hasCJK = /[\u4e00-\u9fff]/.test(text); const hasJP = /[\u3040-\u309f\u30a0-\u30ff]/.test(text); const isChinese = /(?:[^A-Za-z]|^)FHDC(?:[^A-Za-z]|$)/i.test(text) || /[-_]CH?(?:[^A-Za-z]|$)/.test(text) || /(?:中字|中文|字幕|中文字幕|繁體中字|繁体中字|繁體中文|繁体中文|繁體字幕|繁体字幕|繁中|繁字|自提|征用|徵用|漢化|汉化|內嵌|内嵌|內封|内封|雙語|双语)/.test(text) || hasCJK && !hasJP; const is4K = /(?:[^A-Za-z0-9]|^)(?:4K(?:UHD)?|2160P)(?:[^A-Za-z0-9]|$)/i.test(text); const isCracked = /(?:破解|破坏|破壞|破壊|无码|無碼)/.test(text) || /\b(?:uncensored|mosaic)\b/i.test(text) || nmpHasCrackedCode(text); return { isChinese, is4K, isCracked }; } /** 质量标签数组: ['4K','中字','破解'] 的子集 */ function nmpQualityTags(title) { const { isChinese, is4K, isCracked } = nmpClassifyQuality(title); return [ is4K ? "4K" : "", isChinese ? "中字" : "", isCracked ? "破解" : "" ].filter(Boolean); } /** 标签种类 -> CSS data-kind 值 */ function nmpTagKind(value) { if ("4K" === value) return "four-k"; if ("中字" === value) return "subtitle"; if ("破解" === value) return "cracked"; return ""; } /** 排序: size(体积优先) / newest(最新) / oldest(最早), 稳定排序 */ function nmpSortData(data, mode) { return [ ...data ].map(((item, index) => ({ item, index }))).sort(((a, b) => { const sizeDelta = nmpParseSize(b.item && b.item.size) - nmpParseSize(a.item && a.item.size); if ("size" === mode) return sizeDelta || a.index - b.index; const aTime = nmpItemTimestamp(a.item), bTime = nmpItemTimestamp(b.item); if (!aTime && !bTime) return sizeDelta || a.index - b.index; if (!aTime) return 1; if (!bTime) return -1; const timeDelta = "oldest" === mode ? aTime - bTime : bTime - aTime; return timeDelta || sizeDelta || a.index - b.index; })).map((e => e.item)); } /** 取节点纯文本, 可先移除指定选择器的子节点 */ function nmpCleanText(node, removeSelectors = []) { if (!node) return ""; const copy = node.cloneNode(!0); removeSelectors.forEach((sel => copy.querySelectorAll(sel).forEach((el => el.remove())))); return (copy.textContent || "").replace(/\s+/g, " ").trim(); } /** 抓取原生站内标签 (高清/字幕等) */ function nmpParseNativeTags(node) { return [ ...node.querySelectorAll(".tags .tag, .btn-mini-new") ].map((t => nmpCleanText(t))).filter(Boolean); } /** JavBus: 从 #magnet-table 解析磁链条目 */ function nmpParseJavbusItems(container) { return [ ...container.querySelectorAll("tr") ].map((row => { const cells = [ ...row.querySelectorAll(":scope > td") ]; const magnetLink = row.querySelector('a[href^="magnet:"]'); if (!magnetLink || cells.length < 2) return null; const title = nmpCleanText(cells[0], [ ".mag-btn-group", ".btn-mini-new" ]) || nmpCleanText(magnetLink); return { title, maglink: magnetLink.href, size: nmpCleanText(cells[1]), files: "", date: nmpCleanText(cells[2]), qualityTags: nmpQualityTags(title), nativeTags: nmpParseNativeTags(cells[0]) }; })).filter(Boolean); } /** JavDB: 从 #magnets-content 解析磁链条目 */ function nmpParseJavdbItems(container) { return [ ...container.querySelectorAll(".item") ].map((row => { const magnetLink = row.querySelector('.magnet-name a[href^="magnet:"]'); if (!magnetLink) return null; const metaText = nmpCleanText(row.querySelector(".magnet-name .meta")); const [ size = "", files = "" ] = metaText.split(/[,,]/).map((p => p.trim())); const title = nmpCleanText(row.querySelector(".magnet-name .name")) || nmpCleanText(magnetLink); return { title, maglink: magnetLink.href, size, files, date: nmpCleanText(row.querySelector(".date .time, .date")), qualityTags: nmpQualityTags(title), nativeTags: nmpParseNativeTags(row) }; })).filter(Boolean); } function nmpBuildNativeItems(site, container) { if ("javbus" === site) return nmpParseJavbusItems(container); if ("javdb" === site) return nmpParseJavdbItems(container); return []; } function nmpCreateButton(label, className, onClick) { const btn = document.createElement("button"); btn.type = "button", btn.className = className, btn.textContent = label; btn.addEventListener("click", onClick); return btn; } function nmpCreateTagGroup(values, withKinds = !1) { if (!values || !values.length) return null; const tags = document.createElement("div"); tags.className = "jhs-nmp-tags"; values.forEach((value => { const tag = document.createElement("span"); tag.className = "jhs-nmp-tag"; const kind = withKinds ? nmpTagKind(value) : ""; if (kind) tag.dataset.kind = kind; tag.textContent = String(value || "").trim(); tags.appendChild(tag); })); return tags; } function nmpCreateMeta(value, label) { const meta = document.createElement("span"); meta.className = "jhs-nmp-meta", meta.title = label, meta.dataset.label = label; meta.textContent = value || "—"; return meta; } /** 每行的操作按钮: 复制 / 验车 / 115离线 */ function nmpAppendActions(actions, item) { const copyBtn = nmpCreateButton("复制", "jhs-nmp-action jhs-nmp-action-copy", (() => { // 优先 GM_setClipboard (油猴原生, 不受 https 限制), 回退 navigator.clipboard if ("function" == typeof GM_setClipboard) { GM_setClipboard(item.maglink); copyBtn.textContent = "已复制"; setTimeout((() => { copyBtn.textContent = "复制"; }), 900); } else { const write = navigator.clipboard && navigator.clipboard.writeText ? navigator.clipboard.writeText(item.maglink) : Promise.reject(); write.then((() => { copyBtn.textContent = "已复制"; setTimeout((() => { copyBtn.textContent = "复制"; }), 900); })).catch((() => show.error("复制失败, 请手动复制"))); } })); const checkBtn = nmpCreateButton("验车", "jhs-nmp-action jhs-nmp-action-check", (async () => { // WhatsLink 验车: 调用 API 获取资源信息 + 截图, 以 overlay 弹窗展示 nmpShowWhatslinkModal({ _loading: !0 }, item.maglink); try { const url = `https://whatslink.info/api/v1/link?url=${encodeURIComponent(item.maglink)}`; const r = await nmpGmFetch(url, { timeout: 2e4 }); if (!r.loadstuts) throw new Error("WhatsLink 请求失败"); const data = JSON.parse(r.responseText || "{}"); document.querySelector(".jhs-nmp-whatslink-overlay")?.remove(); nmpShowWhatslinkModal(data, item.maglink); } catch (e) { document.querySelector(".jhs-nmp-whatslink-overlay")?.remove(); nmpShowWhatslinkModal({ error: e.message || "查询失败", name: "查询失败", type: "-", file_type: "-", size: 0, count: "-", screenshots: [] }, item.maglink); } })); const offlineBtn = nmpCreateButton("115离线", "jhs-nmp-action jhs-nmp-action-offline", (async () => { // 115 离线: 获取签名 → 提交离线任务 const maglink = item.maglink.substring(0, 60); const offlineUrl = "https://115.com/?tab=offline&mode=wangpan"; const loginUrl = "https://115.com/?mode=login"; try { const tokenR = await nmpGmFetch(`https://115.com/?ct=offline&ac=space&_=${Date.now()}`, { headers: { Accept: "application/json, text/plain, */*", Referer: offlineUrl } }); if (!tokenR.loadstuts) { show.error("115 错误: 无法获取签名, 请检查跨源权限或登录状态"); return; } if (tokenR.responseText.includes("html")) { show.error("115 未登录, 请先登录"); window.open(loginUrl, "_blank"); return; } const json = nmpParseJson(tokenR.responseText); if (!json?.sign || !json?.time) { show.error("115 签名返回异常, 请确认已登录"); return; } const r = await new Promise((resolve => { GM_xmlhttpRequest({ method: "POST", url: "https://115.com/web/lixian/?ct=lixian&ac=add_task_url", timeout: 2e4, headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", Accept: "application/json, text/javascript, */*; q=0.01", Origin: "https://115.com", Referer: offlineUrl, "X-Requested-With": "XMLHttpRequest" }, data: `url=${encodeURIComponent(maglink)}&uid=&sign=${json.sign}&time=${json.time}`, onload(r) { resolve(r); }, onerror() { resolve(null); }, ontimeout() { resolve(null); } }); })); if (!r) { show.error("115 离线推送失败"); return; } const res = nmpParseJson(r.responseText); if (!res) { show.error("115 返回异常, 可能登录已失效"); return; } if (res.state) show.ok("115 离线成功: 任务已添加"); else { const msg = "911" === String(res.errcode) ? "账号使用异常, 请手工验证" : (res.error_msg || res.msg || res.error || "未知错误"); show.error("115 离线失败: " + msg); } } catch (e) { show.error("115 离线出错: " + (e.message || e)); } })); actions.append(copyBtn, checkBtn, offlineBtn); } /** WhatsLink 验车弹窗 */ function nmpShowWhatslinkModal(payload, magnet) { document.querySelector(".jhs-nmp-whatslink-overlay")?.remove(); const overlay = document.createElement("div"); overlay.className = "jhs-nmp-whatslink-overlay"; overlay.style.cssText = "position:fixed;inset:0;z-index:10000040;display:flex;align-items:center;justify-content:center;padding:22px;background:rgba(15,23,42,.66);backdrop-filter:blur(8px);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif"; if (payload._loading) { overlay.innerHTML = '<div style="padding:28px;text-align:center;color:#475569;font-size:14px;background:#fff;border-radius:12px;box-shadow:0 22px 60px rgba(15,23,42,.26)">正在验车...</div>'; document.body.appendChild(overlay); overlay.addEventListener("click", (e => { if (e.target === overlay) overlay.remove(); })); return; } const shots = Array.isArray(payload?.screenshots) ? payload.screenshots.map((s => s?.screenshot)).filter(Boolean) : []; const size = nmpFormatBytes(payload?.size); let index = 0; const modal = document.createElement("div"); modal.style.cssText = "width:min(700px,94vw);max-height:90vh;overflow:auto;background:#fff;border-radius:12px;padding:20px;box-shadow:0 30px 80px rgba(2,8,23,.38)"; modal.innerHTML = ` <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:14px"> <div> <div style="color:#db2777;font-size:12px;font-weight:800;margin-bottom:4px">磁力验车</div> <h2 style="margin:0;font-size:18px;color:#111827;word-break:break-word">${escapeHtml(payload?.name || "未知资源")}</h2> </div> <button class="nmp-wl-close" type="button" style="width:32px;height:32px;border:0;border-radius:8px;color:#64748b;background:transparent;cursor:pointer;font-size:25px;line-height:1">×</button> </div> <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:7px;margin-bottom:14px"> <div style="padding:8px 10px;border:1px solid #e2e8f0;border-radius:11px;background:#fff"><b style="display:block;font-size:13px;color:#172033">${size}</b><span style="font-size:11px;color:#64748b">大小</span></div> <div style="padding:8px 10px;border:1px solid #e2e8f0;border-radius:11px;background:#fff"><b style="display:block;font-size:13px;color:#172033">${payload?.count ?? "-"}</b><span style="font-size:11px;color:#64748b">文件数</span></div> <div style="padding:8px 10px;border:1px solid #e2e8f0;border-radius:11px;background:#fff"><b style="display:block;font-size:13px;color:#172033">${shots.length}</b><span style="font-size:11px;color:#64748b">截图</span></div> </div> <div class="nmp-wl-shots" style="display:${shots.length ? "grid" : "none"};grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px;margin-bottom:14px"></div> ${shots.length ? "" : '<div style="padding:16px;text-align:center;color:#64748b;font-size:13px;border:1px dashed #e2e8f0;border-radius:8px;margin-bottom:14px">暂无截图, 可通过名称/大小/文件数量做基础判断</div>'} <div style="padding:9px;border-radius:8px;background:#f6f8fb;word-break:break-all;font-family:monospace;font-size:12px;color:#334155">${escapeHtml(magnet)}</div> `; const shotsBox = modal.querySelector(".nmp-wl-shots"); shots.forEach((url => { const img = document.createElement("img"); img.src = url, img.alt = "截图", img.style.cssText = "width:100%;border-radius:8px;cursor:pointer;border:2px solid #e2e8f0"; img.addEventListener("click", (() => window.open(url, "_blank"))); shotsBox.appendChild(img); })); overlay.appendChild(modal); document.body.appendChild(overlay); modal.querySelector(".nmp-wl-close").addEventListener("click", (() => overlay.remove())); overlay.addEventListener("click", (e => { if (e.target === overlay) overlay.remove(); })); } function nmpRenderRows(state) { const rows = state.panel.querySelector(".jhs-nmp-rows"); const count = state.panel.querySelector(".jhs-nmp-count"); const source = state.panel.querySelector(".jhs-nmp-source"); const isAggregate = "aggregate" === state.activeTab; const items = isAggregate ? nmpSortData(state.aggregateItems || [], state.sortMode) : nmpSortData(state.nativeItems, state.sortMode); rows.replaceChildren(); count.textContent = `${items.length} 条`; source.textContent = isAggregate ? `聚合:${nmpEngineLabels()[state.engineKey] || state.engineKey}` : "站内原始条目"; if (!items.length) { const empty = document.createElement("div"); empty.className = "jhs-nmp-empty"; empty.textContent = isAggregate ? "没有搜索到可用磁力" : "站内暂时没有可用磁力"; rows.appendChild(empty); return; } items.forEach(((item, index) => { const row = document.createElement("article"); row.className = "jhs-nmp-row"; const rowIndex = document.createElement("span"); rowIndex.className = "jhs-nmp-index"; rowIndex.textContent = String(index + 1).padStart(2, "0"); const name = document.createElement("div"); name.className = "jhs-nmp-name"; const titleLine = document.createElement("div"); titleLine.className = "jhs-nmp-title-line"; const title = document.createElement("a"); title.href = item.maglink; title.target = "_blank"; title.rel = "noopener noreferrer"; title.title = item.title; title.textContent = String(item.title || "").trim(); const qTags = nmpCreateTagGroup(item.qualityTags, !0); if (qTags) titleLine.appendChild(qTags); titleLine.appendChild(title); const nTags = nmpCreateTagGroup(item.nativeTags); if (nTags) titleLine.appendChild(nTags); name.appendChild(titleLine); const metadata = document.createElement("div"); metadata.className = "jhs-nmp-metadata"; metadata.append(nmpCreateMeta(item.size, "大小"), nmpCreateMeta(item.files, "文件"), nmpCreateMeta(item.date, "日期")); const actions = document.createElement("div"); actions.className = "jhs-nmp-actions"; nmpAppendActions(actions, item); row.append(rowIndex, name, metadata, actions); rows.appendChild(row); })); } /** 切换 Tab 时同步按钮选中态与聚合控制栏可见性 */ function nmpSyncTabControls(state) { const nativeBtn = state.panel.querySelector('[data-nmp-tab="native"]'); const aggBtn = state.panel.querySelector('[data-nmp-tab="aggregate"]'); const controls = state.panel.querySelector(".jhs-nmp-controls"); const isAggregate = "aggregate" === state.activeTab; nativeBtn && nativeBtn.setAttribute("aria-selected", String(!isAggregate)); aggBtn && aggBtn.setAttribute("aria-selected", String(isAggregate)); if (controls) controls.hidden = !isAggregate; } /** 拉取聚合搜索结果 (带 6 小时缓存) */ async function nmpLoadAggregate(state) { const rows = state.panel.querySelector(".jhs-nmp-rows"); const select = state.panel.querySelector("[data-nmp-engine]"); const refresh = state.panel.querySelector(".jhs-nmp-refresh"); const empty = document.createElement("div"); empty.className = "jhs-nmp-empty", empty.textContent = "正在搜索聚合磁力…"; rows.replaceChildren(empty); if (select) select.disabled = !0; if (refresh) refresh.disabled = !0; try { const engines = nmpGetEngines(); const search = engines[state.engineKey] || Object.values(engines)[0]; const key = `nmp:${state.engineKey}:${state.avid}`; const result = state.forceRefresh ? await search(state.avid) : await storageManager.cachedRequest(key, 216e5, (() => search(state.avid))); state.forceRefresh = !1; state.aggregateItems = nmpNormalizeAggregateItems(result?.data || [], { nativeItems: state.nativeItems }); state.aggregateUrl = result?.url || ""; nmpRenderRows(state); } catch (err) { clog.error("[磁力面板] 聚合搜索出错:", err); empty.textContent = "聚合搜索失败,请刷新重试"; } finally { if (select) select.disabled = !1; if (refresh) refresh.disabled = !1; } } function nmpActivateTab(state, tab) { state.activeTab = tab; nmpSyncTabControls(state); if ("aggregate" === tab && null === state.aggregateItems) return void nmpLoadAggregate(state); nmpRenderRows(state); } function nmpCreatePanel(site, avid, nativeItems, sortMode, engineKey, defaultTab) { const panel = document.createElement("section"); panel.className = `${NMP_PANEL_CLASS} jhs-nmp-${site}`; panel.dataset.site = site; const state = { panel, site, avid, nativeItems, sortMode: sortMode || "size", engineKey: engineKey || NMP_DEFAULT_ENGINE_KEY, aggregateItems: null, aggregateUrl: "", forceRefresh: !1, activeTab: nativeItems.length ? ("aggregate" === defaultTab ? "aggregate" : "native") : "aggregate" }; const head = document.createElement("header"); head.className = "jhs-nmp-head"; const heading = document.createElement("div"); const title = document.createElement("span"); title.className = "jhs-nmp-title", title.textContent = "磁力资源"; const count = document.createElement("span"); count.className = "jhs-nmp-count"; heading.append(title, count); // Tab 组: 只留「默认」下拉 (站内磁力 / 聚合搜索 两个 Tab 按钮已合并进下拉) const tabs = document.createElement("div"); tabs.className = "jhs-nmp-tabs"; if (nativeItems.length) { const defaultTabSelect = document.createElement("select"); defaultTabSelect.className = "jhs-nmp-default-tab"; defaultTabSelect.setAttribute("aria-label", "默认显示的磁力页签"); defaultTabSelect.title = "切换/设置默认显示的页签, 数值会持久化到下一次打开"; defaultTabSelect.add(new Option("默认:站内磁力", "native")); defaultTabSelect.add(new Option("默认:聚合搜索", "aggregate")); defaultTabSelect.value = state.activeTab; defaultTabSelect.addEventListener("change", (() => { storageManager.saveSettingItem("nmpDefaultTab", defaultTabSelect.value); nmpActivateTab(state, defaultTabSelect.value); })); tabs.appendChild(defaultTabSelect); } const tools = document.createElement("div"); tools.className = "jhs-nmp-tab-tools"; const sortSelect = document.createElement("select"); sortSelect.className = "jhs-nmp-select"; sortSelect.setAttribute("aria-label", "磁力排序"); [ [ "size", "大小优先" ], [ "newest", "最新收录" ], [ "oldest", "最早收录" ] ].forEach((([ value, label ]) => sortSelect.add(new Option(label, value)))); sortSelect.value = state.sortMode; sortSelect.addEventListener("change", (() => { state.sortMode = sortSelect.value; storageManager.saveSettingItem(NMP_SORT_KEY, sortSelect.value); nmpRenderRows(state); })); tools.append(tabs, sortSelect); head.append(heading, tools); // 聚合控制栏: 引擎选择 + 刷新 + 原网页 const controls = document.createElement("div"); controls.className = "jhs-nmp-controls"; const engineSelect = document.createElement("select"); engineSelect.className = "jhs-nmp-select"; engineSelect.dataset.nmpEngine = "1"; engineSelect.setAttribute("aria-label", "磁力引擎"); const engines = nmpGetEngines(), labels = nmpEngineLabels(); Object.keys(engines).forEach((key => engineSelect.add(new Option(labels[key] || key, key, !1, key === state.engineKey)))); engineSelect.addEventListener("change", (() => { state.engineKey = engineSelect.value; state.aggregateItems = null; storageManager.saveSettingItem(NMP_ENGINE_SELECT_KEY, engineSelect.value); nmpLoadAggregate(state); })); const refresh = nmpCreateButton("刷新", "jhs-nmp-refresh", (() => { if ("aggregate" === state.activeTab) { state.aggregateItems = null, state.forceRefresh = !0; nmpLoadAggregate(state); } else nmpRenderRows(state); })); const openSrc = nmpCreateButton("原网页", "jhs-nmp-refresh", (() => { state.aggregateUrl ? window.open(state.aggregateUrl, "_blank", "noopener") : show.info("暂无原网页地址"); })); controls.append(engineSelect, refresh, openSrc); controls.hidden = "aggregate" !== state.activeTab; const rows = document.createElement("div"); rows.className = "jhs-nmp-rows"; const foot = document.createElement("footer"); foot.className = "jhs-nmp-foot"; const source = document.createElement("span"); source.className = "jhs-nmp-source"; const hint = document.createElement("span"); hint.textContent = nativeItems.length ? "聚合结果不会覆盖站内磁力" : "聚合搜索沿用已配置的磁力引擎"; foot.append(source, hint); panel.append(head, controls, rows, foot); panel._jhsNmpState = state; nmpActivateTab(state, state.activeTab); return panel; } function nmpHideNativeSource(source) { source.dataset.jhsNmpDisplay = source.style.display || ""; source.dataset.jhsNmpHidden = "1"; source.style.display = "none"; } function nmpHideNativeParent(parent) { if (!parent || "1" === parent.dataset.jhsNmpParentHidden) return; parent.dataset.jhsNmpParentDisplay = parent.style.display || ""; parent.dataset.jhsNmpParentHidden = "1"; parent.style.display = "none"; } /** 恢复原生磁链区并移除面板 */ function nmpRemove() { document.querySelectorAll(`.${NMP_PANEL_CLASS}`).forEach((p => p.remove())); document.querySelectorAll('[data-jhs-nmp-hidden="1"]').forEach((el => { el.style.display = el.dataset.jhsNmpDisplay || ""; delete el.dataset.jhsNmpDisplay, delete el.dataset.jhsNmpHidden; })); document.querySelectorAll('[data-jhs-nmp-parent-hidden="1"]').forEach((el => { el.style.display = el.dataset.jhsNmpParentDisplay || ""; delete el.dataset.jhsNmpParentDisplay, delete el.dataset.jhsNmpParentHidden; })); } const NMP_ENGINE_CFG = { javdbSearchUrl: "javdb.com", ciligouUrl: "clg55.top", u3c3Url: "www.u3c3.com", btsearchUrl: "btsearch.love", sukebeiUrl: "sukebei.nyaa.si", u9a9Url: "u9a9.com", sokittyUrl: "w1.sokitty.me" }; const NMP_DEFAULT_ENGINE_KEY = "sukebei.nyaa.si"; const NMP_ENGINE_SELECT_KEY = "nmpDefaultEngine"; function nmpGmFetch(url, opts = {}) { return new Promise((resolve => { GM_xmlhttpRequest({ method: "GET", timeout: 2e4, ...opts, url, onload(r) { r.loadstuts = !0, resolve(r); }, onerror(r) { r.loadstuts = !1, resolve(r); }, onabort(r) { r.loadstuts = !1, resolve(r); }, ontimeout(r) { r.loadstuts = !1, r.finalUrl = url, resolve(r); } }); })); } function nmpParseHTML(str) { return new DOMParser().parseFromString(str, "text/html"); } const NMP_JAVDB_API_BASE = "https://jdforrepam.com/api"; const NMP_JAVDB_SIGN_SALT = "71cf27bb3c0bcdf207b64abecddc970098c7421ee7203b9cdae54478478a199e7d5a6e1a57691123c1a931c057842fb73ba3b3c83bcd69c17ccf174081e3d8aa"; let nmpJavdbSignCache = { ts: 0, sign: "" }; function nmpBuildJavdbSignature() { const curr = Math.floor(Date.now() / 1e3); if (nmpJavdbSignCache.sign && curr - nmpJavdbSignCache.ts <= 20) return nmpJavdbSignCache.sign; nmpJavdbSignCache = { ts: curr, sign: `${curr}.lpw6vgqzsp.${md5(`${curr}${NMP_JAVDB_SIGN_SALT}`)}` }; return nmpJavdbSignCache.sign; } function nmpParseJson(text) { try { return JSON.parse(text || "{}"); } catch { return null; } } function nmpFormatJavdbSize(size) { const mb = Number(size); if (!Number.isFinite(mb) || mb <= 0) return ""; return mb >= 1024 ? `${(mb / 1024).toFixed(mb >= 10240 ? 1 : 2)} GB` : `${Math.round(mb)} MB`; } function nmpFormatBytes(bytes) { const num = Number(bytes) || 0; if (!num) return "-"; const units = ["B","KB","MB","GB","TB"]; let value = num, index = 0; for (; value >= 1024 && index < units.length - 1;) value /= 1024, index += 1; return `${value.toFixed(index >= 3 ? 2 : 1)} ${units[index]}`; } function nmpNormalizeFileCount(...values) { for (const value of values) { if (Array.isArray(value)) return value.length ? String(value.length) : ""; if (value && "object" == typeof value) { const c = Object.keys(value).length; if (c) return String(c); continue; } const text = String(value ?? "").trim(); if (!text) continue; const num = parseInt(text.replace(/,/g, ""), 10); if (Number.isFinite(num) && num > 0) return String(num); } return ""; } function nmpCompactJavdbNumber(v) { return String(v || "").toUpperCase().replace(/[^A-Z0-9]/g, ""); } async function nmpFindJavdbMovie(kw, { limit = 5, fallbackFirst = !1 } = {}) { const headers = { accept: "application/json", jdSignature: nmpBuildJavdbSignature() }; const params = new URLSearchParams({ q: kw, page: "1", type: "movie", limit: String(limit), movie_type: "all", from_recent: "false", movie_filter_by: "all", movie_sort_by: "relevance" }); const r = await nmpGmFetch(`${NMP_JAVDB_API_BASE}/v2/search?${params.toString()}`, { headers, timeout: 2e4 }); if (!r.loadstuts || r.status < 200 || r.status >= 400) return null; const json = nmpParseJson(r.responseText); const movies = Array.isArray(json?.data?.movies) ? json.data.movies : []; const ckw = nmpCompactJavdbNumber(kw); return movies.find((m => nmpCompactJavdbNumber(m?.number) === ckw)) || (fallbackFirst ? movies[0] : null) || null; } async function nmpSearchJavDB(kw) { const webBase = "https://" + NMP_ENGINE_CFG.javdbSearchUrl; const movie = await nmpFindJavdbMovie(kw, { limit: 5, fallbackFirst: !0 }); if (!movie?.id) return { url: webBase, data: [] }; const detailUrl = `${webBase}/v/${movie.id}`; const r2 = await nmpGmFetch(`${NMP_JAVDB_API_BASE}/v1/movies/${encodeURIComponent(movie.id)}/magnets`, { headers: { accept: "application/json", jdSignature: nmpBuildJavdbSignature() } }); if (!r2.loadstuts || r2.status < 200 || r2.status >= 400) return { url: detailUrl, data: [] }; const magnets = Array.isArray(nmpParseJson(r2.responseText)?.data?.magnets) ? nmpParseJson(r2.responseText).data.magnets : []; return { url: detailUrl, data: magnets.map((item => { const hash = String(item?.hash || "").trim(); if (!hash) return null; const name = String(item?.name || movie.number || kw).trim(); const title = [name, item?.cnsub ? "-CH" : "", item?.hd ? "HD" : ""].filter(Boolean).join(" "); return { title, maglink: `magnet:?xt=urn:btih:${hash}`, size: nmpFormatJavdbSize(item?.size), date: item?.created_at || "", src: detailUrl, files: nmpNormalizeFileCount(item?.files_count, item?.file_count, item?.fileCount, item?.files) }; })).filter(Boolean) }; } function nmpTitleMatchesKeyword(title, keyword) { const rawTitle = String(title || "").toUpperCase(); const ckw = String(keyword || "").toUpperCase().replace(/[^A-Z0-9]/g, ""); if (!rawTitle || !ckw) return !1; const code = ckw.match(/^([A-Z]{2,15})0*(\d{2,10})$/); if (code) { const n = code[2].replace(/^0+(?=\d)/, ""); return new RegExp(`(?:^|[^A-Z0-9])${code[1]}[\\s._-]*0*${n}(?!\\d)`, "i").test(rawTitle); } return rawTitle.replace(/[^A-Z0-9]/g, "").includes(ckw); } async function nmpSearchCiligou(kw) { const base = "https://" + NMP_ENGINE_CFG.ciligouUrl; const encoded = btoa(unescape(encodeURIComponent(kw))).replace(/=+$/, ""); const searchUrl = `${base}/search?word=${encoded}`; const r = await nmpGmFetch(searchUrl, { headers: { Referer: base + "/", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } }); if (!r.loadstuts) return { url: searchUrl, data: [] }; const doc = nmpParseHTML(r.responseText), data = []; doc.querySelectorAll("#Search_list_wrapper li").forEach((li => { const titleA = li.querySelector("a.SearchListTitle_result_title"); if (!titleA) return; const href = titleA.getAttribute("href") || "", hash = href.split("/").pop(); if (!hash) return; const infoText = li.querySelector(".Search_list_info")?.textContent || ""; const sizeMatch = infoText.match(/文件大小[::]\s*([\d.,]+\s*(?:TiB|GiB|MiB|KiB|TB|GB|MB|KB|B))/i); const dateMatch = infoText.match(/创建时间[::]\s*(\d{4}[-/]\d{1,2}[-/]\d{1,2}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?)/); data.push({ title: titleA.textContent.trim(), maglink: `magnet:?xt=urn:btih:${hash}`, size: sizeMatch ? sizeMatch[1] : "", date: dateMatch ? dateMatch[1] : "", src: base + href }); })); return { url: searchUrl, data }; } async function nmpSearchU3C3(kw) { const base = "https://" + NMP_ENGINE_CFG.u3c3Url; const home = await nmpGmFetch(base + "/", { headers: { Referer: base + "/" } }); if (!home.loadstuts) return { url: base, data: [] }; const homeDoc = nmpParseHTML(home.responseText); const searchScript = [...homeDoc.scripts].map((s => s.textContent || "")).find((t => t.includes("function search21"))) || ""; const token = searchScript.match(/^\s*var\s+nmefafej\s*=\s*["']([^"']+)["'];?/m)?.[1] || ""; if (!token) return { url: base, data: [] }; const searchUrl = `${base}/?search2=${encodeURIComponent(token)}&search=${encodeURIComponent(kw)}`; const r = await nmpGmFetch(searchUrl, { headers: { Referer: base + "/" } }); if (!r.loadstuts) return { url: searchUrl, data: [] }; const doc = nmpParseHTML(r.responseText); const nkw = String(kw || "").toUpperCase().replace(/[-_\s]/g, ""); const data = [...doc.querySelectorAll("table.torrent-list tbody tr.default, table.torrent-list tbody tr.success")].map((row => { const titleA = row.querySelector('td:nth-child(2) a[href*="/view?id="]'); const magnetA = row.querySelector('td:nth-child(3) a[href^="magnet:"]'); const title = titleA?.getAttribute("title")?.trim() || titleA?.textContent?.trim() || ""; if (!title || !magnetA?.href) return null; if (nkw && !title.toUpperCase().replace(/[-_\s]/g, "").includes(nkw)) return null; const href = titleA.getAttribute("href") || ""; return { title, maglink: magnetA.href, size: row.querySelector("td:nth-child(4)")?.textContent?.trim() || "", date: row.querySelector("td:nth-child(5)")?.textContent?.trim() || "", src: href ? new URL(href, base).href : searchUrl }; })).filter(Boolean); return { url: r.finalUrl || searchUrl, data }; } function nmpPickBTSearchItems(json) { return Array.isArray(json?.data) ? json.data : Array.isArray(json?.data?.data) ? json.data.data : []; } function nmpCleanBTSearchText(v) { const raw = String(v || "").trim(); if (!raw) return ""; if (!/[<&]/.test(raw)) return raw.replace(/\s+/g, " "); const doc = nmpParseHTML(`<body>${raw}</body>`); return (doc.body?.textContent || raw.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim(); } function nmpNormalizeBTSearchItem(item, base, searchUrl, kw) { const title = nmpCleanBTSearchText(item?.name), hash = String(item?.hash || "").replace(/^magnet:\?xt=urn:btih:/i, "").replace(/[^a-z0-9]/gi, ""); if (!/^[a-f0-9]{32,40}$/i.test(hash)) return null; const src = item?.id ? `${base}/torrent/${encodeURIComponent(item.id)}?keyword=${encodeURIComponent(kw)}` : searchUrl; const date = item?.created_at || item?.created_time || item?.create_time || item?.createTime || item?.date || item?.added_at || item?.add_time || ""; return { title: title || `magnet:?xt=urn:btih:${hash}`, maglink: `magnet:?xt=urn:btih:${hash}`, size: nmpFormatBytes(item?.size), date, src, files: nmpNormalizeFileCount(item?.files_count, item?.file_count, item?.fileCount, item?.filesCount, item?.files_num, item?.file_num, item?.num_files, item?.count, item?.files, item?.fileList) }; } function nmpRandomNonce(len = 8) { const c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let n = ""; for (let i = 0; i < len; i++) n += c.charAt(Math.floor(Math.random() * c.length)); return n; } function nmpBuildBTSearchHeaders(params, referer) { const ts = Math.floor(Date.now() / 1e3).toString(), nonce = nmpRandomNonce(); const parts = [`timestamp=${ts}`, `nonce=${nonce}`]; Object.keys(params).forEach((k => parts.push(`${k}=${params[k]}`))); return { Accept: "application/json, text/plain, */*", Referer: referer, "x-timestamp": ts, "x-nonce": nonce, "x-sign": md5(`${parts.sort().join("&")}&key=long2ice`).toUpperCase() }; } async function nmpSearchBTSearch(kw) { const base = "https://" + NMP_ENGINE_CFG.btsearchUrl, searchUrl = `${base}/search?keyword=${encodeURIComponent(kw)}`; const params = { keyword: kw, limit: "10", offset: "0", mode: "", time: "", sort: "size", sort_type: "desc", size: "" }; const r = await nmpGmFetch(`${base}/api/search?${new URLSearchParams(params).toString()}`, { headers: nmpBuildBTSearchHeaders(params, searchUrl) }); if (!r.loadstuts || r.status < 200 || r.status >= 400) return { url: searchUrl, data: [] }; return { url: searchUrl, data: nmpPickBTSearchItems(nmpParseJson(r.responseText)).map((item => nmpNormalizeBTSearchItem(item, base, searchUrl, kw))).filter(Boolean) }; } async function nmpSearchSukebei(kw) { const base = "https://" + NMP_ENGINE_CFG.sukebeiUrl; const r = await nmpGmFetch(`${base}/?f=0&c=0_0&q=${kw}`); if (!r.loadstuts) return { url: base, data: [] }; const doc = nmpParseHTML(r.responseText); return { url: r.finalUrl || base, data: [...doc.querySelectorAll("tr.default, tr.success")].map((el => { const dc = el.querySelector("td:nth-child(5)"); return { title: el.querySelector("td:nth-child(2)>a:nth-child(1)")?.title || "", maglink: el.querySelector("td:nth-child(3)>a:last-child")?.href || "", size: el.querySelector("td:nth-child(4)")?.textContent?.trim() || "", date: dc?.textContent?.trim() || "", timestamp: Number(dc?.dataset?.timestamp) || 0, src: base + (el.querySelector("td:nth-child(2)>a:nth-child(1)")?.getAttribute("href") || "") }; })) }; } async function nmpSearchU9A9(kw) { const base = "https://" + NMP_ENGINE_CFG.u9a9Url, searchUrl = `${base}/?type=2&search=${encodeURIComponent(kw)}`; const r = await nmpGmFetch(searchUrl, { headers: { Referer: base + "/", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } }); if (!r.loadstuts) return { url: searchUrl, data: [] }; const doc = nmpParseHTML(r.responseText); return { url: r.finalUrl || searchUrl, data: [...doc.querySelectorAll("table.torrent-list tbody tr.default, table.torrent-list tbody tr.success")].map((el => { const titleA = el.querySelector("td:nth-child(2)>a:nth-child(1)"), magnetA = el.querySelector('td:nth-child(3)>a[href^="magnet:"]'); const href = titleA?.getAttribute("href") || "", src = href ? href.startsWith("http") ? href : new URL(href, base).href : searchUrl; return { title: titleA?.getAttribute("title") || titleA?.textContent?.trim() || "", maglink: magnetA?.href || "", size: el.querySelector("td:nth-child(4)")?.textContent?.trim() || "", date: el.querySelector("td:nth-child(5)")?.textContent?.trim() || "", src }; })).filter((item => item.title && item.maglink && nmpTitleMatchesKeyword(item.title, kw))) }; } async function nmpSearchSokitty(kw) { const base = "https://" + NMP_ENGINE_CFG.sokittyUrl, searchUrl = `${base}/search?key=${encodeURIComponent(kw)}`; const r = await nmpGmFetch(searchUrl, { headers: { Referer: base + "/" } }); if (!r.loadstuts) return { url: searchUrl, data: [] }; const doc = nmpParseHTML(r.responseText), norm = s => s.toUpperCase().replace(/[-_\s]/g, ""), kwN = norm(kw), data = []; doc.querySelectorAll(".panel.search-panel").forEach((panel => { const titleA = panel.querySelector("h3.panel-title > a.list-title"); if (!titleA) return; const href = titleA.getAttribute("href") || ""; if (!href.startsWith("/bt/")) return; const hash = href.replace("/bt/", ""), title = titleA.textContent.trim(); if (!hash || !norm(title).includes(kwN)) return; const info = [...panel.querySelectorAll(".panel-footer .info-item")]; data.push({ title, maglink: `magnet:?xt=urn:btih:${hash}`, size: info[0]?.textContent?.trim() || "", date: info[2]?.textContent?.trim() || "", src: base + href }); })); return { url: searchUrl, data }; } function nmpGetEngines() { return { [NMP_ENGINE_CFG.javdbSearchUrl]: nmpSearchJavDB, [NMP_ENGINE_CFG.ciligouUrl]: nmpSearchCiligou, [NMP_ENGINE_CFG.u3c3Url]: nmpSearchU3C3, [NMP_ENGINE_CFG.btsearchUrl]: nmpSearchBTSearch, [NMP_ENGINE_CFG.sukebeiUrl]: nmpSearchSukebei, [NMP_ENGINE_CFG.u9a9Url]: nmpSearchU9A9, [NMP_ENGINE_CFG.sokittyUrl]: nmpSearchSokitty }; } function nmpEngineLabels() { return { [NMP_ENGINE_CFG.javdbSearchUrl]: "JavDB", [NMP_ENGINE_CFG.ciligouUrl]: "CiliGou", [NMP_ENGINE_CFG.u3c3Url]: "U3C3", [NMP_ENGINE_CFG.btsearchUrl]: "BTSearch", [NMP_ENGINE_CFG.sukebeiUrl]: "Sukebei", [NMP_ENGINE_CFG.u9a9Url]: "U9A9", [NMP_ENGINE_CFG.sokittyUrl]: "SoKitty" }; } function nmpNormalizeAggregateItems(data, { nativeItems = [] } = {}) { const nativeTagMap = new Map(nativeItems.map((item => { const m = String(item?.maglink || "").match(/[?&]xt=urn:btih:([^&]+)/i); return [m ? m[1].toLowerCase() : "", item?.nativeTags || []]; })).filter(([h]) => h)); return data.map((item => { const m = String(item?.maglink || "").match(/[?&]xt=urn:btih:([^&]+)/i); const hash = m ? m[1].toLowerCase() : ""; return { title: String(item?.title || item?.maglink || ""), maglink: item?.maglink || "", src: item?.src || "", size: String(item?.size || ""), files: String(item?.files || ""), date: item?.date || "", timestamp: item?.timestamp || 0, qualityTags: nmpQualityTags(item?.title), nativeTags: nativeTagMap.get(hash) || [] }; })).filter((item => item.title && item.maglink)); } class NativeMagnetPanelPlugin extends X { getName() { return "NativeMagnetPanelPlugin"; } async initCss() { return ` <style> .${NMP_PANEL_CLASS}{width:100%;box-sizing:border-box;margin:14px 0;padding:0;color:#172033;background:#fffdfa;border:1px solid #e6ddd3;border-radius:7px;box-shadow:0 8px 24px rgba(68,49,31,.07)} .jhs-nmp-head,.jhs-nmp-tab-tools,.jhs-nmp-tabs,.jhs-nmp-controls,.jhs-nmp-actions,.jhs-nmp-metadata,.jhs-nmp-title-line{display:flex;align-items:center} .jhs-nmp-controls[hidden]{display:none!important} .jhs-nmp-controls{gap:8px;min-height:48px;padding:9px 14px;background:#fffbf6;border-bottom:1px solid #e6ddd3} .jhs-nmp-tabs{flex-wrap:wrap;gap:5px} .jhs-nmp-tab{min-height:30px;padding:3px 8px;border:1px solid transparent;border-radius:4px;color:#475467;background:#fff;font-size:12px;font-weight:600;cursor:pointer;transition:background .16s ease,border-color .16s ease,color .16s ease} .jhs-nmp-tab[aria-selected="true"]{color:#1d4ed8;background:#e8f0ff;border-color:#c9d9ff} .jhs-nmp-tab:not([aria-selected="true"]):hover{color:#1d4ed8;background:#f5f8fc;border-color:#a8c2ff} .jhs-nmp-default-tab{height:30px;padding:2px 24px 2px 8px;border:1px solid #ddd2c4;border-radius:4px;color:#77543b;background:#fffbf6;font-size:11px;cursor:pointer} .jhs-nmp-default-tab:focus-visible{outline:2px solid #a85b2d;outline-offset:2px} html[data-theme="dark"] .jhs-nmp-default-tab{color:#d9e1ea;background:#253141;border-color:#536276} html[data-theme="dark"] .jhs-nmp-controls{background:#1d2632;border-color:#435062} html[data-theme="dark"] .jhs-nmp-tab{color:#d9e1ea;background:#253141;border-color:#536276} html[data-theme="dark"] .jhs-nmp-tab[aria-selected="true"]{color:#dce9ff;background:#244b89;border-color:#4e82d5} .jhs-nmp-head{justify-content:space-between;gap:14px;min-height:55px;padding:10px 14px;border-bottom:1px solid #e6ddd3} .jhs-nmp-title{color:#172033;font-size:16px;font-weight:720} .jhs-nmp-count{margin-left:9px;color:#667085;font-size:12px;font-weight:500} .jhs-nmp-tab-tools{flex-wrap:wrap;justify-content:flex-end;gap:8px} .jhs-nmp-refresh,.jhs-nmp-action{min-height:30px;padding:3px 8px;border:1px solid #cdd6e2;border-radius:4px;color:#475467;background:#fff;font-size:12px;font-weight:600;cursor:pointer;transition:background .16s ease,border-color .16s ease,color .16s ease} .jhs-nmp-refresh{color:#1d4ed8;border-color:#bdd0ff;background:#f6f9ff} .jhs-nmp-refresh:hover,.jhs-nmp-action:hover{color:#1d4ed8;background:#f5f8fc;border-color:#a8c2ff} .jhs-nmp-refresh:active,.jhs-nmp-action:active{background:#e8f0ff} .jhs-nmp-select{height:30px;max-width:150px;padding:2px 24px 2px 8px;border:1px solid #cdd6e2;border-radius:4px;color:#344054;background:#fff;font-size:12px} .jhs-nmp-rows{display:grid} .jhs-nmp-row{display:grid;grid-template-columns:20px minmax(0,1fr) auto;grid-template-areas:"index name actions" "index metadata actions";align-items:center;column-gap:6px;row-gap:6px;min-height:64px;padding:10px 12px;border-bottom:1px solid #eee7df;transition:background .16s ease} .jhs-nmp-row:last-child{border-bottom:0} .jhs-nmp-row:hover{background:#fff9f1} .jhs-nmp-index{grid-area:index;color:#8a98a9;font-size:12px;font-variant-numeric:tabular-nums;text-align:right} .jhs-nmp-name,.jhs-nmp-title-line{min-width:0} .jhs-nmp-name{grid-area:name} .jhs-nmp-title-line{gap:7px} .jhs-nmp-title-line>a{flex:0 1 auto;max-width:100%;min-width:0;overflow:hidden;color:#243b67;font-size:13px;font-weight:650;line-height:1.45;text-decoration:none;text-overflow:ellipsis;white-space:nowrap} .jhs-nmp-title-line>a:hover{color:#1d4ed8;text-decoration:underline} .jhs-nmp-tags{display:flex;flex:0 0 auto;flex-wrap:wrap;gap:5px} .jhs-nmp-tag{display:inline-flex;align-items:center;min-height:20px;padding:1px 6px;border:1px solid #dfe5eb;border-radius:3px;color:#58677a;background:#f3f5f7;font-size:11px;font-weight:700;line-height:1;white-space:nowrap} .jhs-nmp-tag[data-kind="four-k"]{color:#1d4ed8;background:#e8f0ff;border-color:#c9d9ff} .jhs-nmp-tag[data-kind="subtitle"]{color:#a85b00;background:#fff2d9;border-color:#ffe0a5} .jhs-nmp-tag[data-kind="cracked"]{color:#9f1239;background:#ffe4e6;border-color:#fecdd3} .jhs-nmp-metadata{grid-area:metadata;display:flex;flex-wrap:wrap;gap:5px 15px} .jhs-nmp-meta{display:inline-flex;align-items:baseline;gap:4px;color:#344054;font-size:12px;font-weight:650;font-variant-numeric:tabular-nums;white-space:nowrap} .jhs-nmp-meta::before{content:attr(data-label);color:#7b8796;font-size:11px;font-weight:500} .jhs-nmp-actions{grid-area:actions;align-self:center;justify-content:flex-end;gap:6px;min-width:max-content} .jhs-nmp-action-copy{color:#4b3d87;border-color:#d0c7ee;background:#f6f4ff} .jhs-nmp-action-check{color:#176b9e;border-color:#b8d9ed;background:#f0f9ff} .jhs-nmp-action-offline{color:#137553;border-color:#b7ddce;background:#effaf5} .jhs-nmp-action-copy:hover{color:#3d3075;border-color:#b9aae5;background:#eeebff} .jhs-nmp-action-check:hover{color:#115b89;border-color:#91c5e4;background:#e5f5ff} .jhs-nmp-action-offline:hover{color:#0f6548;border-color:#8ac9b3;background:#e3f6ed} .jhs-nmp-empty{padding:22px 14px;color:#64748b;text-align:center;font-size:13px} .jhs-nmp-foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:10px 14px;color:#667085;background:#fffbf6;border-top:1px solid #eee7df;font-size:11px} html[data-theme="dark"] .${NMP_PANEL_CLASS}{color:#e5e7eb;background:#222b38;border-color:#435062;box-shadow:none} html[data-theme="dark"] .jhs-nmp-head{border-color:#435062} html[data-theme="dark"] .jhs-nmp-foot{background:#1d2632} html[data-theme="dark"] .jhs-nmp-title,html[data-theme="dark"] .jhs-nmp-meta,html[data-theme="dark"] .jhs-nmp-title-line>a{color:#e5e7eb} html[data-theme="dark"] .jhs-nmp-row{border-color:#374454} html[data-theme="dark"] .jhs-nmp-row:hover{background:#293545} html[data-theme="dark"] .jhs-nmp-count,html[data-theme="dark"] .jhs-nmp-foot,html[data-theme="dark"] .jhs-nmp-empty,html[data-theme="dark"] .jhs-nmp-meta::before,html[data-theme="dark"] .jhs-nmp-index{color:#aeb9c7} html[data-theme="dark"] .jhs-nmp-action,html[data-theme="dark"] .jhs-nmp-select{color:#d9e1ea;background:#253141;border-color:#536276} html[data-theme="dark"] .jhs-nmp-refresh{color:#c8dbff;background:#1f3658;border-color:#4775bc} @media (max-width:760px){ .jhs-nmp-head{align-items:flex-start;flex-direction:column;gap:8px;padding:11px 12px} .jhs-nmp-tab,.jhs-nmp-refresh,.jhs-nmp-action,.jhs-nmp-select{min-height:44px} .jhs-nmp-controls{width:100%;align-items:flex-start;flex-wrap:wrap;min-height:0;padding:9px 12px} .jhs-nmp-row{grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"name index" "metadata metadata" "actions actions";gap:8px;min-height:0;padding:11px 12px} .jhs-nmp-index{align-self:start;padding-top:2px} .jhs-nmp-title-line{align-items:flex-start;flex-wrap:wrap} .jhs-nmp-title-line>a{flex:1 1 100%;overflow:visible;white-space:normal} .jhs-nmp-metadata{gap:10px} .jhs-nmp-actions{justify-content:flex-start;flex-wrap:wrap} .jhs-nmp-foot{padding:10px 12px} } </style> `; } /** 挂载: 面板插在原生磁力列表后面(同一个 tab-panel 内), 从而跟随「磁力」标签显隐 */ mount(site, avid, sortMode, engineKey, defaultTab) { if (document.querySelector(`.${NMP_PANEL_CLASS}[data-site="${site}"]`)) return !0; const source = "javbus" === site ? document.querySelector("#magnet-table") : document.querySelector("#magnets-content"); if (!source) return !1; const nativeItems = nmpBuildNativeItems(site, source); if (!nativeItems.length) return !1; const panel = nmpCreatePanel(site, avid, nativeItems, sortMode, engineKey, defaultTab); const article = source.closest("article.message.video-panel") || source.closest("article.message"); if (article && article.parentNode) { article.insertAdjacentElement("afterend", panel); nmpHideNativeParent(article); } else { source.insertAdjacentElement("afterend", panel); nmpHideNativeSource(source); } // 绑定磁力 tab 的点击接管 (修复第二次点击不显示面板的问题) if (r) jhsBindMagnetTab(); return !0; } /** 原生磁链是异步渲染的, 用 MutationObserver 等待其就绪后再挂载 */ scheduleMount(site, avid, sortMode, engineKey, defaultTab) { const tryMount = () => { try { return this.mount(site, avid, sortMode, engineKey, defaultTab); } catch (e) { return clog.error("[磁力面板] 挂载失败", e), !0; } }; if (tryMount()) return; if ("undefined" == typeof MutationObserver || !document.documentElement) { [ 300, 800, 1600, 3e3, 6e3, 1e4 ].forEach((d => setTimeout(tryMount, d))); return; } let pending = !1; const observer = new MutationObserver((() => { if (pending) return; pending = !0, queueMicrotask((() => { pending = !1, tryMount() && observer.disconnect(); })); })); observer.observe(document.documentElement, { childList: !0, subtree: !0 }); setTimeout((() => observer.disconnect()), 3e4); } async handle() { if (!window.isDetailPage) return; const sortMode = await storageManager.getSetting(NMP_SORT_KEY, "size"); const engineKey = await storageManager.getSetting(NMP_ENGINE_SELECT_KEY, NMP_DEFAULT_ENGINE_KEY); const defaultTab = await storageManager.getSetting("nmpDefaultTab", "native"); const site = r ? "javdb" : l ? "javbus" : ""; if (!site) return; const avid = this.getPageInfo().carNum || ""; // JavDB: 直接走 API 拉磁力, 不等原生 DOM, 加速显示 if (r && avid) { this._fastMountJavdb(avid, sortMode, engineKey, defaultTab); } else { this.scheduleMount(site, avid, sortMode, engineKey, defaultTab); } } /** JavDB 详情页: 直接调 API 拉磁力数据并渲染面板, 不等原生 #magnets-content */ async _fastMountJavdb(avid, sortMode, engineKey, defaultTab) { if (document.querySelector(`.${NMP_PANEL_CLASS}[data-site="javdb"]`)) return; // 先立即创建空面板框架(无数据), 让用户马上看到面板UI const magnetsPane = document.getElementById("magnets"); const emptyPanel = nmpCreatePanel("javdb", avid, [], sortMode, engineKey, defaultTab); emptyPanel.querySelector(".jhs-nmp-rows").innerHTML = '<div class="jhs-nmp-empty" style="padding:20px;text-align:center;color:#999;">正在读取磁力数据...</div>'; if (magnetsPane) { magnetsPane.appendChild(emptyPanel); } else { await new Promise(res => { const t = setInterval(() => { const p = document.getElementById("magnets"); if (p) { clearInterval(t); p.appendChild(emptyPanel); res(); } }, 100); setTimeout(() => { clearInterval(t); res(); }, 5000); }); } if (r) jhsBindMagnetTab(); // 后台异步加载数据, 完成后填充面板 (async () => { try { const movieId = window.location.href.split("/").pop().split(/[?#]/)[0]; if (!movieId) throw new Error("无法获取 movieId"); const r2 = await nmpGmFetch(`${NMP_JAVDB_API_BASE}/v1/movies/${encodeURIComponent(movieId)}/magnets`, { headers: { accept: "application/json", jdSignature: nmpBuildJavdbSignature() }, timeout: 2e4 }); if (!r2.loadstuts || r2.status < 200 || r2.status >= 400) throw new Error("API 请求失败 HTTP " + (r2.status || 0)); const json = nmpParseJson(r2.responseText); const magnets = Array.isArray(json?.data?.magnets) ? json.data.magnets : []; const nativeItems = magnets.map(item => { const hash = String(item?.hash || "").trim(); if (!hash) return null; const name = String(item?.name || avid).trim(); const title = [name, item?.cnsub ? "-CH" : "", item?.hd ? "HD" : ""].filter(Boolean).join(" "); return { title, maglink: `magnet:?xt=urn:btih:${hash}`, size: nmpFormatJavdbSize(item?.size), date: item?.created_at || "", src: window.location.href, files: nmpNormalizeFileCount(item?.files_count, item?.file_count, item?.fileCount, item?.files) }; }).filter(Boolean); if (!nativeItems.length) { emptyPanel.querySelector(".jhs-nmp-rows").innerHTML = '<div class="jhs-nmp-empty" style="padding:20px;text-align:center;color:#999;">暂无磁力资源</div>'; return; } // 用数据重新渲染行 const state = emptyPanel._jhsNmpState; if (state) { state.nativeItems = nativeItems; state.items = nativeItems; nmpRenderRows(state); } else { // fallback: 替换整个面板 const newPanel = nmpCreatePanel("javdb", avid, nativeItems, sortMode, engineKey, defaultTab); emptyPanel.replaceWith(newPanel); } // 隐藏原生磁力列表 const source = document.querySelector("#magnets-content"); if (source) { const article = source.closest("article.message.video-panel") || source.closest("article.message"); if (article) nmpHideNativeParent(article); else nmpHideNativeSource(source); } } catch (e) { clog.error("[磁力面板] API 快速加载失败:", e); emptyPanel.querySelector(".jhs-nmp-rows").innerHTML = `<div class="jhs-nmp-empty" style="padding:20px;text-align:center;color:#c00;">磁力加载失败: ${e.message || e}</div>`; } })(); } } const NRP_SETTING_KEY = "enableNativeReviewPanel"; const NRP_INITIAL_LIMIT = 6; const NRP_PAGE_SIZE = 20; /** 统一切换 JavDB 详情页三 tab (磁力/短评/相关清单) 的显隐与高亮. * 仅用 is-hidden 类, 不用内联 display, 避免与 JavDB 原生逻辑冲突导致再次点击失效 */ function jhsSwitchDetailTab(activeId) { const map = { magnets: "magnetTab", reviews: "reviewTab", lists: "listTab" }; Object.entries(map).forEach((([ paneId, target ]) => { const pane = document.getElementById(paneId); if (pane) { pane.style.display = ""; pane.classList.toggle("is-hidden", paneId !== activeId); } const li = document.querySelector(`[data-movie-tab-target="${target}"]`); if (li) li.classList.toggle("is-active", paneId === activeId); })); // 115匹配框位于标签容器外, 需手动只在「磁力」标签下显示 document.querySelectorAll(".jhs-115-match-detail").forEach((el => { el.style.display = "magnets" === activeId ? "" : "none"; })); } /** 给「磁力」tab 绑定点击 (只需一次): 点磁力时统一切到磁力 pane. * 修复: 因短评/相关清单拦截了各自 tab, JavDB 的 active 状态不同步, * 导致再次点磁力时 JavDB 认为已激活而不处理, 磁力面板保持隐藏 */ function jhsBindMagnetTab() { const link = document.querySelector('[data-movie-tab-target="magnetTab"] a'); if (!link || link.dataset.jhsMagBound === "1") return; link.dataset.jhsMagBound = "1"; link.removeAttribute("data-action"); link.removeAttribute("data-url"); link.addEventListener("click", (e => { e.preventDefault(); e.stopImmediatePropagation(); jhsSwitchDetailTab("magnets"); }), !0); } /** 星级渲染 (1-5 星) */ function nrpRenderStars(score) { const v = Math.max(0, Math.min(5, parseInt(score, 10) || 0)); return `<span class="jhs-nrp-stars">${"★".repeat(v)}${"☆".repeat(5 - v)}</span>`; } /** 正文中的 magnet / ed2k / http 链接自动变成可点击 <a> */ function nrpRenderLinkedText(text) { const str = String(text || ""); const re = /((?:magnet:\?|ed2k:\/\/|https?:\/\/)[^\s"'<>]+)/gi; let html = "", last = 0; str.replace(re, ((match, _link, offset) => { html += escapeHtml(str.slice(last, offset)); const safe = escapeHtml(match); html += `<a class="jhs-nrp-link" href="${safe}" target="_blank" rel="noopener noreferrer">${safe}</a>`; last = offset + match.length; return match; })); html += escapeHtml(str.slice(last)); return html.replace(/\n/g, "<br>"); } /** 渲染单条短评 (结构对齐 lao.json javdb-api-review) */ function nrpRenderItem(item, num) { const date = String(item?.created_at || "").slice(0, 10); const likes = Number(item?.likes_count || 0); return `<div class="jhs-nrp-review"> <div class="jhs-nrp-review-head"> <span><strong>#${num}</strong> ${escapeHtml(item?.username || "匿名")}</span> <span>${nrpRenderStars(item?.score)} ${escapeHtml(date)}${likes ? ` · 點讚:${likes}` : ""}</span> </div> <div class="jhs-nrp-review-content">${nrpRenderLinkedText(item?.content || "")}</div> </div>`; } /** 读取 JavDB App 授权 token, 依次尝试: YA3 自身登录 -> 老司机脚本本地存储 -> 老司机脚本 GM 存储 */ async function nrpGetJavdbToken() { // 1) YA3 自身登录后的 token (加密存于 localStorage[jhs_appAuthorization]) try { const enc = localStorage.getItem("jhs_appAuthorization"); if (enc) { const dec = await decryptData(enc); if (dec) return String(dec).replace(/^Bearer\s+/i, ""); } } catch (e) { /* 解密失败, 尝试后续来源 */ } // 2) 老司机脚本存的 token (明文, localStorage) try { const t = localStorage.getItem("laosiji_javdb_app_authorization"); if (t) return String(t).replace(/^Bearer\s+/i, ""); } catch (e) {} // 3) 老司机脚本 GM_setValue 存的 token try { if ("function" === typeof GM_getValue) { const t = GM_getValue("laosiji_javdb_app_authorization", ""); if (t) return String(t).replace(/^Bearer\s+/i, ""); } } catch (e) {} return ""; } /** 生成短评去重键: 优先 id, 兜底 username+created_at+content 前缀 * sort_by=hotly 的榜单会实时抖动, 翻页时页边界条目可能重复出现 */ function nrpReviewKey(item) { if (item && (item.id || item._id)) return String(item.id || item._id); const u = String(item?.username || ""); const t = String(item?.created_at || ""); const c = String(item?.content || "").slice(0, 40); return `${u}|${t}|${c}`; } /** 调用 JavDB API 拉取短评 — 对齐 jhs.json javDbApi.getReviews: * 真分页 page + 固定 limit. API 对 limit 有 50 上限, 放大 limit 无效, 只能靠翻页 */ async function nrpFetchReviews(movieId, page = 1, limit = NRP_PAGE_SIZE) { const params = new URLSearchParams({ page: String(page), limit: String(limit), sort_by: "hotly" }); const url = `${NMP_JAVDB_API_BASE}/v1/movies/${encodeURIComponent(movieId)}/reviews?${params.toString()}`; const headers = { accept: "application/json", "accept-language": "zh-TW", "user-agent": "Dart/3.5 (dart:io)", jdSignature: nmpBuildJavdbSignature() }; const token = await nrpGetJavdbToken(); if (token) headers.authorization = `Bearer ${token}`; const res = await nmpGmFetch(url, { headers, timeout: 2e4 }); if (!res.loadstuts || res.status < 200 || res.status >= 400) throw new Error("短评接口请求失败 HTTP " + (res.status || 0)); const json = nmpParseJson(res.responseText); return Array.isArray(json?.data?.reviews) ? json.data.reviews : []; } class NativeReviewPanelPlugin extends X { getName() { return "NativeReviewPanelPlugin"; } async initCss() { return ` <style> .jhs-nrp-panel{width:100%;box-sizing:border-box;color:#172033;background:#f5f5f5;border:1px solid #dddddd;border-radius:7px;overflow:hidden} .jhs-nrp-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;min-height:55px;padding:10px 14px;border-bottom:1px solid #dddddd;background:#f5f5f5} .jhs-nrp-toolbar-left,.jhs-nrp-toolbar-right{display:flex;align-items:center;gap:8px} .jhs-nrp-title{color:#172033;font-size:16px;font-weight:720} .jhs-nrp-count{margin-left:4px;color:#667085;font-size:12px;font-weight:500} .jhs-nrp-toolbar-label{color:#475467;font-size:13px;font-weight:600} .jhs-nrp-sort,.jhs-nrp-fontsize{height:30px;padding:2px 24px 2px 8px;border:1px solid #cdd6e2;border-radius:4px;color:#344054;background:#fff;font-size:12px;font-weight:600;cursor:pointer} .jhs-nrp-toggle{min-height:30px;padding:0 12px;border:1px solid #cdd6e2;border-radius:4px;background:#fff;color:#344054;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap} .jhs-nrp-toggle:hover{background:#eef2f7;border-color:#a8c2ff} .jhs-nrp-items{--nrp-font-size:15px} .jhs-nrp-items .jhs-nrp-review-content{font-size:var(--nrp-font-size)} .jhs-nrp-loading,.jhs-nrp-empty,.jhs-nrp-error,.jhs-nrp-end{padding:11px 14px;color:#6b7280;text-align:center;font-size:13px;font-weight:600} .jhs-nrp-error{color:#be123c} .jhs-nrp-review{margin:0;padding:11px 14px;border-bottom:1px solid #dddddd;background:#f5f5f5;word-break:break-word} .jhs-nrp-review:last-child{border-bottom:0} .jhs-nrp-review-head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;color:#334155;font-size:13px} .jhs-nrp-review-head strong{color:#4a72d4;font-weight:700;margin-right:4px} .jhs-nrp-stars{color:#f59e0b;letter-spacing:1px} .jhs-nrp-review-content{margin-top:7px;color:#1f2937;font-size:15px;line-height:1.65;white-space:normal} .jhs-nrp-link{color:#1d4ed8;text-decoration:underline} .jhs-nrp-footer{padding:12px 14px;border-top:1px solid #dddddd;background:#f5f5f5} .jhs-nrp-more{width:100%;min-height:34px;padding:6px 12px;border:0;border-radius:0;background:transparent;color:#1d4ed8;font-size:13px;font-weight:800;cursor:pointer} .jhs-nrp-more:hover{background:transparent;text-decoration:underline} .jhs-nrp-more:disabled{opacity:.6;cursor:wait} html[data-theme="dark"] .jhs-nrp-panel{background:#222b38;border-color:#435062} html[data-theme="dark"] .jhs-nrp-review{background:#222b38;border-color:#374454} html[data-theme="dark"] .jhs-nrp-review-head{color:#aeb9c7} html[data-theme="dark"] .jhs-nrp-review-content{color:#e5e7eb} html[data-theme="dark"] .jhs-nrp-footer{background:#1d2632;border-color:#435062} html[data-theme="dark"] .jhs-nrp-more{color:#c8dbff;background:transparent;border:0} html[data-theme="dark"] .jhs-nrp-toolbar{background:#1d2632;border-color:#435062} html[data-theme="dark"] .jhs-nrp-title{color:#e5e7eb} html[data-theme="dark"] .jhs-nrp-count,html[data-theme="dark"] .jhs-nrp-toolbar-label{color:#aeb9c7} html[data-theme="dark"] .jhs-nrp-sort,html[data-theme="dark"] .jhs-nrp-fontsize,html[data-theme="dark"] .jhs-nrp-toggle{color:#e5e7eb;background:#253141;border-color:#536276} </style> `; } /** 接管 JavDB 原生「短评」tab (完整照搬 lao.json 手法) */ mount(movieId) { const pane = document.getElementById("reviews"); const tabLink = document.querySelector('[data-movie-tab-target="reviewTab"] a'); if (!pane || !tabLink) return !1; if (tabLink.dataset.jhsNrpBound === "1") return !0; tabLink.dataset.jhsNrpBound = "1"; // 关键 1: 摘掉 JavDB 原生 Stimulus/Turbo 的加载指令 (VIP 限制的源头) tabLink.removeAttribute("data-action"); tabLink.removeAttribute("data-url"); pane.removeAttribute("src"); pane.removeAttribute("data-url"); pane.removeAttribute("data-src"); // 关键 2: 捕获阶段拦截点击, 阻断 JavDB 自己的处理链, 由脚本渲染 const onTabClick = (e => { e.preventDefault(); e.stopImmediatePropagation(); this._activateTab(tabLink, pane, movieId); }); tabLink.addEventListener("click", onTabClick, !0); // 关键 3: 若「短评」本来就是激活态, 立即渲染 const li = tabLink.closest("li"); if (li && li.classList.contains("is-active")) this._activateTab(tabLink, pane, movieId); return !0; } /** 切到短评 tab: 直接用 _loadEnhancedReviews 渲染短评区 * (评论增强 ReviewPlugin 已移除, 数据直接走 R() API) */ _activateTab(tabLink, pane, movieId) { jhsBindMagnetTab(); jhsSwitchDetailTab("reviews"); if (pane.querySelector(".jhs-nrp-review-wrap")) return; pane.innerHTML = '<div class="jhs-nrp-review-wrap"></div>'; this._loadEnhancedReviews(pane.querySelector(".jhs-nrp-review-wrap"), movieId); } /** 在短评 pane 里渲染评论区, UI 风格与「相关清单面板」(jhs-nlp-*) 保持一致: * 同一套 jhs-nrp-* 配色 — 面板外框 + toolbar 标题栏 + 卡片列表 + footer 加载更多. * 数据走 R() (与「评论增强」同一个 reviews 接口), 分页对齐 jhs.json: page 递增, * 本页不满一页即到底 */ async _loadEnhancedReviews(wrap, movieId) { const $wrap = $(wrap); const limit = await storageManager.getSetting("reviewCount", 20); $wrap.html('<section class="jhs-nrp-panel"><div class="jhs-nrp-loading">正在读取短评...</div></section>'); let data = null; try { data = await R(movieId, 1, limit); } catch (e) { clog.error("[短评面板] 加载失败:", e); $wrap.html(`<section class="jhs-nrp-panel"><div class="jhs-nrp-error">短评读取失败: ${escapeHtml(e.message || String(e))}</div></section>`); return; } if (!data || !data.length) { $wrap.html('<section class="jhs-nrp-panel"><div class="jhs-nrp-empty">暂无短评</div></section>'); return; } // 关键词过滤 (与「评论增强」displayReviews 的过滤规则一致) const keywords = await storageManager.getReviewFilterKeywordList(); const filterRe = keywords && keywords.length ? new RegExp(keywords.map((k => String(k).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))).join("|")) : null; // 数量优先取原生「短评」tab 上的总数, 取不到则用已加载数 const nativeTabText = document.querySelector('[data-movie-tab-target="reviewTab"] a')?.textContent || ""; const countText = (nativeTabText.match(/(\d+)/) || [])[1] || String(data.length); $wrap.html(` <section class="jhs-nrp-panel"> <header class="jhs-nrp-toolbar"> <div class="jhs-nrp-toolbar-left"> <span class="jhs-nrp-title">短评</span> <span class="jhs-nrp-count">${escapeHtml(countText)} 条</span> </div> <div class="jhs-nrp-toolbar-right"> <span class="jhs-nrp-toolbar-label">排序</span> <select class="jhs-nrp-sort"> <option value="time" selected>时间</option> <option value="score">星级</option> <option value="likes">点赞</option> </select> <button type="button" class="jhs-nrp-toggle">收起短评</button> </div> </header> <div class="jhs-nrp-items"></div> <div class="jhs-nrp-footer"></div> </section>`); const $items = $wrap.find(".jhs-nrp-items"); const $footer = $wrap.find(".jhs-nrp-footer"); // 累积到全量列表, 排序时重绘 (排序按当前已加载的数据本地排序) const all = []; let sortMode = "time"; const sortList = (arr => { const list = arr.slice(); if (sortMode === "score") { return list.sort(((a, b) => (Number(b?.score) || 0) - (Number(a?.score) || 0))); } if (sortMode === "likes") { return list.sort(((a, b) => (Number(b?.likes_count) || 0) - (Number(a?.likes_count) || 0))); } // time: 最新在前 return list.sort(((a, b) => (Date.parse(b?.created_at) || 0) - (Date.parse(a?.created_at) || 0))); }); const renderAll = () => { const sorted = sortList(all).filter((it => !(filterRe && filterRe.test(String(it?.content || ""))))); const html = sorted.map(((it, i) => nrpRenderItem(it, i + 1))).join(""); $items.html(html); }; const addBatch = (list => { if (!list || !list.length) return; all.push(...list); renderAll(); }); addBatch(data); // 排序变更 $wrap.find(".jhs-nrp-sort").on("change", (function() { sortMode = this.value; renderAll(); })); // 收起/展开 $wrap.find(".jhs-nrp-toggle").on("click", (function() { const willCollapse = $items.is(":visible"); $items.toggle(!willCollapse); $footer.toggle(!willCollapse); $(this).text(willCollapse ? "展开短评" : "收起短评"); })); // 加载更多: page 递增, 本页不满一页即到底 let page = 1; const renderFooter = (hasMore => { if (!hasMore) { $footer.html('<div class="jhs-nrp-end">已加载全部短评</div>'); return; } $footer.html('<button type="button" class="jhs-nrp-more">加载更多短评</button>'); $footer.find(".jhs-nrp-more").on("click", (async () => { const $btn = $footer.find(".jhs-nrp-more"); $btn.text("加载中...").prop("disabled", !0); page++; let next = null; try { next = await R(movieId, page, limit); } catch (e) { $btn.text("加载失败, 点击重试").prop("disabled", !1); return; } addBatch(next); renderFooter((next || []).length >= limit); })); }); renderFooter(data.length >= limit); } /** 原生 pane 是异步渲染的, 用 MutationObserver 等它出现 */ scheduleMount(movieId) { const tryMount = () => { try { return this.mount(movieId); } catch (e) { return clog.error("[短评面板] 挂载失败", e), !0; } }; if (tryMount()) return; if ("undefined" == typeof MutationObserver || !document.documentElement) { [ 300, 800, 1600, 3e3, 6e3, 1e4 ].forEach((d => setTimeout(tryMount, d))); return; } let pending = !1; const observer = new MutationObserver((() => { if (pending) return; pending = !0, queueMicrotask((() => { pending = !1, tryMount() && observer.disconnect(); })); })); observer.observe(document.documentElement, { childList: !0, subtree: !0 }); setTimeout((() => observer.disconnect()), 3e4); } async _load(panel, movieId) { try { const reviews = await nrpFetchReviews(movieId, 1, NRP_PAGE_SIZE); const seen = new Set(); for (const rv of reviews) seen.add(nrpReviewKey(rv)); panel._jhsAllReviews = reviews; panel._jhsSeenKeys = seen; panel._jhsPage = 1; // jhs.json 手法: 本页取满一页就认为还有下一页, 完全以 API 返回为准 panel._jhsHasMore = reviews.length >= NRP_PAGE_SIZE; panel._jhsSortMode = "time"; panel._jhsFontSize = "中"; panel._jhsCollapsed = !1; this._render(panel, panel._jhsAllReviews, movieId, 0); } catch (e) { clog.error("[短评面板] 加载失败:", e); panel.innerHTML = `<div class="jhs-nrp-error">短评读取失败: ${escapeHtml(e.message || String(e))}</div>`; } } /** 排序短评 */ _sortReviews(reviews, mode) { const arr = [ ...reviews ]; if ("score" === mode) return arr.sort(((a, b) => (Number(b.score) || 0) - (Number(a.score) || 0))); if ("likes" === mode) return arr.sort(((a, b) => (Number(b.likes_count) || 0) - (Number(a.likes_count) || 0))); // 默认 time: 最新在前 return arr.sort(((a, b) => { const ta = Date.parse(a.created_at) || 0, tb = Date.parse(b.created_at) || 0; return tb - ta; })); } _render(panel, reviews, movieId, offset) { if (!reviews.length) { panel.innerHTML = '<div class="jhs-nrp-empty">暂无短评</div>'; return; } const sorted = this._sortReviews(reviews, panel._jhsSortMode || "time"); const items = sorted.map(((item, i) => nrpRenderItem(item, offset + i + 1))).join(""); const fontSize = panel._jhsFontSize || "中"; const collapsed = panel._jhsCollapsed || !1; // 数量优先取原生「短评」tab 上的总数, 保持与按钮同步; 取不到则用已加载数 const nativeTabText = document.querySelector('[data-movie-tab-target="reviewTab"] a')?.textContent || ""; const nativeCount = (nativeTabText.match(/(\d+)/) || [])[1]; const countText = nativeCount || String(sorted.length); panel.innerHTML = ` <header class="jhs-nrp-toolbar"> <div class="jhs-nrp-toolbar-left"> <span class="jhs-nrp-title">短评</span> <span class="jhs-nrp-count">${countText} 条</span> </div> <div class="jhs-nrp-toolbar-right"> <span class="jhs-nrp-toolbar-label">排序</span> <select class="jhs-nrp-sort"> <option value="time"${panel._jhsSortMode === "time" ? " selected" : ""}>时间</option> <option value="score"${panel._jhsSortMode === "score" ? " selected" : ""}>评分</option> <option value="likes"${panel._jhsSortMode === "likes" ? " selected" : ""}>点赞</option> </select> <span class="jhs-nrp-toolbar-label">字号</span> <select class="jhs-nrp-fontsize"> <option value="大"${"大" === fontSize ? " selected" : ""}>大</option> <option value="中"${"中" === fontSize ? " selected" : ""}>中</option> <option value="小"${"小" === fontSize ? " selected" : ""}>小</option> </select> <button type="button" class="jhs-nrp-toggle">${collapsed ? "展开短评" : "收起短评"}</button> </div> </header> <div class="jhs-nrp-items" data-fontsize="${fontSize}" style="${collapsed ? "display:none" : ""}">${items}</div> <div class="jhs-nrp-footer" style="${collapsed ? "display:none" : ""}"></div>`; this._applyFontSize(panel, fontSize); this._bindToolbar(panel, movieId, offset); if (!collapsed) this._renderFooter(panel, movieId, offset + sorted.length, panel._jhsHasMore !== false); } _applyFontSize(panel, size) { const map = { "大": "17px", "中": "15px", "小": "13px" }; const items = panel.querySelector(".jhs-nrp-items"); if (items) items.style.setProperty("--nrp-font-size", map[size] || "15px"); } _bindToolbar(panel, movieId, offset) { // 排序 panel.querySelector(".jhs-nrp-sort")?.addEventListener("change", (e => { panel._jhsSortMode = e.target.value; this._render(panel, panel._jhsAllReviews || [], movieId, 0); })); // 字号 panel.querySelector(".jhs-nrp-fontsize")?.addEventListener("change", (e => { panel._jhsFontSize = e.target.value; this._applyFontSize(panel, e.target.value); const items = panel.querySelector(".jhs-nrp-items"); if (items) items.dataset.fontsize = e.target.value; })); // 收起/展开 panel.querySelector(".jhs-nrp-toggle")?.addEventListener("click", (() => { panel._jhsCollapsed = !panel._jhsCollapsed; const items = panel.querySelector(".jhs-nrp-items"); const footer = panel.querySelector(".jhs-nrp-footer"); const btn = panel.querySelector(".jhs-nrp-toggle"); if (items) items.style.display = panel._jhsCollapsed ? "none" : ""; if (footer) footer.style.display = panel._jhsCollapsed ? "none" : ""; if (btn) btn.textContent = panel._jhsCollapsed ? "展开短评" : "收起短评"; })); } _renderFooter(panel, movieId, shown, hasMore) { const footer = panel.querySelector(".jhs-nrp-footer"); if (!footer) return; if (!hasMore) { footer.innerHTML = '<div class="jhs-nrp-end">已加载全部短评</div>'; return; } footer.innerHTML = '<button type="button" class="jhs-nrp-more">加载更多短评</button>'; const btn = footer.querySelector(".jhs-nrp-more"); btn.addEventListener("click", (async () => { btn.textContent = "加载中...", btn.disabled = !0; try { // API 对 limit 有 50 上限, 靠递增 page 翻页; hotly 排序会抖动, 需去重 const nextPage = (panel._jhsPage || 1) + 1; const next = await nrpFetchReviews(movieId, nextPage, NRP_PAGE_SIZE); const seen = panel._jhsSeenKeys || new Set(); const fresh = []; for (const rv of next) { const k = nrpReviewKey(rv); if (seen.has(k)) continue; seen.add(k), fresh.push(rv); } if (fresh.length) panel._jhsAllReviews = (panel._jhsAllReviews || []).concat(fresh); panel._jhsSeenKeys = seen; panel._jhsPage = nextPage; // jhs.json 手法: 本页不满一页 => 到底了. 另外若本页全是重复(fresh 为空), // 说明已经在原地打转, 也停止, 避免按钮点了没反应 panel._jhsHasMore = next.length >= NRP_PAGE_SIZE && fresh.length > 0; this._render(panel, panel._jhsAllReviews, movieId, 0); } catch (err) { btn.textContent = "加载失败, 点击重试", btn.disabled = !1; } })); } async handle() { if (!r || !window.isDetailPage) return; const movieId = window.location.href.split("/").pop().split(/[?#]/)[0]; if (!movieId) return; this.scheduleMount(movieId); } } const NLP_PAGE_SIZE = 20; /** 渲染单条清单 */ function nlpRenderItem(item, num) { const href = `/lists/${encodeURIComponent(item?.id || "")}`; const date = String(item?.created_at || "").slice(0, 10); const desc = item?.description ? `<div class="jhs-nlp-list-desc">${nrpRenderLinkedText(item.description)}</div>` : ""; return `<div class="jhs-nlp-list"> <div class="jhs-nlp-list-head"> <span><strong>#${num}</strong> <a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(item?.name || "未命名清单")}</a></span> <span>${escapeHtml(date)}</span> </div> ${desc} <div class="jhs-nlp-list-meta"> <span>影片:${escapeHtml(String(item?.movies_count ?? "-"))}</span> <span>收藏:${escapeHtml(String(item?.collections_count ?? "-"))}</span> <span>浏览:${escapeHtml(String(item?.views_count ?? "-"))}</span> </div> </div>`; } /** 调用 JavDB API 获取相关清单 */ async function nlpFetchLists(movieId, page = 1, limit = NLP_PAGE_SIZE) { const params = new URLSearchParams({ movie_id: movieId, page: String(page), limit: String(limit) }); const url = `${NMP_JAVDB_API_BASE}/v1/lists/related?${params.toString()}`; const headers = { accept: "application/json", "accept-language": "zh-TW", "user-agent": "Dart/3.5 (dart:io)", jdSignature: nmpBuildJavdbSignature() }; const token = await nrpGetJavdbToken(); if (token) headers.authorization = `Bearer ${token}`; const res = await nmpGmFetch(url, { headers, timeout: 2e4 }); if (!res.loadstuts || res.status < 200 || res.status >= 400) throw new Error("相关清单接口请求失败"); const json = nmpParseJson(res.responseText); return Array.isArray(json?.data?.lists) ? json.data.lists : []; } class NativeListPanelPlugin extends X { getName() { return "NativeListPanelPlugin"; } async initCss() { return ` <style> .jhs-nlp-panel{width:100%;box-sizing:border-box;color:#172033;background:#f5f5f5;border:1px solid #dddddd;border-radius:7px;overflow:hidden} .jhs-nlp-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;min-height:55px;padding:10px 14px;border-bottom:1px solid #dddddd;background:#f5f5f5} .jhs-nlp-toolbar-left,.jhs-nlp-toolbar-right{display:flex;align-items:center;gap:8px} .jhs-nlp-titletext{color:#172033;font-size:16px;font-weight:720} .jhs-nlp-count{margin-left:4px;color:#667085;font-size:12px;font-weight:500} .jhs-nlp-toolbar-label{color:#475467;font-size:13px;font-weight:600} .jhs-nlp-sort{height:30px;padding:2px 24px 2px 8px;border:1px solid #cdd6e2;border-radius:4px;color:#344054;background:#fff;font-size:12px;font-weight:600;cursor:pointer} .jhs-nlp-toggle{min-height:30px;padding:0 12px;border:1px solid #cdd6e2;border-radius:4px;background:#fff;color:#344054;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap} .jhs-nlp-toggle:hover{background:#eef2f7;border-color:#a8c2ff} .jhs-nlp-loading,.jhs-nlp-empty,.jhs-nlp-error,.jhs-nlp-end{padding:22px 14px;color:#64748b;text-align:center;font-size:13px;font-weight:600} .jhs-nlp-error{color:#be123c} .jhs-nlp-list{margin:0;padding:11px 14px;border-bottom:1px solid #dddddd;background:#f5f5f5;word-break:break-word} .jhs-nlp-list:last-child{border-bottom:0} .jhs-nlp-list-head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;color:#334155;font-size:13px} .jhs-nlp-list-head strong{color:#4a72d4;font-weight:700;margin-right:6px} .jhs-nlp-list-head a{color:#243b67;font-size:14px;font-weight:650;text-decoration:none} .jhs-nlp-list-head a:hover{color:#1d4ed8;text-decoration:underline} .jhs-nlp-list-desc{margin-top:7px;color:#1f2937;font-size:14px;line-height:1.6} .jhs-nlp-list-meta{display:flex;gap:14px;flex-wrap:wrap;margin-top:7px;color:#64748b;font-size:12px;font-variant-numeric:tabular-nums} .jhs-nlp-footer{padding:12px 14px;border-top:1px solid #dddddd;background:#f5f5f5} .jhs-nlp-more{width:100%;min-height:34px;padding:6px 12px;border:0;border-radius:0;background:transparent;color:#1d4ed8;font-size:13px;font-weight:800;cursor:pointer} .jhs-nlp-more:hover{background:transparent;text-decoration:underline} .jhs-nlp-more:disabled{opacity:.6;cursor:wait} html[data-theme="dark"] .jhs-nlp-panel{background:#222b38;border-color:#435062} html[data-theme="dark"] .jhs-nlp-list{background:#222b38;border-color:#374454} html[data-theme="dark"] .jhs-nlp-list-head{color:#aeb9c7} html[data-theme="dark"] .jhs-nlp-list-head a,.jhs-nlp-list-desc{color:#e5e7eb} html[data-theme="dark"] .jhs-nlp-list-meta{color:#aeb9c7} html[data-theme="dark"] .jhs-nlp-footer{background:#1d2632;border-color:#435062} html[data-theme="dark"] .jhs-nlp-more{color:#c8dbff;background:transparent;border:0} html[data-theme="dark"] .jhs-nlp-toolbar{background:#1d2632;border-color:#435062} html[data-theme="dark"] .jhs-nlp-titletext{color:#e5e7eb} html[data-theme="dark"] .jhs-nlp-count,html[data-theme="dark"] .jhs-nlp-toolbar-label{color:#aeb9c7} html[data-theme="dark"] .jhs-nlp-sort,html[data-theme="dark"] .jhs-nlp-toggle{color:#e5e7eb;background:#253141;border-color:#536276} </style> `; } /** 接管 JavDB 原生「相关清单」tab (与短评相同手法) */ mount(movieId) { const pane = document.getElementById("lists"); const tabLink = document.querySelector('[data-movie-tab-target="listTab"] a'); if (!pane || !tabLink) return !1; if (tabLink.dataset.jhsNlpBound === "1") return !0; tabLink.dataset.jhsNlpBound = "1"; tabLink.removeAttribute("data-action"); tabLink.removeAttribute("data-url"); pane.removeAttribute("src"); pane.removeAttribute("data-url"); pane.removeAttribute("data-src"); const onTabClick = (e => { e.preventDefault(); e.stopImmediatePropagation(); this._activateTab(tabLink, pane, movieId); }); tabLink.addEventListener("click", onTabClick, !0); const li = tabLink.closest("li"); if (li && li.classList.contains("is-active")) this._activateTab(tabLink, pane, movieId); return !0; } _activateTab(tabLink, pane, movieId) { jhsBindMagnetTab(); jhsSwitchDetailTab("lists"); if (pane.querySelector(".jhs-nlp-panel")) return; pane.innerHTML = ""; const panel = document.createElement("section"); panel.className = "jhs-nlp-panel"; panel.innerHTML = '<div class="jhs-nlp-loading">正在读取相关清单...</div>'; pane.appendChild(panel); this._load(panel, movieId); } scheduleMount(movieId) { const tryMount = () => { try { return this.mount(movieId); } catch (e) { return clog.error("[相关清单面板] 挂载失败", e), !0; } }; if (tryMount()) return; if ("undefined" == typeof MutationObserver || !document.documentElement) { [ 300, 800, 1600, 3e3, 6e3, 1e4 ].forEach((d => setTimeout(tryMount, d))); return; } let pending = !1; const observer = new MutationObserver((() => { if (pending) return; pending = !0, queueMicrotask((() => { pending = !1, tryMount() && observer.disconnect(); })); })); observer.observe(document.documentElement, { childList: !0, subtree: !0 }); setTimeout((() => observer.disconnect()), 3e4); } async _load(panel, movieId) { try { const lists = await nlpFetchLists(movieId, 1, NLP_PAGE_SIZE); panel._jhsAllLists = lists; panel._jhsPage = 1; panel._jhsHasMore = lists.length >= NLP_PAGE_SIZE; panel._jhsSortMode = panel._jhsSortMode || "time"; panel._jhsCollapsed = panel._jhsCollapsed || !1; this._render(panel, movieId); } catch (e) { clog.error("[相关清单面板] 加载失败:", e); panel.innerHTML = `<div class="jhs-nlp-error">相关清单读取失败: ${escapeHtml(e.message || String(e))}</div>`; } } /** 排序清单: time(最新) / movies(影片数) / collections(收藏) / views(浏览), 均由高到低 */ _sortLists(lists, mode) { const arr = [ ...lists ]; const num = (v) => Number(v) || 0; if ("movies" === mode) return arr.sort(((a, b) => num(b.movies_count) - num(a.movies_count))); if ("collections" === mode) return arr.sort(((a, b) => num(b.collections_count) - num(a.collections_count))); if ("views" === mode) return arr.sort(((a, b) => num(b.views_count) - num(a.views_count))); return arr.sort(((a, b) => (Date.parse(b.created_at) || 0) - (Date.parse(a.created_at) || 0))); } _render(panel, movieId) { const all = panel._jhsAllLists || []; if (!all.length) { panel.innerHTML = '<div class="jhs-nlp-empty">暂无相关清单</div>'; return; } const sorted = this._sortLists(all, panel._jhsSortMode || "time"); const items = sorted.map(((item, i) => nlpRenderItem(item, i + 1))).join(""); const collapsed = panel._jhsCollapsed || !1; const nativeTabText = document.querySelector('[data-movie-tab-target="listTab"] a')?.textContent || ""; const nativeCount = (nativeTabText.match(/(\d+)/) || [])[1]; const countText = nativeCount || String(sorted.length); panel.innerHTML = ` <header class="jhs-nlp-toolbar"> <div class="jhs-nlp-toolbar-left"> <span class="jhs-nlp-titletext">相关清单</span> <span class="jhs-nlp-count">${countText} 个</span> </div> <div class="jhs-nlp-toolbar-right"> <span class="jhs-nlp-toolbar-label">排序</span> <select class="jhs-nlp-sort"> <option value="time"${panel._jhsSortMode === "time" ? " selected" : ""}>时间</option> <option value="movies"${panel._jhsSortMode === "movies" ? " selected" : ""}>影片数</option> <option value="collections"${panel._jhsSortMode === "collections" ? " selected" : ""}>收藏数</option> <option value="views"${panel._jhsSortMode === "views" ? " selected" : ""}>浏览数</option> </select> <button type="button" class="jhs-nlp-toggle">${collapsed ? "展开相关清单" : "收起相关清单"}</button> </div> </header> <div class="jhs-nlp-items" style="${collapsed ? "display:none" : ""}">${items}</div> <div class="jhs-nlp-footer" style="${collapsed ? "display:none" : ""}"></div>`; this._bindToolbar(panel, movieId); if (!collapsed) this._renderFooter(panel, movieId, panel._jhsPage || 1, all.length, panel._jhsHasMore); } _bindToolbar(panel, movieId) { panel.querySelector(".jhs-nlp-sort")?.addEventListener("change", (e => { panel._jhsSortMode = e.target.value; this._render(panel, movieId); })); panel.querySelector(".jhs-nlp-toggle")?.addEventListener("click", (() => { panel._jhsCollapsed = !panel._jhsCollapsed; const items = panel.querySelector(".jhs-nlp-items"); const footer = panel.querySelector(".jhs-nlp-footer"); const btn = panel.querySelector(".jhs-nlp-toggle"); if (items) items.style.display = panel._jhsCollapsed ? "none" : ""; if (footer) footer.style.display = panel._jhsCollapsed ? "none" : ""; if (btn) btn.textContent = panel._jhsCollapsed ? "展开相关清单" : "收起相关清单"; })); } _renderFooter(panel, movieId, page, shown, hasMore) { const footer = panel.querySelector(".jhs-nlp-footer"); if (!footer) return; if (!hasMore) { footer.innerHTML = '<div class="jhs-nlp-end">已加载全部清单</div>'; return; } footer.innerHTML = '<button type="button" class="jhs-nlp-more">加载更多清单</button>'; const btn = footer.querySelector(".jhs-nlp-more"); btn.addEventListener("click", (async () => { btn.textContent = "加载中...", btn.disabled = !0; try { const nextPage = page + 1; const next = await nlpFetchLists(movieId, nextPage, NLP_PAGE_SIZE); // 累积到全量缓存后按当前排序重绘, 保证排序一致 panel._jhsAllLists = (panel._jhsAllLists || []).concat(next); panel._jhsPage = nextPage; panel._jhsHasMore = next.length >= NLP_PAGE_SIZE; this._render(panel, movieId); } catch (err) { btn.textContent = "加载失败, 点击重试", btn.disabled = !1; } })); } async handle() { if (!r || !window.isDetailPage) return; const movieId = window.location.href.split("/").pop().split(/[?#]/)[0]; if (!movieId) return; this.scheduleMount(movieId); } } class de extends X { getName() { return "FoldCategoryPlugin"; } async initCss() { const e = await storageManager.getSetting(); return `\n <style>\n #tags a.tag, .tags a.tag {\n position:relative;\n }\n .highlight-btn {\n position: absolute;\n top: -10px;\n right: -10px;\n background-color: #4CAF50;\n color: white;\n border: none;\n border-radius: 50%;\n width: 24px;\n height: 24px;\n font-size: 14px;\n line-height: 24px;\n text-align: center;\n cursor: pointer;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n display: none;\n z-index: 999;\n }\n /* 当父元素被高亮时,按钮变为其他颜色 */\n .highlighted .highlight-btn {\n background-color: #FF5722;\n }\n /* 高亮状态下的标签样式 */\n .highlighted {\n /* 浅黄色 */\n border: ${e.highlightedTagNumber || 1}px solid ${e.highlightedTagColor || "#ce2222"};\n }\n </style>\n `; } async handle() { window.isListPage && (o.includes("advanced_search") || (this.highlightTag(), utils.loopDetector((() => $("#waitCheckBtn").length), (() => { this.createFoldBtn(); }), 1, 1e4, !0), $("#tags .tag-category .tag-expand").each(((e, t) => { $(t).parent().hasClass("collapse") && t.click(); })))); } highlightTag() { (async () => { const e = await storageManager.getHighlightedTags(); e && e.forEach((e => { $(`#tags a.tag:contains(${e})`).addClass("highlighted"), $(`.tags a.tag:contains(${e})`).addClass("highlighted"); })); })().then(), $("#tags a.tag, .tags a.tag").hover((function() { const e = $(this), t = $('<button class="highlight-btn" title="高亮显示">★</button>'); e.append(t), t.fadeIn(0); }), (function() { $(this).find(".highlight-btn").fadeOut(0, (function() { $(this).remove(); })); })), $(document).on("click", ".highlight-btn", (async function(e) { e.stopPropagation(), e.preventDefault(); const t = $(this).closest("a.tag"), n = t.clone(); n.find(".highlight-btn").remove(); const a = n.text().trim().replace(/\s*\(\d+\)$/, ""); let i = await storageManager.getHighlightedTags(); i.includes(a) ? (i = i.filter((e => e !== a)), t.removeClass("highlighted")) : (i.push(a), t.addClass("highlighted")), await storageManager.setHighlightedTags(i); })); } async createFoldBtn() { let t = $("#tags"), n = $("#tags dl div.tag.is-info").map((function() { return $(this).text().replaceAll("\n", "").replaceAll(" ", ""); })).get().join(" "); if (!n) return; $(".tabs").append(`\n <div style="display: flex;align-items: center;flex-grow:1;justify-content: flex-end;">\n <div>已选分类: <span id="jhs-check-tag">${n}</span></div>\n <a class="menu-btn main-tab-btn" id="foldCategoryBtn" style="background-color:#d23e60 !important;">\n <span></span>\n <i style="margin-left: 10px"></i>\n </a>\n\n </div>\n `); let a = $("h2.section-title"); if (a.length > 0 && (a.append('\n <div id="foldCategoryBtn">\n <a class="menu-btn" style="background-color:#d23e60 !important;margin-left: 20px;border-bottom:none !important;border-radius:3px;">\n <span></span>\n <i style="margin-left: 10px"></i>\n </a>\n </div>\n '), t = $("section > div > div.box")), !t) return; let i = $("#foldCategoryBtn"), s = localStorage.getItem("jhs_foldCategory") === _, [o, r] = s ? [ "展开", "icon-angle-double-down" ] : [ "折叠", "icon-angle-double-up" ]; i.find("span").text(o).end().find("i").attr("class", r), window.location.href.includes("noFold=1") || t[s ? "hide" : "show"](), i.on("click", (async e => { e.preventDefault(), s = !s, localStorage.setItem("jhs_foldCategory", s ? _ : C); const [n, a] = s ? [ "展开", "icon-angle-double-down" ] : [ "折叠", "icon-angle-double-up" ]; i.find("span").text(n).end().find("i").attr("class", a), t[s ? "hide" : "show"](); })); } } class he extends X { constructor() { super(...arguments), i(this, "apiUrl", "https://ja.wikipedia.org/wiki/"); } getName() { return "ActressInfoPlugin"; } async handle() { "yes" === await storageManager.getSetting("enableLoadActressInfo", "yes") && this.loadActressInfo(); } loadActressInfo() { this.handleDetailPage().then(), this.handleStarPage().then(); } async initCss() { return "\n <style>\n .info-tag {\n background-color: #ecf5ff;\n display: inline-block;\n height: 24px;\n padding: 0 5px;\n line-height: 22px;\n font-size: 11px;\n color: #409eff;\n border: 1px solid #d9ecff;\n border-radius: 4px;\n box-sizing: border-box;\n white-space: nowrap;\n margin: 2px 3px 2px 0;\n }\n </style>\n "; } /** 定位详情页「演員/演员」所在的 panel-block (兼容 JavDB 改版) */ _findActressBlock() { // JavBus: 演员链接所在的容器 (ul.star-show / p), 兼容传统 star_ 结构 if (l) { const $star = this._findBusStarLinks().first(); if (!$star.length) return null; const $ul = $star.closest("ul"); if ($ul.length) return $ul; const $p = $star.closest("p, li"); return $p.length ? $p : $star.parent(); } let $anchor = null; $(".movie-panel-info .panel-block, .panel-block").each(((_, el) => { const $el = $(el), label = $el.find("strong").first().text().trim(); if (label.includes("演員") || label.includes("演员")) return $anchor = $el, !1; })); return $anchor; } /** JavBus 详情页: 演员链接集合 (限定在 .info 信息栏内, 避免误取"推薦"区) */ _findBusStarLinks() { let $links = $(".info span[onmouseover*='star_'] a"); if (!$links.length) $links = $(".info li.star-name a[href*='/star/']"); if (!$links.length) $links = $(".info a[href*='/star/']"); return $links; } /** 从详情页解析女演员名列表 (多重选择器兼容改版, 排除男演员) */ _parseFemaleNames($anchor) { // JavBus: 信息栏只列女优, 直接取 /star/ 链接文本 if (l) { const busNames = this._findBusStarLinks().map(((_, x) => $(x).text().trim())).get().filter(Boolean); return [ ...new Set(busNames) ]; } // 1) 传统: .female 标记前一个元素即演员名 let names = $(".female").prev().map(((e, t) => $(t).text().trim())).get().filter(Boolean); if (names.length) return [ ...new Set(names) ]; // 2) 新版: 演員 block 内的 /actors/ 链接, 跳过带 male 标记的(男演员) const collect = $scope => { const out = []; $scope.find("a[href*='/actors/']").each(((_, x) => { const $a = $(x), $next = $a.next(); if ($next.length && $next.hasClass("male")) return; const nm = $a.text().trim(); nm && out.push(nm); })); return out; }; if ($anchor && $anchor.length) names = collect($anchor); // 3) 兜底: 整个信息面板 if (!names.length) names = collect($(".movie-panel-info")); return [ ...new Set(names) ]; } async handleDetailPage() { if ($(".actress-info").length > 0) return; const $anchor = this._findActressBlock(); const names = this._parseFemaleNames($anchor); if (!names.length) return void clog.debug("加载女优信息: 未解析到演员名, 跳过 (页面结构可能已改版)"); const cacheKey = "jhs_actress_info"; const cache = localStorage.getItem(cacheKey) ? JSON.parse(localStorage.getItem(cacheKey)) : {}; // 在演员栏每个女演员名字后面追加状态徽章(仅一次, 跳过男优) // 仅在已取到 wiki 数据时才显示徽章, 避免先显示"现役"再改成"引退"的误导 const actorSelector = l ? "a[href*='/star/']" : "a[href*='/actors/']"; const appendBadgeToActorLinks = () => { if (!$anchor || !$anchor.length) return; // 让演员链接的直接父容器变为 flex 居中, 确保徽章与文字垂直对齐 $anchor.find(actorSelector).parent().css({ "display": "flex", "align-items": "center", "flex-wrap": "wrap", "column-gap": "4px", "row-gap": "4px" }); $anchor.find(actorSelector).each(((_, el) => { const $a = $(el); if ($a.next(".jhs-actress-status").length) return; // 已追加过 // 跳过男优: 后面紧跟 .male 图标的是男演员 (JavDB); JavBus 信息栏本身只列女优 if (!l) { const $next = $a.next(); if ($next.length && ($next.hasClass("male") || $next.find(".male").length)) return; } // 支持逗号分隔别名, 从任一别名缓存里取数据 const rawText = ($a.text() || "").trim(); const aliases = rawText.split(/[,,]/).map(a => a.trim()).filter(Boolean); let data = null; for (const alias of aliases) { if (cache[alias]) { data = cache[alias]; break; } } if (!data) return; // 尚无 wiki 数据 → 暂不显示徽章 const status = this._computeStatus(data.activeRaw, data.retireRaw, data.bodyRetired); $a.after(this._statusBadge(status)); })); }; // 非阻塞: 有缓存直接用, 无缓存后台请求不阻塞页面 let html = ""; const uncached = []; for (const rawName of names) { // 支持逗号分隔的别名(如 "新有菜, 新ありな"): 主名 = 第一个, 查询时逐个尝试 const aliases = rawName.split(/[,,]/).map(a => a.trim()).filter(Boolean); const primary = aliases[0] || rawName; // 命中任一别名缓存(优先命中有数据的) let cachedData = undefined, matchedAlias = null; for (const alias of aliases) { if (cache[alias] !== undefined) { if (cache[alias]) { cachedData = cache[alias]; matchedAlias = alias; break; } if (cachedData === undefined) { cachedData = null; matchedAlias = alias; } } } if (matchedAlias !== null) { if (cachedData) { html += `\n <div class="panel-block actress-info">\n <strong>${primary}:</strong>\n <a href="${cachedData.url}" style="margin-left: 5px" target="_blank">\n <span class="info-tag">${cachedData.birthday} ${cachedData.age}</span>\n <span class="info-tag">${cachedData.height} ${cachedData.weight}</span>\n <span class="info-tag">${cachedData.threeSizeText} ${cachedData.braSize}</span>\n </a>\n </div>\n `; } // 无 wiki 数据 → 隐藏 } else { uncached.push({ primary: primary, aliases: aliases }); html += `<div class="panel-block actress-info" id="jhs-actress-${primary.replace(/[^a-zA-Z0-9]/g, '_')}"><strong>${primary}:</strong> <span style="color:#999;font-size:12px">加载中...</span></div>`; } } // 先插入已有缓存 + 占位, 不阻塞后续插件 const insertHtml = (h) => { if ($anchor && $anchor.length) $anchor.after(h); else { const $legacy = $('strong:contains("演員"), strong:contains("演员")').first().parent(); $legacy.length ? $legacy.after(h) : $(l ? ".info" : ".movie-panel-info").append(h); } }; insertHtml(html); appendBadgeToActorLinks(); // 后台异步加载未缓存的(不阻塞 processPlugins) if (uncached.length) { (async () => { for (const item of uncached) { let data = null; // 逐个别名尝试查询维基, 命中即停 for (const alias of item.aliases) { try { data = await this.searchInfo(alias); } catch (e) { clog.debug("查询演员维基失败:", alias); } if (data) { cache[alias] = data; break; } cache[alias] = null; } const id = `jhs-actress-${item.primary.replace(/[^a-zA-Z0-9]/g, '_')}`; const $el = $(`#${id}`); if ($el.length) { if (data) { $el.replaceWith(`<div class="panel-block actress-info"><strong>${item.primary}:</strong><a href="${data.url}" style="margin-left: 5px" target="_blank"><span class="info-tag">${data.birthday} ${data.age}</span><span class="info-tag">${data.height} ${data.weight}</span><span class="info-tag">${data.threeSizeText} ${data.braSize}</span></a></div>`); } else { $el.remove(); // 无 wiki 数据 → 移除占位, 不显示 } } } localStorage.setItem(cacheKey, JSON.stringify(cache)); appendBadgeToActorLinks(); // 异步加载完后再次更新演员栏徽章 })(); } else { localStorage.setItem(cacheKey, JSON.stringify(cache)); } } async handleStarPage() { if ($(".actress-info").length > 0) return; let e = [], t; if (l) { // JavBus 演员页: 名字在 .avatar-box .photo-info .pb10, 无 section-meta t = $(".avatar-box .photo-info .pb10"); if (!t.length) return; t.text().trim().split(/[,,]/).forEach((n => { e.push(n.trim()); })); } else { t = $(".actor-section-name"); t.length && t.text().trim().split(",").forEach((n => { e.push(n.trim()); })); let n = $(".section-meta:not(:contains('影片'))"); if (n.length && n.text().trim().split(",").forEach((m => { e.push(m.trim()); })), !e.length) return; } e = e.filter(Boolean); if (!e.length) return; const a = "jhs_actress_info", i = localStorage.getItem(a) ? JSON.parse(localStorage.getItem(a)) : {}; // 先尝试从缓存命中 let s = null; for (let l = 0; l < e.length; l++) { if (i[e[l]]) { s = i[e[l]]; break; } } if (s) { this._renderStarPage(s, t, e, i, a); return; } // 缓存未命中: 非阻塞, 后台异步查询不阻塞其他插件 (async () => { for (let l = 0; l < e.length; l++) { try { s = await this.searchInfo(e[l]); } catch (r) { clog.debug("该名称查询失败,尝试其它名称:", e[l]); } if (s) break; } s && e.forEach((name => { i[name] = s; })); if (s) this._renderStarPage(s, t, e, i, a); localStorage.setItem(a, JSON.stringify(i)); })(); } /** 演员页渲染信息(从 handleStarPage 抽出, 避免重复代码) */ _renderStarPage(s, t, e, i, a) { const status = this._computeStatus(s.activeRaw, s.retireRaw, s.bodyRetired); const $nameEl = t.length ? t : (l ? $(".avatar-box .photo-info .pb10") : $(".actor-section-name")); if ($nameEl.length && !$nameEl.find(".jhs-actress-status").length) { $nameEl.css({ "display": "inline-flex", "align-items": "center" }); $nameEl.append(this._statusBadge(status)); $nameEl.append(this._wikiIcon(s.url)); } // JavBus 演员页本身已有完整资料栏, 只挂徽章 + wiki 图标, 不再重复插入信息块 if (l) { e.forEach((name => { i[name] = s; })); localStorage.setItem(a, JSON.stringify(i)); return; } const row1 = [], row2 = []; if (s.birthday) row1.push(`出生: ${s.birthday}`); if (s.age) row1.push(`年龄: ${s.age}`); if (s.height) row1.push(`身高: ${s.height}`); if (s.threeSizeText) row2.push(`三围: ${s.threeSizeText}`); if (s.braSize) row2.push(`罩杯: ${s.braSize}`); if (s.weight) row2.push(`体重: ${s.weight}`); const spanStyle = 'display:inline-block;width:33%;min-width:200px;'; const makeRow = arr => arr.length ? `<div style="display:flex;margin-bottom:10px;">${arr.map(f => `<span style="${spanStyle}">${f}</span>`).join("")}</div>` : ""; if (row1.length || row2.length) { const o = `\n <div class="actress-info" style="font-size: 15px; font-weight: normal; margin-top: 5px; color: #666;">\n ${makeRow(row1)}${makeRow(row2)}\n </div>\n `; $nameEl.parent().append(o); } e.forEach((name => { i[name] = s; })); localStorage.setItem(a, JSON.stringify(i)); } async searchInfo(e) { "三上悠亞" === e && (e = "三上悠亜"); let t = this.apiUrl + e; const n = await gmHttp.get(t), a = new DOMParser, i = $(a.parseFromString(n, "text/html")); const pick = (sel, fn) => { try { const v = fn(i.find(sel)); return v ? String(v).trim() : ""; } catch { return ""; } }; const birthday = pick('a[title="誕生日"]', $x => $x.parent().parent().find("td").text()); const ageRaw = pick("th:contains('現年齢')", () => i.find("th:contains('現年齢')").parent().find("td").text()); const age = ageRaw && !isNaN(parseInt(ageRaw)) ? parseInt(ageRaw) + "岁" : ""; const heightRaw = pick('tr:has(a[title="身長"]) td', $x => $x.text()); const height = heightRaw ? heightRaw.split(" ")[0] + "cm" : ""; const weightRaw = pick('tr:has(a[title="体重"]) td', $x => $x.text()); const weightParts = weightRaw ? weightRaw.split("/") : []; let weight = (weightParts.length > 1 ? weightParts[1] : weightParts[0] || "").trim(); "― kg" === weight && (weight = ""); // 活動期間: 用于判断女优状态(引退/现役) const activeRaw = pick("th:contains('活動期間')", () => i.find("th:contains('活動期間')").parent().find("td").text()); // 引退字段: 部分演员 wiki 有专门的「引退」th, 若非空/非空占位符即视为已引退 const retireRaw = pick("th:contains('引退'), th:contains('退役')", $x => $x.first().parent().find("td").text()); // 正文关键短语判断: 正文中含「元AV女優」「元セクシー女優」「AV引退」等即为引退 const bodyText = i.find("#mw-content-text").text() || ""; const bodyRetired = /元AV女優|元セクシー女優|AV引退|AV女優を引退|完全引退/.test(bodyText); return { birthday: birthday, age: age, height: height, weight: weight, threeSizeText: pick('a[title="スリーサイズ"]', $x => $x.closest("tr").find("td").text().replace("cm", "")), braSize: pick('th:contains("ブラサイズ")', $x => $x.next("td").contents().first().text()), activeRaw: activeRaw, retireRaw: retireRaw, bodyRetired: bodyRetired, url: t }; } /** * 判断女优状态(引退/现役), 判断依据: * 1) wiki 词条含独立「引退/退役」字段且非空占位符 → 引退 * 2) wiki 正文含「元AV女優/元セクシー女優/AV引退/完全引退」→ 引退 * 3) wiki 活動期間字段中含 引退/退役 关键字 → 引退 * 4) wiki 活動期間解析出结束日期, 距今 > 6 个月 → 引退 (半年以上无作品) * 5) 其余情况 → 现役 */ _computeStatus(activeRaw, retireRaw, bodyRetired) { // 1) 明确的引退字段 const rr = (retireRaw || "").trim(); if (rr && rr !== "―" && rr !== "-" && rr !== "?" && rr !== "?") return "引退"; // 2) 正文关键短语 if (bodyRetired) return "引退"; if (!activeRaw) return "现役"; // 3) 活動期間文本含 引退/退役 if (/引退|退役/.test(activeRaw)) return "引退"; // 显式仍活跃标识 if (/現在|現役|活動中|継続中/.test(activeRaw)) return "现役"; // 4) 半年以上无作品(以活動期間结束日期为准) const years = activeRaw.match(/\d{4}/g); if (!years || years.length < 2) return "现役"; // 仅开始年份 → 现役 const endYear = parseInt(years[years.length - 1]); if (isNaN(endYear)) return "现役"; const mmMatch = activeRaw.match(new RegExp(endYear + "\\D+?(\\d{1,2})月")); const endMonth = mmMatch ? parseInt(mmMatch[1]) - 1 : 11; const endDate = new Date(endYear, endMonth + 1, 0); // 该月末 const halfYearAgo = new Date(); halfYearAgo.setMonth(halfYearAgo.getMonth() - 6); return endDate < halfYearAgo ? "引退" : "现役"; } /** 状态徽章: 引退红色/现役绿色 (小尺寸, flex 容器内自动垂直居中) */ _statusBadge(status) { const bg = status === "引退" ? "#ff4d4f" : "#52c41a"; return `<span class="jhs-actress-status" style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;height:16px;background:${bg};color:#fff;padding:0 6px;border-radius:3px;font-size:10px;line-height:1;margin-left:4px;vertical-align:middle;font-weight:normal;">${status}</span>`; } /** wiki 链接图标(与状态徽章同高同行, 使用维基百科官方 apple-touch 图标) */ _wikiIcon(url) { return `<a class="jhs-wiki-icon" href="${url}" target="_blank" title="Wikipedia" style="display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:6px;text-decoration:none;"><img src="https://ja.wikipedia.org/static/apple-touch/wikipedia.png" alt="W" style="width:16px;height:16px;display:block;border-radius:2px;"></a>`; } } class pe extends X { constructor() { super(), i(this, "$contentBox", $(".section .container")); } getName() { return "HitShowPlugin"; } handle() { $('a[href*="rankings/playback"]').on("click", (e => { e.preventDefault(), e.stopPropagation(), window.location.href = "/advanced_search?handlePlayback=1&period=daily"; })), this.handlePlayback().then(); } hookPage() { let e = $("h2.section-title"); e.contents().first().replaceWith("热播"), e.css("marginBottom", "0"), $(".empty-message").remove(), $(".section .container .box").remove(), $("#sort-toggle-btn").remove(), this.$contentBox.append('<div class="tool-box" style="margin-top: 10px"></div>'), this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>'); } async handlePlayback() { if (!window.location.href.includes("handlePlayback=1")) return; let e = new URLSearchParams(window.location.search).get("period"); this.toolBar(e), this.hookPage(); let t = $(".movie-list"); t.html(""); let n = loading(); let a = !1; for (let s = 1; s <= 3 && !a; s++) try { const n = await W(e); let i = this.markDataListHtml(n); t.html(i), this.loadScore(n), a = !0; } catch (i) { s < 3 ? (clog.error(`获取热播数据失败 (第 ${s} 次重试)`, i), await new Promise((e => setTimeout(e, 1e3)))) : clog.error("所有重试尝试均失败,无法获取数据。", i); } finally { (a || 3 === s) && n.close(); } } toolBar(e) { let t = `\n <div class="button-group" style="margin-top:18px">\n <div class="buttons has-addons" id="conditionBox">\n <a style="padding:18px 18px !important;" class="button is-small ${"daily" === e ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=daily">日榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"weekly" === e ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=weekly">周榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"monthly" === e ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=monthly">月榜</a>\n </div>\n </div>\n `; this.$contentBox.append(t); } getStarRating(e) { let t = ""; const n = Math.floor(e); for (let a = 0; a < n; a++) t += '<i class="icon-star"></i>'; for (let a = 0; a < 5 - n; a++) t += '<i class="icon-star gray"></i>'; return t; } loadScore(e) { if (0 === e.length) return; (async () => { let t = "jhs_score_info"; for (const a of e) try { const e = a.id; if (!$(`#score_${e}`).length) return; if ($(`#${e}`).is(":hidden")) continue; const n = localStorage.getItem(t) ? JSON.parse(localStorage.getItem(t)) : {}, i = n[e]; if (i) { this.appendScoreHtml(e, i); continue; } for (;!document.hasFocus(); ) await new Promise((e => setTimeout(e, 500))); const s = await V(e); let o = s.score, r = s.watchedCount, l = `\n <span class="value">\n <span class="score-stars">${this.getStarRating(o)}</span> \n ${o}分,由${r}人評價\n </span>\n `; this.appendScoreHtml(e, l), n[e] = l, localStorage.setItem(t, JSON.stringify(n)), await new Promise((e => setTimeout(e, 500))); } catch (n) { clog.error(`解析评分数据失败 | 编号: ${a.number}\n`, `错误详情: ${n.message}\n`, n.stack ? `调用栈:\n${n.stack}` : ""); } })(); } appendScoreHtml(e, t) { let n = $(`#score_${e}`); n.length && "" === n.html().trim() && n.slideUp(0, (function() { $(this).html(t).slideDown(500); })); } markDataListHtml(e) { let t = ""; return e.forEach((e => { t += `\n <div class="item" id="${escapeHtml(e.id)}">\n <a href="/v/${escapeHtml(e.id)}" class="box" title="${escapeHtml(e.origin_title)}">\n <div class="cover ">\n <img loading="lazy" src="${e.cover_url.replace(/https:\/\/[^/]+\/rhe951l4q/, "https://c0.jdbstatic.com")}" alt="">\n </div>\n <div class="video-title"><strong>${escapeHtml(e.number)}</strong> ${escapeHtml(e.origin_title)}</div>\n <div class="score" id="score_${escapeHtml(e.id)}">\n </div>\n <div class="meta">\n ${escapeHtml(e.release_date)}\n </div>\n <div class="tags has-addons">\n ${e.has_cnsub ? '<span class="tag is-warning">含中字磁鏈</span>' : e.magnets_count > 0 ? '<span class="tag is-success">含磁鏈</span>' : '<span class="tag is-info">无磁鏈</span>'}\n ${e.new_magnets ? '<span class="tag is-info">今日新種</span>' : ""}\n </div>\n </a>\n </div>\n `; })), t; } } const me = "jhs_appAuthorization"; class ue extends X { constructor() { super(), i(this, "has_cnsub", ""), i(this, "$contentBox", $(".section .container")), i(this, "movies", []); } getName() { return "TOP250Plugin"; } handle() { $('.main-tabs ul li:contains("猜你喜歡")').html('<a href="/rankings/top"><span>Top250</span></a>'), $('a[href*="rankings/top"]').on("click", (e => { e.preventDefault(), e.stopPropagation(); const t = $(e.target), n = (t.is("a") ? t : t.closest("a")).attr("href"); let a = n.includes("?") ? n.split("?")[1] : n; const i = new URLSearchParams(a); this.checkLogin(e, i); })), this.handleTop().then(); } hookPage() { $("h2.section-title").contents().first().replaceWith("Top250"), $(".empty-message").remove(), $(".section .container .box").remove(), $("#sort-toggle-btn").remove(), this.$contentBox.append('<div class="tool-box" style="margin-top: 10px"></div>'), this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>'), this.renderPagination(); } renderPagination() { const e = new URLSearchParams(window.location.search); let t = parseInt(e.get("page")) || 1; this.$contentBox.append((e => { const t = e >= 5; let n = ""; for (let a = 1; a <= 5; a++) { n += `<li><a class="pagination-link ${e === a ? "is-current" : ""}" data-page="${a}">${a}</a></li>`; } return `\n <nav class="pagination">\n <a class="pagination-previous ${e <= 1 ? "do-hide" : ""}" data-page="${e - 1}">上一頁</a>\n <a class="pagination-next ${t ? "do-hide" : ""}" data-page="${e + 1}">下一頁</a>\n \n <ul class="pagination-list">\n ${n}\n </ul>\n </nav>\n `; })(t)), this.$contentBox.on("click", ".pagination-link, .pagination-previous, .pagination-next", (t => { t.preventDefault(); const n = parseInt($(t.currentTarget).data("page")); !isNaN(n) && n > 0 && (t => { e.set("page", t), window.history.pushState({}, "", "?" + e.toString()), window.location.reload(); })(n); })); } async handleTop() { if (!window.location.href.includes("handleTop=1")) return; const e = new URLSearchParams(window.location.search); let t = e.get("handleType") || "all", n = e.get("type_value") || ""; this.has_cnsub = e.get("has_cnsub") || ""; let a = e.get("page") || 1; this.toolBar(t, n, a), this.hookPage(); let i = $(".movie-list"); i.html(""); let s = loading(); let o = !1; for (let l = 1; l <= 3 && !o; l++) try { const e = await q(t, n, a, 50); let r = e.success, l = e.message, c = e.action; if (1 === r) { let t = e.data.movies; if (0 === t.length) return show.error("无数据"), void s.close(); this.movies = t; const n = t.filter((e => "1" === this.has_cnsub ? e.has_cnsub : "0" !== this.has_cnsub || !e.has_cnsub)), a = this.getBean("HitShowPlugin"); let r = a.markDataListHtml(n); i.html(r), a.loadScore(n), o = !0; } else console.error(e), i.html(`<h3>${escapeHtml(l)}</h3>`), show.error(l), "JWTVerificationError" === c && (await localStorage.removeItem(me), await this.checkLogin(null, new URLSearchParams(window.location.search))), o = !0; } catch (r) { l < 3 ? (clog.error(`获取Top数据失败 (第 ${l} 次重试):`, r), await new Promise((e => setTimeout(e, 1e3)))) : (clog.error("所有重试尝试均失败,无法获取Top数据。", r), i.html("<h3>无法加载数据,请稍后再试。</h3>")); } finally { (o || 3 === l) && s.close(); } } toolBar(e, t, n) { "5" === n.toString() && $(".pagination-next").remove(), $(".pagination-ellipsis").closest("li").remove(), $(".pagination-list li a").each((function() { parseInt($(this).text()) > 5 && $(this).closest("li").remove(); })); let a = ""; for (let s = (new Date).getFullYear(); s >= 2008; s--) a += `\n <a style="padding:18px 18px !important;" \n class="button is-small ${t === s.toString() ? "is-info" : ""}" \n href="/advanced_search?handleTop=1&handleType=year&type_value=${s}&has_cnsub=${this.has_cnsub}">\n ${s}\n </a>\n `; let i = `\n <div class="button-group">\n <div class="buttons has-addons" id="conditionBox" style="margin-bottom: 0!important;">\n <a style="padding:18px 18px !important;" class="button is-small ${"all" === e ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=all&type_value=&has_cnsub=${this.has_cnsub}">全部</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === t ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=0&has_cnsub=${this.has_cnsub}">有码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"1" === t ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=1&has_cnsub=${this.has_cnsub}">无码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"2" === t ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=2&has_cnsub=${this.has_cnsub}">欧美</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"3" === t ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=3&has_cnsub=${this.has_cnsub}">Fc2</a>\n \n <a style="padding:18px 18px !important;margin-left: 50px" class="button is-small ${"1" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="1">含中字磁鏈</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="0">无字幕</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-cnsub-value="">重置</a>\n </div>\n \n <div class="buttons has-addons" id="conditionBox">\n ${a}\n </div>\n </div>\n `; this.$contentBox.append(i), $("a[data-cnsub-value]").on("click", (e => { const t = $(e.currentTarget).data("cnsub-value"); this.has_cnsub = t.toString(), $("a[data-cnsub-value]").removeClass("is-info"), $(e.currentTarget).addClass("is-info"), $(".toolbar a.button").not("[data-cnsub-value]").each(((e, n) => { const a = $(n), i = new URL(a.attr("href"), window.location.origin); i.searchParams.set("has_cnsub", t), a.attr("href", i.toString()); })); const n = this.movies.filter((e => "1" === this.has_cnsub ? e.has_cnsub : "0" !== this.has_cnsub || !e.has_cnsub)), a = this.getBean("HitShowPlugin"); let i = a.markDataListHtml(n); $(".movie-list").html(i), a.loadScore(n); })); } async checkLogin(e, t) { if (!localStorage.getItem(me)) return show.error("该类别依赖移动端接口,请先完成登录"), void this.openLoginDialog(); let n = "all", a = "", i = t.get("t") || ""; /^y\d+$/.test(i) ? (n = "year", a = i.substring(1)) : "" !== i && (n = "video_type", a = i); let s = `/advanced_search?handleTop=1&handleType=${n}&type_value=${a}`; e && (e.ctrlKey || e.metaKey) ? GM_openInTab(window.location.origin + s, { insert: 0 }) : window.location.href = s; } openLoginDialog() { layer.open({ type: 1, title: "JavDB", closeBtn: 1, area: utils.getResponsiveArea([ "360px", "auto" ]), shadeClose: !1, content: '\n <div style="padding: 30px; font-family: \'Helvetica Neue\', Arial, sans-serif;">\n <div style="margin-bottom: 25px;">\n <input type="text" id="username" name="username" \n style="width: 100%; padding: 12px 15px; border: 1px solid #e0e0e0; border-radius: 4px; \n box-sizing: border-box; transition: all 0.3s; font-size: 14px;\n background: #f9f9f9; color: #333;"\n placeholder="用户名 | 邮箱"\n onfocus="this.style.borderColor=\'#4a8bfc\'; this.style.background=\'#fff\'"\n onblur="this.style.borderColor=\'#e0e0e0\'; this.style.background=\'#f9f9f9\'">\n </div>\n \n <div style="margin-bottom: 15px;">\n <input type="password" id="password" name="password" \n style="width: 100%; padding: 12px 15px; border: 1px solid #e0e0e0; border-radius: 4px; \n box-sizing: border-box; transition: all 0.3s; font-size: 14px;\n background: #f9f9f9; color: #333;"\n placeholder="密码"\n onfocus="this.style.borderColor=\'#4a8bfc\'; this.style.background=\'#fff\'"\n onblur="this.style.borderColor=\'#e0e0e0\'; this.style.background=\'#f9f9f9\'">\n </div>\n \n <button id="loginBtn" \n style="width: 100%; padding: 12px; background: #4a8bfc; color: white; \n border: none; border-radius: 4px; font-size: 15px; cursor: pointer;\n transition: background 0.3s;"\n onmouseover="this.style.background=\'#3a7be0\'"\n onmouseout="this.style.background=\'#4a8bfc\'">\n 登录\n </button>\n </div>\n ', success: (e, t) => { $("#loginBtn").click((function() { const e = $("#username").val(), n = $("#password").val(); if (!e || !n) return void show.error("请输入用户名和密码"); let a = loading(); (async (e, t) => { let n = `${U}/v1/sessions?username=${encodeURIComponent(e)}&password=${encodeURIComponent(t)}&device_uuid=04b9534d-5118-53de-9f87-2ddded77111e&device_name=iPhone&device_model=iPhone&platform=ios&system_version=17.4&app_version=official&app_version_number=1.9.29&app_channel=official`, a = { "user-agent": "Dart/3.5 (dart:io)", "accept-language": "zh-TW", "content-type": "multipart/form-data; boundary=--dio-boundary-2210433284", jdsignature: await O() }; return await gmHttp.post(n, null, a); })(e, n).then((async e => { let n = e.success; if (0 === n) show.error(e.message); else { if (1 !== n) throw clog.error("登录失败", e), new Error(e.message); { let n = e.data.token; await localStorage.setItem(me, await encryptData(n)), show.ok("登录成功"), layer.close(t), window.location.href = "/advanced_search?handleTop=1&period=daily"; } } })).catch((e => { clog.error("登录异常:", e), show.error(e.message); })).finally((() => { a.close(); })); })); } }); } } class fe extends X { getName() { return "NavBarPlugin"; } async initCss() { return "\n .highlight-red {\n /* 核心要求:高亮红色文本 */\n color: red !important; \n \n /* 建议:增加字体加粗,效果更明显 */\n font-weight: bold;\n \n /* 建议:增加背景色,效果更突出 */\n /* background-color: yellow; */ \n}\n "; } handle() { if (!r) return; this.margeNav(), this.hookSearch(), this.hookOldSearch(), this.toggleOtherNavItem(), $(window).resize(() => this.toggleOtherNavItem()); if (window.location.href.includes("/search")) { const e = new URLSearchParams(window.location.search); let t = e.get("q"), n = e.get("f"); $("#search-keyword").val(t), n && $("#search-type").val(n), t && this.highlightKeyword(t); } } highlightKeyword(e) { const t = e.trim(); if (!t) return; const n = t.toLowerCase(); $(".video-title strong, .actor-box strong").each((function() { const e = $(this); e.text().toLowerCase().includes(n) && e.addClass("highlight-red"); })); } hookSearch() { $("#navbar-menu-hero").after('\n <div class="navbar-menu" id="search-box">\n <div class="navbar-start" style="display: flex; align-items: center; gap: 5px;">\n <select id="search-type" style="padding: 8px 12px; border: 1px solid #555; border-radius: 4px; background-color: #333; color: #eee; font-size: 14px; outline: none;">\n <option value="all">影片</option>\n <option value="actor">演员</option>\n <option value="series">系列</option>\n <option value="maker">片商</option>\n <option value="director">导演</option>\n <option value="code">番号</option>\n <option value="list">清单</option>\n </select>\n <input id="search-keyword" type="text" placeholder="输入影片番号,演员名等" style="padding: 8px 12px; border: 1px solid #555; border-radius: 4px; flex-grow: 1; font-size: 14px; background-color: #333; color: #eee; outline: none;">\n <a href="/advanced_search?noFold=1" title="高级搜索" style="padding: 6px 12px; background-color: #444; border-radius: 4px; text-decoration: none; color: #ddd; font-size: 14px; border: 1px solid #555;"><span>...</span></a>\n <a id="search-img-btn" style="padding: 6px 16px; background-color: #444; color: #fff; border-radius: 4px; text-decoration: none; font-weight: 500; cursor: pointer; border: 1px solid #555;">识图</a>\n <a id="search-btn" style="padding: 6px 16px; background-color: #444; color: #fff; border-radius: 4px; text-decoration: none; font-weight: 500; cursor: pointer; border: 1px solid #555;">搜索</a>\n </div>\n </div>\n '), $("#search-keyword").on("paste", (e => { const t = e.originalEvent.clipboardData.items; for (let n = 0; n < t.length; n++) if (-1 !== t[n].type.indexOf("image")) { const e = t[n].getAsFile(); $("#search-keyword").blur(); const a = this.getBean("SearchByImagePlugin"); return void a.open((() => { a.handleImageFile(e), a.resetSearchUI(); })); } })).on("keypress", (e => { "Enter" === e.key && setTimeout((() => { $("#search-btn").click(); }), 0); })), $("#search-btn").on("click", (e => { let t = $("#search-keyword").val(), n = $("#search-type option:selected").val(); "" !== t && (window.location.href.includes("/search") ? window.location.href = "/search?q=" + t + "&f=" + n : window.open("/search?q=" + t + "&f=" + n)); })), $("#search-img-btn").on("click", (() => { this.getBean("SearchByImagePlugin").open(); })); } hookOldSearch() { const e = document.querySelector(".search-image"); if (!e) return; const t = e.cloneNode(!0); e.parentNode.replaceChild(t, e), $("#button-search-image").attr("data-tooltip", "以图识图"), $(".search-image").on("click", (e => { this.getBean("SearchByImagePlugin").open(); })); } margeNav() { $('a[href*="/feedbacks/new"]').remove(), $('a[href*="theporndude.com"]').remove(), $('a.navbar-link[href="/makers"]').parent().after('\n <div class="navbar-item has-dropdown is-hoverable">\n <a class="navbar-link">其它</a>\n <div class="navbar-dropdown is-boxed">\n <a class="navbar-item" href="/feedbacks/new" target="_blank" >反饋</a>\n <a class="navbar-item" rel="nofollow noopener" target="_blank" href="https://theporndude.com/zh">ThePornDude</a>\n </div>\n </div>\n '); } toggleOtherNavItem() { const e = $("#search-box"), t = $("#search-bar-container"), n = $(window).width(); // 原逻辑要求宽度 >1600 才显示自定义搜索框, 1024~1600 及 <=1023 会出现"导航栏没加载"的现象 n >= 1280 ? (e.show(), t.hide()) : (e.hide(), t.show()); } } class ve { constructor() { this.queue = Promise.resolve(); } addTask(e) { this.queue = this.queue.then((() => e())).catch((e => { clog.error("执行异步队列任务失败:", e); })); } async waitAllFinished() { return this.queue; } } class be extends X { constructor() { super(...arguments), i(this, "okBackgroundColor", "#7bc73b"), i(this, "errorBackgroundColor", "#de3333"), i(this, "warnBackgroundColor", "#d7a80c"), i(this, "domainErrorBackgroundColor", "#d7780c"), i(this, "siteConfigs", [ { id: "javTrailersBtn", getBaseUrl: async () => await this.getJavTrailersUrl(), itemSelector: ".videos-list .video-link", searchPath: (e, t) => `${e}/search/${t}`, getDetailPageHref: e => e.attr("href"), findCarNumOrTitle: e => e.find("p.card-text").text() }, { id: "123AvBtn", getBaseUrl: async () => await this.getAv123Url() + "/ja", itemSelector: ".box-item", searchPath: (e, t) => `${e}/search?keyword=${t}`, getDetailPageHref: e => e.find(".detail a").attr("href"), findCarNumOrTitle: e => e.find("img").attr("title") }, { id: "jableBtn", getBaseUrl: async () => await this.getjableUrl(), itemSelector: "#list_videos_videos_list_search_result .detail .title a", searchPath: (e, t) => `${e}/search/${t}/`, getDetailPageHref: e => e.attr("href"), findCarNumOrTitle: e => e.text() }, { id: "missAvBtn", getBaseUrl: async () => await this.getMissAvUrl(), itemSelector: ".text-secondary", searchPath: (e, t) => `${e}/search/${t}`, getDetailPageHref: e => e.attr("href"), findCarNumOrTitle: e => e.text() }, { id: "supJavBtn", getBaseUrl: async () => await this.getSupJavUrl(), itemSelector: ".posts post", searchPath: (e, t) => `${e}/?s=${t}`, getDetailPageHref: (e, t, n) => e.attr("href"), findCarNumOrTitle: e => e.attr("title") }, { id: "javDbBtn", getBaseUrl: async () => await this.getJavDbUrl(), itemSelector: ".movie-list .item", searchPath: (e, t) => `${e}/search?q=${t}`, getDetailPageHref: e => e.find("a").attr("href"), findCarNumOrTitle: e => e.find(".video-title").text(), condition: e => l }, { id: "javBusBtn", getBaseUrl: async () => await this.getJavBusUrl(), itemSelector: ".container h3", searchPath: (e, t) => `${e}/${t}`, getDetailPageHref: (e, t, n) => `${t}/${n}`, findCarNumOrTitle: e => e.text(), condition: e => r && e && !e.includes("FC2") }, { id: "fanzaBtn", getBaseUrl: async () => await this.getFanzaUrl(), itemSelector: ".tmb a", searchPath: (e, t) => `${e}/search/=/searchstr=${t}`, getDetailPageHref: e => e.attr("href"), findCarNumOrTitle: e => e.attr("title") || e.text(), condition: e => e && !e.includes("FC2") } ]), i(this, "settingCache", null), i(this, "lastFetchTime", 0), i(this, "CACHE_DURATION", 1e4); } getName() { return "OtherSitePlugin"; } async initCss() { return "\n <style>\n .site-btn {\n position: relative !important;\n min-width: 80px;\n display: inline-block;\n padding: 5px 10px;\n color: white !important;\n background-color:#938585;\n text-decoration: none;\n border-radius: 4px;\n text-align: center;\n margin-bottom: 5px;\n }\n .site-btn:hover {\n color: white;\n transform: translateY(-2px);\n box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n }\n .site-tag {\n position: absolute; \n top: -15px; \n right: 0; \n background-color: #ffc107; \n color: #333; \n font-size: 12px; \n padding: 2px 6px; \n border-radius: 4px;\n }\n </style>\n "; } async handle() { isDetailPage && this.loadOtherSite(null, null, { autoDetect: !1 }).then(); } async loadOtherSite(e, t, n = {}) { if ("yes" !== await storageManager.getSetting("enableLoadOtherSite", "yes")) return; $("#otherSiteBox").remove(); e || (e = this.getPageInfo().carNum); // 读取自定义名称 const _st = await storageManager.getSetting(); const siteNameMap = { javTrailersBtn: _st.siteNameJavTrailer || "JavTrailer", "123AvBtn": _st.siteNameAv123 || "123Av", jableBtn: _st.siteNameJable || "Jable", missAvBtn: _st.siteNameMissAv || "MissAv", supJavBtn: _st.siteNameSupJav || "SupJav", javDbBtn: _st.siteNameJavDb || "JavDb", javBusBtn: _st.siteNameJavBus || "JavBus", fanzaBtn: _st.siteNameFanza || "Fanza" }; const hiddenSites = jhsGetHiddenSites(), i = `\n <div id="otherSiteBox" class="panel-block" style="${r ? "margin-top:8px;font-size:13px" : "margin-top:10px;font-size:13px"}; user-select: none; ">\n <div style="display: flex;gap: 5px;flex-wrap: wrap">\n ${this.siteConfigs.map((e => { if (e.sourceCarNum = t, e.condition && !1 === e.condition(e.sourceCarNum)) return ""; const label = siteNameMap[e.id] || e.id.replace("Btn", ""); const siteKey = JHS_BTN_TO_SITEKEY[e.id] || ""; const isHidden = siteKey && hiddenSites.includes(siteKey); return `<a target="_blank" class="site-btn" style="${isHidden ? "display:none" : ""}" id="${e.id}"><span>${label}</span></a>`; })).join("")}\n </div>\n </div>\n `; $(".movie-panel-info").append(i), $(".container .info").append(i), $("#javTrailersBtn").on("click", (async t => { t.preventDefault(); let o = $("#javTrailersBtn").attr("href"), r = o + "?handle=1"; t && (t.ctrlKey || t.metaKey) && (r = o), utils.openPage(r, e, !1, t); })), await Promise.all(this.siteConfigs.map((async t => { t.condition && !1 === t.condition(t.sourceCarNum) || await this.prepareSiteLink(e, t, _st); }))), n.autoDetect && await this.detectOtherSites(e); } async prepareSiteLink(e, t, settings) { const n = $(`#${t.id}`); if (t.initUrl) return void (n.attr("href", t.initUrl(e)), n.css("backgroundColor", this.warnBackgroundColor), n.attr("title", "点击前往外部搜索页")); try { // 优先使用面板配置的「搜索地址」; 支持两种写法: // 1. 完整 URL (以 http/https 开头) => `搜索地址 + 番号` // 2. 相对路径 (如 search?q= 或 /search?q=) => `域名(原始字段) + / + 路径 + 番号` // 相对路径直接使用「域名」字段的原始值, 不经过 getBaseUrl 附加的额外后缀(如 123Av 会加 /ja), // 以保证按钮跳转与用户在面板中看到的域名一致. // 留空则回退内置 searchPath 规则(此时使用 getBaseUrl). const searchKey = SITE_SEARCH_KEY_MAP[t.id]; const customSearch = searchKey && settings ? String(settings[searchKey] || "").trim() : ""; let i; if (customSearch) { if (/^https?:\/\//i.test(customSearch)) { i = `${customSearch}${e}`; } else { const domainKey = SITE_DOMAIN_KEY_MAP[t.id]; let base = domainKey && settings ? String(settings[domainKey] || "").trim() : ""; if (!base) base = await t.getBaseUrl(); base = base.replace(/\/+$/, ""); const sep = customSearch.startsWith("/") ? "" : "/"; i = `${base}${sep}${customSearch}${e}`; } } else { const a = await t.getBaseUrl(); i = t.searchPath(a, e); } n.attr("href", i), n.attr("title", "点击前往外部搜索页"), n.css("backgroundColor", this.warnBackgroundColor); } catch (a) { n.attr("title", "外部站点地址未配置或不可用"), n.css("backgroundColor", this.domainErrorBackgroundColor); } } async detectOtherSites(e) { const t = $("#detectOtherSiteBtn"), n = t.text(); return t.text("检测中").css("backgroundColor", "#938585"), await Promise.all(this.siteConfigs.map((async t => { t.condition && !1 === t.condition(t.sourceCarNum) || await this.handleSite(e, t); }))), t.text(n).css("backgroundColor", "#1677ff"); } async handleSite(e, t) { const n = $(`#${t.id}`); n.removeAttr("href").find(".site-tag").remove(); if (t.initUrl && (n.attr("href", t.initUrl(e)), n.css("backgroundColor", this.warnBackgroundColor)), t.noHandle && !0 === t.noHandle) { const t = "jhs_other_site_dmm", a = (localStorage.getItem(t) ? JSON.parse(localStorage.getItem(t)) : {})[e]; a && ("single" === a.type ? (n.attr("href", a.url), n.css("backgroundColor", this.okBackgroundColor)) : "multiple" === a.type && (n.attr("href", a.url), n.css("backgroundColor", this.okBackgroundColor))); } else try { if (n.attr("href")) return; if (utils.isHidden(n)) return; const a = "jhs_other_site", i = localStorage.getItem(a) ? JSON.parse(localStorage.getItem(a)) : {}, s = e + "_" + t.id.replace("Btn", ""), o = i[s], m = Date.now(); if (o && o.time && m - o.time < 864e5) return void ("single" === o.type ? (n.attr("href", o.url), n.css("backgroundColor", this.okBackgroundColor)) : "multiple" === o.type && (n.attr("href", o.url), n.append('<span class="site-tag" style="top:-15px">多结果</span>'), n.css("backgroundColor", this.okBackgroundColor))); const r = await t.getBaseUrl(), l = t.searchPath(r, e); n.attr("href", l); /* 预检查仅用于 UI 展示,实际拦截依赖 gmRequest 内部熔断检查 */ const _breaker = gmHttp.isDomainCircuitBroken(l); if (_breaker) { n.attr("title", `站点已熔断,${_breaker.remaining}秒后重试`), n.css("backgroundColor", this.domainErrorBackgroundColor); return; } const c = await storageManager.cachedRequest(`other-site:${t.id}:${e}`, 864e5, (() => gmHttp.get(l, null, t.headers, !0))), d = utils.htmlTo$dom(c), h = []; d.find(t.itemSelector).each(((n, a) => { const i = $(a); if (!t.findCarNumOrTitle(i).toLowerCase().includes(e.toLowerCase())) return; let s = t.getDetailPageHref(i, r, e); if (!s) throw new Error("解析href失败"); s.includes("http") || (s = r + (s.startsWith("/") ? s : "/" + s)), h.push(s); })); let g = "", p = null; if (1 === h.length) { let e = h[0]; n.attr("href", e), n.css("backgroundColor", this.okBackgroundColor), p = { type: "single", url: e, time: m }; } else h.length > 1 ? (n.attr("href", l), g += '<span class="site-tag" style="top:-15px">多结果</span>', n.css("backgroundColor", this.okBackgroundColor), p = { type: "multiple", url: l, time: m }) : (n.attr("href", l), n.attr("title", "未查询到, 点击前往搜索页"), n.css("backgroundColor", this.errorBackgroundColor)); if (p) { const e = localStorage.getItem(a) ? JSON.parse(localStorage.getItem(a)) : {}; e[s] = p, localStorage.setItem(a, JSON.stringify(e)); } g && n.append(g); } catch (a) { const e = String(a), i = t.id.replace("Btn", ""); a._circuitBroken ? (n.attr("title", e), n.css("backgroundColor", this.domainErrorBackgroundColor), clog.warn(`检测第三方资源跳过, ${i} 已熔断`)) : e.includes("Just a moment") ? (n.attr("title", "请求失败:Cloudflare 安全检查。"), n.css("backgroundColor", this.warnBackgroundColor), clog.warn(`检测第三方资源失败, ${i} 需Cloudflare安全检查`)) : e.includes("重定向") ? (n.attr("title", "域名失效"), n.css("backgroundColor", this.domainErrorBackgroundColor), clog.warn(`检测第三方资源失败, ${i} 域名被重定向`)) : e.includes("404 Page Not Found") ? (n.attr("title", "未查询到, 点击前往搜索页"), n.css("backgroundColor", this.errorBackgroundColor)) : (console.error(a), n.attr("title", "请求失败。"), n.css("backgroundColor", this.errorBackgroundColor), clog.warn(`检测第三方资源失败, ${i}`)); } } async getSettingCache() { const e = Date.now(); return (!this.settingCache || e - this.lastFetchTime > this.CACHE_DURATION) && (this.settingCache = await storageManager.getSetting(), this.lastFetchTime = e), this.settingCache; } async getMissAvUrl() { return (await this.getSettingCache()).missAvUrl || "https://missav.live"; } async getjableUrl() { return (await this.getSettingCache()).jableUrl || "https://jable.tv"; } async getAvgleUrl() { return (await this.getSettingCache()).avgleUrl || "https://jav.rs"; } async getJavTrailersUrl() { return (await this.getSettingCache()).javTrailersUrl || "https://javtrailers.com"; } async getAv123Url() { return (await this.getSettingCache()).av123Url || "https://123av.com"; } async getJavDbUrl() { return (await this.getSettingCache()).javDbUrl || "https://javdb.com"; } async getJavBusUrl() { return (await this.getSettingCache()).javBusUrl || "https://www.javbus.com"; } async getSupJavUrl() { return (await this.getSettingCache()).supJavUrl || "https://supjav.com"; } async getFanzaUrl() { return (await this.getSettingCache()).fanzaUrl || "https://www.dmm.co.jp"; } } class we extends X { getName() { return "BusDetailPagePlugin"; } async initCss() { if (!window.isDetailPage) return ""; $("h4:contains('推薦')").hide(); } async handle() { if (window.location.href.includes("/star/")) { const e = $(".avatar-box"); if (e.length > 0) { let t = e.parent(); t.css("position", "initial"), t.insertBefore(t.parent()); } } this.alignBusTopAlert(); $(".genre a").each((function() { const e = $(this).attr("href"); e && (e.startsWith("http://") || e.startsWith("https://") || e.startsWith("/")) && $(this).attr("target", "_blank"); })), this.addCopyCarNumBtn(); } /** JavBus: 顶部「已有磁力/全部影片」提示条与头像信息栏 .avatar-box 左右对齐 */ alignBusTopAlert() { const align = () => { // 对齐目标优先取 .avatar-box (和封面首列左对齐), 回退到 .masonry 减去内边距 const $ref = $(".avatar-box").length ? $(".avatar-box") : $(".masonry"); if (!$ref.length) return; const $bar = $("div.alert-success, div.alert-common, div.alert").filter(((_, el) => /已有磁力|全部影片|當前顯示|当前显示/.test(el.textContent || ""))).first(); if (!$bar.length) return; const ref = $ref[0].getBoundingClientRect(), el = $bar[0]; el.style.setProperty("box-sizing", "border-box", "important"); el.style.setProperty("width", Math.round(ref.width) + "px", "important"); el.style.setProperty("max-width", Math.round(ref.width) + "px", "important"); el.style.setProperty("margin-left", "0", "important"); el.style.setProperty("margin-right", "0", "important"); const b = el.getBoundingClientRect(); el.style.setProperty("margin-left", Math.round(ref.left - b.left) + "px", "important"); }; utils.loopDetector((() => $(".masonry").length > 0 || $(".avatar-box").length > 0), (() => { align(), setTimeout(align, 300), $(window).on("resize", align); }), 1, 1e4, !1); } addCopyCarNumBtn() { let e = null; const t = document.querySelectorAll("span.header"); for (const n of t) if ("識別碼:" === n.textContent.trim()) { e = n; break; } if (e) { const t = e.nextElementSibling; if (t && "SPAN" === t.tagName) { const e = t.textContent.trim(), n = document.createElement("button"); n.type = "button"; n.className = "jav-avid-copy"; n.textContent = "复制番号"; n.title = `复制番号:${e}`; // 复制番号按钮样式 (移植自 老司机脚本) n.style.cssText = 'display:inline-block;margin-left:6px;padding:1px 6px;font-size:12px;font-family:-apple-system,BlinkMacSystemFont,"Microsoft YaHei","PingFang SC","Noto Sans CJK SC","Segoe UI",sans-serif;font-weight:500;text-align:center;background:#e8f4fd;border:1px solid #90c5e8;border-radius:3px;cursor:pointer;color:#1a6fa8;vertical-align:middle;white-space:nowrap;box-sizing:border-box;line-height:1.4;'; n.addEventListener("click", (function(t) { t.preventDefault(), t.stopPropagation(); const n = () => { this.textContent = "已复制", setTimeout((() => { this.textContent = "复制番号"; }), 900); }; navigator.clipboard && navigator.clipboard.writeText && navigator.clipboard.writeText(e).then((() => n())).catch((t => { console.error("无法通过标准API复制:", t), alert("复制失败,请手动复制: " + e); })); })), t.parentNode.insertBefore(n, t.nextSibling); } } } } class ye extends X { getName() { return "DetailPageButtonPlugin"; } constructor() { super(), this.answerCount = 1; } async handle() { this.hideVideoControls(), window.isDetailPage && this.createMenuBtn(); this.bindHotkey().catch((e => clog.error("详情页快捷键绑定失败", e))); } /** 详情页快捷键: 屏蔽/收藏/已下载/已观看/快进 */ async bindHotkey() { const s = await storageManager.getSetting(), handlers = {}; s.filterHotKey && (handlers[s.filterHotKey] = () => { this.answerCount >= 2 ? this.filterOne(null, !0) : this.filterOne(null), this.answerCount++; }); s.favoriteHotKey && (handlers[s.favoriteHotKey] = () => this.favoriteOne()); s.hasDownHotKey && (handlers[s.hasDownHotKey] = () => this.hasDownOne()); s.hasWatchHotKey && (handlers[s.hasWatchHotKey] = () => this.hasWatchOne()); s.speedVideoHotKey && (handlers[s.speedVideoHotKey] = () => { const b = $("#speed-btn"); b.length ? b[0].click() : $(".preview-video-container").length ? $(".preview-video-container")[0].click() : $("#javTrailersBtn").click(); }); Object.entries(handlers).forEach((([key, handler]) => { if (!HotkeyManager.isHotkeyFormat(key)) return; HotkeyManager.registerHotkey(key, (() => { const ae = document.activeElement; "INPUT" === ae.tagName || "TEXTAREA" === ae.tagName || ae.isContentEditable || window.isDetailPage && handler(); })); })); } async createMenuBtn() { const e = this.getPageInfo(), t = e.carNum, n = `\n <div class="jhs-detail-btn-row" style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:${f}; color: white; text-align: center; padding: 8px 0;">\n <span>${m}</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:${w}; color: white; text-align: center; padding: 8px 0;">\n <span>${v}</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:${x}; color: white; text-align: center; padding: 8px 0;">\n <span>${y}</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:${S}; color: white; text-align: center; padding: 8px 0;">\n <span>${k}</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="enable-magnets-filter" class="menu-btn" style="width: 140px; background-color: #c2bd4c; color: white; text-align: center; padding: 8px 0;">\n <span id="magnets-span">关闭磁力过滤</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="search-subtitle-btn" class="menu-btn" style="width: 160px; background: linear-gradient(to bottom, #8d5656, rgb(196,159,91)); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (SubTitleCat)</span>\n </a>\n </div>\n </div>\n `; r && $(".tabs").after(n), l && $("#mag-submit-show").before(n), $("#favoriteBtn").on("click", (async () => this.toggleStatus(h))), $("#filterBtn").on("click", (async (e) => this.toggleStatus(d, e))), $("#hasDownBtn").on("click", (async () => this.toggleStatus(g))), $("#hasWatchBtn").on("click", (async () => this.toggleStatus(p))), $("#magnetSearchBtn").on("click", (() => { let t = this.getBean("MagnetHubPlugin").createMagnetHub(e.carNum); layer.open({ type: 1, title: "磁力搜索 " + e.carNum, content: '<div id="magnetHubBox"></div>', area: utils.getResponsiveArea([ "60%", "80%" ]), scrollbar: !1, success: () => { $("#magnetHubBox").append(t); } }); })); const a = this.getBean("HighlightMagnetPlugin"), i = await storageManager.getSetting("enableMagnetsFilter", _); $("#enable-magnets-filter").removeClass("do-hide"); $("#magnets-span").text(i === _ ? "关闭磁力过滤" : "开启磁力过滤"), i === _ && a.doFilterMagnet(), $("#enable-magnets-filter").on("click", (e => { let t = $("#magnets-span"); "关闭磁力过滤" === t.text() ? (a.showAll(), t.text("开启磁力过滤"), storageManager.saveSettingItem("enableMagnetsFilter", C)) : (a.doFilterMagnet(), t.text("关闭磁力过滤"), storageManager.saveSettingItem("enableMagnetsFilter", _)); })), $("#search-subtitle-btn").on("click", (e => utils.openPage(`https://subtitlecat.com/index.php?search=${t}`, t, !1, e))), $("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(t))), this.showStatus(t).then(); } async showStatus(e) { const t = $("#filterBtn span"), n = $("#favoriteBtn span"), a = $("#hasDownBtn span"), i = $("#hasWatchBtn span"); t.text(m), n.text(v), a.text(y), i.text(k); const o = await storageManager.getCar(e); if (o) switch (o.status) { case d: t.text(u); break; case h: n.text(b); break; case g: a.text("已标记下载"); break; case p: i.text("已标记观看"); } } /** 详情页鉴定按钮统一切换逻辑: 首次点击=标记, 再次点击=取消. 屏蔽需要二次确认. */ async toggleStatus(actionType, evt) { try { const info = this.getPageInfo(), carNum = info.carNum; if (!carNum) return void show.error("番号为空!"); const existing = await storageManager.getCar(carNum); const isSame = existing && existing.status === actionType; const doSave = async () => { await storageManager.saveCar({ carNum: carNum, url: info.url, names: info.actress, actionType: actionType, publishTime: info.publishTime }), this.showStatus(carNum).then(), window.refresh(), show.ok("操作成功"); }; const doRemove = async () => { await storageManager.removeCar(carNum), this.showStatus(carNum).then(), window.refresh(), show.ok("已取消鉴定"); }; if (isSame) { actionType === d ? utils.q(evt, `是否取消屏蔽${carNum}?`, doRemove) : await doRemove(); } else { actionType === d ? utils.q(evt, `是否屏蔽${carNum}?`, doSave) : await doSave(); } } catch (err) { console.error("鉴定操作失败:", err), show.error("操作失败"); } } async favoriteOne() { try { let e = this.getPageInfo(); await storageManager.saveCar({ carNum: e.carNum, url: e.url, names: e.actress, actionType: h, publishTime: e.publishTime }), this.showStatus(e.carNum).then(), window.refresh(), utils.closePage(); } catch (t) { console.error("收藏操作失败:", t), show.error("操作失败"); } } async hasDownOne() { try { let e = this.getPageInfo(); await storageManager.saveCar({ carNum: e.carNum, url: e.url, names: e.actress, actionType: g, publishTime: e.publishTime }), this.showStatus(e.carNum).then(), window.refresh(), utils.closePage(); } catch (t) { console.error("标记已下载失败:", t), show.error("操作失败"); } } async hasWatchOne() { try { let e = this.getPageInfo(); await storageManager.saveCar({ carNum: e.carNum, url: e.url, names: e.actress, actionType: p, publishTime: e.publishTime }), this.showStatus(e.carNum).then(), window.refresh(), utils.closePage(); } catch (t) { console.error("标记已观看失败:", t), show.error("操作失败"); } } searchXunLeiSubtitle(e) { let t = loading(); gmHttp.get(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?gcid=&cid=&name=${e}`).then((t => { let n = t.data; n && 0 !== n.length ? layer.open({ type: 1, title: "迅雷字幕", content: '\n <div style="height: 100%;overflow:hidden;"> \n <div id="xunlei-table-container" style="height: 100%;padding-bottom: 20px"></div>\n </div>\n ', scrollbar: !1, area: utils.getResponsiveArea([ "60%", "70%" ]), anim: -1, success: (t, a) => { new Tabulator("#xunlei-table-container", { layout: "fitColumns", placeholder: "暂无数据", virtualDom: !0, data: n, responsiveLayout: "collapse", responsiveLayoutCollapse: !0, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, columns: [ { title: "文件名", field: "name", headerSort: !1, responsive: 0 }, { title: "类型", field: "ext", headerSort: !1, responsive: 0 }, { title: "操作", responsive: 0, headerSort: !1, formatter: (t, n, a) => { const i = t.getData(); return a((() => { const n = t.getElement().querySelector(".a-primary"), a = t.getElement().querySelector(".a-success"); n && n.addEventListener("click", (async t => { let n = i.url, a = e + "." + i.ext; this.previewSubtitle(n, a); })), a && a.addEventListener("click", (async t => { let n = i.url, a = e + "." + i.ext, s = await gmHttp.get(n); utils.download(s, a); })); })), '\n <a class="a-primary">预览</a>\n <a class="a-success">下载</a>\n '; } } ], locale: "zh-cn", langs: { "zh-cn": { pagination: { first: "首页", first_title: "首页", last: "尾页", last_title: "尾页", prev: "上一页", prev_title: "上一页", next: "下一页", next_title: "下一页", all: "所有", page_size: "每页行数" } } } }), utils.setupEscClose(a); } }) : show.error("迅雷中找不到相关字幕!"); })).catch((e => { console.error(e), show.error(e); })).finally((() => { t.close(); })); } async filterOne(e, t) { e && e.preventDefault(); let n = this.getPageInfo(); t ? (await storageManager.saveCar({ carNum: n.carNum, url: n.url, names: n.actress, actionType: d, publishTime: n.publishTime }), this.showStatus(n.carNum).then(), window.refresh(), utils.closePage(), layer.closeAll(), this.answerCount = 1) : utils.q(e, `是否屏蔽${n.carNum}?`, (async () => { await storageManager.saveCar({ carNum: n.carNum, url: n.url, names: n.actress, actionType: d, publishTime: n.publishTime }), this.showStatus(n.carNum).then(), window.refresh(), utils.closePage(); }), (() => { this.answerCount = 1; })); } hideVideoControls() { $(document).on("mouseenter", "#preview-video", (function() { $(this).prop("controls", !0); })); } async previewSubtitle(e, t) { if (!e) return void console.error("未提供文件URL"); const n = e.split(".").pop().toLowerCase(); if ("ass" === n || "srt" === n) try { let a = await gmHttp.get(e), i = "字幕预览"; "ass" === n ? i = "ASS字幕预览 - " + t : "srt" === n && (i = "SRT字幕预览 - " + t); const s = a.split("\n"); let o = ""; const r = String(s.length).length; s.forEach(((e, t) => { const n = String(t + 1).padStart(r, " "); o += `<span style="color:#AAA;">${n}. </span>${e}\n`; })); const l = o; layer.open({ type: 1, title: i, area: utils.getResponsiveArea([ "80%", "80%" ]), scrollbar: !1, content: `<div style="padding:15px 5px;background:#1E1E1E;color:#FFF;font-family:Consolas,Monaco,monospace;white-space:pre-wrap;overflow:auto;height:100%;">${l}</div>`, btn: [ "下载", "关闭" ], btn1: function(e, n, i) { return utils.download(a, t), !1; } }); } catch (a) { show.error(`预览失败: ${a.message}`), console.error("预览字幕文件出错:", a); } else show.error("仅支持预览ASS和SRT字幕文件"); } } class xe extends X { constructor() { super(...arguments), i(this, "tableObj", null); } getName() { return "HistoryPlugin"; } async initCss() { return "\n <style>\n /* 下拉菜单容器(相对定位) */\n .sub-btns {\n position: relative;\n display: inline-block;\n }\n \n /* 下拉菜单内容(默认隐藏) */\n .sub-btns-menu {\n display: none;\n position: absolute;\n right: 80px;\n top:-10px;\n background: white;\n padding:10px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n z-index: 100;\n border-radius: 4px;\n overflow: hidden;\n }\n \n \n /* 点击后显示菜单(JS 控制) */\n .sub-btns-menu.show {\n display: flex !important;\n flex-direction: column;\n }\n \n .table-link-param {\n cursor: pointer;\n }\n </style\n "; } handleResize() { $(".navbar-search").is(":hidden") ? ($(".historyBtnBox").show(), $(".miniHistoryBtnBox").hide()) : ($(".historyBtnBox").hide(), $(".miniHistoryBtnBox").show()); } handle() { r && ($(".navbar-end").prepend('<div class="navbar-item has-sub-btns is-hoverable historyBtnBox">\n <a id="historyBtn" class="navbar-link nav-btn" style="color: #aade66 !important;padding-right:15px !important;">\n 鉴定记录\n </a>\n </div>'), $(".navbar-search").css("margin-left", "0").before('\n <div class="navbar-item miniHistoryBtnBox">\n <a id="miniHistoryBtn" class="navbar-link nav-btn" style="color: #aade66 !important;padding-left:0 !important;padding-right:0 !important;">\n 鉴定记录\n </a>\n </div>\n '), this.handleResize(), $(window).resize((() => { this.handleResize(); })), $("#historyBtn,#miniHistoryBtn").on("click", (e => this.openHistory()))), l && utils.loopDetector((() => $("#setting-btn").length), (() => { $("#top-right-box").append('\n <a id="historyBtn" class="menu-btn main-tab-btn" style="background-color:#b68625 !important; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; height: 30px; padding: 0 12px; line-height: 1; margin: 0 !important;">\n 鉴定记录\n </a>\n '), $("#historyBtn,#miniHistoryBtn").on("click", (e => this.openHistory())); }), 1, 1e4, !1), this.bindClick(); } openHistory() { let e = `\n <div style="padding: 10px 20px; height: 100%;overflow:hidden;"> \n <div id="filterBox" style="display: flex;gap: 5px;">\n <select id="dataType" style="text-align: center;min-width: 150px;">\n <option value="all" selected>所有</option>\n <option value="filter">${u}</option>\n <option value="favorite">${b}</option>\n <option value="hasDown">${y}</option>\n <option value="hasWatch">${k}</option>\n </select>\n <input id="searchCarNum" type="text" placeholder="搜索番号|演员" style="padding: 4px 5px;">\n <a id="clearSearchbtn" class="a-info" style="margin-left: 0">重置</a>\n </div>\n <div id="allSelectBox" style="margin-top: 8px;display: none">\n <a class="menu-btn multiple-history-deleteBtn" style="background-color:#8c8080; color:white; margin-bottom: 5px;"> <span>移除</span> </a>\n <a class="menu-btn multiple-history-hasWatchBtn" style="background-color:${S};margin-bottom: 5px">${k}</a>\n <a class="menu-btn multiple-history-hasDownBtn" style="background-color:${x};margin-bottom: 5px">${y}</a>\n <a class="menu-btn multiple-history-favoriteBtn" style="background-color:${w};margin-bottom: 5px">${v}</a>\n <a class="menu-btn multiple-history-filterBtn" style="background-color:${f};margin-bottom: 5px">${m}</a>\n </div>\n <div id="table-container" style="height: calc(100% - 50px); overflow-x:hidden;"></div>\n </div>\n `; layer.open({ type: 1, title: "鉴定记录", content: e, scrollbar: !1, shadeClose: !0, area: utils.getResponsiveArea([ "70%", "90%" ]), anim: -1, success: async e => { await this.loadTableData(), $(".layui-layer-content").on("click", "#clearSearchbtn", (async e => { $("#searchCarNum").val(""), $("#dataType").val("all"), await this.reloadTable(), $("#allSelectBox").hide(); })).on("focusout keydown", "#searchCarNum", (async e => { if ("focusout" === e.type || "Enter" === e.key) { if ("Enter" === e.key && e.preventDefault(), "keydown" === e.type && "Enter" !== e.key) return; await this.reloadTable(); } })).on("click", ".table-link-param", (async e => { let t = $(e.currentTarget); $("#searchCarNum").val(t.text()), await this.reloadTable(); })).on("change", "#dataType", (async () => { await this.reloadTable(); })); }, end: () => { this.tableObj && (this.tableObj.destroy(), this.tableObj = null), window.refresh(); } }); } async reloadTable() { this.tableObj.deselectRow(), this.tableObj.setPage(1); } bindClick() { document.addEventListener("click", (function(e) { if (e.target.closest(".sub-btns-toggle")) { const t = e.target.closest(".sub-btns").querySelector(".sub-btns-menu"); document.querySelectorAll(".sub-btns-menu.show").forEach((e => { e !== t && e.classList.remove("show"); })), t.classList.toggle("show"); } else document.querySelectorAll(".sub-btns-menu.show").forEach((e => { e.classList.remove("show"); })); })), $(document).on("click", ".history-deleteBtn, .history-filterBtn, .history-favoriteBtn, .history-hasDownBtn, .history-hasWatchBtn, .history-detailBtn", (e => { e.preventDefault(), e.stopPropagation(); const t = $(e.currentTarget), n = t.closest(".action-btns"), a = n.attr("data-car-num"), i = n.attr("data-href"), s = async e => { try { await storageManager.saveCar({ carNum: a, url: i, names: null, actionType: e }), window.refresh(), await this.reloadTable(); } catch (s) { console.error("历史记录操作失败:", s), show.error("操作失败"); } }; t.hasClass("history-filterBtn") ? utils.q(e, `是否屏蔽${a}?`, (() => s(d))) : t.hasClass("history-favoriteBtn") ? s(h).then() : t.hasClass("history-hasDownBtn") ? s(g).then() : t.hasClass("history-hasWatchBtn") ? s(p).then() : t.hasClass("history-deleteBtn") ? this.handleDelete(e, a) : t.hasClass("history-detailBtn") && this.handleClickDetail(e, { carNum: a, url: i }).then(); })), $(document).on("click", ".multiple-history-deleteBtn, .multiple-history-filterBtn, .multiple-history-favoriteBtn, .multiple-history-hasDownBtn, .multiple-history-hasWatchBtn", (e => { e.preventDefault(), e.stopPropagation(); const t = $(e.currentTarget); let n = this.tableObj.getSelectedData(), a = "", i = ""; t.hasClass("multiple-history-filterBtn") ? (a = "屏蔽", i = d) : t.hasClass("multiple-history-favoriteBtn") ? (a = "收藏", i = h) : t.hasClass("multiple-history-hasDownBtn") ? (a = "已下载", i = g) : t.hasClass("multiple-history-hasWatchBtn") ? (a = "已观看", i = p) : t.hasClass("multiple-history-deleteBtn") && (a = "移除", i = "delete"), utils.q(e, `当前已勾选${n.length}条数据, 是否全标记为 ${a}?`, (async () => { let e = loading(); try { if ("delete" === i) { const e = n.map((e => e.carNum)), t = await storageManager.batchRemoveCars(e); t > 0 ? show.ok(`已成功删除 ${t} 个番号`) : !1 === t && show.error("提供的番号中没有一个存在于列表中。"); } else { const e = JSON.parse(JSON.stringify(n)); e.forEach((e => { e.actionType = i; })), await storageManager.saveCarList(e), show.ok("操作成功"); } this.tableObj.deselectRow(), this.reloadTable().then(); } catch (t) { console.error(t); } finally { e.close(); } })); })); } async getDataList(e, t, n) { let a = await storageManager.getCarList(); this.allCount = a.length, this.filterCount = 0, this.favoriteCount = 0, this.hasDownCount = 0, this.hasWatchCount = 0, this.waitCheckCount = 0, a.forEach((e => { switch (e.status) { case d: this.filterCount++; break; case h: this.favoriteCount++; break; case g: this.hasDownCount++; break; case p: this.hasWatchCount++; break; default: this.waitCheckCount++; } })), $('#dataType option[value="all"]').text(`所有 (${this.allCount})`), $('#dataType option[value="waitCheck"]').text(`待鉴定 (${this.waitCheckCount})`), $('#dataType option[value="filter"]').text(`${u} (${this.filterCount})`), $('#dataType option[value="favorite"]').text(`${b} (${this.favoriteCount})`), $('#dataType option[value="hasDown"]').text(`${y} (${this.hasDownCount})`), $('#dataType option[value="hasWatch"]').text(`${k} (${this.hasWatchCount})`); const i = $("#dataType").val(); let s = "all" === i ? a : "waitCheck" === i ? a.filter((e => "" === e.status || !e.status)) : a.filter((e => e.status === i)); const o = $("#searchCarNum").val().trim(); if (o) { let e = o.toLowerCase().replace("-c", "").replace("-uc", "").replace("-4k", ""); s = s.filter((t => { const n = t.carNum.toLowerCase().includes(e); const a = (t.names ? t.names : "").toLowerCase().includes(e); return n || a; })); } if (n && n.length > 0) { const e = n[0], t = e.field, a = e.dir; s.sort(((e, n) => { const i = e[t], s = n[t], o = null == i || "" === i, r = null == s || "" === s; return o && !r ? 1 : !o && r ? -1 : o && r ? 0 : i < s ? "asc" === a ? -1 : 1 : i > s ? "asc" === a ? 1 : -1 : 0; })); } const r = s.length, l = Math.ceil(r / t), c = (e - 1) * t, m = c + t; return s = s.slice(c, m), { maxPage: l, dataList: s, totalCount: r }; } async loadTableData() { this.tableObj = new Tabulator("#table-container", { layout: "fitColumns", placeholder: "暂无数据", virtualDom: !0, pagination: !0, paginationMode: "remote", sortMode: "remote", ajaxURL: "queryRealm", dataLoader: !1, ajaxRequestFunc: async (e, t, n) => { const a = n.page, i = n.size, s = n.sort; return await this.getDataList(a, i, s); }, dataReceiveParams: { last_page: "maxPage", last_row: "totalCount", data: "dataList" }, paginationSize: 50, paginationSizeSelector: [ 50, 100, 1e3, 99999 ], paginationCounter: (e, t, n, a, i) => `共 ${a} 条记录`, responsiveLayout: "collapse", responsiveLayoutCollapse: !0, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, selectableRowsPersistence: !1, index: "carNum", columns: [ { formatter: "rowSelection", titleFormatter: "rowSelection", hozAlign: "center", headerSort: !1, responsive: 0, width: 40, titleFormatterParams: { rowRange: "active" }, cellClick: (e, t) => { t.getRow().toggleSelect(); } }, { title: "番号", field: "carNum", width: 120, sorter: "string", responsive: 0, formatter: (e, t, n) => { const a = e.getData().carNum, i = a.indexOf("-"); if (-1 === i) return a; return `<a class="table-link-param">${a.substring(0, i + 1)}</a>${a.substring(i + 1)}`; } }, { title: "演员", field: "names", minWidth: 200, sorter: "string", responsive: 5, headerSort: !0, formatter: (e, t, n) => (e.getData().names || "").split(" ").filter((e => "" !== e.trim())).map((e => `<a class="table-link-param">${e}</a>`)).join(" ") }, { title: "创建时间", field: "createDate", width: 170, sorter: "string", responsive: 4 }, { title: "修改时间", field: "updateDate", width: 170, sorter: "string", responsive: 4 }, { title: "发行时间", field: "publishTime", width: 170, sorter: "string", responsive: 4 }, { title: "来源", field: "url", width: 80, sorter: "string", responsive: 5, hozAlign: "left", formatter: (e, t, n) => { let a = e.getData().url; return a ? a.includes("javdb") ? '<span style="color:#d34f9e">Javdb</span>' : a.includes("javbus") ? '<span style="color:#eaa813">JavBus</span>' : a.includes("123av") ? '<span style="color:#eaa813">123Av</span>' : `<span style="color:#050505">${a}</span>` : ""; } }, { title: "状态", field: "status", width: 100, sorter: "string", responsive: 1, headerSort: !1, formatter: (e, t, n) => { const a = e.getData().status; let i = "", s = ""; switch (a) { case "filter": i = f, s = m; break; case "favorite": i = w, s = v; break; case "hasDown": i = x, s = y; break; case "hasWatch": i = S, s = k; break; default: s = a; } return `<span style="color:${i}">${s}</span>`; } }, { title: "备注", field: "remark", width: 100, sorter: "string", responsive: 6 }, { title: "操作", sorter: "string", minWidth: 150, cssClass: "action-cell-dropdown", responsive: 0, headerSort: !1, formatter: (e, t, n) => { const a = e.getData(); return n((() => { var t; null == (t = e.getElement().querySelector(".history-editBtn")) || t.addEventListener("click", (e => { this.editRecord(a); })); })), `\n <div class="action-btns" style="display: flex; gap: 5px;justify-content:center" data-car-num="${a.carNum}" data-href="${a.url ? a.url : ""}">\n <div class="sub-btns">\n <a class="menu-btn sub-btns-toggle" style="background-color:#c59d36; color:white; margin-bottom: 5px;">\n <span>变更</span>\n </a>\n <div class="sub-btns-menu">\n <a class="menu-btn history-editBtn" style="background-color:#007bff; color:white; margin-bottom: 5px;"> <span>编辑</span> </a>\n <a class="menu-btn history-deleteBtn" style="background-color:#8c8080; color:white; margin-bottom: 5px;"> <span>移除</span> </a>\n <a class="menu-btn history-hasWatchBtn" style="background-color:${S};margin-bottom: 5px">${k}</a>\n <a class="menu-btn history-hasDownBtn" style="background-color:${x};margin-bottom: 5px">${y}</a>\n <a class="menu-btn history-favoriteBtn" style="background-color:${w};margin-bottom: 5px">${v}</a>\n <a class="menu-btn history-filterBtn" style="background-color:${f};margin-bottom: 5px">${m}</a>\n </div>\n </div>\n \n <a class="menu-btn history-detailBtn" style="background-color:#3397de; color:white; margin-bottom: 5px;"> <span>详情页</span> </a>\n \n </div>\n `; } } ], initialSort: [ { column: "updateDate", dir: "desc" } ], locale: "zh-cn", langs: { "zh-cn": { pagination: { first: "首页", first_title: "首页", last: "尾页", last_title: "尾页", prev: "上一页", prev_title: "上一页", next: "下一页", next_title: "下一页", all: "所有", page_size: "每页行数" } } } }), this.tableObj.on("rowSelectionChanged", ((e, t, n, a) => { const i = $("#allSelectBox"), s = $("#filterBox"); e && e.length > 0 ? (s.hide(), i.show()) : (s.show(), i.hide()); })), this.tableObj.on("rowDblClick", (function(e, t) { t.toggleSelect(); })), this.tableObj.on("tableBuilt", (async () => {})); } handleDelete(e, t) { utils.q(e, `是否移除${t}?`, (async () => { await storageManager.removeCar(t), this.getBean("ListPagePlugin").showCarNumBox(t), this.reloadTable(null).then(); })); } async handleClickDetail(e, t) { if (r) if (t.carNum.includes("FC2-")) { const e = this.parseMovieId(t.url); this.getBean("Fc2Plugin").openFc2Dialog(e, t.carNum, t.url); } else { if (!t.url) return void window.open("/search?q=" + t.carNum, "_blank"); utils.openPage(t.url, t.carNum, !1, e); } if (l) { let n = t.url; if (n.includes("javdb")) if (t.carNum.includes("FC2-")) { const e = this.parseMovieId(n); await this.getBean("Fc2Plugin").openFc2Page(e, t.carNum, n); } else window.open(n, "_blank"); else utils.openPage(t.url, t.carNum, !1, e); } } async editRecord(e) { const t = e.carNum, n = e.names || "", a = e.url || "", i = e.status, s = e.remark || "", o = "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; min-height: 60px; overflow-y: hidden;", r = "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;", l = [ { value: d, text: m }, { value: h, text: v }, { value: g, text: y }, { value: p, text: k } ]; clog.debug(l); const c = `\n <div style="padding: 20px;">\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">番号:</label>\n <input type="text" id="edit-carNum" value="${t}" style="${r} background-color: #f0f0f0;" readonly>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">演员 (用空格隔开):</label>\n <textarea id="edit-names" style="${o}">${n}</textarea>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">状态:</label>\n <select id="edit-status" style="width: 100%; padding: 10px; border: 1px solid #ddd;">\n <option value="" ${"" === i ? "selected" : ""}>-- 请选择 --</option>\n ${l.map((e => `\n <option value="${e.value}" ${i === e.value ? "selected" : ""}>${e.text}</option>\n `)).join("")}\n </select>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">链接:</label>\n <input type="text" id="edit-url" value="${a}" style="${r}">\n </div>\n \n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">备注:</label>\n <textarea id="edit-remark" style="${o}">${s}</textarea>\n </div>\n </div>\n `; layer.open({ type: 1, title: `编辑记录: ${t}`, area: utils.getResponsiveArea([ "500px", "650px" ]), content: c, btn: [ "保存", "取消" ], success: (e, t) => { const n = e => { e.css("height", "auto"), e.css("height", e[0].scrollHeight + 15 + "px"); }, a = $("#edit-names"); a.on("input", (function() { n($(this)); })), n(a); const i = $("#edit-remark"); i.on("input", (function() { n($(this)); })), n(i); }, yes: async t => { const n = $("#edit-names").val().trim(), a = $("#edit-status").val(), i = $("#edit-url").val().trim(), s = $("#edit-remark").val().trim(); if (!a) return show.error("请选择状态"), !1; const o = { ...e, names: n, actionType: a, url: i, remark: s }; await storageManager.updateCarInfo(o), this.tableObj.setData(), layer.close(t); } }); } } class $e extends X { constructor() { super(...arguments), i(this, "floorIndex", 1), i(this, "isInit", !1); } getName() { return "ReviewPlugin"; } async handle() { if (window.isDetailPage) { if (r) { const e = this.parseMovieId(window.location.href); await this.showReview(e); } if (l) { let e = this.getPageInfo().carNum; const t = await (async e => { let t = `${U}/v2/search`, n = { "user-agent": "Dart/3.5 (dart:io)", "accept-language": "zh-TW", host: "jdforrepam.com", jdsignature: await O() }, a = { q: e, page: 1, type: "movie", limit: 1, movie_type: "all", from_recent: "false", movie_filter_by: "all", movie_sort_by: "relevance" }; return (await gmHttp.get(t, a, n)).data.movies; })(e); let n = null; for (let a = 0; a < t.length; a++) { let i = t[a]; if (i.number.toLowerCase() === e.toLowerCase()) { n = i.id; break; } } if (!n) return; this.showReview(n, $("#sample-waterfall")).then(); } } } async showReview(e, t) { const n = await storageManager.getSetting("enableLoadReview", _), a = t || $("#magnets-content"); a.append(`\n <div style="display: flex; align-items: center; margin: 16px 0; color: #666; font-size: 14px;">\n <span style="flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent);"></span>\n <span style="padding: 0 10px;" data-tip="想要发表评论? 滑上去, 点击上面的按钮-看过">评论区</span>\n <a id="reviewsFold" style="margin-left: 8px; color: #1890ff; text-decoration: none; display: flex; align-items: center;">\n <span class="toggle-text">${n === _ ? "折叠" : "展开"}</span>\n <span class="toggle-icon" style="margin-left: 4px;">${n === _ ? "▲" : "▼"}</span>\n </a>\n <span style="flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent);"></span>\n </div>\n `), $("#reviewsFold").on("click", (t => { t.preventDefault(), t.stopPropagation(); const n = $("#reviewsFold .toggle-text"), a = $("#reviewsFold .toggle-icon"), i = "展开" === n.text(); n.text(i ? "折叠" : "展开"), a.text(i ? "▲" : "▼"), i ? ($("#reviewsContainer").show(), $("#reviewsFooter").show(), this.isInit || (this.fetchAndDisplayReviews(e), this.isInit = !0), storageManager.saveSettingItem("enableLoadReview", _)) : ($("#reviewsContainer").hide(), $("#reviewsFooter").hide(), storageManager.saveSettingItem("enableLoadReview", C)); })), a.append('<div id="reviewsContainer"></div>'), a.append('<div id="reviewsFooter"></div>'), n === _ && await this.fetchAndDisplayReviews(e); } async fetchAndDisplayReviews(e) { const t = $("#reviewsContainer"), n = $("#reviewsFooter"); t.append('<div id="reviewsLoading" style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取评论中...</div>'); const a = await storageManager.getSetting("reviewCount", 20); let i = null; try { i = await R(e, 1, a); } catch (o) { o.toString().includes("簽名已過期") && show.error("生成签名失败, 请检查系统时间及时区是否正确!"), clog.error("获取评论失败:", o), console.error("获取评论失败:", o); } finally { $("#reviewsLoading").remove(); } if (!i) return t.append('\n <div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">\n 获取评论失败\n <a id="retryFetchReviews" href="javascript:;" style="margin-left: 10px; color: #1890ff; text-decoration: none;">重试</a>\n </div>\n '), void $("#retryFetchReviews").on("click", (async () => { $("#retryFetchReviews").parent().remove(), await this.fetchAndDisplayReviews(e); })); if (0 === i.length) return void t.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">无评论</div>'); const s = await storageManager.getReviewFilterKeywordList(); if (this.displayReviews(i, t, s), i.length === a && R(e, 2, a).catch((() => {})), i.length === a) { n.html('\n <button id="loadMoreReviews" style="width:100%; background-color: #e1f5fe; border:none; padding:10px; margin-top:10px; cursor:pointer; color:#0277bd; font-weight:bold; border-radius:4px;">\n 加载更多评论\n </button>\n <div id="reviewsEnd" style="display:none; text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部评论</div>\n '); let i = 1, r = $("#loadMoreReviews"); r.on("click", (async () => { let n; r.text("加载中...").prop("disabled", !0), i++; try { n = await R(e, i, a); } catch (o) { console.error("加载更多评论失败:", o); } finally { r.text("加载失败, 请点击重试").prop("disabled", !1); } n && (this.displayReviews(n, t, s), n.length < a ? (r.remove(), $("#reviewsEnd").show()) : r.text("加载更多评论").prop("disabled", !1)); })); } else n.html('<div style="text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部评论</div>'); } displayReviews(e, t, n) { if (!e.length) return; const o = [], c = n.length > 0 ? new RegExp(n.map((e => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))).join("|")) : null; e.forEach((e => { if (c && c.test(e.content)) return; const a = Array(e.score).fill('<i class="icon-star"></i>').join(""), i = e.content.replace(/ed2k:\/\/\|file\|[^|]+\|\d+\|[a-fA-F0-9]{32}\|\/|magnet:\?[^\s"'<>`\u4e00-\u9fa5,。?!()【】]+|https?:\/\/[^\s"'<>`\u4e00-\u9fa5,。?!()【】]+/g, (e => e.startsWith("ed2k://") ? `\n <span style="word-break: break-all;background: #e0f2fe;color: #0369a1;">${e}</span>\n\n ` : e.startsWith("magnet:") ? `\n <a href="${e}" class="a-primary" style="padding:0; word-break: break-all; white-space: pre-wrap;" target="_blank" rel="noopener noreferrer">${e}</a>\n\n ` : e.startsWith("http://") || e.startsWith("https://") ? `\n <a href="${e}" class="a-primary" style="padding:0; word-break: break-all; white-space: pre-wrap;" target="_blank" rel="noopener noreferrer">${e}</a>\n ` : e)), s = `\n <div class="item columns is-desktop" style="display:block;margin-top:6px;background-color:#ffffff;padding:10px;margin-left: -10px;word-break: break-word;position:relative;">\n <span style="position:absolute;top:5px;right:10px;color:#999;font-size:12px;">#${this.floorIndex++}楼</span>\n ${e.username} <span class="score-stars">${a}</span> \n <span class="time">${utils.formatDate(e.created_at)}</span> \n 点赞:${e.likes_count}\n <p class="review-content" style="margin-top: 5px;"> ${i} </p>\n </div>\n `; o.push(s); })), o.length && t.append(o.join("")), this.rightClickFilter(); } async rightClickFilter() { await storageManager.getSetting("enableTitleSelectFilter", _) === _ && utils.rightClick(document.body, ".review-content", (async e => { const t = window.getSelection().toString(); t && (e.preventDefault(), await utils.q(e, `是否将 '${t}' 加入评论区关键词?`, (async () => { await storageManager.saveReviewFilterKeyword(t), show.ok("操作成功, 刷新页面后生效"); }))); })); } } class ke extends X { getName() { return "FilterTitleKeywordPlugin"; } async handle() { if (!isDetailPage && !isFc2Page) return; if (await storageManager.getSetting("enableTitleSelectFilter", _) !== _) return; let e; r ? e = ".title strong, .current-title" : l && (e = "h3"), utils.rightClick(document.body, e, (e => { const t = window.getSelection().toString(); if (t) { e.preventDefault(); let n = { clientX: e.clientX, clientY: e.clientY + 80 }; utils.q(n, `是否屏蔽标题关键词 ${t}?`, (async () => { await storageManager.saveTitleFilterKeyword(t), window.refresh(), utils.closePage(); })); } })); } } class Se extends X { getName() { return "BlacklistPlugin"; } async addBlacklist(e) { let t = { clientX: e.clientX, clientY: e.clientY + 80 }; const n = $("#addBlacklistBtn span").text().includes("已加入"); let a, i; if (o.includes("/tags")) { const e = new URL(o); e.searchParams.delete("page"); const t = $("#jhs-check-tag").text().trim(); a = { starId: "no-" + t, name: "虚拟演员-" + t, allName: [ "虚拟演员" ], role: "虚拟演员", movieType: t, blacklistUrl: e.toString() }, i = `是否将分类 <span style="color: #f40">${t}</span> 加入到黑名单中?`, n && (i = `分类 <span style="color: #f40">${t}</span> 已在黑名单中, 是否从当前页开始追加屏蔽?`); } else a = this.getActressPageInfo(), i = `是否将该演员 <span style="color: #f40">${a.name}</span> 加入到黑名单中?`, n && (i = `演员 <span style="color: #f40">${a.name}</span> 已在黑名单中, 是否从当前页开始追加屏蔽?`); const {starId: s, name: r, allName: c, role: d, movieType: h, blacklistUrl: g} = a; if (o.includes("page") && !o.includes("page=1") && (i += "<br/> 注意: 当前页面非第一页, 屏蔽数据将从此页面开始"), l) { const e = o.split("/star/")[1].split("/"); if (e.length > 1) { parseInt(e[1]) > 1 && (i += "<br/> 注意: 当前页面非第一页, 屏蔽数据将从此页面开始"); } } utils.q(t, i, (async () => { const e = this.getBean("TaskPlugin"); navigator.locks.request(e.singleTaskKey, { ifAvailable: !0 }, (async e => { if (clog.debug("获取锁", e), e) { this.loadObj = loading(); try { await storageManager.addBlacklistItem({ starId: s, name: r, allName: c, role: d, movieType: h, url: g }), await this.filterActorVideo(r, s); const e = show.ok(`屏蔽结束,是否跳转到最后一页: ${this.lastPageLink}`, { duration: -1, close: !0, onClick: () => { e.closeShow(), window.location.href = this.lastPageLink; } }); } catch (t) { clog.error(t); const e = show.error("发生错误, 是否填转到解析失败的那一页? (点击并跳转)", { duration: -1, close: !0, onClick: () => { e.closeShow(), window.location.href = this.nextPageLink; } }); } finally { this.loadObj.close(); } } else show.error("当前有定时任务在后台执行中, 无法发起此操作"); })).catch((e => { console.error("锁任务出现错误:", e), clog.error("锁任务出现错误:", e); })); })); } async resetBtnTip() { const e = this.getBean("TaskPlugin"), t = localStorage.getItem(e.lastCheckBlacklistTimeKey) || "无", n = await storageManager.getSetting("checkBlacklist_intervalTime", 12); this.checkBlacklist_ruleTime = await storageManager.getSetting("checkBlacklist_ruleTime", 8760), $("#checkBlacklistBtn").attr("data-tip", `上次检测时间: ${t}; 检测间隔时间: ${n}小时`); } async openBlacklistDialog() { const e = this.getBean("TaskPlugin"), t = await storageManager.getSetting(); let n = `\n <div style="padding: 10px 20px; height: 100%;overflow:hidden;"> \n <div style="display: flex;justify-content: space-between;">\n <div style="display: flex; gap:5px">\n <a id="checkBlacklistBtn" class="a-danger" data-tip="上次检测时间: ${localStorage.getItem(e.lastCheckBlacklistTimeKey) || "无"}; 检测间隔时间: ${t.checkBlacklist_intervalTime}小时">${this.blacklistSvg} 手动检测黑名单</a>\n <a class="a-info" id="toSetting">${this.settingSvg} 配置</a>\n </div>\n <div style="display: flex; gap:5px">\n <select id="dataType" style="text-align: center;min-width: 150px;">\n <option value="" selected>所有</option>\n <option value="actor">男演员</option>\n <option value="actress">女演员</option>\n </select>\n <select id="statusType" style="text-align: center;min-width: 150px;">\n <option value="" selected>--检测状态--</option>\n <option value="normal">正常检测</option>\n <option value="stop">停止检测</option>\n </select>\n <select id="urlType" data-tip="在演员页屏蔽时,是否选择了分类" style="text-align: center;min-width: 150px; ${r ? "" : "display: none;"}">\n <option value="" selected>--屏蔽类型--</option>\n <option value="hasT">按所选分类屏蔽</option>\n <option value="noT">未筛选分类</option>\n </select>\n <input id="searchValue" type="text" placeholder="搜索演员" style="padding: 4px 5px;">\n <a id="cleanQueryBtn" class="a-info" style="margin-left: 0">重置</a>\n </div>\n\n </div>\n <div id="table-container" style="height: calc(100% - 50px);"></div>\n </div>\n `; layer.open({ type: 1, title: "演员黑名单", content: n, scrollbar: !1, area: utils.getResponsiveArea([ "80%", "90%" ]), anim: -1, success: async t => { await this.loadTableData(), $(".layui-layer-content").on("click", "#cleanQueryBtn", (async e => { $("#searchValue").val(""), $("#dataType").val(""), $("#statusType").val(""), await this.reloadTable(); })).on("focusout keydown", "#searchValue", (async e => { if ("focusout" === e.type || "Enter" === e.key) { if ("Enter" === e.key && e.preventDefault(), "keydown" === e.type && "Enter" !== e.key) return; $("#dataType").val(""), await this.reloadTable(); } })).on("change", "#dataType", (async () => { $("#searchValue").val(""), await this.reloadTable(); })).on("change", "#statusType", (async () => { await this.reloadTable(); })).on("change", "#urlType", (async () => { await this.reloadTable(); })).on("click", "#toSetting", (() => { this.getBean("SettingPlugin").openSettingDialog("task-panel", (() => { $("#setting-blacklist").css({ border: "1px solid #f40" }); })); })).on("click", ".open-url", (e => { e.preventDefault(); const t = $(e.currentTarget), n = t.attr("data-url"), a = t.attr("data-name"); utils.openPage(n, a, !0, e); })).on("click", "#checkBlacklistBtn", (t => { utils.q({ clientX: t.clientX, clientY: t.clientY + 20 }, "是否手动检测黑名单?", (() => { navigator.locks.request(e.singleTaskKey, { ifAvailable: !0 }, (async t => { t ? (await e.loadConfig(), await e.checkBlacklist(!0)) : show.error("当前有定时任务在后台执行中, 无法发起手动任务"); })).catch((e => { console.error("锁任务出现错误:", e), clog.error("锁任务出现错误:", e); })); })); })); }, end: () => { this.tableObj && (this.tableObj.destroy(), this.tableObj = null), window.refresh(); } }); } async reloadTable() { if (!this.tableObj) return; const e = await this.getTableData(); this.tableObj.setData(e); } async getTableData() { const e = this.getBean("TaskPlugin"), t = await storageManager.getBlacklist(), n = await storageManager.getBlacklistCarList(), a = $("#searchValue").val(), i = $("#statusType").val(), s = $("#dataType"), o = s.val(), r = $("#urlType").val(), l = t.length; let c = 0, d = 0; const h = t.map((t => { t.role === B ? c++ : t.role === P && d++; let n = !1; return t.lastPublishTime && (n = !e.isUnnecessaryCheck(t.lastPublishTime, this.checkBlacklist_ruleTime)), { ...t, isUnCheck: n }; })).filter((e => !(a && !e.name.includes(a)) && (("normal" !== i || !e.isUnCheck) && (!("stop" === i && !e.isUnCheck) && (o ? e.role === o : !("hasT" === r && !e.url.includes("t=")) && ("noT" !== r || !e.url.includes("t="))))))); s.html(`\n <option value="">所有 (${l})</option>\n <option value="actor">男演员 (${c})</option>\n <option value="actress">女演员 (${d})</option>\n `), s.val(o); const g = new Map; for (const m of n) { const e = m.starId; g.has(e) || g.set(e, []), g.get(e).push(m); } const p = h.map((e => { const t = e.starId, n = g.get(t) || []; return { ...e, carList: n, count: n.length }; })); return this.currentCarCount = p.reduce(((e, t) => e + (t.count || 0)), 0), p; } async loadTableData() { this.checkBlacklist_ruleTime = await storageManager.getSetting("checkBlacklist_ruleTime") || 8760; const e = await this.getTableData(); this.tableObj = new Tabulator("#table-container", { layout: "fitColumns", placeholder: "暂无数据", virtualDom: !0, data: e, pagination: !0, paginationMode: "local", paginationSize: 20, paginationSizeSelector: [ 20, 50, 100, 1e3, 99999 ], paginationCounter: (e, t, n, a, i) => `演员: ${a} 番号总数: ${this.currentCarCount} <span id="checkBlacklistMsg" style="margin-left: 10px"></span>`, responsiveLayout: "collapse", responsiveLayoutCollapse: !0, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, index: "starId", columns: [ { title: "演员", field: "name", sorter: "string", minWidth: 100, responsive: 0, headerSort: !1, formatter: (e, t, n) => { const a = e.getData(); return `<a class="open-url" data-url="${a.url}" href="${a.url}" data-name="${a.name}" target="_blank">${a.name}</a>`; } }, { title: "性别角色", field: "role", sorter: "string", width: 120, responsive: 5, formatter: (e, t, n) => { const a = e.getData().role; let i = a; return a === B ? i = "男演员" : a === P && (i = "女演员"), i; } }, { title: "影视类别", field: "movieType", sorter: "string", width: 120, responsive: 5, formatter: (e, t, n) => { const a = e.getData().movieType; let i = a; return a === D ? i = "有码" : a === A && (i = "无码"), i; } }, { title: "屏蔽类型", field: "url", sorter: "string", minWidth: 120, responsive: 4, visible: r, formatter: (e, t, n) => { let a = e.getData().url.includes("t="); return `<span style="${a ? "color:#cc4444" : ""}">${a ? "按所选分类屏蔽" : "未筛选分类"}</span>`; } }, { title: "番号数量", field: "count", sorter: "number", width: 170, responsive: 1 }, { title: "创建时间", field: "createTime", sorter: "string", width: 170, responsive: 5 }, { title: "最后发行时间", field: "lastPublishTime", sorter: "string", width: 170, responsive: 1 }, { title: "状态", field: "isUnCheck", sorter: "string", width: 120, responsive: 1, formatter: (e, t, n) => { let a = "", i = "正常检测"; return e.getData().isUnCheck && (a = `停更${this.checkBlacklist_ruleTime / 24 / 365}年以上, 下轮任务不再进行检测`, i = "停止检测"), `<span data-tip="${a}" style="${a ? "color: #cc4444;" : ""}">${i}</span>`; } }, { title: "操作", sorter: "string", cssClass: "action-cell-dropdown", minWidth: 150, responsive: 0, headerSort: !1, formatter: (e, t, n) => { const a = e.getData(); return n((() => { var t, n; null == (t = e.getElement().querySelector(".delete-btn")) || t.addEventListener("click", (e => { const t = a.name, n = a.starId; t ? n ? utils.q(e, `是否移除对 ${t} 的屏蔽?`, (async () => { await storageManager.removeBlacklistCarList(n), await storageManager.deleteBlacklistItem(n), show.info("操作成功"), this.reloadTable().then(); })) : show.error("获取starId失败") : show.error("获取名称失败"); })), null == (n = e.getElement().querySelector(".keyword-btn")) || n.addEventListener("click", (e => { const t = a.carList.reduce(((e, t) => { const n = t.carNum.split("-")[0] + "-"; return e[n] = (e[n] || 0) + 1, e; }), {}), n = Object.entries(t).map((([e, t]) => ({ prefix: e, count: t }))).sort(((e, t) => t.count - e.count)); clog.debug(n); })); })), '\n \x3c!-- <a class="a-normal keyword-btn"> <span>提取屏蔽词</span> </a>--\x3e\n <a class="a-danger delete-btn"> <span>删除</span> </a>\n '; } } ], initialSort: [ { column: "createTime", dir: "desc" } ], locale: "zh-cn", langs: { "zh-cn": { pagination: { first: "首页", first_title: "首页", last: "尾页", last_title: "尾页", prev: "上一页", prev_title: "上一页", next: "下一页", next_title: "下一页", all: "所有", page_size: "每页行数" } } } }); } getCurrentStarUrl() { let e = window.location.href.replace(/([&?])sort_type=[^&]+(&|$)/, "$1"); e = e.replace(/[&?]$/, ""), e = e.replace(/\?&/, "?"); let t = e; return t = t.replace(/([&?])page=\d+(&|$)/, "$1"), t = t.replace(/[&?]$/, ""), t = t.replace(/\?&/, "?"), t = t.replace(/\/(\d+)(?:\/(\d+))?(\?|$)/, ((e, t, n, a) => void 0 !== n ? `/${t}${a}` : e)), t; } parseUrlId(e) { if (!e) throw new Error("url未传入"); return new URL(e).pathname.split("/").filter((e => "" !== e.trim())).pop(); } async filterAllVideo(e, t) { let n, a; if (t ? (l && t.find(".avatar-box").length > 0 && t.find(".avatar-box").parent().remove(), n = t.find(this.getSelector().requestDomItemSelector), a = t.find(this.getSelector().nextPageSelector).attr("href")) : (n = $(this.getSelector().itemSelector), a = $(this.getSelector().nextPageSelector).attr("href")), a && 0 === n.length) throw show.error("解析列表失败"), new Error("解析列表失败"); for (const s of n) { const t = $(s), {carNum: n, url: a, publishTime: o} = this.getBean("ListPagePlugin").findCarNumAndHref(t); if (a && n) try { if (await storageManager.getCar(n)) continue; await storageManager.saveCar({ carNum: n, url: a, names: e, actionType: d, publishTime: o }), clog.log("屏蔽演员番号", e, n); } catch (i) { console.error(`保存失败 [${n}]:`, i); } } if (a) { show.info("请不要关闭窗口, 正在解析下一页:" + a), await new Promise((e => setTimeout(e, 500))); const t = await gmHttp.get(a), n = new DOMParser, i = $(n.parseFromString(t, "text/html")); await this.filterAllVideo(e, i); } else show.ok("执行结束!"), window.refresh(); } async batchSaveAllVideos(e, t) { let n, a; n = $(this.getSelector().itemSelector), a = $(this.getSelector().nextPageSelector).attr("href"); if (a && 0 === n.length) throw show.error("解析列表失败"), new Error("解析列表失败"); for (const i of n) { const n = $(i), {carNum: a, url: o, publishTime: r} = this.getBean("ListPagePlugin").findCarNumAndHref(n); if (o && a) try { if (await storageManager.getCar(a)) continue; await storageManager.saveCar({carNum: a, url: o, names: e, actionType: t, publishTime: r}), clog.log("批量操作", e, a, t); } catch (s) { console.error(`保存失败 [${a}]:`, s); } } if (a) { show.info("请不要关闭窗口, 正在解析下一页:" + a), await new Promise((e => setTimeout(e, 500))); const i = await gmHttp.get(a), s = new DOMParser, o = $(s.parseFromString(i, "text/html")); await this.batchSaveAllVideos(e, t); } else show.ok("执行结束!"), window.refresh(); } async filterActorVideo(e, t, n) { let {nextPageLink: a} = await this.parseAndSaveFilterInfo(n, e, t); if (this.nextPageLink = a, a) { let n; this.lastPageLink = a, show.info("请不要关闭窗口, 正在解析下一页:" + a); clog.log("正在请求下一页内容:", a); const i = await gmHttp.get(a); n = utils.htmlTo$dom(i); await this.filterActorVideo(e, t, n); } else show.ok("执行结束!"), window.refresh(); } async parseAndSaveFilterInfo(e, t, n) { let a, i; if (e) { let t = !1, n = T; e.text().includes(I) && (t = !0, n = I), t && e.find(".avatar-box").length > 0 && e.find(".avatar-box").parent().remove(), a = e.find(this.getSelector(n).requestDomItemSelector), i = e.find(this.getSelector(n).nextPageSelector).attr("href"); } else a = $(this.getSelector().itemSelector), i = $(this.getSelector().nextPageSelector).attr("href"); if (i && 0 === a.length) return { nextPageLink: null, lastPublishTime: null }; let s = [], o = null; for (const l of a) { const e = $(l), {carNum: a, url: i, publishTime: r} = this.getBean("ListPagePlugin").findCarNumAndHref(e); o || (o = r), i && a && s.push({ carNum: a, url: i, names: t, actionType: d, starId: n, publishTime: r }); } try { await storageManager.batchSaveBlacklistCarList(s); } catch (r) { clog.error("保存失败:", r), console.error("保存失败:", r); } return { nextPageLink: i, lastPublishTime: o }; } } class Ce extends X { getName() { return "ListPageButtonPlugin"; } async handle() { if (!window.isListPage) return; await this.createMenuBtn(), this.bindEvent(); (await storageManager.getSetting("showWaitCheckBtn", _)) === C && $("#waitCheckBtn").hide(); (await storageManager.getSetting("showWaitDownBtn", _)) === C && $("#waitDownBtn").hide(); await storageManager.getSetting("autoPage") === _ ? $("#sort-toggle-btn").hide() : this.sortItems().then(); } async createMenuBtn() { if (r) { const e = o.includes("/actors/"); let t = $(".main-tabs, .tabs"), n = "加入黑名单", a = "#d22020", i = "", s = null; if (e) { t = $(".toolbar, .section-addition").filter(":last"); const e = await storageManager.getBlacklist(), i = this.getActressPageInfo(); e.find((e => e.starId === i.starId)) && (n = "已加入黑名单", a = "#885d5d"); } else o.includes("/tags") && utils.loopDetector((() => $("#jhs-check-tag").text().trim()), (async () => { const e = $("#addBlacklistBtn"); e.attr("data-tip", "将当前分类标签加入到黑名单, 后续有作品更新也会纳入屏蔽中"); const t = $("#jhs-check-tag").text().trim(); if (!t) return; const n = "no-" + t, a = await storageManager.getBlacklist(); s = a.find((e => e.starId === n)), s && (e.css("backgroundColor", "#885d5d"), $("#addBlacklistBtn span").text("已加入黑名单")); })); const r = o.includes("advanced_search"); r ? t = $("h2.section-title") : i = "flex-grow:1;"; const l = localStorage.getItem("jhs_sortMethod"), d = "当前排序方式: " + ("rateCount" === l ? "评价人数" : "date" === l ? "时间" : "默认"); t.append(`\n <div class="jhs-list-btn-row" style="display: flex;align-items: center; ${i} ">\n <a id="waitCheckBtn" class="menu-btn main-tab-btn" style="background-color:#56c938 !important;"><span>打开待鉴定</span></a>\n <a id="waitDownBtn" class="menu-btn main-tab-btn" style="background-color:#2caac0 !important;"><span>打开已收藏</span></a>\n ${e ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${a} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${n}</span></a>\n <a id="filterAllVideo" class="menu-btn main-tab-btn" style="background-color:#e8ab39 !important;" data-tip="一键屏蔽已选分类的视频列表至鉴定记录中"><span>一键屏蔽所有作品</span></a>\n <a id="favoriteAllVideo" class="menu-btn main-tab-btn" style="background-color:#25b1dc !important;" data-tip="一键收藏当前页面所有作品"><span>一键收藏所有作品</span></a>\n <a id="hasDownAllVideo" class="menu-btn main-tab-btn" style="background-color:#7bc73b !important;margin-right: 30px!important;" data-tip="一键标记当前页面所有作品为已下载"><span>一键已下载所有作品</span></a>\n ` : ""}\n ${o.includes("/tags") ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${a} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${n}</span></a>\n ` : ""}\n </div>\n <div class="jhs-list-btn-row" style="display: flex;align-items: center;">\n <a id="newVideoBtn" class="menu-btn main-tab-btn" style="background-color:#2c6cc0 !important;"><span>新作品检测 (<span id="newVideoCount">0</span>)</span></a>\n <a id="blacklistBtn" class="menu-btn main-tab-btn" style="background-color:#34393f !important;"><span>演员黑名单</span></a>\n ${c || r ? "" : `<a id="sort-toggle-btn" class="menu-btn main-tab-btn" style="background-color:#8783ab !important;"> ${d} </a>`}\n </div>\n `); } if (l) { const e = o.includes("/star/"); let t = "加入黑名单", n = "#d22020"; if (e) { const e = await storageManager.getBlacklist(), a = this.getActressPageInfo(); e.find((e => e.starId === a.starId)) && (t = "已加入黑名单", n = "#885d5d"); } $(".masonry").parent().prepend(`\n <div class="jhs-list-btn-row" style="margin: 10px; display: flex;">\n <a id="waitCheckBtn" class="menu-btn main-tab-btn" style="background-color:#56c938 !important;"><span>打开待鉴定</span></a>\n <a id="waitDownBtn" class="menu-btn main-tab-btn" style="background-color:#2caac0 !important;"><span>打开已收藏</span></a>\n ${e ? ` \n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${n} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${t}</span></a>\n <a id="filterAllVideo" class="menu-btn main-tab-btn" style="background-color:#e8ab39 !important;" data-tip="一键屏蔽已选分类的视频列表至鉴定记录中"><span>一键屏蔽所有作品</span></a>\n <a id="favoriteAllVideo" class="menu-btn main-tab-btn" style="background-color:#25b1dc !important;" data-tip="一键收藏当前页面所有作品"><span>一键收藏所有作品</span></a>\n <a id="hasDownAllVideo" class="menu-btn main-tab-btn" style="background-color:#7bc73b !important;" data-tip="一键标记当前页面所有作品为已下载"><span>一键已下载所有作品</span></a>\n ` : '<a id="blacklistBtn" class="menu-btn main-tab-btn" style="background-color:#34393f !important;"><span>演员黑名单</span></a>'}\n </div>\n `); } } bindEvent() { $("#waitCheckBtn").on("click", (e => { this.openWaitCheck(e).then(); })), $("#waitDownBtn").on("click", (e => { this.openFavorite().then(); })), $("#newVideoBtn").on("click", (e => { this.getBean("NewVideoPlugin").openDialog(); })), $("#blacklistBtn").on("click", (e => { this.getBean("BlacklistPlugin").openBlacklistDialog(); })), $("#sort-toggle-btn").on("click", (e => { const t = localStorage.getItem("jhs_sortMethod"); let n; n = t && "default" !== t ? "rateCount" === t ? "date" : "default" : "rateCount"; const a = { default: "默认", rateCount: "评价人数", date: "时间" }[n]; $(e.target).text(`当前排序方式: ${a}`), localStorage.setItem("jhs_sortMethod", n), this.sortItems().then(); })); const e = this.getBean("BlacklistPlugin"); $("#addBlacklistBtn").on("click", (async t => { await e.addBlacklist(t); })), $("#filterAllVideo").on("click", (async t => { let n = { clientX: t.clientX, clientY: t.clientY + 80 }, a = r ? $(".actor-section-name") : $(".avatar-box .photo-info .pb10"); if (0 === a.length) return void show.error("获取演员名称失败"); let i = a.text().trim().split(",")[0]; utils.q(n, "一键屏蔽视频列表?", (async () => { this.loadObj = loading(); try { await e.filterAllVideo(i), window.refresh(); } catch (t) { console.error(t); } finally { this.loadObj.close(); } })); })), $("#favoriteAllVideo").on("click", (async t => { let n = {clientX: t.clientX, clientY: t.clientY + 80}, a = r ? $(".actor-section-name") : $(".avatar-box .photo-info .pb10"); if (0 === a.length) return void show.error("获取演员名称失败"); let i = a.text().trim().split(",")[0]; utils.q(n, "一键收藏所有可见作品?", (async () => { this.loadObj = loading(); try { await e.batchSaveAllVideos(i, h), window.refresh(); } catch (t) { console.error(t); } finally { this.loadObj.close(); } })); })), $("#hasDownAllVideo").on("click", (async t => { let n = {clientX: t.clientX, clientY: t.clientY + 80}, a = r ? $(".actor-section-name") : $(".avatar-box .photo-info .pb10"); if (0 === a.length) return void show.error("获取演员名称失败"); let i = a.text().trim().split(",")[0]; utils.q(n, "一键已下载所有可见作品?", (async () => { this.loadObj = loading(); try { await e.batchSaveAllVideos(i, g), window.refresh(); } catch (t) { console.error(t); } finally { this.loadObj.close(); } })); })); } async sortItems() { if (o.includes("handle") || o.includes("advanced_search")) return; const e = await storageManager.getSetting("autoPage"); if (c || e === _) return; const t = localStorage.getItem("jhs_sortMethod"); if (!t) return; $(".movie-list .item").each((function(e) { $(this).attr("data-original-index") || $(this).attr("data-original-index", e); })); const n = $(".movie-list"), a = $(".item", n); if ("default" === t) a.sort((function(e, t) { return $(e).data("original-index") - $(t).data("original-index"); })).appendTo(n); else { const e = a.get(); e.sort((function(e, n) { if ("rateCount" === t) { const t = e => { const t = $(e).find(".score .value").text().match(/由(\d+)人/); return t ? parseFloat(t[1]) : 0; }; return t(n) - t(e); } { const t = e => { const t = $(e).find(".meta").text().trim(); return new Date(t); }; return t(n) - t(e); } })), n.empty().append(e); } } async openWaitCheck() { let e = this.getSelector(); const t = await storageManager.getSetting("waitCheckCount", 5), n = [ u, b, y, k ]; let a = 0; $(`${e.itemSelector}:visible`).each(((e, i) => { if (a >= t) return !1; const s = $(i); if (n.some((e => s.find(`span.tag:contains('${e}')`).length > 0))) return; const {carNum: o, aHref: r} = this.getBean("ListPagePlugin").findCarNumAndHref(s); if (o.includes("FC2-")) { const e = this.parseMovieId(r); this.getBean("Fc2Plugin").openFc2Page(e, o, r); } else { let e = r + (r.includes("?") ? "&autoPlay=1" : "?autoPlay=1"); window.open(e); } a++; })), 0 === a && show.info("没有需鉴定的视频"); } async openFavorite() { const openCount = await storageManager.getSetting("waitCheckCount", 5); const isRandom = await storageManager.getSetting("randomOpenWaitDown", C) === _; const dataList = await storageManager.getCarList(); const favoriteList = isRandom ? dataList.filter((e => e.status === h)).sort((() => Math.random() - .5)) : dataList.filter((e => e.status === h)).sort(((e, t) => (t.createDate || 0) - (e.createDate || 0))); if (0 === favoriteList.length) return show.info("暂无已收藏作品"); for (let i = 0; i < openCount && i < favoriteList.length; i++) { const {carNum: carNum2, url} = favoriteList[i]; if (carNum2 && carNum2.includes("FC2-")) { const movieId = this.parseMovieId(url); await this.getBean("Fc2Plugin").openFc2Page(movieId, carNum2, url); } else if (url) window.open(url); } } } const _e = async (e, t = "ja", n = "zh-CN") => { if (!e) throw new Error("翻译文本不能为空"); const a = "https://translate-pa.googleapis.com/v1/translate?" + new URLSearchParams({ "params.client": "gtx", dataTypes: "TRANSLATION", key: "AIzaSyDLEeFI5OtFBwYBIoK_jj5m32rZK5CkCXA", "query.sourceLanguage": t, "query.targetLanguage": n, "query.text": e }), i = await fetch(a); if (!i.ok) throw new Error(`${i.status} ${i.statusText}`); return (await i.json()).translation; }, Te = { IS_FILTERED: { text: u, color: f, reasonType: "单番号屏蔽", isCounted: !0, countKey: "currentPageFilterCount" }, IS_FAVORITE: { text: b, color: w, reasonType: "", isCounted: !0, countKey: "currentPageFavoriteCount" }, IS_HAS_DOWN: { text: y, color: x, reasonType: "", isCounted: !0, countKey: "currentPageHasDownCount" }, IS_HAS_WATCH: { text: k, color: S, reasonType: "", isCounted: !0, countKey: "currentPageHasWatchCount" }, IS_KEYWORD_FILTER: { text: "关键词屏蔽", color: "#de3333", reasonType: "", isCounted: !0, countKey: "currentPageKeywordFilterCount" }, IS_ACTOR_FILTER: { text: "男演员屏蔽", color: "#b22222", reasonType: "", isCounted: !0, countKey: "currentPageActorFilterCount" }, IS_ACTRESS_FILTER: { text: "女演员屏蔽", color: "#cd5c5c", reasonType: "", isCounted: !0, countKey: "currentPageActorFilterCount" }, IS_WAIT_CHECK: { text: "", color: "", reasonType: "", isCounted: !0, countKey: "currentPageWaitCheckCount" } }; class Ie extends X { constructor() { super(...arguments), i(this, "currentPageFilterCount", 0), i(this, "currentPageFavoriteCount", 0), i(this, "currentPageHasDownCount", 0), i(this, "currentPageHasWatchCount", 0), i(this, "currentPageKeywordFilterCount", 0), i(this, "currentPageActorFilterCount", 0), i(this, "currentPageWaitCheckCount", 0), i(this, "currentPageTotalCount", 0), i(this, "cache", localStorage.getItem("jhs_translate") ? JSON.parse(localStorage.getItem("jhs_translate")) : {}), i(this, "writeQueue", Promise.resolve()), i(this, "_debouncedTranslateWrite", null); } getName() { return "ListPagePlugin"; } async handle() { new BroadcastChannel("channel-refresh").addEventListener("message", (async e => { let t = e.data.type; if ("refresh" === t) { await this.doFilter(), this.applyVisibility(); const e = this.getBean("HistoryPlugin"); e.tableObj && e.tableObj.setData(); const t = this.getBean("NewVideoPlugin"); t && (t.showNewVideoCount().then(), t.loadData()); } else "cleanCache_filter_actor_actress_car_list" === t ? storageManager._invalidateCache(storageManager.blacklist_car_list_key) : "clean_cacheSettingObj" === t && storageManager._invalidateCache(storageManager.setting_key); })), this.cleanRepeatId(), this.replaceHdImg(), this.addJumpPageControl(), this.fixBusTitleBox(), await this.doFilter(), await this.createQuickFilter(), this.applyVisibility(), this.bindClick().then(), this.bindListPageHotKey().catch((e => clog.error("列表页快捷键绑定失败", e))), this.rememberTagExpand(), $(this.getSelector().itemSelector + " a").attr("target", "_blank"), this.checkDom(); } /** 列表页快捷键: 切换显示状态 / 折叠 / 控制台 / 图片悬停触发屏蔽等 */ async bindListPageHotKey() { this.$currentImage = null; $(document).on("mouseenter", this.getSelector().coverImgSelector, (e => { this.$currentImage = $(e.currentTarget); })).on("mouseleave", this.getSelector().coverImgSelector, (() => { this.$currentImage = null; })); const s = await storageManager.getSetting(); const REGISTRY = { showFilterItemHotKey: "showFilterItem", showFilterActorItemHotKey: "showFilterActorItem", showFilterKeywordItemHotKey: "showFilterKeywordItem", showFavoriteItemHotKey: "showFavoriteItem", showHasDownItemHotKey: "showHasDownItem", showHasWatchItemHotKey: "showHasWatchItem", showAllItemHotKey: "showAllItem" }; for (const [hotkeyName, settingKey] of Object.entries(REGISTRY)) { const key = s[hotkeyName]; if (key && HotkeyManager.isHotkeyFormat(key)) HotkeyManager.registerHotkey(key, (async () => { const cur = await storageManager.getSetting(settingKey); await storageManager.saveSettingItem(settingKey, cur === _ ? C : _); window.refresh && window.refresh(); })); } if (s.clogHotKey && HotkeyManager.isHotkeyFormat(s.clogHotKey)) HotkeyManager.registerHotkey(s.clogHotKey, (() => clog.toggleExpandCollapsed && clog.toggleExpandCollapsed())); if (s.foldCategoryHotKey && HotkeyManager.isHotkeyFormat(s.foldCategoryHotKey)) HotkeyManager.registerHotkey(s.foldCategoryHotKey, (() => { const $b = $("#foldCategoryBtn"); $b.length && $b[0].click(); })); if ((s.enableImageHotKey || C) !== _) return; const handleAction = async (boxInfo, status) => { setTimeout((async () => { let actorName = await this.parseActressName(boxInfo.url); await storageManager.saveCar({ carNum: boxInfo.carNum, url: boxInfo.url, names: actorName, actionType: status, publishTime: boxInfo.publishTime }); window.refresh(); show.ok("操作成功"); })); }; const handlers = {}; s.filterHotKey && (handlers[s.filterHotKey] = b => handleAction(b, d)); s.favoriteHotKey && (handlers[s.favoriteHotKey] = b => handleAction(b, h)); s.hasDownHotKey && (handlers[s.hasDownHotKey] = b => handleAction(b, g)); s.hasWatchHotKey && (handlers[s.hasWatchHotKey] = b => handleAction(b, p)); Object.entries(handlers).forEach((([key, handler]) => { if (!HotkeyManager.isHotkeyFormat(key)) return; HotkeyManager.registerHotkey(key, (() => { const ae = document.activeElement; if ("INPUT" === ae.tagName || "TEXTAREA" === ae.tagName || ae.isContentEditable) return; if (!this.$currentImage) return; try { const $box = this.$currentImage.closest(".item"), info = this.findCarNumAndHref($box); handler({ carNum: info.carNum, url: info.aHref || info.url, publishTime: info.publishTime }); } catch (e) { clog.error("列表页图片快捷键失败", e); } })); })); } async createQuickFilter() { if ($("#jhs-quick-filter").length) return; if (await storageManager.getSetting("enableQuickFilter", _) === C) return; const e = this.getSelector(), t = '\n <div id="jhs-quick-filter" style="margin:8px 0;display:flex;gap:6px;flex-wrap:wrap;align-items:center">\n <span style="font-size:12px;color:#888;margin-right:4px">筛选:</span>\n <a class="jhs-filter-btn active" data-jhs-filter="all" style="padding:3px 10px;border-radius:12px;font-size:12px;cursor:pointer;background:#485fc7;color:#fff;border:none">全部</a>\n <a class="jhs-filter-btn" data-jhs-filter="waitCheck" style="padding:3px 10px;border-radius:12px;font-size:12px;cursor:pointer;background:#f5f5f5;color:#666;border:1px solid #ddd">待鉴定</a>\n <a class="jhs-filter-btn" data-jhs-filter="favorite" style="padding:3px 10px;border-radius:12px;font-size:12px;cursor:pointer;background:#f5f5f5;color:#666;border:1px solid #ddd">已收藏</a>\n <a class="jhs-filter-btn" data-jhs-filter="hasDown" style="padding:3px 10px;border-radius:12px;font-size:12px;cursor:pointer;background:#f5f5f5;color:#666;border:1px solid #ddd">已下载</a>\n <a class="jhs-filter-btn" data-jhs-filter="hasWatch" style="padding:3px 10px;border-radius:12px;font-size:12px;cursor:pointer;background:#f5f5f5;color:#666;border:1px solid #ddd">已观看</a>\n <a class="jhs-filter-btn" data-jhs-filter="filter" style="padding:3px 10px;border-radius:12px;font-size:12px;cursor:pointer;background:#f5f5f5;color:#666;border:1px solid #ddd">已屏蔽</a>\n </div>'; r ? $(e.boxSelector).before(t) : l && $(".masonry").before(t); // JavBus: 筛选条与下方封面首列左边缘对齐 (.masonry 有内边距, 兄弟节点不会自动跟随) if (l) { const align = () => { const $qf = $("#jhs-quick-filter"), $first = $(".masonry .item").first(); if (!$qf.length || !$first.length) return; $qf.css("padding-left", 0); const delta = $first.offset().left - $qf.offset().left; if (delta > 0) $qf.css("padding-left", delta + "px"); }; align(), setTimeout(align, 300), $(window).on("resize", align); } const n = this; this.activeQuickFilter = "all", this.applyQuickFilter("all"), $("#jhs-quick-filter").on("click", ".jhs-filter-btn", (function() { const t = $(this).data("jhs-filter"); $("#jhs-quick-filter .jhs-filter-btn").css({ background: "#f5f5f5", color: "#666", border: "1px solid #ddd" }).removeClass("active"), $(this).css({ background: "#485fc7", color: "#fff", border: "none" }).addClass("active"), n.activeQuickFilter = t, n.applyQuickFilter(t); })); } applyVisibility() { const e = this.activeQuickFilter || "all", t = this.getSelector().itemSelector, n = ["filter", "keywordFilter", "actorFilter"]; $(t).each((function() { const t = $(this), a = t.attr("data-hide") === "yes", i = t.attr("data-jhs-status") || "waitCheck"; // 如果已被 filterMovieList 标记为隐藏/半透明, 保持其处理方式 if (a) { return; } if (e === "all") { n.includes(i) ? t.hide() : t.show(); return; } if (i === e) { t.show(); return; } t.hide(); })); } applyQuickFilter(e) { this.activeQuickFilter = e, this.applyVisibility(); } rememberTagExpand() { if (!window.location.href.includes("actors")) return; const e = $(".tag-expand"); if (0 === e.length) return; const t = "jhs_tag_expand", n = "true" === localStorage.getItem(t), a = $(".actor-tags .content"); n && a.hasClass("collapse") && e[0].click(), e.on("click", (function() { const e = !$(".actor-tags .content").hasClass("collapse"); clog.debug("触发"), localStorage.setItem(t, e.toString()); })); } checkDom() { if (!window.isListPage) return; const e = this.getSelector(), t = document.querySelector(e.boxSelector); if (!t) return void console.error("没有找到容器节点!"); let n = null; const a = new MutationObserver((() => { n && clearTimeout(n), n = setTimeout((async () => { this.replaceHdImg(), this.addJumpPageControl(), this.fixBusTitleBox(), await this.doFilter(), this.applyVisibility(), await this.getBean("ListPageButtonPlugin").sortItems(), this.getBean("CoverButtonPlugin").addSvgBtn(), $(this.getSelector().itemSelector + " a").attr("target", "_blank"), this.getBean("AutoPagePlugin").checkLoad(); }), 100); })); a.observe(t, { childList: !0, subtree: !1 }); } fixBusTitleBox() { if (!l) return; $(this.getSelector().itemSelector).toArray().forEach((e => { var t; let n = $(e); if (n.find(".avatar-box").length > 0) return; const a = (null == (t = n.find("img").attr("title")) ? void 0 : t.trim()) || ""; n.find(".photo-info span:first").contents().first().wrap(`<span class="video-title" title="${a}">${a}</span>`), n.find("br").remove(); })); } cleanRepeatId() { if (!l) return; $("#waterfall_h").removeAttr("id").attr("id", "no-page"); const e = $('[id="waterfall"]'); 0 !== e.length && e.each((function() { const e = $(this); if (!e.hasClass("masonry")) { e.children().insertAfter(e), e.remove(); } })); } async doFilter() { if (!window.isListPage) return; let e = $(this.getSelector().itemSelector).toArray(); if (!e.length) return; try { await this.filterMovieList(e); } catch (t) { clog.error("列表页过滤失败", t); } const w = this.getBean("WangPan115MatchPlugin"); w && w.matchMovieList(e).catch((t => clog.error("115匹配失败", t))); l && setTimeout((() => { this.getBean("BusImgPlugin").logImageHeightsByRow().catch((e => clog.error("JavBus图片高度修正失败", e))); })); } async yieldListFrame() { await new Promise((e => { window.requestAnimationFrame ? window.requestAnimationFrame((() => setTimeout(e))) : setTimeout(e); })); } findMatchedTitleKeyword(e, t, n) { for (const a of e) if (t.includes(a) || n.startsWith(a)) return a; return null; } getStatusKey(e) { return e === Te.IS_FILTERED ? "filter" : e === Te.IS_FAVORITE ? "favorite" : e === Te.IS_HAS_DOWN ? "hasDown" : e === Te.IS_HAS_WATCH ? "hasWatch" : e === Te.IS_KEYWORD_FILTER ? "keywordFilter" : e === Te.IS_ACTOR_FILTER ? "actorFilter" : e === Te.IS_ACTRESS_FILTER ? "actorFilter" : "waitCheck"; } async translateListItems(e) { for (let t = 0; t < e.length; t++) t > 0 && t % 8 == 0 && await this.yieldListFrame(), await this.translate(e[t]); } async filterMovieList(e) { utils.time("累计耗费时间"), utils.time("读取数据耗时"); const [n, a, i, s, m] = await Promise.all([ storageManager.getTitleFilterKeyword(), storageManager.getBlacklistMap(), storageManager.getBlacklistCarList(), storageManager.getSetting(), storageManager.getStatusMap() ]), o = utils.time("读取数据耗时"); utils.time("组装数据耗时"); const u = a, {actorCarNumToNameMap: f, actressCarNumToNameMap: v} = i.reduce(((e, t) => { const n = u.get(t.starId)?.role; if (!n) return clog.error("黑名单数据源丢失演员信息", t), e; const a = n === B ? e.actorCarNumToNameMap : e.actressCarNumToNameMap; return a.has(t.carNum) || a.set(t.carNum, t.names), e; }), { actorCarNumToNameMap: new Map, actressCarNumToNameMap: new Map }), b = utils.time("组装数据耗时"), w = (null == s ? void 0 : s.showFilterItem) ?? C, y = (null == s ? void 0 : s.showFilterActorItem) ?? C, x = (null == s ? void 0 : s.showFilterKeywordItem) ?? C, k = (null == s ? void 0 : s.showFavoriteItem) ?? _, S = (null == s ? void 0 : s.showHasDownItem) ?? _, T = (null == s ? void 0 : s.showHasWatchItem) ?? _, I = (null == s ? void 0 : s.showAllItem) ?? C, P = (null == s ? void 0 : s.tagPosition) || "rightTop", V = (null == s ? void 0 : s.movieShowType) || "normal"; const O = n.filter((e => e)); this.currentPageFilterCount = 0, this.currentPageFavoriteCount = 0, this.currentPageHasDownCount = 0, this.currentPageHasWatchCount = 0, this.currentPageKeywordFilterCount = 0, this.currentPageActorFilterCount = 0, this.currentPageWaitCheckCount = 0, this.currentPageTotalCount = 0, utils.time("处理页面耗时"); const R = []; for (let n = 0; n < e.length; n++) { n > 0 && n % 50 == 0 && await this.yieldListFrame(); let t = $(e[n]); if (l && t.find(".avatar-box").length > 0) continue; const {carNum: a, title: i} = this.findCarNumAndHref(t), {filter: s, favorite: o, hasDown: d, hasWatch: h} = m, g = o.has(a), p = d.has(a), u = h.has(a), b = s.has(a), B = f.has(a), D = v.has(a), A = B || D, L = this.findMatchedTitleKeyword(O, i, a), M = !!L; if (!c) { let e = k === C && g || S === C && p || T === C && u || w === C && b && !(g || p || u) || y === C && A || x === C && M; const n = t.attr("data-hide") === _; I === _ && (e = !1); if ("visibility" === V) { e && !n ? t.css({ opacity: .25 }).attr("data-hide", _) : !e && n && t.css({ opacity: "" }).removeAttr("data-hide"); t.show(); } else if ("normal" === V) { // 正常: 只显示鉴定角标, 不隐藏也不半透明 t.css({ opacity: "" }).removeAttr("data-hide").show(); } else { t.css({ opacity: "" }); e && !n ? t.hide().attr("data-hide", _) : !e && n && t.show().removeAttr("data-hide"); } } let N = Te.IS_WAIT_CHECK, j = null; b ? N = Te.IS_FILTERED : g ? N = Te.IS_FAVORITE : p ? N = Te.IS_HAS_DOWN : u ? N = Te.IS_HAS_WATCH : M ? (N = Te.IS_KEYWORD_FILTER, j = L || "未知") : B ? (N = Te.IS_ACTOR_FILTER, j = f.get(a) || "") : D && (N = Te.IS_ACTRESS_FILTER, j = v.get(a) || ""), j || (j = N.reasonType), N.isCounted && this[N.countKey]++, this.currentPageTotalCount++; const q = this.getStatusKey(N), F = t.attr("data-jhs-status") !== q || t.attr("data-jhs-tip") !== j || t.attr("data-jhs-tag-position") !== P; t.attr("data-jhs-status", q).attr("data-jhs-tip", j).attr("data-jhs-tag-position", P); const E = "rightTop" === P ? "right: 0; top:5px;" : "left: 0; top:5px;"; if (F && (t.find(".status-tag").remove(), N.text)) { const e = r ? `<span class="tag is-success status-tag" data-tip="${j}" title=""\n style="margin-right: 5px; border-radius:10px; position:absolute; \n z-index:10; background-color: ${N.color} !important; ${E}">\n ${N.text}\n </span>` : `<span class="status-tag" data-tip="${j}" title=""\n style="display:inline-block; margin-right: 5px; padding: 0 6px; line-height: 18px; font-size: 12px; color: #fff !important; border-radius:10px; position:absolute; \n z-index:10; background-color: ${N.color} !important; ${E}">${N.text}</span>`; if (r && t.find(".tags").append(e), l) { const n = t.find(".item-tag"); n.length ? n.append(e) : t.find(".photo-info > span > div").append(e); } } R.push(t); } this.translateListItems(R).catch((e => clog.error("列表页翻译任务失败", e))); const D = utils.time("处理页面耗时"), A = utils.time("累计耗费时间"); clog.log(`\n <table class="countTable" style='border-collapse: collapse; width: 100%'>\n <tr>\n <td colspan="2" style='padding: 3px; border: 1px solid #ccc;'>${o}</td>\n <td colspan="2" style='padding: 3px; border: 1px solid #ccc;'>${b}</td>\n </tr>\n \n <tr>\n <td colspan="2" style='padding: 3px; border: 1px solid #ccc;'>${D}</td>\n <td colspan="2" style='padding: 3px; border: 1px solid #ccc;'>${A}</td>\n </tr>\n <tr>\n <td style='padding: 3px; border: 1px solid #ccc; font-weight: bold;'>项目</td>\n <td style='padding: 3px; border: 1px solid #ccc; font-weight: bold;'>数量</td>\n <td style='padding: 3px; border: 1px solid #ccc; font-weight: bold;'>项目</td>\n <td style='padding: 3px; border: 1px solid #ccc; font-weight: bold;'>数量</td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'>屏蔽单番号</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageFilterCount}</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'>收藏</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageFavoriteCount}</strong></td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'>屏蔽演员</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageActorFilterCount}</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'>已下载</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageHasDownCount}</strong></td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'>屏蔽关键词</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageKeywordFilterCount}</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'>已观看</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageHasWatchCount}</strong></td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'>待鉴定</td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageWaitCheckCount}</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'></td>\n <td style='padding: 3px; border: 1px solid #ccc;'></td>\n </tr>\n \n <tr>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>总数</strong></td>\n <td style='padding: 3px; border: 1px solid #ccc;'><strong>${this.currentPageTotalCount}</strong></td>\n </tr>\n </table>\n `); } async bindClick() { let e = this.getSelector(); $(e.boxSelector).on("click", ".item img", (async e => { try { if (e.preventDefault(), e.stopPropagation(), $(e.target).closest("div.meta-buttons").length) return; const t = $(e.target).closest(".item"), {carNum: n, aHref: a} = this.findCarNumAndHref(t); const dialogOpenDetail = await storageManager.getSetting("dialogOpenDetail", _); if (n.includes("FC2-")) { let e = this.parseMovieId(a); this.getBean("Fc2Plugin").openFc2Dialog(e, n, a), this.$currentImage = null; } else if (dialogOpenDetail === _) { utils.openPage(a, n, !0, e), this.$currentImage = null; } else { window.open(a); } } catch (t) { console.error("点击图片处理失败:", t); } })), $(e.boxSelector).on("click", ".item video", (async e => { const t = e.currentTarget; t.paused ? t.play().catch((e => console.error("播放失败:", e))) : t.pause(), e.preventDefault(), e.stopPropagation(); })), $(e.boxSelector).on("click", ".item .video-title", (async e => { if ($(e.target).closest('[class^="jhs-match-"]').length) return; const t = $(e.currentTarget).closest(".item"), {carNum: n, aHref: a} = this.findCarNumAndHref(t); if (n.includes("FC2-")) { e.preventDefault(); let t = this.parseMovieId(a); this.getBean("Fc2Plugin").openFc2Dialog(t, n, a); } })), $(e.boxSelector).on("contextmenu", ".item img, .item video", (async e => { try { e.preventDefault(); const t = $(e.target).closest(".item"), {carNum: n, url: a, publishTime: i} = this.findCarNumAndHref(t); let s = r ? $(".actor-section-name") : $(".avatar-box .photo-info .pb10"), o = ""; s.length && (o = s.text().trim().split(",")[0].replace("(無碼)", "")), utils.q(e, `是否屏蔽番号 ${n}?`, (async () => { try { o || (o = await this.parseActressName(a)), await storageManager.saveCar({ carNum: n, url: a, names: o, actionType: d, publishTime: i }), window.refresh(), show.ok("操作成功"); } catch (s) { console.error("屏蔽操作失败:", s), show.error("操作失败"); } })); } catch (t) { console.error("右键菜单处理失败:", t); } })); } async parseActressName(e) { let t = null; if (await storageManager.getSetting("enableSaveActressCarInfo", C) === _) { clog.debug("鉴定补录演员信息-已启用, 开始解析详情页"), clog.debug("开始解析演员详情页", e); try { const n = await gmHttp.get(e), a = utils.htmlTo$dom(n); if (r) { // JavDB: 多重选择器兼容改版 // 1) 传统: 每个 .female 图标前的 <a> 是演员名 const oldNames = a.find(".female").prev().map((e, t) => $(t).text().trim()).get().filter(Boolean); if (oldNames.length) t = oldNames.join(" "); else { // 2) 新版: panel-block 中含「演員/演员」label 的 value 内 <a> const $blocks = a.find(".movie-panel-info .panel-block, .panel-block"); $blocks.each((_, el) => { const $el = $(el), label = $el.find("strong").first().text().trim(); if (label.includes("演員") || label.includes("演员")) { const names = $el.find(".value a, a[href*='/actors/']").map((_, a) => $(a).text().trim()).get().filter(Boolean); if (names.length) t = names.join(" "); return !1; } }); // 3) 兜底: 所有指向 /actors/ 的链接文字(过滤演员图鉴/其他) if (!t) { const names = a.find(".movie-panel-info a[href*='/actors/'], .video-detail a[href*='/actors/']").map((_, x) => $(x).text().trim()).get().filter(Boolean); if (names.length) t = [...new Set(names)].join(" "); } } } else if (l) { // JavBus: 传统 star_ + 兜底 star_ href const oldNames = a.find('span[onmouseover*="star_"] a').map((e, t) => $(t).text().trim()).get().filter(Boolean); if (oldNames.length) t = oldNames.join(" "); else { const names = a.find("a[href*='/star/']").map((_, x) => $(x).text().trim()).get().filter(Boolean); if (names.length) t = [...new Set(names)].join(" "); } } clog.debug("解析到名称:", t); } catch (err) { clog.error("解析演员名称失败:", e, err); } } return t; } findCarNumAndHref(e) { var t, n; let a, i, s, o = e.find("a"), r = o.attr("href"), l = e.find(".video-title"); if (l.length > 0) { let t = l.find("strong"); t.length > 0 && (a = t.text().trim()), i = o.attr("title") ? o.attr("title").trim() : a ? l.text().replace(a, "").trim() : l.text().trim(), s = e.find(".meta").text().trim(); } if (!a) { let o = e.find("img"); r && o.length > 0 && (i = (null == (t = o.attr("title")) ? void 0 : t.trim()) || (null == (n = o.attr("data-title")) ? void 0 : n.trim())); const l = e => /^\d{4}-\d{1,2}-\d{1,2}$/.test(e); s = e.find("date").map(((e, t) => $(t).text().trim())).get().find(l), a = e.find("date").map(((e, t) => $(t).text().trim())).get().find((e => !l(e))); } if (!a) { const e = "提取番号信息失败"; throw show.error(e), new Error(e); } return { carNum: a, aHref: r, url: r, title: i, publishTime: s }; } showCarNumBox(e) { const t = $(".movie-list .item").toArray().find((t => $(t).find(".video-title strong").text() === e)); if (t) { const n = $(t); n.attr("data-hide") === "yes" && (n.show(), n.removeAttr("data-hide")); } } _replaceSingleHdImg(e) { if ("true" === e.dataset.hdReplaced) return; if (r) { const isJavdbCdn = /jdbstatic\.com|javdb\.com/i.test(e.src); if (isJavdbCdn) { const originalSrc = e.src; e.src = e.src.replace("thumbs", "covers"); e.dataset.hdReplaced = "true"; e.title = ""; e.onerror = function() { if (this.src !== originalSrc) { this.src = originalSrc; this.onerror = null; } }; } } else if (l) { const t = /\/(imgs|pics)\/(thumb|thumbs)\//, n = /(\.jpg|\.jpeg|\.png)$/i; t.test(e.src) ? (e.src = e.src.replace(t, "/$1/cover/").replace(n, "_b$1"), e.dataset.hdReplaced = "true", e.dataset.title = e.title, e.title = "") : /ps(\.jpg|\.jpeg|\.png)$/i.test(e.src) && (e.src = e.src.replace(/ps(\.jpg|\.jpeg|\.png)$/i, "pl$1"), e.dataset.hdReplaced = "true", e.dataset.title = e.title, e.title = ""); } } replaceHdImg(e) { if (e && "string" == typeof e.jquery && (e = e.toArray()), e || (e = document.querySelectorAll(this.getSelector().coverImgSelector)), !e.length) return; const t = Array.from(e), n = t.slice(0, 12), a = t.slice(12); if (n.forEach((e => this._replaceSingleHdImg(e))), a.length && "IntersectionObserver" in window) { const t = new IntersectionObserver((e => { e.forEach((e => { e.isIntersecting && (this._replaceSingleHdImg(e.target), t.unobserve(e.target)); })); }), { rootMargin: "200px" }); a.forEach((e => t.observe(e))); } else a.forEach((e => this._replaceSingleHdImg(e))); storageManager.getSetting("hoverBigImg", C).then((e => { e === _ && (window.imageHoverPreviewObj ? window.imageHoverPreviewObj.bindEvents() : window.imageHoverPreviewObj = new ImageHoverPreview({ selector: this.getSelector().coverImgSelector })); })); } async translate(e) { if (await storageManager.getSetting("translateTitle", _) !== _) return; if (l && e.find(".avatar-box").length) return; // JavBus 演员页头像卡片无标题, 跳过 let t, n, a = e.find(".video-title"); if (r) { t = a.contents().filter(((e, t) => 3 === t.nodeType && "" !== t.textContent.trim())).text().trim(); n = e.find(".video-title strong").text().trim(); } else { const $img = e.find("img"), $a = e.find("a"); t = ($img.attr("data-title") || $img.attr("title") || "").trim(); n = (($a.attr("href") || "").split("/").filter(Boolean).pop() || "").trim(); } if (!t || !n) return; // 无标题或无番号(如演员页头像卡片)时跳过, 避免 undefined.trim() 报错 if (this.cache[n]) { let e = this; return a.contents().each((function() { 3 === this.nodeType && "" !== this.textContent.trim() && (this.textContent = " " + e.cache[n] + " "); })), void a.attr("title", e.cache[n]); } _e(t).then((e => { r ? (a.contents().each((function() { 3 !== this.nodeType || "" === this.textContent.trim() || this.textContent.includes(n) || (this.textContent = " " + e + " "); })), a.attr("title", e)) : a.text(e), this.cache[n] = e, this._debouncedTranslateWrite && clearTimeout(this._debouncedTranslateWrite), this._debouncedTranslateWrite = setTimeout((() => { localStorage.setItem("jhs_translate", JSON.stringify(this.cache)); }), 500); })).catch((e => { console.error("翻译失败:", e); })); } async revertTranslation() { $(this.getSelector().itemSelector).toArray().forEach((e => { let t = $(e); const n = t.find(".box").attr("title") || t.find(".video-title").attr("title") || t.find("img").attr("data-title"); let a; r && (a = t.find(".video-title strong").text().trim()); const i = t.find(".video-title"); i.contents().each((function() { 3 !== this.nodeType || "" === this.textContent.trim() || this.textContent.includes(a) || (this.textContent = " " + n + " "); })), i.removeAttr("title"); })); } addJumpPageControl() { const e = "gemini-jump-page-control"; if ($("#" + e).length > 0) return; if (0 === $(".pagination-link.is-current").length) return; const t = utils.getUrlParam(o, "page") || 1, n = $("<input>", { type: "number", id: "jumpPageInput", placeholder: "页码", min: "1", style: "width: 60px; margin-left: 10px; padding: 10px; border: 1px solid #ccc; font-size: 14px;", value: t + 1 }), a = $("<button>", { text: "跳转", style: "margin-left: 5px; padding: 9px 8px; cursor: pointer; border: 1px solid #ccc; background-color: #f0f0f0; font-size: 14px;" }), i = $("<li>", { id: e }).append(n).append(a); $(".pagination-list").append(i); const s = () => { const e = parseInt(n.val(), 10); if (isNaN(e) || e < 1) return void n.focus(); const t = new URL(window.location.href); t.searchParams.set("page", e.toString()), window.location.href = t.toString(); }; a.on("click", s), n.on("keypress", (function(e) { 13 === e.which && (s(), e.preventDefault()); })); } } class Be extends X { constructor() { super(...arguments), i(this, "preloadDistance", 1500), i(this, "currentPage", this.getInitialPageNumber()), i(this, "pageItems", []); } getName() { return "AutoPagePlugin"; } async initCss() { return "\n <style>\n .jhs-scroll {\n text-align: center;\n padding-top: 20px;\n font-size: 14px;\n }\n .jhs-scroll.waterfall-loading { color: #000; }\n .jhs-scroll.waterfall-error { color: #f44336; cursor: pointer; }\n .jhs-scroll.waterfall-no-more { color: #4CAF50; }\n </style>\n "; } async handle() { this.waterfall().then(); } getInitialPageNumber() { if (l) { const e = o.match(/\/(page|star\/[^/]+)\/(\d+)/); return e ? parseInt(e[2], 10) : 1; } if (r) { const e = o.match(/[?&]page=(\d+)/); return e ? parseInt(e[1], 10) : 1; } return 1; } async waterfall() { if (await this.shouldDisablePaging()) return; const e = this.getSelector(); if (this.container = document.querySelector(e.boxSelector), !this.container) return void console.error("没有找到容器节点,停止瀑布流!"); this.loader = document.createElement("div"), this.loader.className = "jhs-scroll", this.container.parentNode.insertBefore(this.loader, this.container.nextSibling), this.pageItems.push({ page: this.currentPage, top: 0, url: window.location.href }), this.loader.addEventListener("click", (() => { if (this.loader.classList.contains("waterfall-error")) this.loadNextPage().then(); })), (() => { let t = !1; window.addEventListener("scroll", (() => { t || (t = !0, requestAnimationFrame((() => { this.checkLoad(), this.checkScrollPosition(), t = !1; }))); })); })(); const t = document.querySelector(e.nextPageSelector); this.nextUrl = null == t ? void 0 : t.href, this.hasMore = !!this.nextUrl, setTimeout((() => { this.checkLoad(); }), 300), this.hasMore || this.setState("waterfall-no-more", "已经到底了"); } async loadNextPage() { var e; // 同步占位: 必须在任何 await 之前置位, 否则连续 scroll 会并发进入同一页, // 触发重复车牌检测而误判"页码受限", 导致自动触底加载彻底失效 if (this.isLoading || !this.nextUrl) return; this.isLoading = !0; const t = this.getSelector(); try { if (await storageManager.getSetting("autoPage", _) === C) return void this.setState("waterfall-loading", ""); this.setState("waterfall-loading", "加载中..."); const i = await gmHttp.get(this.nextUrl); clog.log("请求下一页内容:", this.nextUrl); const s = utils.htmlTo$dom(i); l && s.find(".avatar-box").length > 0 && s.find(".avatar-box").parent().remove(); let c = s.find(this.getSelector().requestDomItemSelector); const d = this.getBoxCarInfoList(), h = this.getBoxCarInfoList(c); if (this.checkDuplicateCarNumbers(d, h)) return this.nextUrl = null, this.hasMore = !1, void this.setState("waterfall-error", "翻页内容出现重复数据, 页码受JavDB限制, 已停止瀑布流"); const g = this.container.scrollHeight; this.pageItems.push({ page: this.currentPage + 1, top: g, url: this.nextUrl }); const p = this.getBean("ListPagePlugin"); let m = s.find(this.getSelector().coverImgSelector); p.replaceHdImg(m), $(this.getSelector().boxSelector).append(c), this.nextUrl = null == (e = s.find(t.nextPageSelector)) ? void 0 : e.attr("href"), this.hasMore = !!this.nextUrl; let u = s.find(".pagination"); $(".pagination").replaceWith(u), this.setState("waterfall-loading", ""), this.hasMore || this.setState("waterfall-no-more", "已经到底了"); // 新内容不足一屏时 loader 仍在视口内, 主动续拉一次, 否则会卡住 if (this.hasMore && this.nextUrl) setTimeout((() => this.checkLoad()), 0); } catch (n) { clog.error("加载失败:", n), this.setState("waterfall-error", "加载失败,点击重试"); } finally { this.isLoading = !1; } } checkScrollPosition() { const e = window.scrollY; for (let t = this.pageItems.length - 1; t >= 0; t--) { const n = this.pageItems[t]; if (e >= n.top) { this.currentPage !== n.page && (this.currentPage = n.page, this.updatePageUrl(n.url)); break; } } } checkLoad() { if (!this.loader) return; if (this.loader.getBoundingClientRect().top < window.innerHeight + this.preloadDistance) { // 拦住 加载中/已到底/加载失败 三种态, 避免并发与死循环 if (this.isLoading || !this.hasMore || !this.nextUrl) return; if (this.loader.classList.contains("waterfall-no-more") || this.loader.classList.contains("waterfall-error")) return; this.loadNextPage().then(); } } async shouldDisablePaging() { if (!window.isListPage) return !0; return [ "search?q", "handlePlayback=1", "handleTop=1", "/want_watch_videos", "/watched_videos", "/advanced_search?type=100" ].some((e => o.includes(e))); } updatePageUrl_old(e) { if (window.history.pushState({}, "", e), l) { const t = e.match(/\/(page|star\/.*?)\/(\d+)/), n = t ? parseInt(t[2], 10) : null; document.title = document.title.replace(/第\d+頁/, "第" + n + "頁"); } } updatePageUrl(e) { window.history.replaceState({}, "", e), l && (document.title = document.title.replace(/第\d+頁/, `第${this.currentPage}頁`)); } setState(e, t) { this.loader.className = `jhs-scroll ${e}`, this.loader.textContent = t; } } /** Standalone WebDAV client for remote backup/restore operations. */ class De { constructor(e, t, n) { this.davUrl = e.endsWith("/") ? e : e + "/", this.username = t, this.password = n, this.folderName = null; } _getAuthHeaders() { return { Authorization: `Basic ${btoa(`${this.username}:${this.password}`)}`, Depth: "1" }; } _sendRequest(e, t, n = {}, a) { return new Promise(((i, s) => { const o = this.davUrl + t, r = { ...this._getAuthHeaders(), ...n }; GM_xmlhttpRequest({ method: e, url: o, headers: r, data: a, onload: e => { e.status >= 200 && e.status < 300 ? i(e) : (console.error(e), s(new Error(`请求失败 ${e.status}: ${e.statusText}`))); }, onerror: e => { console.error("请求WebDav发生错误:", e), s(new Error("请求WebDav失败, 请检查服务是否启动, 凭证是否正确")); } }); })); } async _ensureFolder(e) { try { await this._sendRequest("MKCOL", e); } catch (t) { if (!/请求失败 (405|409):/.test(t.message)) throw t; } } async backup(e, t, n) { await this._ensureFolder(e); const a = e + "/" + t; await this._sendRequest("PUT", a, { "Content-Type": "text/plain" }, n); } async getFileList(e) { var t, n, a; const i = (await this._sendRequest("PROPFIND", e, { "Content-Type": "application/xml" }, '<?xml version="1.0"?>\n <d:propfind xmlns:d="DAV:">\n <d:prop>\n <d:displayname />\n <d:getcontentlength />\n <d:creationdate />\n <d:getlastmodified />\n <d:iscollection />\n </d:prop>\n </d:propfind>\n ')).responseText, s = (new DOMParser).parseFromString(i, "text/xml").getElementsByTagNameNS("DAV:", "response"), o = []; for (let r = 0; r < s.length; r++) { if (0 === r) continue; let e = s[r]; const i = e.getElementsByTagNameNS("DAV:", "displayname")[0].textContent, l = (null == (t = e.getElementsByTagNameNS("DAV:", "getcontentlength")[0]) ? void 0 : t.textContent) || "0", c = (null == (n = e.getElementsByTagNameNS("DAV:", "creationdate")[0]) ? void 0 : n.textContent) || (null == (a = e.getElementsByTagNameNS("DAV:", "getlastmodified")[0]) ? void 0 : a.textContent) || ""; "0" !== l && o.push({ fileId: i, name: i, size: Number(l), createTime: c }); } return o.reverse(), o; } async deleteFile(e) { let t = this.folderName + "/" + encodeURI(e); await this._sendRequest("DELETE", t, { "Cache-Control": "no-cache" }); } async getBackupList(e) { return this.folderName = e, await this._ensureFolder(e), this.getFileList(e); } async getFileContent(e) { let t = this.folderName + "/" + e; return (await this._sendRequest("GET", t, { Accept: "application/octet-stream" })).responseText; } } /** Generate the settings page CSS based on container config and site type. */ function buildSettingCss(containerWidth, containerColumns, isJavBus, isJavDB) { let base; if (isJavBus) { base = ` .container-fluid .row{ max-width: 1000px !important; min-width: ${containerWidth}%; margin: auto auto; } .container { max-width: 1000px !important; min-width: 80%; margin: auto auto; } .masonry { grid-template-columns: repeat(${containerColumns}, minmax(0, 1fr)); } `; } else { base = ` section .container{ max-width: 1000px !important; min-width: ${containerWidth}%; } .movie-list, .movie-list.v{ grid-template-columns: repeat(${containerColumns}, minmax(0, 1fr)); } .movie-list > :not(.item) { display: none !important; } `; } return ` <style> ${base} .nav-btn::after { content:none !important; } #cache-data-display pre { font-family: Consolas, Monaco, 'Andale Mono', monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; color: #333; border: 1px solid #ddd; } .cache-item { transition: all 0.2s ease; } .cache-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); transform: translateY(-2px); } .tooltip-icon { display: inline-block; width: 16px; height: 16px; line-height: 16px; text-align: center; border-radius: 50%; background-color: #ccc; color: white; font-size: 12px; margin-right: 5px; cursor: help; } .setting-item { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; padding: 4px 0; } .simple-setting .setting-item{ align-items:center; } .setting-label { font-size: 14px; min-width: 180px; font-weight: bold; margin-right: 10px; } .form-content{ max-width: 200px; min-width: 180px; flex-shrink: 0; } .form-content * { width: 100%; padding: 5px; margin-right: 10px; text-align: left; box-sizing: border-box; } /* 统一 input / select 的高度与字号, 否则 select 与只读框会与上方输入框错位. 必须排除 checkbox, 否则会覆盖后面 .mini-switch 的宽高 (本规则特异性更高) */ .form-content > input:not([type="checkbox"]), .form-content > select { height: 30px; font-size: 13px; line-height: normal; border: 1px solid #ccc; border-radius: 4px; background-color: #fff; vertical-align: middle; margin-right: 0; } .keyword-label { display: inline-flex; align-items: center; padding: 4px 8px; border-radius: 4px; font-size: 14px; position: relative; margin-left: 8px; margin-bottom: 5px; } .keyword-remove { margin-left: 6px; cursor: pointer; font-size: 12px; line-height: 1; } .keyword-input { padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; float:right; } .add-tag-btn { padding: 6px 12px; background-color: #e2e8f0; color: #334155; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; margin-left: 8px; float:right; } .add-tag-btn:hover { background-color: #cbd5e1; } .tag-box { margin-top:15px; } #saveBtn,#moreBtn,#helpBtn,#clean-all { padding: 8px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-top: 10px; } #saveBtn:hover { background-color: #45a049; } #moreBtn { background-color: #5cb85c; color: white; } #moreBtn:hover { background-color: #4cae4c; } #helpBtn { background-color: #e67e22; color: white; } #helpBtn:hover { background-color: #d35400; } .simple-setting, .mini-simple-setting { display: none; background: rgba(255,255,255,1); position: absolute; top: ${isJavDB ? "35px" : "25px"}; right: ${isJavDB ? "-300%" : "0"}; z-index: 1000; border: 1px solid #ddd; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); padding: 0; margin-top: 5px; color: #333; width: 320px; } /* ===== 快捷设置面板 jhs-qs 样式 ===== */ .jhs-qs-panel { display: flex; flex-direction: column; max-height: 80vh; } .jhs-qs-header { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 14px 16px 10px; border-bottom: 1px solid #f1f5f9; background: linear-gradient(180deg, #f8fafc 0%, #fff 100%); } .jhs-qs-title { font-size: 15px; font-weight: 700; color: #1a1a1a; } .jhs-qs-sub { font-size: 11px; color: #999; } .jhs-qs-body { flex: 1; overflow-y: auto; padding: 8px 12px 12px; } .jhs-qs-section { background: #f8fafc; border: 1px solid #f1f5f9; border-radius: 10px; padding: 10px 12px; margin-bottom: 10px; } .jhs-qs-section-title { font-size: 11px; font-weight: 700; color: #94a3b8; margin-bottom: 6px; letter-spacing: 0.08em; } .jhs-qs-subcard { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 4px 10px; margin-top: 6px; } .jhs-qs-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 5px 0; min-height: 30px; } .jhs-qs-row-stacked { flex-direction: column; align-items: stretch; gap: 4px; } .jhs-qs-label { display: flex; align-items: center; gap: 5px; font-size: 13px; font-weight: 500; color: #334155; line-height: 1.3; flex: 1; min-width: 0; } .jhs-qs-label > span:not(.jhs-qs-tip) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .jhs-qs-control { flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; } .jhs-qs-row-stacked .jhs-qs-control { width: 100%; } .jhs-qs-tip { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; border-radius: 50%; background: #e2e8f0; color: #64748b; font-size: 10px; font-weight: 700; cursor: help; flex-shrink: 0; } .jhs-qs-footer { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 0 15px 10px; border-top: 1px solid #f1f5f9; background: #fafbfc; position: relative; top: -2px; } .jhs-qs-btn { flex: 1; margin: 0 14px; padding: 2px 8px; line-height: 1; border: none; border-radius: 4px; font-size: 12px; font-weight: 500; cursor: pointer; transition: background .15s ease, transform .12s ease, box-shadow .15s ease; } .jhs-qs-btn:hover { transform: translateY(-1px); } .jhs-qs-btn:active { transform: translateY(0); } .jhs-qs-btn-ghost { background: #fff; color: #64748b; border: 1px solid #e2e8f0; } .jhs-qs-btn-ghost:hover { background: #f8fafc; color: #475569; border-color: #cbd5e1; } .jhs-qs-btn-primary { background: linear-gradient(135deg, #5d87c2, #4a90d9); color: #fff; box-shadow: 0 2px 8px rgba(74, 144, 217, 0.28); } .jhs-qs-btn-primary:hover { box-shadow: 0 4px 12px rgba(74, 144, 217, 0.35); } .jhs-qs-range { display: flex; align-items: center; gap: 8px; width: 100%; } .jhs-qs-range input[type="range"] { flex: 1; height: 4px; -webkit-appearance: none; appearance: none; background: #ddd; border-radius: 2px; outline: none; } .jhs-qs-range input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #5d87c2; cursor: pointer; } .jhs-qs-range span { min-width: 28px; font-size: 12px; color: #666; text-align: right; } .mini-switch { appearance: none; -webkit-appearance: none; width: 40px; height: 20px; background: #e0e0e0; border-radius: 20px; position: relative; cursor: pointer; outline: none; } .mini-switch:checked { background: #4CAF50; } .mini-switch::before { content: ""; position: absolute; width: 16px; height: 16px; border-radius: 50%; background: white; top: 2px; left: 2px; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } .mini-switch:checked::before { left: calc(100% - 18px); } .side-menu-item { padding: 12px 12px; cursor: pointer; color: #333; border-left: 3px solid transparent; transition: all 0.2s; display: flex; gap: 5px; } .side-menu-item .icon { height: 24px; width: 24px; } .side-menu-item:hover { background-color: #e9e9e9; } .side-menu-item.active { background-color: #e0e0e0; border-left: 3px solid #5d87c2; font-weight: bold; } .content-panel { display: none; margin-top:20px; padding: 0 10px 10px 0; height: 100%; overflow-x: hidden; overflow-y: auto; } .content-panel.active { display: block; } input[type="checkbox"]:disabled { opacity: 0.6; cursor: default !important; } </style> `; } /** Toggle between vertical (cover-fit) and normal (contain) image display modes. */ /** 详情页比例: 封面/信息 两列的 flex 权重. JavDB 与 JavBus 详情页均生效 */ async function applyDetailFlex() { if (!window.isDetailPage) return; const setting = await storageManager.getSetting(); const cover = (parseInt(setting.detailCoverRatio) || 135) / 100; const info = (parseInt(setting.detailInfoRatio) || 105) / 100; if (r) { // JavDB: .column.column-video-cover + 兄弟 .column const coverCol = document.querySelector(".column.column-video-cover"); if (!coverCol) return; const infoCol = coverCol.parentElement?.querySelector(".column:not(.column-video-cover)"); coverCol.style.flex = `${cover} 1 0%`; if (infoCol) infoCol.style.flex = `${info} 1 0%`; return; } if (l) { // JavBus: .row.movie > .col-md-9.screencap + .col-md-3.info (Bootstrap 栅格) // 用 flex 覆盖 float+width 的 Bootstrap 默认样式 const row = document.querySelector(".row.movie"); if (!row) return; const coverCol = row.querySelector(".screencap"); const infoCol = row.querySelector(".info"); if (!coverCol || !infoCol) return; row.style.display = "flex"; row.style.flexWrap = "nowrap"; row.style.alignItems = "flex-start"; [ coverCol, infoCol ].forEach((c => { c.style.float = "none"; c.style.maxWidth = "none"; c.style.width = "auto"; })); coverCol.style.flex = `${cover} 1 0%`; infoCol.style.flex = `${info} 1 0%`; } } async function applyImageMode() { $("#verticalImgStyle").remove(); if (await storageManager.getSetting("enableVerticalModel", C) === _) { let e = "100% 50% !important"; window.location.href.includes("/advanced_search?type=100") && (e = "50% 50% !important"); const t = ` .cover { min-height: 350px !important; overflow: hidden !important; padding-top: 142% !important; } .cover img { object-fit: cover !important; object-position: ${e}; } /* bus的 */ .masonry .movie-box .photo-frame { height: 0 !important; padding-top: 142% !important; overflow: hidden !important; position: relative !important; } .masonry .movie-box img { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; max-height: none !important; min-height: 0 !important; object-fit: cover !important; object-position: top right; } `; $("<style>").attr("id", "verticalImgStyle").text(t).appendTo("head"); } else { const e = ` .cover { min-height:auto !important; padding-top: 67% !important; } .cover img { object-fit: contain !important; object-position: 50% 50% !important } /* bus的 */ .masonry .movie-box .photo-frame { height: 0 !important; padding-top: 67% !important; overflow: hidden !important; position: relative !important; } .masonry .movie-box img { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; max-height: none !important; min-height: 0 !important; object-fit: contain !important; object-position: top; } `; $("<style>").attr("id", "verticalImgStyle").text(e).appendTo("head"); } l && window.getBeanForSetting("BusImgPlugin").logImageHeightsByRow(); } /** * Build the plugin categories configuration shared between inject and render. * Returns { categories, corePlugins }. */ function getPluginCategories() { return { categories: { status: { label: "状态管理", plugins: ["DetailPagePlugin","ListPagePlugin","NavBarPlugin","BusNavBarPlugin","BusDetailPagePlugin","DetailPageButtonPlugin","ListPageButtonPlugin","HighlightMagnetPlugin","NativeMagnetPanelPlugin","NativeReviewPanelPlugin","NativeListPanelPlugin","FoldCategoryPlugin","AutoPagePlugin","HistoryPlugin","WantAndWatchedVideosPlugin"] }, blacklist: { label: "屏蔽过滤", plugins: ["BlacklistPlugin","FilterTitleKeywordPlugin"] }, favorite: { label: "收藏", plugins: ["FavoriteActressesPlugin"] }, "new-video": { label: "新作品", plugins: ["NewVideoPlugin","TaskPlugin"] }, "external-search": { label: "外部搜索", plugins: ["OtherSitePlugin","Fc2Plugin","Fc2By123AvPlugin","HitShowPlugin","TOP250Plugin","MagnetHubPlugin","JavTrailersPlugin"] }, "image-viewer": { label: "图片预览", plugins: ["CoverButtonPlugin","PreviewVideoPlugin","BusPreviewVideoPlugin","ScreenShotPlugin","BusImgPlugin"] }, avatar: { label: "演员信息", plugins: ["ActressInfoPlugin","SearchByImagePlugin"] }, translate: { label: "翻译", plugins: ["TranslatePlugin"] }, subtitle: { label: "字幕", plugins: ["SubTitleCatPlugin"] }, backup: { label: "备份设置", plugins: ["SettingPlugin","LocalPlugin"] }, "one-two-three": { label: "云盘", plugins: ["WangPan115MatchPlugin"] }, stats: { label: "统计", plugins: ["StatsPlugin"] } }, corePlugins: ["SettingPlugin","StatsPlugin"], displayNames: { DetailPagePlugin: "详情页", ListPagePlugin: "列表页", NavBarPlugin: "导航栏", BusNavBarPlugin: "JavBus 导航栏", BusDetailPagePlugin: "JavBus 详情页", DetailPageButtonPlugin: "详情页按钮", ListPageButtonPlugin: "列表页按钮", HighlightMagnetPlugin: "磁链高亮", NativeMagnetPanelPlugin: "磁力面板", NativeReviewPanelPlugin: "短评面板", NativeListPanelPlugin: "相关清单", FoldCategoryPlugin: "分类折叠", AutoPagePlugin: "自动翻页", HistoryPlugin: "历史记录", WantAndWatchedVideosPlugin: "想看/已看", BlacklistPlugin: "黑名单", FilterTitleKeywordPlugin: "标题关键词过滤", FavoriteActressesPlugin: "收藏演员", NewVideoPlugin: "新作品检测", TaskPlugin: "定时任务", OtherSitePlugin: "第三方站点", Fc2Plugin: "FC2", Fc2By123AvPlugin: "FC2 (123AV 源)", HitShowPlugin: "热播", TOP250Plugin: "TOP250", ReviewPlugin: "评论增强", MagnetHubPlugin: "磁力搜索", JavTrailersPlugin: "预告片", CoverButtonPlugin: "封面按钮", PreviewVideoPlugin: "预览视频", BusPreviewVideoPlugin: "JavBus 预览视频", ScreenShotPlugin: "截图预览", BusImgPlugin: "JavBus 图片", ActressInfoPlugin: "演员信息", SearchByImagePlugin: "以图识图", TranslatePlugin: "翻译", SubTitleCatPlugin: "字幕搜索", SettingPlugin: "设置", LocalPlugin: "本地备份", WangPan115MatchPlugin: "115 视频匹配", MobileBottomBarPlugin: "移动端底部栏插件", StatsPlugin: "统计" } }; } /** Build the HTML for the cache items grid in the settings dialog. */ function buildCacheItemsHtml(cacheItems) { return cacheItems.map(e => ` <div class="cache-item" style="border: 1px solid #eee; border-radius: 8px; padding: 12px;"> <div style="font-weight: bold; margin-bottom: 8px;">${e.text}</div> <div style="display: flex; gap: 8px;"> <a class="menu-btn clean-btn" data-key="${e.key}" style="background-color:#448cc2; flex:1; text-align:center;" title="${e.title}"> <span>清理</span> </a> <a class="menu-btn view-btn" data-key="${e.key}" style="background-color:#b2bec0; flex:1; text-align:center;" > <span>查看</span> </a> </div> </div> `).join(""); } /** Build the quality options HTML for the video quality select. */ function buildVideoQualityOptions() { let a = ""; L.forEach(e => { e.canSelect && (a += `<option value="${e.quality}">${e.text}</option>`); }); return a; } /** Build the main settings dialog HTML template. */ function buildSettingDialogHtml(activePanel, cacheItems, coverButtonPlugin) { const n = buildCacheItemsHtml(cacheItems); const a = buildVideoQualityOptions(); const isMobile = utils.isMobileMode(); const sidebarDir = isMobile ? "column" : "row"; return ` <div style="display: flex; flex-direction: ${sidebarDir}; height: 100%;"> <div class="jhs-mobile-sidebar" style="width: 140px; flex-shrink: 0; padding: 15px 0; background: #f5f5f5; border-right: 1px solid #ddd;"> <div class="side-menu-item ${"backup-panel" === activePanel ? "active" : ""}" data-panel="backup-panel">数据备份</div> <div class="side-menu-item ${"base-panel" === activePanel ? "active" : ""}" data-panel="base-panel">基础配置</div> <div class="side-menu-item ${"appraisal-panel" === activePanel ? "active" : ""}" data-panel="appraisal-panel">鉴定配置</div> <div class="side-menu-item ${"filter-panel" === activePanel ? "active" : ""}" data-panel="filter-panel">屏蔽配置</div> <div class="side-menu-item ${"task-panel" === activePanel ? "active" : ""}" data-panel="task-panel">定时任务</div> <div class="side-menu-item ${"domain-panel" === activePanel ? "active" : ""}" data-panel="domain-panel" title="第三方视频资源域名配置">外部网站</div> <div class="side-menu-item ${"cache-panel" === activePanel ? "active" : ""}" data-panel="cache-panel">清理缓存</div> </div> <div style="flex: 1; display: flex; flex-direction: column; height: 100%; "> <div style="flex: 1; margin: 0 10px; padding-bottom: 20px;overflow-y: auto;overflow-x: hidden"> <div id="backup-panel" class="content-panel" style="display: ${"backup-panel" === activePanel ? "block" : "none"};"> <div class="setting-item"> <span class="setting-label">数据管理</span> <div> <a id="importBtn" class="menu-btn" style="background-color:#d25a88"><span>导入数据</span></a> <a id="exportBtn" class="menu-btn" style="background-color:#85d0a3"><span>导出数据</span></a> </div> </div> <div class="setting-item"> <span class="setting-label">WebDav备份</span> <div> <a id="webdavBackupListBtn" class="menu-btn" style="background-color:#5d87c2"><span>查看备份</span></a> <a id="webdavBackupBtn" class="menu-btn" style="background-color:#64bb69"><span>备份数据</span></a> </div> </div> <div class="setting-item"> <span class="setting-label">服务地址:</span> <div class="form-content"> <input id="webDavUrl"> </div> </div> <div class="setting-item"> <span class="setting-label">用户名:</span> <div class="form-content"> <input id="webDavUsername"> </div> </div> <div class="setting-item"> <span class="setting-label">密码:</span> <div class="form-content"> <input id="webDavPassword" type="password"> </div> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> 备份口令: <span data-tip="设置后,备份文件将使用该口令加密(V2 格式), 可跨设备恢复; 未设置则使用本机密钥加密, 仅本机可恢复">(?)</span> </span> <div class="form-content"> <input id="backupPassword" type="password" placeholder="加密备份的口令 (V2 格式)"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div class="setting-item"> <span class="setting-label">自动备份到WebDav</span> <div class="form-content"> <input type="checkbox" id="enableAutoBackup" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">备份频率:</span> <div class="form-content"> <select id="autoBackupFrequency"> <option value="daily">每天第一次打开(推荐)</option> <option value="everyOpen">每次打开</option> <option value="disabled">不自动备份</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label">本机凭证:</span> <div class="form-content"> <input id="credentialIdDisplay" readonly style="font-family:monospace; background:#f5f5f5; cursor:default"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div class="setting-item"> <span class="setting-label">清空所有数据</span> <div> <a id="cleanJhsDataBtn" class="menu-btn" style="background-color:#d22020"><span>清空数据</span></a> </div> </div> </div> <div id="base-panel" class="content-panel" style="display: ${"base-panel" === activePanel ? "block" : "none"};"> <div class="setting-item" style="margin-top:10px"> <span class="setting-label"> 封面快捷按钮 </span> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> ${coverButtonPlugin.screenSvg}长缩略图: </span> <div class="form-content"> <input type="checkbox" id="enableScreenSvg" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> ${coverButtonPlugin.videoSvg}预览视频: </span> <div class="form-content"> <input type="checkbox" id="enableVideoSvg" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> ${coverButtonPlugin.handleSvg}鉴定按钮: </span> <div class="form-content"> <input type="checkbox" id="enableHandleSvg" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> ${coverButtonPlugin.siteSvg}第三方跳转: </span> <div class="form-content"> <input type="checkbox" id="enableSiteSvg" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> ${coverButtonPlugin.copySvg}复制按钮: </span> <div class="form-content"> <input type="checkbox" id="enableCopySvg" class="mini-switch"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div class="setting-item"> <span class="setting-label">预览视频默认画质:</span> <div class="form-content"> <select id="videoQuality"> ${a} </select> </div> </div> <div class="setting-item"> <span class="setting-label">评论区条数:</span> <div class="form-content"> <select id="reviewCount"> <option value="10">10条</option> <option value="20">20条</option> <option value="30">30条</option> <option value="40">40条</option> <option value="50">50条</option> </select> </div> </div> <div class="setting-item ${r ? "" : "do-hide"}"> <span class="setting-label"> 高亮已收藏演员 <span data-tip="详情页, 对已收藏的演员进行边框高亮提醒">(?)</span> </span> <div class="form-content"> <input type="checkbox" id="enableFavoriteActresses" class="mini-switch"> </div> </div> <div class="setting-item ${r ? "" : "do-hide"}"> <span id="highlightedTagLabel" class="setting-label"> 分类标签|高亮演员-边框样式: </span> <div class="form-content" style="display: flex; align-items: center;"> <input type="number" id="highlightedTagNumber" min="0" max="20"> <input type="color" id="highlightedTagColor"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div class="setting-item"> <span class="setting-label">请求超时时间(毫秒):</span> <div class="form-content"> <input type="number" id="httpTimeout" min="1000" max="10000" style="width: 100%;"> </div> </div> <div class="setting-item"> <span class="setting-label">请求失败重试次数:</span> <div class="form-content"> <input type="number" id="httpRetryCount" min="0" max="10" style="width: 100%;"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div class="setting-item"> <span class="setting-label"> 启用控制台日志: </span> <div class="form-content"> <select id="enableClog"> <option value="no">禁用</option> <option value="yes">开启</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label">日志最大行数:</span> <div class="form-content"> <input type="number" id="clogMsgCount" min="100" max="3000" style="width: 100%;"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div class="setting-item"> <span class="setting-label"> 移动端模式 <span data-tip="auto=自动检测设备, on=强制移动端UI, off=强制桌面端UI">(?)</span> </span> <div class="form-content"> <select id="mobileMode"> <option value="auto">自动检测</option> <option value="on">强制开启</option> <option value="off">强制关闭</option> </select> </div> </div> </div> <div id="task-panel" class="content-panel" style="display: ${"task-panel" === activePanel ? "block" : "none"};"> <div class="setting-item"> <span class="setting-label">请求并发数量:</span> <div class="form-content"> <input type="number" id="checkConcurrencyCount" min="2" max="5" style="width: 100%;"> </div> </div> <div class="setting-item"> <span class="setting-label">请求间隔时间(毫秒):</span> <div class="form-content"> <input type="number" id="checkRequestSleep" min="0" max="3000" style="width: 100%;"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div id="setting-blacklist" style="border: 1px solid #ccc; padding: 10px; margin-bottom: 15px;"> <span style="font-size: 14px; font-weight: bold; padding:3px">自动检测屏蔽黑名单演员</span> <div class="setting-item"> <span class="setting-label"> 任务开关: <span data-tip="变更后, 刷新页面生效">(?)</span> </span> <div class="form-content"> <select id="enableCheckBlacklist"> <option value="no">禁用</option> <option value="yes">开启</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label">任务间隔时间:</span> <div class="form-content"> <select id="checkBlacklist_intervalTime"> <option value="2">每2小时</option> <option value="3">每3小时</option> <option value="6">每6小时</option> <option value="12">每12小时</option> <option value="24">每24小时</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label">检测规则:</span> <div class="form-content"> <select id="checkBlacklist_ruleTime"> <option value="0">全部检测</option> <option value="8760">不检测停更1年以上</option> <option value="17520">不检测停更2年以上</option> <option value="26280">不检测停更3年以上</option> </select> </div> </div> </div> <div id="setting-checkFavoriteActress" style="border: 1px solid #ccc; padding: 10px; margin-bottom: 15px;" class="${r ? "" : "do-hide"}"> <span style="font-size: 14px; font-weight: bold; padding:3px">自动同步已收藏的演员</span> <div class="setting-item"> <span class="setting-label"> 任务开关: <span data-tip="变更后, 刷新页面生效">(?)</span> </span> <div class="form-content"> <select id="enableCheckFavoriteActress"> <option value="no">禁用</option> <option value="yes">开启</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label">任务间隔时间:</span> <div class="form-content"> <select id="checkFavoriteActress_IntervalTime"> <option value="12">每12小时</option> <option value="24">每24小时</option> </select> </div> </div> </div> <div id="setting-checkNewVideo" style="border: 1px solid #ccc; padding: 10px; margin-bottom: 15px;" class="${r ? "" : "do-hide"}"> <span style="font-size: 14px; font-weight: bold; padding:3px">自动检测已收藏演员的最新作品</span> <div class="setting-item"> <span class="setting-label"> 任务开关: <span data-tip="变更后, 刷新页面生效">(?)</span> </span> <div class="form-content"> <select id="enableCheckNewVideo"> <option value="no">禁用</option> <option value="yes">开启</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label">任务间隔时间:</span> <div class="form-content"> <select id="checkNewVideo_intervalTime"> <option value="2">每2小时</option> <option value="3">每3小时</option> <option value="6">每6小时</option> <option value="12">每12小时</option> <option value="24">每24小时</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label">检测规则:</span> <div class="form-content"> <select id="checkNewVideo_ruleTime"> <option value="0">全部检测</option> <option value="8760">不检测停更1年以上</option> <option value="17520">不检测停更2年以上</option> <option value="26280">不检测停更3年以上</option> </select> </div> </div> </div> </div> <div id="domain-panel" class="content-panel" style="display: ${"domain-panel" === activePanel ? "block" : "none"};"> <style>#domain-panel .setting-item{justify-content:flex-start;gap:0}#domain-panel .setting-label{min-width:auto;margin-right:4px;flex:0 0 auto}#domain-panel .form-content{max-width:none;min-width:0;flex:1;display:flex;gap:8px;align-items:center;justify-content:flex-start}#domain-panel .form-content input[placeholder="域名"]{margin-left:auto}</style> <div class="setting-item"> <span class="setting-label">名称:</span> <div class="form-content" style="display:flex;gap:8px;align-items:center"> <input id="siteNameMissAv" placeholder="名称" style="width:90px;flex:0 0 auto"> <input id="missAvUrl" placeholder="域名" style="width:180px;flex:0 0 auto"> <input id="siteSearchMissAv" placeholder="搜索地址(留空=默认)" style="flex:1 1 180px;min-width:140px"> <input type="checkbox" class="jhs-site-toggle mini-switch" data-site-key="missAv"> <input type="checkbox" class="jhs-cover-toggle mini-switch" data-site-key="missAv" title="封面显示"> </div> </div> <div class="setting-item"> <span class="setting-label">名称:</span> <div class="form-content" style="display:flex;gap:8px;align-items:center"> <input id="siteNameJable" placeholder="名称" style="width:90px;flex:0 0 auto"> <input id="jableUrl" placeholder="域名" style="width:180px;flex:0 0 auto"> <input id="siteSearchJable" placeholder="搜索地址(留空=默认)" style="flex:1 1 180px;min-width:140px"> <input type="checkbox" class="jhs-site-toggle mini-switch" data-site-key="jable"> <input type="checkbox" class="jhs-cover-toggle mini-switch" data-site-key="jable" title="封面显示"> </div> </div> <div class="setting-item"> <span class="setting-label">名称:</span> <div class="form-content" style="display:flex;gap:8px;align-items:center"> <input id="siteNameJavTrailer" placeholder="名称" style="width:90px;flex:0 0 auto"> <input id="javTrailersUrl" placeholder="域名" style="width:180px;flex:0 0 auto"> <input id="siteSearchJavTrailer" placeholder="搜索地址(留空=默认)" style="flex:1 1 180px;min-width:140px"> <input type="checkbox" class="jhs-site-toggle mini-switch" data-site-key="javTrailer"> <input type="checkbox" class="jhs-cover-toggle mini-switch" data-site-key="javTrailer" title="封面显示"> </div> </div> <div class="setting-item"> <span class="setting-label">名称:</span> <div class="form-content" style="display:flex;gap:8px;align-items:center"> <input id="siteNameAv123" placeholder="名称" style="width:90px;flex:0 0 auto"> <input id="av123Url" placeholder="域名" style="width:180px;flex:0 0 auto"> <input id="siteSearchAv123" placeholder="搜索地址(留空=默认)" style="flex:1 1 180px;min-width:140px"> <input type="checkbox" class="jhs-site-toggle mini-switch" data-site-key="av123"> <input type="checkbox" class="jhs-cover-toggle mini-switch" data-site-key="av123" title="封面显示"> </div> </div> <div class="setting-item"> <span class="setting-label">名称:</span> <div class="form-content" style="display:flex;gap:8px;align-items:center"> <input id="siteNameJavDb" placeholder="名称" style="width:90px;flex:0 0 auto"> <input id="javDbUrl" placeholder="域名" style="width:180px;flex:0 0 auto"> <input id="siteSearchJavDb" placeholder="搜索地址(留空=默认)" style="flex:1 1 180px;min-width:140px"> <input type="checkbox" class="jhs-site-toggle mini-switch" data-site-key="javDb"> <input type="checkbox" class="jhs-cover-toggle mini-switch" data-site-key="javDb" title="封面显示"> </div> </div> <div class="setting-item"> <span class="setting-label">名称:</span> <div class="form-content" style="display:flex;gap:8px;align-items:center"> <input id="siteNameJavBus" placeholder="名称" style="width:90px;flex:0 0 auto"> <input id="javBusUrl" placeholder="域名" style="width:180px;flex:0 0 auto"> <input id="siteSearchJavBus" placeholder="搜索地址(留空=默认)" style="flex:1 1 180px;min-width:140px"> <input type="checkbox" class="jhs-site-toggle mini-switch" data-site-key="javBus"> <input type="checkbox" class="jhs-cover-toggle mini-switch" data-site-key="javBus" title="封面显示"> </div> </div> <div class="setting-item"> <span class="setting-label">名称:</span> <div class="form-content" style="display:flex;gap:8px;align-items:center"> <input id="siteNameSupJav" placeholder="名称" style="width:90px;flex:0 0 auto"> <input id="supJavUrl" placeholder="域名" style="width:180px;flex:0 0 auto"> <input id="siteSearchSupJav" placeholder="搜索地址(留空=默认)" style="flex:1 1 180px;min-width:140px"> <input type="checkbox" class="jhs-site-toggle mini-switch" data-site-key="supJav"> <input type="checkbox" class="jhs-cover-toggle mini-switch" data-site-key="supJav" title="封面显示"> </div> </div> <div class="setting-item"> <span class="setting-label">名称:</span> <div class="form-content" style="display:flex;gap:8px;align-items:center"> <input id="siteNameFanza" placeholder="名称" style="width:90px;flex:0 0 auto"> <input id="fanzaUrl" placeholder="域名" style="width:180px;flex:0 0 auto"> <input id="siteSearchFanza" placeholder="搜索地址(留空=默认)" style="flex:1 1 180px;min-width:140px"> <input type="checkbox" class="jhs-site-toggle mini-switch" data-site-key="fanza"> <input type="checkbox" class="jhs-cover-toggle mini-switch" data-site-key="fanza" title="封面显示"> </div> </div> </div> <div id="appraisal-panel" class="content-panel" style="display: ${"appraisal-panel" === activePanel ? "block" : "none"};"> <div class="setting-item"> <span class="setting-label">已鉴定标签展示位置:</span> <div class="form-content"> <select id="tagPosition"> <option value="rightTop">右上</option> <option value="leftTop">左上</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label">已鉴定内容处理方式:</span> <div class="form-content"> <select id="movieShowType"> <option value="normal">正常</option> <option value="hide">隐藏</option> <option value="visibility">透明</option> </select> </div> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> 首页鉴定筛选功能 <span data-tip="列表页顶部显示 全部/待鉴定/已收藏/已下载/已观看/已屏蔽 的快捷筛选按钮">(?)</span> </span> <div class="form-content"> <input type="checkbox" id="enableQuickFilter" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> 鉴定补录演员信息 <span data-tip="在列表页进行鉴定是获取不到演员名称的, 开启后, 额外解析详情页补录演员名称, 因发请求解析费时, 会被以往慢1秒左右">(?)</span> </span> <div class="form-content"> <input type="checkbox" id="enableSaveActressCarInfo" class="mini-switch"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div class="setting-item" style="margin-top:10px"> <span class="setting-label"> 列表页功能按钮 </span> </div> <div class="setting-item"> <span class="setting-label">按钮-打开待鉴定:</span> <div class="form-content"> <input type="checkbox" id="showWaitCheckBtn" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">按钮-打开已收藏:</span> <div class="form-content"> <input type="checkbox" id="showWaitDownBtn" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">打开待鉴定|已收藏 窗口数:</span> <div class="form-content"> <input type="number" id="waitCheckCount" min="1" max="20" style="width: 100%;"> </div> </div> <div class="setting-item"> <span class="setting-label">随机打开已收藏:</span> <div class="form-content"> <input type="checkbox" id="randomOpenWaitDown" class="mini-switch"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div class="setting-item" style="margin-top:10px"> <span class="setting-label">列表显示</span> </div> <div class="setting-item"> <span class="setting-label" style="display:flex; align-items:center; gap:5px"> <span data-tip="开启后忽略下方分项,强制显示全部已鉴定内容">(?)</span> 显示所有已鉴定 </span> <div class="form-content"> <input type="checkbox" id="showAllItem2" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">屏蔽单番号:</span> <div class="form-content"> <input type="checkbox" id="showFilterItem2" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">屏蔽演员:</span> <div class="form-content"> <input type="checkbox" id="showFilterActorItem2" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">屏蔽关键词:</span> <div class="form-content"> <input type="checkbox" id="showFilterKeywordItem2" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">收藏:</span> <div class="form-content"> <input type="checkbox" id="showFavoriteItem2" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">已下载:</span> <div class="form-content"> <input type="checkbox" id="showHasDownItem2" class="mini-switch"> </div> </div> <div class="setting-item"> <span class="setting-label">已观看:</span> <div class="form-content"> <input type="checkbox" id="showHasWatchItem2" class="mini-switch"> </div> </div> </div> <div id="filter-panel" class="content-panel" style="display: ${"filter-panel" === activePanel ? "block" : "none"};"> <div class="setting-item"> <span class="setting-label"> 启用划词屏蔽 <span data-tip="视频详情页中, 标题或评论区选中文字, 按右键可快捷加入屏蔽词">(?) </span> </span> <div style="display: flex"> <input type="checkbox" id="enableTitleSelectFilter" class="mini-switch"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div id="reviewKeywordContainer"> <div class="setting-item"> <span class="setting-label">评论区屏蔽词:</span> <div style="display: flex"> <input type="text" class="keyword-input" placeholder="添加屏蔽词"> <button class="add-tag-btn">添加</button> </div> </div> <div class="tag-box"> </div> </div> <hr style="border: 0; height: 1px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(159,137,137,0.75), rgba(0,0,0,0));"/> <div id="filterKeywordContainer"> <div class="setting-item"> <span class="setting-label">视频标题屏蔽词:</span> <div style="display: flex"> <input type="text" class="keyword-input" placeholder="添加屏蔽词"> <button class="add-tag-btn">添加</button> </div> </div> <div class="tag-box"> </div> </div> </div> <div id="cache-panel" class="content-panel" style="display: ${"cache-panel" === activePanel ? "block" : "none"};"> <h1 style="text-align:center;font-size: 20px;font-weight: bold">以下操作, 不会对核心数据造成影响</h1> <br/> <div class="jhs-cache-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 20px;"> ${n} </div> <div id="cache-data-display" style="margin-top: 20px; display: none;"> <pre style="background: #f5f5f5; padding: 10px; border-radius: 5px; max-height: 400px; overflow: auto;"></pre> </div> </div> <div id="hotkey-panel" class="content-panel" style="display: ${"hotkey-panel" === activePanel ? "block" : "none"};"> <p style="color: #c62222; font-size: 14px;font-weight: bold;margin-bottom: 10px;">快捷键修改后, 刷新页面生效</p> <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 15px;"> <div class="setting-item"> <span class="setting-label">屏蔽:</span> <div class="form-content"><input id="filterHotKey" placeholder="录入快捷键" data-default-hotkey="a"></div> </div> <div class="setting-item"> <span class="setting-label">收藏:</span> <div class="form-content"><input id="favoriteHotKey" placeholder="录入快捷键" data-default-hotkey="s"></div> </div> <div class="setting-item"> <span class="setting-label">已下载:</span> <div class="form-content"><input id="hasDownHotKey" placeholder="录入快捷键"></div> </div> <div class="setting-item"> <span class="setting-label">已观看:</span> <div class="form-content"><input id="hasWatchHotKey" placeholder="录入快捷键"></div> </div> <div class="setting-item"> <span class="setting-label"><span data-tip="列表页,鼠标放置图片上时可使用快捷键">? </span>对视频列表页启用快捷键:</span> <div class="form-content"><input type="checkbox" id="enableImageHotKey" class="mini-switch"></div> </div> </div> <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 15px;"> <div class="setting-item"> <span class="setting-label">快进:</span> <div class="form-content"><input id="speedVideoHotKey" placeholder="录入快捷键" data-default-hotkey="z"></div> </div> <div class="setting-item"> <span class="setting-label">折叠:</span> <div class="form-content"><input id="foldCategoryHotKey" placeholder="录入快捷键"></div> </div> <div class="setting-item"> <span class="setting-label">控制台:</span> <div class="form-content"><input id="clogHotKey" placeholder="录入快捷键"></div> </div> <div class="setting-item"> <span class="setting-label">悬浮图片保存:</span> <div class="form-content"><input id="saveHoverImageHotKey" placeholder="录入快捷键" data-default-hotkey="q"></div> </div> </div> <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 15px;"> <span style="font-size: 14px; font-weight: bold; padding:3px">显示已鉴定内容</span> <div class="setting-item"><span class="setting-label">屏蔽单番号:</span><div class="form-content"><input id="showFilterItemHotKey" placeholder="录入快捷键"></div></div> <div class="setting-item"><span class="setting-label">屏蔽演员:</span><div class="form-content"><input id="showFilterActorItemHotKey" placeholder="录入快捷键"></div></div> <div class="setting-item"><span class="setting-label">屏蔽关键词:</span><div class="form-content"><input id="showFilterKeywordItemHotKey" placeholder="录入快捷键"></div></div> <div class="setting-item"><span class="setting-label">收藏:</span><div class="form-content"><input id="showFavoriteItemHotKey" placeholder="录入快捷键"></div></div> <div class="setting-item"><span class="setting-label">已下载:</span><div class="form-content"><input id="showHasDownItemHotKey" placeholder="录入快捷键"></div></div> <div class="setting-item"><span class="setting-label">已观看:</span><div class="form-content"><input id="showHasWatchItemHotKey" placeholder="录入快捷键"></div></div> <div class="setting-item"><span class="setting-label">显示所有:</span><div class="form-content"><input id="showAllItemHotKey" placeholder="录入快捷键"></div></div> </div> </div> </div> <div class="jhs-setting-footer" style="flex-shrink: 0; padding: 15px 20px; display: flex; gap: 8px; justify-content: space-between; align-items: baseline; border-top: 1px solid #eee; background: white;"> <div>当前为Pro版,TG: <a href="https://t.me/guowennews" target="_blank">https://t.me/guowennews</a></div> <div style="display:flex;gap:8px;"> <button id="saveBtn">保存设置</button> <button id="clean-all" style="display: none">清理全部缓存</button> </div> </div> </div> </div> `; } /** Inject the Data Health sidebar item and panel HTML into the dialog. */ function injectHealthPanel() { const e = $(".side-menu-item").parent(); e.length && !e.find('[data-panel="health-panel"]').length && e.append('<div class="side-menu-item" data-panel="health-panel">数据体检</div>'); const t = $(".content-panel").parent(); t.length && !$("#health-panel").length && t.append(` <div id="health-panel" class="content-panel" style="display:none;"> <div style="display:flex; gap:8px; margin-bottom:12px;"> <a id="runHealthCheckBtn" class="menu-btn" style="background-color:#448cc2"><span>重新体检</span></a> <a id="repairHealthBtn" class="menu-btn" style="background-color:#64bb69"><span>备份并修复</span></a> </div> <div id="health-data-display" style="background:#f8f9fa; border:1px solid #ddd; border-radius:5px; padding:12px; min-height:180px;">点击重新体检查看结果</div> </div> `); } /** Inject the Plugin Management sidebar item and panel HTML into the dialog. */ function injectPluginMgmtPanel() { const e = $(".side-menu-item").parent(); e.length && !e.find('[data-panel="plugin-mgmt-panel"]').length && e.append('<div class="side-menu-item" data-panel="plugin-mgmt-panel">插件管理</div>'); const t = $(".content-panel").parent(); if (!t.length || $("#plugin-mgmt-panel").length) return; let i = '<div id="plugin-mgmt-panel" class="content-panel" style="display:none;">'; i += '<div style="display:flex;gap:8px;margin-bottom:15px;flex-wrap:wrap;">'; i += '<div style="flex:1;min-width:120px;background:#f0f7ff;border-radius:8px;padding:10px;text-align:center"><div id="pm-total" style="font-size:20px;font-weight:bold;color:#25b1dc">0</div><div style="font-size:12px;color:#888">总插件数</div></div>'; i += '<div style="flex:1;min-width:120px;background:#f0fff4;border-radius:8px;padding:10px;text-align:center"><div id="pm-enabled" style="font-size:20px;font-weight:bold;color:#7bc73b">0</div><div style="font-size:12px;color:#888">已启用</div></div>'; i += '<div style="flex:1;min-width:120px;background:#fff5f5;border-radius:8px;padding:10px;text-align:center"><div id="pm-disabled" style="font-size:20px;font-weight:bold;color:#de3333">0</div><div style="font-size:12px;color:#888">已禁用</div></div>'; i += '</div>'; i += '<p style="color:#666;font-size:0.85em;margin-bottom:10px;">禁用插件后需刷新页面生效。核心插件不可禁用。</p>'; i += '<div id="plugin-mgmt-list"></div>'; i += '<hr style="border:0;height:1px;margin:20px 0;background-image:linear-gradient(to right,rgba(0,0,0,0),rgba(159,137,137,0.75),rgba(0,0,0,0));"/>'; i += '<h3 style="font-size:15px;font-weight:bold;margin-bottom:10px;">插件执行耗时</h3>'; i += '<p style="color:#666;font-size:0.85em;margin-bottom:8px;">页面加载时各插件 handle() 的执行时间。</p>'; i += '<div id="plugin-timing-table"></div>'; i += '<hr style="border:0;height:1px;margin:20px 0;background-image:linear-gradient(to right,rgba(0,0,0,0),rgba(159,137,137,0.75),rgba(0,0,0,0));"/>'; i += '<h3 style="font-size:15px;font-weight:bold;margin-bottom:10px;">错误日志</h3>'; i += '<div style="display:flex;gap:8px;margin-bottom:8px;"><a id="pm-clear-log" class="menu-btn" style="background-color:#e74c3c"><span>清空日志</span></a></div>'; i += '<div id="plugin-error-log" style="max-height:250px;overflow:auto;background:#f8f9fa;border:1px solid #ddd;border-radius:5px;padding:10px;font-size:13px;">无错误记录</div>'; i += '<hr style="border:0;height:1px;margin:20px 0;background-image:linear-gradient(to right,rgba(0,0,0,0),rgba(159,137,137,0.75),rgba(0,0,0,0));"/>'; i += '<h3 style="font-size:15px;font-weight:bold;margin-bottom:10px;">缓存命中率</h3>'; i += '<div id="cache-hit-stats" style="background:#f8f9fa;border:1px solid #ddd;border-radius:5px;padding:10px;"></div>'; i += '</div>'; t.append(i); } /** Inject the Snapshot sidebar item and panel HTML into the dialog. */ function injectSnapshotPanel() { const e = $(".side-menu-item").parent(); e.length && !e.find('[data-panel="snapshot-panel"]').length && e.append('<div class="side-menu-item" data-panel="snapshot-panel">恢复点</div>'); const t = $(".content-panel").parent(); if (!t.length || $("#snapshot-panel").length) return; let n = '<div id="snapshot-panel" class="content-panel" style="display:none;">'; n += '<div style="display:flex;gap:8px;margin-bottom:15px;">'; n += '<a id="createSnapshotBtn" class="menu-btn" style="background-color:#64bb69"><span>创建快照</span></a>'; n += '</div>'; n += '<p style="color:#666;font-size:0.85em;margin-bottom:10px;">快照保存当前全部数据状态,可用于恢复。最多保留 10 个,超出自动清理最旧的。</p>'; n += '<div id="snapshot-list"></div>'; n += '</div>'; t.append(n); } /** Inject the Network/External Requests sidebar item and panel HTML into the dialog. */ function injectNetworkPanel() { const e = $(".side-menu-item").parent(); e.length && !e.find('[data-panel="network-panel"]').length && e.append('<div class="side-menu-item" data-panel="network-panel">外部请求</div><div class="side-menu-item" data-panel="hotkey-panel">快捷键配置</div>'); const t = $(".content-panel").parent(); if (!t.length || $("#network-panel").length) return; let n = '<div id="network-panel" class="content-panel" style="display:none;">'; n += '<h3 style="font-size:15px;font-weight:bold;margin-bottom:10px;">熔断器配置</h3>'; n += '<p style="color:#666;font-size:0.85em;margin-bottom:10px;">连续请求失败达到阈值后,自动停止对该站点的请求,避免拖慢整体体验。</p>'; n += '<div class="setting-item"><span class="setting-label">熔断阈值(次):</span><div class="form-content"><input type="number" id="circuitBreakerThreshold" min="2" max="10" style="width:100%;"></div></div>'; n += '<div class="setting-item"><span class="setting-label">冷却时间(秒):</span><div class="form-content"><input type="number" id="circuitBreakerCooldownSec" min="10" max="300" style="width:100%;"></div></div>'; n += '<div style="display:flex;gap:8px;margin:10px 0;"><a id="resetAllBreakersBtn" class="menu-btn" style="background-color:#e74c3c"><span>重置全部熔断</span></a></div>'; n += '<hr style="border:0;height:1px;margin:20px 0;background-image:linear-gradient(to right,rgba(0,0,0,0),rgba(159,137,137,0.75),rgba(0,0,0,0));"/>'; n += '<h3 style="font-size:15px;font-weight:bold;margin-bottom:10px;">站点健康状态</h3>'; n += '<p style="color:#666;font-size:0.85em;margin-bottom:10px;">各外部站点的熔断状态和请求统计。</p>'; n += '<div id="site-health-table"></div>'; n += '<hr style="border:0;height:1px;margin:20px 0;background-image:linear-gradient(to right,rgba(0,0,0,0),rgba(159,137,137,0.75),rgba(0,0,0,0));"/>'; n += '<h3 style="font-size:15px;font-weight:bold;margin-bottom:10px;">域名使用统计</h3>'; n += '<p style="color:#666;font-size:0.85em;margin-bottom:10px;">脚本实际请求过的域名及次数。</p>'; n += '<div style="display:flex;gap:8px;margin-bottom:8px;"><a id="clearDomainStatsBtn" class="menu-btn" style="background-color:#e74c3c"><span>清空统计</span></a></div>'; n += '<div id="domain-stats-table"></div>'; n += '</div>'; t.append(n); } /** Build the HTML for the hover dropdown quick-settings panel. */ function buildSimpleSettingHtml() { return ` <div class="jhs-qs-panel"> <div class="jhs-qs-header"> <span class="jhs-qs-title">快捷设置</span> <span class="jhs-qs-sub">改动即时生效</span> </div> <div class="jhs-qs-body jhs-scrollbar"> <div class="jhs-qs-section"> <div class="jhs-qs-section-title">浏览行为</div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>弹窗打开详情页</span></div><div class="jhs-qs-control"><input type="checkbox" id="dialogOpenDetail" class="mini-switch"></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>鉴定后关闭详情页</span></div><div class="jhs-qs-control"><input type="checkbox" id="needClosePage" class="mini-switch"></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>瀑布流模式</span></div><div class="jhs-qs-control"><input type="checkbox" id="autoPage" class="mini-switch"></div></div> </div> <div class="jhs-qs-section"> <div class="jhs-qs-section-title">增强功能</div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>标题翻译</span></div><div class="jhs-qs-control"><input type="checkbox" id="translateTitle" class="mini-switch"></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>悬浮大图</span></div><div class="jhs-qs-control"><input type="checkbox" id="hoverBigImg" class="mini-switch"></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>115视频匹配</span></div><div class="jhs-qs-control"><input type="checkbox" id="enable115Match" class="mini-switch"></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>加载女优信息</span></div><div class="jhs-qs-control"><input type="checkbox" id="enableLoadActressInfo" class="mini-switch"></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>第三方视频资源</span></div><div class="jhs-qs-control"><input type="checkbox" id="enableLoadOtherSite" class="mini-switch"></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>加载长缩略图</span></div><div class="jhs-qs-control"><input type="checkbox" id="enableLoadScreenShot" class="mini-switch"></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>更高画质预览视频</span></div><div class="jhs-qs-control"><input type="checkbox" id="enableLoadPreviewVideo" class="mini-switch"></div></div> </div> <div class="jhs-qs-section"> <div class="jhs-qs-section-title">页面布局</div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>竖图模式</span></div><div class="jhs-qs-control"><input type="checkbox" id="enableVerticalModel" class="mini-switch"></div></div> <div class="jhs-qs-row jhs-qs-row-stacked"><div class="jhs-qs-label"><span>列数</span></div><div class="jhs-qs-control"><div class="jhs-qs-range"><input type="range" id="containerColumns" min="2" max="10" step="1"><span id="showContainerColumns"></span></div></div></div> <div class="jhs-qs-row jhs-qs-row-stacked"><div class="jhs-qs-label"><span>宽度</span></div><div class="jhs-qs-control"><div class="jhs-qs-range"><input type="range" id="containerWidth" min="0" max="30" step="1"><span id="showContainerWidth"></span></div></div></div> <div class="jhs-qs-row"><div class="jhs-qs-label"><span>详情比例</span></div></div> <div class="jhs-qs-row jhs-qs-row-stacked"><div class="jhs-qs-label"><span>封面</span></div><div class="jhs-qs-control"><div class="jhs-qs-range"><input type="range" id="detailCoverRatio" min="50" max="200" step="5"><span id="showDetailCoverRatio"></span></div></div></div> <div class="jhs-qs-row jhs-qs-row-stacked"><div class="jhs-qs-label"><span>信息</span></div><div class="jhs-qs-control"><div class="jhs-qs-range"><input type="range" id="detailInfoRatio" min="50" max="200" step="5"><span id="showDetailInfoRatio"></span></div></div></div> </div> </div> <div class="jhs-qs-footer"> <button type="button" id="helpBtn" class="jhs-qs-btn jhs-qs-btn-ghost">常见问题</button> <button type="button" id="moreBtn" class="jhs-qs-btn jhs-qs-btn-primary">更多设置</button> </div> </div> `; } /** Render the network/external requests panel: circuit breaker status, domain stats. */ async function renderNetworkPanel() { const e = gmHttp.getCircuitBreakerStatus(), t = gmHttp.getDomainStats(), n = await storageManager.getSetting("circuitBreakerThreshold", 3), a = await storageManager.getSetting("circuitBreakerCooldown", 6e4); $("#circuitBreakerThreshold").val(n), $("#circuitBreakerCooldownSec").val(Math.round(a / 1e3)); const i = Object.entries(e); if (i.length) { let t = '<table style="width:100%;border-collapse:collapse;font-size:13px;">'; t += '<tr style="background:#f0f0f0;"><th style="text-align:left;padding:6px;border-bottom:1px solid #ddd;">域名</th><th style="text-align:center;padding:6px;border-bottom:1px solid #ddd;">状态</th><th style="text-align:center;padding:6px;border-bottom:1px solid #ddd;">失败次数</th><th style="text-align:center;padding:6px;border-bottom:1px solid #ddd;">操作</th></tr>'; for (const [n, a] of i) { const i = "open" === a.state ? "熔断" : "half-open" === a.state ? "半开" : "正常", s = "open" === a.state ? `剩余${Math.ceil((a.cooldownMs - (Date.now() - a.openTime)) / 1e3)}秒` : ""; t += `<tr style="border-bottom:1px solid #f0f0f0;"><td style="padding:4px 6px;">${escapeHtml(n)}</td><td style="text-align:center;padding:4px 6px;">${i} ${s}</td><td style="text-align:center;padding:4px 6px;">${Number(a.failCount) || 0}</td><td style="text-align:center;padding:4px 6px;"><a class="a-danger reset-breaker" data-domain="${escapeHtml(n)}">重置</a></td></tr>`; } t += '</table>', $("#site-health-table").html(t); } else $("#site-health-table").html('<p style="color:#888;font-size:13px;">暂无熔断记录</p>'); const s = Object.entries(t).sort(((e, t) => t[1].count - e[1].count)); if (s.length) { let e = '<table style="width:100%;border-collapse:collapse;font-size:13px;">'; e += '<tr style="background:#f0f0f0;"><th style="text-align:left;padding:6px;border-bottom:1px solid #ddd;">域名</th><th style="text-align:right;padding:6px;border-bottom:1px solid #ddd;">请求数</th><th style="text-align:right;padding:6px;border-bottom:1px solid #ddd;">错误数</th><th style="text-align:center;padding:6px;border-bottom:1px solid #ddd;">最后使用</th></tr>'; for (const [t, n] of s) { const a = n.lastUsed ? new Date(n.lastUsed).toLocaleTimeString() : "-"; e += `<tr style="border-bottom:1px solid #f0f0f0;"><td style="padding:4px 6px;">${escapeHtml(t)}</td><td style="text-align:right;padding:4px 6px;">${n.count}</td><td style="text-align:right;padding:4px 6px;color:${n.errors > 0 ? "#e74c3c" : "#333"};">${n.errors}</td><td style="text-align:center;padding:4px 6px;">${a}</td></tr>`; } e += '</table>', e += `<p style="color:#888;font-size:12px;margin-top:8px;">共 ${s.length} 个域名</p>`, $("#domain-stats-table").html(e); } else $("#domain-stats-table").html('<p style="color:#888;font-size:13px;">暂无统计数据</p>'); $(".reset-breaker").off("click").on("click", (e => { const t = $(e.target).data("domain"); gmHttp.resetCircuitBreaker(t), show.ok(`已重置 ${t} 的熔断状态`), renderNetworkPanel(); })), $("#resetAllBreakersBtn").off("click").on("click", (() => { gmHttp.resetAllCircuitBreakers(), show.ok("已重置全部熔断状态"), renderNetworkPanel(); })), $("#clearDomainStatsBtn").off("click").on("click", (() => { gmHttp.clearDomainStats(), show.ok("已清空域名统计"), renderNetworkPanel(); })); } /** Render the snapshot list as a Tabulator table with restore/download/delete actions. */ async function renderSnapshotPanel() { const e = await storageManager.getSnapshotList(), t = { "manual": "手动创建", "auto-import": "导入前自动", "auto-repair": "修复前自动", "auto-restore": "恢复前自动" }; if (0 === e.length) return void $("#snapshot-list").html('<div style="text-align:center;color:#999;padding:30px;">暂无快照,点击上方按钮创建</div>'); $("#snapshot-list").find(".tabulator").length && $("#snapshot-list").empty(); const n = new Tabulator("#snapshot-list", { layout: "fitColumns", placeholder: "暂无数据", data: e, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, columns: [ { title: "名称", field: "name", width: 200, headerSort: !1 }, { title: "来源", field: "source", width: 100, headerSort: !1, formatter: e => t[e.getValue()] || e.getValue() }, { title: "时间", field: "time", width: 170, headerSort: !1 }, { title: "数据量", field: "itemCount", width: 80, headerSort: !1 }, { title: "操作", minWidth: 220, headerSort: !1, formatter: (e, t, a) => { const i = e.getData(); return a((() => { const t = e.getElement().querySelector(".snap-restore"), a = e.getElement().querySelector(".snap-download"), s = e.getElement().querySelector(".snap-delete"); t && t.addEventListener("click", (async e => { utils.q(e, `恢复到快照「${escapeHtml(i.name)}」? 当前数据会自动备份。`, (async () => { let e = loading(); try { await storageManager.restoreSnapshot(i.id), show.ok("恢复成功, 页面将刷新"), setTimeout(() => location.reload(), 1e3); } catch (t) { console.error(t), show.error("恢复失败: " + t.message); } finally { e.close(); } })); })), a && a.addEventListener("click", (async e => { let t = loading(); try { const e = await storageManager.getSnapshot(i.id); if (!e) throw new Error("快照不存在"); utils.download(JSON.stringify(e.data), `snapshot_${escapeHtml(i.name)}.json`), show.ok("下载成功"); } catch (n) { show.error("下载失败: " + n.message); } finally { t.close(); } })), s && s.addEventListener("click", (async e => { utils.q(e, `删除快照「${escapeHtml(i.name)}」?`, (async () => { try { await storageManager.deleteSnapshot(i.id), show.ok("已删除"), renderSnapshotPanel(); } catch (t) { console.error(t), show.error("删除失败: " + t.message); } })); })); })), '<a class="a-success snap-restore">恢复</a> <a class="a-primary snap-download">下载</a> <a class="a-danger snap-delete">删除</a>'; } } ], locale: "zh-cn" }); } /** Show a data diff preview dialog before importing data. */ function showDiffPreview(e, t, n = null) { const a = e.summary, i = []; for (const [s, o] of Object.entries(e.stores)) { if ("unchanged" === o.status) continue; const e = { store: s, status: o.status, oldCount: o.oldCount, newCount: o.newCount, added: o.added.length, removed: o.removed.length, modified: o.modified.length }; i.push(e); } let s = '<div style="padding:15px;">'; s += '<div style="display:flex;gap:10px;margin-bottom:15px;flex-wrap:wrap;">'; s += `<div style="flex:1;min-width:100px;background:#f0fff4;border-radius:8px;padding:10px;text-align:center"><div style="font-size:20px;font-weight:bold;color:#7bc73b">${a.added}</div><div style="font-size:12px;color:#888">新增数据源</div></div>`; s += `<div style="flex:1;min-width:100px;background:#fff5f5;border-radius:8px;padding:10px;text-align:center"><div style="font-size:20px;font-weight:bold;color:#de3333">${a.removed}</div><div style="font-size:12px;color:#888">缺失数据源</div></div>`; s += `<div style="flex:1;min-width:100px;background:#fff8e1;border-radius:8px;padding:10px;text-align:center"><div style="font-size:20px;font-weight:bold;color:#f59e0b">${a.modified}</div><div style="font-size:12px;color:#888">有变更</div></div>`; s += `<div style="flex:1;min-width:100px;background:#f5f5f5;border-radius:8px;padding:10px;text-align:center"><div style="font-size:20px;font-weight:bold;color:#999">${a.unchanged}</div><div style="font-size:12px;color:#888">无变化</div></div>`; s += '</div>'; if (i.length > 0) { s += '<div style="max-height:350px;overflow:auto;border:1px solid #eee;border-radius:5px;">'; s += '<table style="width:100%;border-collapse:collapse;font-size:13px;">'; s += '<thead><tr style="background:#f5f5f5;"><th style="padding:8px;text-align:left;">数据源</th><th style="padding:8px;">状态</th><th style="padding:8px;">当前</th><th style="padding:8px;">导入</th><th style="padding:8px;">新增</th><th style="padding:8px;">删除</th><th style="padding:8px;">修改</th></tr></thead><tbody>'; const o = { added: "新增", removed: "缺失", modified: "变更", unchanged: "无变化" }; for (const r of i) s += `<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;">${escapeHtml(r.store)}</td><td style="padding:6px 8px;">${o[r.status] || r.status}</td><td style="padding:6px 8px;text-align:center;">${r.oldCount}</td><td style="padding:6px 8px;text-align:center;">${r.newCount}</td><td style="padding:6px 8px;text-align:center;color:#7bc73b;">${r.added || "-"}</td><td style="padding:6px 8px;text-align:center;color:#de3333;">${r.removed || "-"}</td><td style="padding:6px 8px;text-align:center;color:#f59e0b;">${r.modified || "-"}</td></tr>`; s += '</tbody></table></div>'; } else { s += '<div style="text-align:center;color:#999;padding:20px;">数据完全一致,无需导入</div>'; } s += '<div style="margin-top:12px;color:#e74c3c;font-size:12px;">导入将覆盖当前数据,建议先创建快照备份</div>'; s += '</div>'; const r = layer.open({ type: 1, title: "数据差异预览", content: s, area: utils.getResponsiveArea(["700px", "auto"]), btn: ["确认导入", "取消"], anim: -1, yes: async s => { layer.close(s); let o = loading(); try { await storageManager.createSnapshot("导入前自动备份", "auto-import"), n ? (await storageManager.importData(n), show.ok("导入成功!"), void setTimeout(() => location.reload(), 1e3)) : t && (await storageManager.importData(t), show.ok("导入成功!"), void setTimeout(() => location.reload(), 1e3)); } catch (r) { console.error(r), show.error("导入失败: " + r.message); } finally { o.close(); } } }); } /** Render the plugin management panel: categorized plugin list, timing, errors, cache stats. */ async function renderPluginMgmtPanel() { const disabled = JSON.parse(await storageManager.getSetting("disabledPlugins", "[]")); const allNames = unsafeWindow.pluginManager.getPluginNames(); const { categories, corePlugins, displayNames } = getPluginCategories(); const getDisplayName = (name) => (displayNames && displayNames[name]) ? displayNames[name] : name; const registeredSet = new Set(allNames); let html = ""; for (const [catKey, cat] of Object.entries(categories)) { const visiblePlugins = cat.plugins.filter(p => registeredSet.has(p)); if (!visiblePlugins.length) continue; html += `<div style="border:1px solid #eee;border-radius:8px;padding:10px;margin-bottom:10px;">`; html += `<div style="font-weight:bold;font-size:14px;margin-bottom:8px;">${escapeHtml(cat.label)}</div>`; for (const pName of visiblePlugins) { const isCore = corePlugins.includes(pName); const isDisabled = disabled.includes(pName); html += `<div style="display:flex;align-items:center;justify-content:space-between;padding:4px 0;">`; html += `<span style="font-size:13px;" title="${escapeHtml(pName)}">${escapeHtml(getDisplayName(pName))}${isCore ? ' <span style="color:#888;font-size:11px;">[核心]</span>' : ""}</span>`; if (isCore) { html += `<input type="checkbox" class="mini-switch" checked disabled>`; } else { html += `<input type="checkbox" class="mini-switch pm-toggle" data-plugin="${escapeHtml(pName)}" ${isDisabled ? "" : "checked"}>`; } html += `</div>`; } html += `</div>`; } $("#plugin-mgmt-list").html(html); const enabledCount = allNames.length - disabled.length; $("#pm-total").text(allNames.length); $("#pm-enabled").text(enabledCount); $("#pm-disabled").text(disabled.length); $(".pm-toggle").off("change").on("change", async (e) => { const name = $(e.target).data("plugin"); let list = JSON.parse(await storageManager.getSetting("disabledPlugins", "[]")); if ($(e.target).is(":checked")) { list = list.filter(x => x !== name); } else { if (!list.includes(name)) list.push(name); } await storageManager.saveSettingItem("disabledPlugins", JSON.stringify(list)); const all = unsafeWindow.pluginManager.getPluginNames(); $("#pm-total").text(all.length); $("#pm-enabled").text(all.length - list.length); $("#pm-disabled").text(list.length); show.ok(`插件 "${getDisplayName(name)}" 已${$(e.target).is(":checked") ? "启用" : "禁用"},刷新后生效`); }); const timings = unsafeWindow.pluginManager.getTimings(); if (timings.length) { const sorted = [...timings].sort((a, b) => b.elapsed - a.elapsed); let tHtml = '<table style="width:100%;border-collapse:collapse;font-size:13px;">'; tHtml += '<tr style="background:#f0f0f0;"><th style="text-align:left;padding:6px;border-bottom:1px solid #ddd;">插件</th><th style="text-align:right;padding:6px;border-bottom:1px solid #ddd;">耗时(ms)</th><th style="text-align:center;padding:6px;border-bottom:1px solid #ddd;">状态</th></tr>'; for (const t of sorted) { const color = t.status === "disabled" ? "#ccc" : t.elapsed > 500 ? "#e74c3c" : t.elapsed > 200 ? "#f39c12" : "#333"; const statusText = t.status === "disabled" ? "已禁用" : t.status === "error" ? "错误" : "正常"; tHtml += `<tr><td style="padding:4px 6px;border-bottom:1px solid #f0f0f0;color:${color}" title="${escapeHtml(t.name)}">${escapeHtml(getDisplayName(t.name))}</td><td style="text-align:right;padding:4px 6px;border-bottom:1px solid #f0f0f0;color:${color};font-weight:${t.elapsed > 500 ? "bold" : "normal"}">${t.elapsed.toFixed(1)}</td><td style="text-align:center;padding:4px 6px;border-bottom:1px solid #f0f0f0;">${statusText}</td></tr>`; } tHtml += '</table>'; $("#plugin-timing-table").html(tHtml); } else { $("#plugin-timing-table").html('<p style="color:#888;font-size:13px;">暂无数据,刷新页面后自动采集。</p>'); } const errorLog = unsafeWindow.pluginManager.getErrorLog(); if (errorLog.length) { let eHtml = '<table style="width:100%;border-collapse:collapse;font-size:12px;">'; eHtml += '<tr style="background:#f0f0f0;"><th style="text-align:left;padding:4px;">时间</th><th style="text-align:left;padding:4px;">插件</th><th style="text-align:left;padding:4px;">阶段</th><th style="text-align:left;padding:4px;">错误信息</th></tr>'; for (const err of [...errorLog].reverse()) { eHtml += `<tr><td style="padding:3px 4px;border-bottom:1px solid #f0f0f0;color:#888;white-space:nowrap;">${escapeHtml(err.time.substring(11, 19))}</td><td style="padding:3px 4px;border-bottom:1px solid #f0f0f0;" title="${escapeHtml(err.plugin)}">${escapeHtml(getDisplayName(err.plugin))}</td><td style="padding:3px 4px;border-bottom:1px solid #f0f0f0;">${escapeHtml(err.phase)}</td><td style="padding:3px 4px;border-bottom:1px solid #f0f0f0;color:#e74c3c;word-break:break-all;">${escapeHtml(err.message)}</td></tr>`; } eHtml += '</table>'; $("#plugin-error-log").html(eHtml); } else { $("#plugin-error-log").text("无错误记录"); } const cacheStats = storageManager.getCacheHitStats(); $("#cache-hit-stats").html(`<div style="display:flex;gap:15px;flex-wrap:wrap;"><span>命中: <strong style="color:#7bc73b">${cacheStats.hits}</strong></span><span>未命中: <strong style="color:#e74c3c">${cacheStats.misses}</strong></span><span>总计: <strong>${cacheStats.total}</strong></span><span>命中率: <strong style="color:#25b1dc">${cacheStats.rate}</strong></span></div>`); } /** Render the data health check panel with totals and issue breakdown. */ async function renderDataHealthPanel() { const e = $("#health-data-display"); if (!e.length) return; e.text("体检中..."); try { const t = await storageManager.inspectDataHealth(), n = t.fixable.reduce(((e, t) => e + t.count), 0), a = t.readonly.reduce(((e, t) => e + t.count), 0), i = t => t.length ? t.map((e => `<li><strong>${escapeHtml(e.message)}</strong>:${e.count}</li>`)).join("") : "<li>无</li>"; e.html(` <div style="display:grid; grid-template-columns: repeat(4, 1fr); gap:8px; margin-bottom:12px;"> <div>番号记录:<strong>${t.totals.carList}</strong></div> <div>收藏演员:<strong>${t.totals.favoriteActresses}</strong></div> <div>黑名单演员:<strong>${t.totals.blacklist}</strong></div> <div>黑名单作品:<strong>${t.totals.blacklistCarList}</strong></div> </div> <div style="margin-bottom:8px;">体检时间:${escapeHtml(t.checkedAt)};可修复问题 <strong>${n}</strong> 项,只读问题 <strong>${a}</strong> 项。</div> <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;"> <div><div style="font-weight:bold;margin-bottom:4px;">可安全修复</div><ul>${i(t.fixable)}</ul></div> <div><div style="font-weight:bold;margin-bottom:4px;">仅报告</div><ul>${i(t.readonly)}</ul></div> </div> `); } catch (t) { console.error(t), e.text("体检失败: " + t); } } /** Repair data health issues after auto-backing up current data. */ async function repairDataHealthWithBackup() { const e = JSON.stringify(await storageManager.exportData()), t = `health-backup-${utils.getNowStr("_", "_")}.json`; utils.download(e, t); const n = await storageManager.repairDataHealth(); show.ok(`已修复 ${n.fixedGroups} 组数据问题,修复前备份已下载`), await renderDataHealthPanel(); } /** Load all settings from storage into the main settings dialog form fields. */ /** 外部站点 隐藏/显示 状态管理 (统一控制封面快捷按钮 + 详情页第三方按钮) */ const JHS_SITE_BTN_MAP = { missAv: { detail: "missAvBtn", cover: "site-miss-av" }, jable: { detail: "jableBtn", cover: "site-jable" }, av123: { detail: "123AvBtn", cover: "site-123-av" }, javTrailer: { detail: "javTrailersBtn", cover: null }, supJav: { detail: "supJavBtn", cover: null }, fanza: { detail: "fanzaBtn", cover: null }, javDb: { detail: null, cover: null }, javBus: { detail: null, cover: null } }; const JHS_BTN_TO_SITEKEY = { missAvBtn: "missAv", jableBtn: "jable", "123AvBtn": "av123", javTrailersBtn: "javTrailer", supJavBtn: "supJav", javDbBtn: "javDb", javBusBtn: "javBus", fanzaBtn: "fanza" }; // 每个按钮对应的「自定义搜索地址」设置键(留空则回退到内置 searchPath 规则; fanza 无域名/搜索地址故不含) const SITE_SEARCH_KEY_MAP = { javTrailersBtn: "siteSearchJavTrailer", "123AvBtn": "siteSearchAv123", jableBtn: "siteSearchJable", missAvBtn: "siteSearchMissAv", supJavBtn: "siteSearchSupJav", javDbBtn: "siteSearchJavDb", javBusBtn: "siteSearchJavBus", fanzaBtn: "siteSearchFanza" }; // 每个按钮对应的「域名」设置键(用于拼接相对搜索路径时取原始域名, 避免 getBaseUrl 附加的额外后缀如 123Av 的 /ja) const SITE_DOMAIN_KEY_MAP = { javTrailersBtn: "javTrailersUrl", "123AvBtn": "av123Url", jableBtn: "jableUrl", missAvBtn: "missAvUrl", supJavBtn: "supJavUrl", javDbBtn: "javDbUrl", javBusBtn: "javBusUrl", fanzaBtn: "fanzaUrl" }; function jhsGetHiddenSites() { try { return JSON.parse(localStorage.getItem("jhs_hidden_sites") || "[]"); } catch (e) { return []; } } function jhsIsSiteHidden(key) { return jhsGetHiddenSites().includes(key); } function jhsSetSiteHidden(key, hidden) { let arr = jhsGetHiddenSites(); hidden ? (arr.includes(key) || arr.push(key)) : (arr = arr.filter(k => k !== key)); localStorage.setItem("jhs_hidden_sites", JSON.stringify(arr)); } // 封面按钮显示管理: 控制哪些站点出现在封面快捷按钮中(默认 jable, missAv, av123) const JHS_COVER_DEFAULTS = ["jable", "missAv", "av123"]; function jhsGetCoverSites() { try { const v = localStorage.getItem("jhs_cover_sites"); return v ? JSON.parse(v) : JHS_COVER_DEFAULTS.slice(); } catch (e) { return JHS_COVER_DEFAULTS.slice(); } } function jhsSetCoverSite(key, enabled) { let arr = jhsGetCoverSites(); enabled ? (arr.includes(key) || arr.push(key)) : (arr = arr.filter(k => k !== key)); localStorage.setItem("jhs_cover_sites", JSON.stringify(arr)); } // 封面按钮的 CSS class 后缀映射 const COVER_SITE_CLASS = { missAv: "site-miss-av", jable: "site-jable", av123: "site-123-av", javTrailer: "site-javtrailer", supJav: "site-supjav", javDb: "site-javdb", javBus: "site-javbus", fanza: "site-fanza" }; // 封面按钮对应的搜索地址设置键(与 SITE_SEARCH_KEY_MAP 同键名) const COVER_SEARCH_KEY = { missAv: "siteSearchMissAv", jable: "siteSearchJable", av123: "siteSearchAv123", javTrailer: "siteSearchJavTrailer", supJav: "siteSearchSupJav", javDb: "siteSearchJavDb", javBus: "siteSearchJavBus", fanza: "siteSearchFanza" }; const COVER_DOMAIN_KEY = { missAv: "missAvUrl", jable: "jableUrl", av123: "av123Url", javTrailer: "javTrailersUrl", supJav: "supJavUrl", javDb: "javDbUrl", javBus: "javBusUrl", fanza: "fanzaUrl" }; const COVER_DEFAULT_SEARCH = { missAv: "/search/", jable: "/search/", av123: "/ja/search?keyword=", javTrailer: "/search/", supJav: "/?s=", javDb: "/search?q=", javBus: "/", fanza: "/search/=/searchstr=" }; const COVER_DEFAULT_DOMAIN = { missAv: "https://missav.live", jable: "https://jable.tv", av123: "https://123av.com", javTrailer: "https://javtrailers.com", supJav: "https://supjav.com", javDb: "https://javdb.com", javBus: "https://www.javbus.com", fanza: "https://www.dmm.co.jp" }; const COVER_ALL_KEYS = ["missAv", "jable", "av123", "javTrailer", "supJav", "javDb", "javBus", "fanza"]; const COVER_DEFAULT_NAME = { missAv: "MissAv", jable: "Jable", av123: "123Av", javTrailer: "JavTrailer", supJav: "SupJav", javDb: "JavDb", javBus: "JavBus", fanza: "Fanza" }; const COVER_NAME_KEY = { missAv: "siteNameMissAv", jable: "siteNameJable", av123: "siteNameAv123", javTrailer: "siteNameJavTrailer", supJav: "siteNameSupJav", javDb: "siteNameJavDb", javBus: "siteNameJavBus", fanza: "siteNameFanza" }; /** 依据设置计算某站点的封面按钮跳转 URL(与番号页按钮同一套自定义搜索地址逻辑) */ function jhsBuildCoverUrl(siteKey, carNum, setting) { const search = String((setting && setting[COVER_SEARCH_KEY[siteKey]]) || COVER_DEFAULT_SEARCH[siteKey] || "/").trim(); if (/^https?:\/\//i.test(search)) return `${search}${carNum}`; let base = String((setting && setting[COVER_DOMAIN_KEY[siteKey]]) || "").trim(); if (!base) base = COVER_DEFAULT_DOMAIN[siteKey] || ""; base = base.replace(/\/+$/, ""); const sep = search.startsWith("/") ? "" : "/"; return `${base}${sep}${search}${carNum}`; } async function loadSettingForm(getBean) { let e = await storageManager.getSetting(); $("#videoQuality").val(e.videoQuality), $("#reviewCount").val(e.reviewCount || 20), $("#tagPosition").val(e.tagPosition || "rightTop"), $("#waitCheckCount").val(e.waitCheckCount || 5), $("#movieShowType").val(e.movieShowType || "normal"), $("#enableQuickFilter").prop("checked", !e.enableQuickFilter || e.enableQuickFilter === _), $("#showWaitCheckBtn").prop("checked", !e.showWaitCheckBtn || e.showWaitCheckBtn === _), $("#showWaitDownBtn").prop("checked", !e.showWaitDownBtn || e.showWaitDownBtn === _), $("#randomOpenWaitDown").prop("checked", !!e.randomOpenWaitDown && e.randomOpenWaitDown === _), // 鉴定配置面板 - 列表显示(镜像自快捷设置, 两处 ID 用 2 后缀区分) $("#showAllItem2").prop("checked", !!e.showAllItem && e.showAllItem === _), $("#showFilterItem2").prop("checked", !!e.showFilterItem && e.showFilterItem === _), $("#showFilterActorItem2").prop("checked", !!e.showFilterActorItem && e.showFilterActorItem === _), $("#showFilterKeywordItem2").prop("checked", !!e.showFilterKeywordItem && e.showFilterKeywordItem === _), $("#showFavoriteItem2").prop("checked", !e.showFavoriteItem || e.showFavoriteItem === _), $("#showHasDownItem2").prop("checked", !e.showHasDownItem || e.showHasDownItem === _), $("#showHasWatchItem2").prop("checked", !e.showHasWatchItem || e.showHasWatchItem === _), // 双向即时保存 - 主开关联动禁用状态 (() => { const $subs = $("#showFilterItem2, #showFilterActorItem2, #showFilterKeywordItem2, #showFavoriteItem2, #showHasDownItem2, #showHasWatchItem2"); const applyDisable = () => { const on = $("#showAllItem2").is(":checked"); $subs.prop("disabled", on); on ? $subs.attr("data-tip", "请先关闭显示所有才可点击") : $subs.removeAttr("data-tip"); }; applyDisable(); $("#showAllItem2").off("change.appraisal").on("change.appraisal", (async () => { await storageManager.saveSettingItem("showAllItem", $("#showAllItem2").is(":checked") ? _ : C); applyDisable(); window.refresh(); })); const bind = (id, key) => $(id).off("change.appraisal").on("change.appraisal", (async () => { await storageManager.saveSettingItem(key, $(id).is(":checked") ? _ : C); window.refresh(); })); bind("#showFilterItem2", "showFilterItem"); bind("#showFilterActorItem2", "showFilterActorItem"); bind("#showFilterKeywordItem2", "showFilterKeywordItem"); bind("#showFavoriteItem2", "showFavoriteItem"); bind("#showHasDownItem2", "showHasDownItem"); bind("#showHasWatchItem2", "showHasWatchItem"); })(); $("#checkConcurrencyCount").val(e.checkConcurrencyCount || 2), $("#checkRequestSleep").val(e.checkRequestSleep || 100), $("#enableCheckBlacklist").val(e.enableCheckBlacklist || _), $("#checkBlacklist_intervalTime").val(e.checkBlacklist_intervalTime || 12), $("#checkBlacklist_ruleTime").val(e.checkBlacklist_ruleTime || 8760), $("#enableCheckFavoriteActress").val(e.enableCheckFavoriteActress || _), $("#checkFavoriteActress_IntervalTime").val(e.checkFavoriteActress_IntervalTime || 24), $("#enableCheckNewVideo").val(e.enableCheckNewVideo || _), $("#checkNewVideo_intervalTime").val(e.checkNewVideo_intervalTime || 12), $("#checkNewVideo_ruleTime").val(e.checkNewVideo_ruleTime || 8760); const t = e.highlightedTagNumber || 1, n = e.highlightedTagColor || "#ce2222"; $("#highlightedTagNumber").val(e.highlightedTagNumber || 1), $("#highlightedTagColor").val(e.highlightedTagColor || "#ce2222"), $("#highlightedTagLabel").css("border", `${t}px solid ${n}`), $("#enableClog").val(e.enableClog || _), $("#clogMsgCount").val(e.clogMsgCount || 2e3), $("#mobileMode").val(e.mobileMode || "auto"), $("#httpTimeout").val(e.httpTimeout || 5e3), $("#httpRetryCount").val(e.httpRetryCount || 3), $("#webDavUrl").val(e.webDavUrl || ""), $("#webDavUsername").val(e.webDavUsername || ""), $("#webDavPassword").val(await decryptCredential(e.webDavPassword) || ""), $("#backupPassword").val(await decryptCredential(e.backupPassword) || ""), (() => { const cfg = e.autoBackupConfig || DEFAULT_AUTO_BACKUP_CONFIG; $("#enableAutoBackup").prop("checked", !!cfg.enabled), $("#autoBackupFrequency").val(cfg.frequency || "daily"); $("#credentialIdDisplay").val(getCredentialId()); })(), $("#enableTitleSelectFilter").prop("checked", !e.enableTitleSelectFilter || e.enableTitleSelectFilter === _), $("#enableFavoriteActresses").prop("checked", !e.enableFavoriteActresses || e.enableFavoriteActresses === _), $("#enableSaveActressCarInfo").prop("checked", !!e.enableSaveActressCarInfo && e.enableSaveActressCarInfo === _), $("#enableScreenSvg").prop("checked", !e.enableScreenSvg || e.enableScreenSvg === _), $("#enableVideoSvg").prop("checked", !e.enableVideoSvg || e.enableVideoSvg === _), $("#enableHandleSvg").prop("checked", !e.enableHandleSvg || e.enableHandleSvg === _), $("#enableSiteSvg").prop("checked", !e.enableSiteSvg || e.enableSiteSvg === _), $("#enableCopySvg").prop("checked", !e.enableCopySvg || e.enableCopySvg === _); const a = getBean("OtherSitePlugin"), i = await a.getMissAvUrl(), s = await a.getjableUrl(), r = await a.getJavTrailersUrl(), l = await a.getAv123Url(), c = await a.getJavDbUrl(), d = await a.getJavBusUrl(), h = await a.getSupJavUrl(); $("#missAvUrl").val(i), $("#jableUrl").val(s), $("#javTrailersUrl").val(r), $("#av123Url").val(l), $("#javDbUrl").val(c), $("#javBusUrl").val(d), $("#supJavUrl").val(h); // 外部网站显示名称回填 const setting = await storageManager.getSetting(); $("#siteNameMissAv").val(setting.siteNameMissAv || "MissAv"); $("#siteNameJable").val(setting.siteNameJable || "Jable"); $("#siteNameJavTrailer").val(setting.siteNameJavTrailer || "JavTrailer"); $("#siteNameAv123").val(setting.siteNameAv123 || "123Av"); $("#siteNameJavDb").val(setting.siteNameJavDb || "JavDb"); $("#siteNameJavBus").val(setting.siteNameJavBus || "JavBus"); $("#siteNameSupJav").val(setting.siteNameSupJav || "SupJav"); $("#siteNameFanza").val(setting.siteNameFanza || "Fanza"); $("#fanzaUrl").val(setting.fanzaUrl || "https://www.dmm.co.jp"); // 外部网站「搜索地址」回填(留空则使用内置默认 searchPath; // 为方便用户修改, 若用户未自定义则展示内置默认前缀作为当前值) $("#siteSearchMissAv").val(setting.siteSearchMissAv || "/search/"); $("#siteSearchJable").val(setting.siteSearchJable || "/search/"); $("#siteSearchJavTrailer").val(setting.siteSearchJavTrailer || "/search/"); $("#siteSearchAv123").val(setting.siteSearchAv123 || "/ja/search?keyword="); $("#siteSearchJavDb").val(setting.siteSearchJavDb || "/search?q="); $("#siteSearchJavBus").val(setting.siteSearchJavBus || "/"); $("#siteSearchSupJav").val(setting.siteSearchSupJav || "/?s="); $("#siteSearchFanza").val(setting.siteSearchFanza || "/search/=/searchstr="); // 隐藏/显示 开关(mini-switch 样式 checkbox): 初始化 + 绑定 change (即时生效) // 勾选=显示, 取消勾选=隐藏 (() => { const hidden = jhsGetHiddenSites(); $("#domain-panel .jhs-site-toggle").each((function() { const key = this.dataset.siteKey; this.checked = !hidden.includes(key); })); $("#domain-panel").off("change.jhsSiteToggle").on("change.jhsSiteToggle", ".jhs-site-toggle", (function() { const key = this.dataset.siteKey; const nowHidden = !this.checked; jhsSetSiteHidden(key, nowHidden); // 立即同步当前页已渲染的对应按钮 const map = JHS_SITE_BTN_MAP[key]; if (map) { if (map.detail) $(`#${map.detail}`).toggle(!nowHidden); if (map.cover) $(`.${map.cover}`).toggle(!nowHidden); } })); // 名称和域名即时保存(不需要点保存按钮) $("#domain-panel input[id^='siteName']").off("change.jhsSiteName").on("change.jhsSiteName", (async function() { const id = this.id; // e.g. siteNameMissAv await storageManager.saveSettingItem(id, ($(this).val() || "").trim()); })); $("#domain-panel input[id$='Url']").off("change.jhsSiteUrl").on("change.jhsSiteUrl", (async function() { const id = this.id; // e.g. missAvUrl await storageManager.saveSettingItem(id, ($(this).val() || "").trim().replace(/\/$/, "")); })); // 搜索地址即时保存 $("#domain-panel input[id^='siteSearch']").off("change.jhsSiteSearch").on("change.jhsSiteSearch", (async function() { const id = this.id; // e.g. siteSearchJavDb await storageManager.saveSettingItem(id, ($(this).val() || "").trim()); })); // 「封面显示」开关: 控制封面快捷按钮是否展示该站点 const coverSites = jhsGetCoverSites(); $("#domain-panel .jhs-cover-toggle").each((function() { const key = this.dataset.siteKey; this.checked = coverSites.includes(key); })); $("#domain-panel").off("change.jhsCoverToggle").on("change.jhsCoverToggle", ".jhs-cover-toggle", (function() { const key = this.dataset.siteKey; jhsSetCoverSite(key, this.checked); // 立即同步当前页已渲染的封面按钮 const cls = COVER_SITE_CLASS[key]; if (cls) $(`.${cls}`).toggle(this.checked); })); })(); let g = await storageManager.getReviewFilterKeywordList(), p = await storageManager.getTitleFilterKeyword(); g && g.forEach((e => { addLabelTag("#reviewKeywordContainer", e); })), p && p.forEach((e => { addLabelTag("#filterKeywordContainer", e); })), [ "#reviewKeywordContainer", "#filterKeywordContainer" ].forEach((e => { $(`${e} .add-tag-btn`).on("click", (t => addKeyword(t, e))), $(`${e} .keyword-input`).on("keypress", (t => { "Enter" === t.key && addKeyword(t, e); })); })); // 快捷键面板初始化 initHotkeyPanel(e); } /** 快捷键面板: 加载已保存的值, 绑定 keydown 录入 */ function initHotkeyPanel(settingObj) { $("#hotkey-panel input[id]").each(((idx, el) => { const $el = $(el), id = el.id; if ("enableImageHotKey" === id) { $el.prop("checked", !!settingObj.enableImageHotKey && settingObj.enableImageHotKey === _); return; } const defaultVal = void 0 !== settingObj[id] ? settingObj[id] : $el.attr("data-default-hotkey") || ""; $el.val(defaultVal).on("input", (event => { let value = $(event.target).val(); if (/[\u4e00-\u9fa5]/.test(value) || /^Shift[a-zA-Z0-9]+$/.test(value)) { $(event.target).val(""); show.error("非法输入:不能输入中文或输入法转换错误"); } })).on("keydown", (event => { event.preventDefault(); const hotkey = parseHotkeyInput(event); if ("" === hotkey) { $el.val(""); return; } if (isDuplicateHotkey(hotkey, id)) { show.error("该快捷键已被其他功能使用!"); return; } $el.val(hotkey); })); })); } function parseHotkeyInput(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); [ "Control", "Shift", "Alt", "Meta" ].includes(event.key) || keys.push(key); return keys.length > 0 ? keys.join("+") : ""; } function isDuplicateHotkey(hotkey, currentInputId) { let dup = !1; $("#hotkey-panel input[id]").each(((i, el) => { if (el.id !== currentInputId && hotkey && hotkey === $(el).val()) { dup = !0; return !1; } })); return dup; } /** Initialize the hover quick-settings form with current values and bind event handlers. */ async function initSimpleSettingForm(getBean, getSelector, openSettingDialogFn) { let e = await storageManager.getSetting(); $("#containerColumns").val(e.containerColumns || 5), $("#showContainerColumns").text(e.containerColumns || 5), $("#containerWidth").val((e.containerWidth || 100) - 70), $("#showContainerWidth").text((e.containerWidth || 100) + "%"), // 详情比例: 封面 + 信息 (() => { const coverVal = e.detailCoverRatio || 135, infoVal = e.detailInfoRatio || 105; $("#detailCoverRatio").val(coverVal), $("#showDetailCoverRatio").text((coverVal / 100).toFixed(2).replace(/\.?0+$/, "")); $("#detailInfoRatio").val(infoVal), $("#showDetailInfoRatio").text((infoVal / 100).toFixed(2).replace(/\.?0+$/, "")); $("#detailCoverRatio").on("input", (async () => { const v = parseInt($("#detailCoverRatio").val()) || 135; $("#showDetailCoverRatio").text((v / 100).toFixed(2).replace(/\.?0+$/, "")); await storageManager.saveSettingItem("detailCoverRatio", v); applyDetailFlex(); })); $("#detailInfoRatio").on("input", (async () => { const v = parseInt($("#detailInfoRatio").val()) || 105; $("#showDetailInfoRatio").text((v / 100).toFixed(2).replace(/\.?0+$/, "")); await storageManager.saveSettingItem("detailInfoRatio", v); applyDetailFlex(); })); })(), $("#needClosePage").prop("checked", !e.needClosePage || e.needClosePage === _), $("#autoPage").prop("checked", !e.autoPage || e.autoPage === _), $("#translateTitle").prop("checked", !e.translateTitle || e.translateTitle === _), $("#dialogOpenDetail").prop("checked", !e.dialogOpenDetail || e.dialogOpenDetail === _), $("#dialogOpenDetail").on("change", (async t => { const n = $("#dialogOpenDetail").is(":checked") ? _ : C; await storageManager.saveSettingItem("dialogOpenDetail", n); })), $("#enableLoadActressInfo").prop("checked", !e.enableLoadActressInfo || e.enableLoadActressInfo === _), $("#enableLoadOtherSite").prop("checked", !e.enableLoadOtherSite || e.enableLoadOtherSite === _), $("#containerColumns").on("input", (async t => { let n = $("#containerColumns").val(); if ($("#showContainerColumns").text(n), r) { document.querySelector(".movie-list").style.gridTemplateColumns = `repeat(${n}, minmax(0, 1fr))`; } if (l) { document.querySelector(".masonry").style.gridTemplateColumns = `repeat(${n}, minmax(0, 1fr))`; } await storageManager.saveSettingItem("containerColumns", n), applyImageMode(); })), $("#containerWidth").on("input", (async t => { let n = parseInt($(t.target).val()); const a = n + 70 + "%"; if ($("#showContainerWidth").text(a), r) { document.querySelector("section .container").style.minWidth = a; } if (l) { document.querySelector(".container-fluid .row").style.minWidth = a; } storageManager.saveSettingItem("containerWidth", n + 70); })), $("#needClosePage").on("change", (async t => { await storageManager.saveSettingItem("needClosePage", $("#needClosePage").is(":checked") ? _ : C), window.refresh(); })), $("#autoPage").on("change", (async t => { const n = $("#autoPage").is(":checked") ? _ : C; await storageManager.saveSettingItem("autoPage", n), n === _ ? $("#sort-toggle-btn").hide() : $("#sort-toggle-btn").show(); })), $("#translateTitle").on("change", (async t => { const n = $("#translateTitle").is(":checked") ? _ : C; await storageManager.saveSettingItem("translateTitle", n), n === _ ? (await getBean("ListPagePlugin").doFilter(), isDetailPage && await getBean("TranslatePlugin").translate()) : (await getBean("ListPagePlugin").revertTranslation(), $(".translated-title").remove()); })), $("#hoverBigImg").prop("checked", !!e.hoverBigImg && e.hoverBigImg === _), $("#hoverBigImg").on("change", (async t => { const n = $("#hoverBigImg").is(":checked") ? _ : C; await storageManager.saveSettingItem("hoverBigImg", n), n === _ ? window.imageHoverPreviewObj ? window.imageHoverPreviewObj.bindEvents() : window.imageHoverPreviewObj = new ImageHoverPreview({ selector: getSelector().coverImgSelector }) : window.imageHoverPreviewObj && (window.imageHoverPreviewObj.destroy(), window.imageHoverPreviewObj = null); })), $("#enableLoadActressInfo").on("change", (async t => { const n = $("#enableLoadActressInfo").is(":checked") ? _ : C; await storageManager.saveSettingItem("enableLoadActressInfo", n), n === _ ? getBean("ActressInfoPlugin").loadActressInfo() : $(".actress-info").remove(); })), $("#enableLoadOtherSite").on("change", (async t => { const n = $("#enableLoadOtherSite").is(":checked") ? _ : C; await storageManager.saveSettingItem("enableLoadOtherSite", n), n === _ ? getBean("OtherSitePlugin").loadOtherSite().then() : $("#otherSiteBox").remove(); })), $("#enableLoadScreenShot").prop("checked", !e.enableLoadScreenShot || e.enableLoadScreenShot === _), $("#enableLoadScreenShot").on("change", (async t => { const n = $("#enableLoadScreenShot").is(":checked") ? _ : C; await storageManager.saveSettingItem("enableLoadScreenShot", n), n === _ ? getBean("ScreenShotPlugin").loadScreenShot().then() : $(".screen-container").remove(); })), $("#enableLoadPreviewVideo").prop("checked", !e.enableLoadPreviewVideo || e.enableLoadPreviewVideo === _), $("#enableLoadPreviewVideo").on("change", (async t => { const n = $("#enableLoadPreviewVideo").is(":checked") ? _ : C; await storageManager.saveSettingItem("enableLoadPreviewVideo", n); })), $("#enable115Match").prop("checked", !!e.enable115Match && e.enable115Match === _), $("#enable115Match").on("change", (async t => { const n = $("#enable115Match").is(":checked") ? _ : C; await storageManager.saveSettingItem("enable115Match", n); let a = $(getSelector().itemSelector).toArray(); await getBean("WangPan115MatchPlugin").matchMovieList(a); })), $("#enableVerticalModel").prop("checked", !!e.enableVerticalModel && e.enableVerticalModel === _), $("#enableVerticalModel").on("change", (async t => { const n = $("#enableVerticalModel").is(":checked") ? _ : C; await storageManager.saveSettingItem("enableVerticalModel", n), applyImageMode(); })), $("#moreBtn").on("click", (() => { $(".simple-setting").html("").hide(), openSettingDialogFn("base-panel"); })), $("#helpBtn").on("click", (() => { layer.open({ type: 1, title: "", shadeClose: !0, scrollbar: !1, content: '\n<style>\n .help-container {\n font-family: \'Segoe UI\', Tahoma, Geneva, Verdana, sans-serif;\n color: #333;\n padding: 15px;\n max-height: 100%;\n overflow-y: auto;\n }\n \n .help-section {\n margin-bottom: 25px;\n }\n \n .help-section summary {\n font-size: 18px;\n color: #3498db;\n margin-bottom: 12px;\n cursor: pointer;\n }\n \n .help-content {\n background-color: #f9f9f9;\n border-radius: 5px;\n padding: 15px;\n border-left: 4px solid #3498db;\n }\n \n .help-content p {\n line-height: 1.6;\n margin-bottom: 10px;\n }\n .help-section img {\n max-width: 100%;\n height: auto;\n border: 1px solid #ddd;\n border-radius: 4px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n\n</style>\n\n<div class="help-container">\n <h1 style="font-size: 22px; margin-bottom: 20px; color: #2c3e50; border-bottom: 1px solid #eee; padding-bottom: 10px;">使用说明</h1>\n \n <details class="help-section">\n <summary>1. 无法查看预览视频,提示分流?</summary>\n <div class="help-content">\n <p>JavDB限制日本IP的访问,而预览视频来自DMM,需要日本IP才能访问。</p>\n <p>这样会导致二者无法同时使用,需要对其一进行代理转发。</p>\n <p>将 cc3001.dmm.co.jp 及 dmm.co 分流到日本ip。</p>\n <p><a href="https://youtu.be/wQUK8z_YeU4?t=121" target="_blank">Clash Verge分流规则设置 </a> (如果你是别的代理软件,自行搜索如何分流)</p>\n </div>\n </details>\n \n <details class="help-section">\n <summary>2. 如何屏蔽某一系列的番号?</summary>\n <div class="help-content">\n <p>方法一:设置中-添加视频标题关键词,如: VENX-</p>\n <p>方法二:进入详情页,选中标题文字,右键可加入</p>\n <img src="https://i.imgur.com/lVnhK5A.png" alt="进入详情页,选中标题,进行右键"/>\n </div>\n </details>\n\n <details class="help-section">\n <summary>3. 屏蔽某演员,如何只屏蔽单体影片?</summary>\n <div class="help-content">\n <p>屏蔽演员前,先筛选分类,再点屏蔽</p>\n <img src="https://imgur.com/Ue7eCAi.png" alt="屏蔽演员前,先筛选分类,再点屏蔽"/>\n </div>\n </details>\n \n \n</div>\n', area: utils.getResponsiveArea([ "50%", "90%" ]) }); })); } /** Read all form values and save to storage. */ async function saveSettingForm(getBean) { let e = await storageManager.getSetting(); e.videoQuality = $("#videoQuality").val(), e.reviewCount = $("#reviewCount").val(), e.tagPosition = $("#tagPosition").val(), e.waitCheckCount = $("#waitCheckCount").val(), e.highlightedTagNumber = $("#highlightedTagNumber").val(), e.movieShowType = $("#movieShowType").val(), e.enableQuickFilter = $("#enableQuickFilter").is(":checked") ? _ : C, e.showWaitCheckBtn = $("#showWaitCheckBtn").is(":checked") ? _ : C, e.showWaitDownBtn = $("#showWaitDownBtn").is(":checked") ? _ : C, e.randomOpenWaitDown = $("#randomOpenWaitDown").is(":checked") ? _ : C, e.highlightedTagColor = $("#highlightedTagColor").val(), e.checkConcurrencyCount = $("#checkConcurrencyCount").val(), e.checkRequestSleep = $("#checkRequestSleep").val(), e.enableCheckBlacklist = $("#enableCheckBlacklist").val(), e.checkBlacklist_intervalTime = $("#checkBlacklist_intervalTime").val(), e.checkBlacklist_ruleTime = $("#checkBlacklist_ruleTime").val(), e.enableCheckFavoriteActress = $("#enableCheckFavoriteActress").val(), e.checkFavoriteActress_IntervalTime = $("#checkFavoriteActress_IntervalTime").val(), e.enableCheckNewVideo = $("#enableCheckNewVideo").val(), e.checkNewVideo_intervalTime = $("#checkNewVideo_intervalTime").val(), e.checkNewVideo_ruleTime = $("#checkNewVideo_ruleTime").val(), e.httpTimeout = Number($("#httpTimeout").val()) || 5e3, e.httpRetryCount = Number($("#httpRetryCount").val()) || 3, e.circuitBreakerThreshold = Number($("#circuitBreakerThreshold").val()) || 3, e.circuitBreakerCooldown = Number($("#circuitBreakerCooldownSec").val()) * 1e3, e.enableClog = $("#enableClog").val(), e.enableClog === _ ? clog.show() : clog.hide(), e.clogMsgCount = $("#clogMsgCount").val(), e.mobileMode = $("#mobileMode").val(), e.webDavUrl = $("#webDavUrl").val(), e.webDavUsername = $("#webDavUsername").val(), e.webDavPassword = await encryptCredential($("#webDavPassword").val()), e.backupPassword = await encryptCredential($("#backupPassword").val()), e.autoBackupConfig = { enabled: $("#enableAutoBackup").is(":checked"), frequency: $("#autoBackupFrequency").val() }, e.missAvUrl = $("#missAvUrl").val().replace(/\/$/, ""), e.jableUrl = $("#jableUrl").val().replace(/\/$/, ""), e.javTrailersUrl = $("#javTrailersUrl").val().replace(/\/$/, ""), e.av123Url = $("#av123Url").val().replace(/\/$/, ""), e.javDbUrl = $("#javDbUrl").val().replace(/\/$/, ""), e.javBusUrl = $("#javBusUrl").val().replace(/\/$/, ""), e.supJavUrl = $("#supJavUrl").val().replace(/\/$/, ""), e.siteNameMissAv = ($("#siteNameMissAv").val() || "MissAv").trim(), e.siteNameJable = ($("#siteNameJable").val() || "Jable").trim(), e.siteNameJavTrailer = ($("#siteNameJavTrailer").val() || "JavTrailer").trim(), e.siteNameAv123 = ($("#siteNameAv123").val() || "123Av").trim(), e.siteNameJavDb = ($("#siteNameJavDb").val() || "JavDb").trim(), e.siteNameJavBus = ($("#siteNameJavBus").val() || "JavBus").trim(), e.siteNameSupJav = ($("#siteNameSupJav").val() || "SupJav").trim(), e.siteNameFanza = ($("#siteNameFanza").val() || "Fanza").trim(), e.fanzaUrl = ($("#fanzaUrl").val() || "").trim().replace(/\/$/, ""), e.siteSearchMissAv = ($("#siteSearchMissAv").val() || "").trim(), e.siteSearchJable = ($("#siteSearchJable").val() || "").trim(), e.siteSearchJavTrailer = ($("#siteSearchJavTrailer").val() || "").trim(), e.siteSearchAv123 = ($("#siteSearchAv123").val() || "").trim(), e.siteSearchJavDb = ($("#siteSearchJavDb").val() || "").trim(), e.siteSearchJavBus = ($("#siteSearchJavBus").val() || "").trim(), e.siteSearchSupJav = ($("#siteSearchSupJav").val() || "").trim(), e.siteSearchFanza = ($("#siteSearchFanza").val() || "").trim(), e.enableTitleSelectFilter = $("#enableTitleSelectFilter").is(":checked") ? _ : C, e.enableFavoriteActresses = $("#enableFavoriteActresses").is(":checked") ? _ : C, e.enableSaveActressCarInfo = $("#enableSaveActressCarInfo").is(":checked") ? _ : C, e.enableScreenSvg = $("#enableScreenSvg").is(":checked") ? _ : C, e.enableVideoSvg = $("#enableVideoSvg").is(":checked") ? _ : C, e.enableHandleSvg = $("#enableHandleSvg").is(":checked") ? _ : C, e.enableSiteSvg = $("#enableSiteSvg").is(":checked") ? _ : C, e.enableCopySvg = $("#enableCopySvg").is(":checked") ? _ : C; // 保存快捷键配置 $("#hotkey-panel input[id]").each(((idx, el) => { if ("enableImageHotKey" === el.id) { e.enableImageHotKey = $(el).is(":checked") ? _ : C; } else { e[el.id] = $(el).val(); } })); await storageManager.saveSetting(e); let t = []; $("#reviewKeywordContainer .keyword-label").toArray().forEach((e => { let n = $(e).text().replace("×", "").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim(); t.push(n); })), await storageManager.saveReviewFilterKeyword(t); let n = []; $("#filterKeywordContainer .keyword-label").toArray().forEach((e => { let t = $(e).text().replace("×", "").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim(); n.push(t); })), await storageManager.saveTitleFilterKeyword(n), show.ok("保存成功"), window.refresh(); const a = getBean("NewVideoPlugin"); a && a.resetBtnTip(), getBean("BlacklistPlugin").resetBtnTip(), getBean("BlacklistPlugin").reloadTable(); } /** Create a removable keyword label tag in the filter panel. */ function addLabelTag(e, t) { const n = $(`${e} .tag-box`); let a, i = "#cbd5e1", s = "#333"; /^[a-z]{2,}-/i.test(t) && r ? (s = "#3477ad", a = $(` <a class="keyword-label" data-keyword="${t}" style="background-color: ${i}; color: ${s}" href="/video_codes/${t.replace("-", "")}" target="_blank"> ${t} <span class="keyword-remove">×</span> </a> `)) : a = $(` <div class="keyword-label" data-keyword="${t}" style="background-color: ${i}; color: ${s}"> ${t} <span class="keyword-remove">×</span> </div> `), a.find(".keyword-remove").click((e => { e.stopPropagation(), e.preventDefault(); const t = $(e.currentTarget); const n = t.closest(".keyword-label").attr("data-keyword").split(" ")[0]; utils.q(e, `是否移除屏蔽词 ${n}?`, (async () => { t.parent().remove(); })); })), n.append(a); } /** Add a keyword from the input field to the tag box. */ function addKeyword(e, t) { let n = $(`${t} .keyword-input`); const a = n.val().trim(); a && (addLabelTag(t, a), n.val("")); } /** Handle JSON file import via file input, run diff analysis, show preview. */ async function importSettingData(showDiffPreviewFn) { try { const input = document.createElement("input"); input.type = "file", input.accept = ".json"; const cleanup = () => { try { document.body.removeChild(input); } catch (e) {} }; input.onchange = async e => { const t = e.target.files[0]; if (!t) return void cleanup(); const n = new FileReader; n.onload = async e => { cleanup(); try { const t = e.target.result.toString(), n = JSON.parse(t); if (!n || "object" != typeof n || Array.isArray(n)) throw new Error("文件内容不是有效的数据对象"); const a = loading(); try { const e = await storageManager.exportData(), t = await storageManager.diffData(e, n); a.close(), showDiffPreviewFn(t, n, null); } catch (i) { a.close(), console.error(i), show.error("差异分析失败: " + i.message); } } catch (t) { console.error(t), show.error("导入失败:文件内容不是有效的JSON格式 " + t.message); } }, n.onerror = () => { cleanup(), show.error("读取文件时出错"); }, n.readAsText(t); }, document.body.appendChild(input), input.click(); setTimeout(cleanup, 3e5); } catch (e) { console.error(e), show.error("导入数据时出错: " + e.message); } } /** Create encrypted backup and upload via WebDAV. */ async function backupDataByWebDav(folderName) { const t = await storageManager.getSetting(), n = t.webDavUrl; if (!n) return void show.error("请填写webDav服务地址并保存后, 再试此功能"); const a = t.webDavUsername; if (!a) return void show.error("请填写webDav用户名并保存后, 再试此功能"); const i = await decryptCredential(t.webDavPassword); if (!i) return void show.error("请填写webDav密码并保存后, 再试此功能"); let s = utils.getNowStr("_", "_") + ".json", o = JSON.stringify(await storageManager.exportData()); o = await encryptBackupData(o); let r = loading(); try { const e = new De(n, a, i); await e.backup(folderName, s, o), show.ok("备份完成"); } catch (l) { console.error(l), show.error(l.toString()); } finally { r.close(); } } /** List WebDAV backups and open the file list dialog. */ async function backupListBtnByWebDav(folderName, openFileListDialogFn) { const t = await storageManager.getSetting(), n = t.webDavUrl; if (!n) return void show.error("请填写webDav服务地址并保存后, 再试此功能"); const a = t.webDavUsername; if (!a) return void show.error("请填写webDav用户名并保存后, 再试此功能"); const i = await decryptCredential(t.webDavPassword); if (!i) return void show.error("请填写webDav密码并保存后, 再试此功能"); let s = loading(); try { const e = new De(n, a, i), t = await e.getBackupList(folderName); openFileListDialogFn(t, e, "WebDav"); } catch (o) { console.error(o), show.error(`发生错误: ${o ? o.message : o}`); } finally { s.close(); } } /** Mobile-specific backup file list dialog using card-based UI. */ function openFileListDialogMobile(e, t, n, folderName, showDiffPreviewFn) { const formatSize = (size) => { const units = ["B", "KB", "MB", "GB", "TB", "PB"]; let i = 0, s = size; for (; s >= 1024 && i < units.length - 1;) s /= 1024, i++; return `${s % 1 == 0 ? s.toFixed(0) : s.toFixed(2)} ${units[i]}`; }; const renderCards = (files) => { if (!files || files.length === 0) { return '<div class="jhs-backup-empty">暂无数据</div>'; } return files.map((file, idx) => ` <div class="jhs-backup-card" data-idx="${idx}"> <div class="jhs-backup-card-name">${escapeHtml(file.name)}</div> <div class="jhs-backup-card-meta">${formatSize(file.size)} · ${utils.getNowStr("-", ":", file.createTime)}</div> <div class="jhs-backup-card-actions"> <button class="jhs-backup-btn jhs-backup-btn-danger" data-action="delete" data-idx="${idx}">删除</button> <button class="jhs-backup-btn jhs-backup-btn-primary" data-action="download" data-idx="${idx}">下载</button> <button class="jhs-backup-btn jhs-backup-btn-success" data-action="import" data-idx="${idx}">导入</button> </div> </div> `).join(""); }; const containerId = "jhs-backup-card-list"; layer.open({ type: 1, title: n + "备份文件", content: `<div id="${containerId}" style="padding:0 4px;">${renderCards(e)}</div>`, area: utils.getResponsiveArea(["800px", "70%"]), anim: -1, success: (layerEl) => { const container = $(layerEl).find(`#${containerId}`); container.on("click", ".jhs-backup-btn", async (ev) => { const btn = $(ev.currentTarget); const action = btn.data("action"); const idx = btn.data("idx"); const file = e[idx]; if (!file) return; if (action === "delete") { layer.confirm(`是否删除 ${file.name} ?`, { icon: 3, title: "提示", btn: ["确定", "取消"] }, async (confirmIdx) => { layer.close(confirmIdx); let load = loading(); try { await t.deleteFile(file.fileId); e = await t.getBackupList(folderName); container.html(renderCards(e)); layer.alert("删除成功"); } catch (err) { console.error(err), show.error(`发生错误: ${err ? err.message : err}`); } finally { load.close(); } }); } else if (action === "download") { let load = loading(); try { const data = await decryptBackupData(await t.getFileContent(file.fileId)); utils.download(data, file.name); } catch (err) { clog.error(err), show.error("下载失败: " + err); } finally { load.close(); } } else if (action === "import") { let load = loading(); try { let data = await t.getFileContent(file.fileId); data = await decryptBackupData(data); const parsed = JSON.parse(data); const currentData = await storageManager.exportData(); const diff = await storageManager.diffData(currentData, parsed); load.close(); showDiffPreviewFn(diff, null, parsed); } catch (err) { load.close(), console.error(err), show.error("预览失败: " + (err ? err.message : err)); } } }); } }); } /** Desktop backup file list dialog using Tabulator table. */ function openFileListDialog(e, t, n, folderName, showDiffPreviewFn) { if (utils.isMobileMode()) { openFileListDialogMobile(e, t, n, folderName, showDiffPreviewFn); return; } layer.open({ type: 1, title: n + "备份文件", content: '\n <div style="height: 100%;overflow:hidden;"> \n <div id="table-container" style="margin:auto auto !important;"></div>\n </div>\n ', area: utils.getResponsiveArea([ "800px", "70%" ]), anim: -1, success: a => { const i = new Tabulator("#table-container", { layout: "fitColumns", placeholder: "暂无数据", virtualDom: !0, data: e, responsiveLayout: "collapse", responsiveLayoutCollapse: !0, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, columns: [ { title: "文件名", field: "name", width: 200, headerSort: !1, responsive: 0 }, { title: "文件大小", field: "size", responsive: 1, headerSort: !1, formatter: (e, t, n) => { const a = [ "B", "KB", "MB", "GB", "TB", "PB" ]; let i = 0, s = e.getData().size; for (;s >= 1024 && i < a.length - 1; ) s /= 1024, i++; return `${s % 1 == 0 ? s.toFixed(0) : s.toFixed(2)} ${a[i]}`; } }, { title: "备份日期", field: "createTime", responsive: 2, headerSort: !1, formatter: (e, t, n) => { const a = e.getData(); return `${utils.getNowStr("-", ":", a.createTime)}`; } }, { title: "操作", minWidth: 250, responsive: 0, headerSort: !1, formatter: (e, a, s) => { const o = e.getData(); return s((() => { const a = e.getElement().querySelector(".a-danger"), s = e.getElement().querySelector(".a-primary"), r = e.getElement().querySelector(".a-success"); a && a.addEventListener("click", (e => { layer.confirm(`是否删除 ${o.name} ?`, { icon: 3, title: "提示", btn: [ "确定", "取消" ] }, (async e => { layer.close(e); let a = loading(); try { await t.deleteFile(o.fileId); let e = await t.getBackupList(folderName); i.replaceData(e), layer.alert("删除成功"); } catch (s) { console.error(s), show.error(`发生错误: ${s ? s.message : s}`); } finally { a.close(); } })); })), s && s.addEventListener("click", (async e => { let a = loading(); try { const e = await decryptBackupData(await t.getFileContent(o.fileId)); utils.download(e, o.name); } catch (i) { clog.error(i), show.error("下载失败: " + i); } finally { a.close(); } })), r && r.addEventListener("click", (async e => { let a = loading(); try { let e = await t.getFileContent(o.fileId); e = await decryptBackupData(e); const n = JSON.parse(e), i = await storageManager.exportData(), s = await storageManager.diffData(i, n); a.close(), showDiffPreviewFn(s, null, n); } catch (i) { a.close(), console.error(i), show.error("预览失败: " + (i ? i.message : i)); } })); })), '\n <a class="a-danger">删除</a>\n <a class="a-primary">下载</a>\n <a class="a-success">导入</a>\n '; } } ], locale: "zh-cn", langs: { "zh-cn": { pagination: { first: "首页", first_title: "首页", last: "尾页", last_title: "尾页", prev: "上一页", prev_title: "上一页", next: "下一页", next_title: "下一页", all: "所有", page_size: "每页行数" } } } }); } }); } /** Export all data as a downloadable JSON file. */ async function exportSettingData() { try { const e = JSON.stringify(await storageManager.exportData()), t = `${utils.getNowStr("_", "_")}.json`; utils.download(e, t), show.ok("数据导出成功"); } catch (t) { console.error(t), show.error("导出数据时出错: " + t.message); } } class Ae extends X { constructor() { super(...arguments), i(this, "folderName", "JHS-数据备份"), i(this, "cacheItems", [ { key: "jhs_dmm_video", text: "预览视频缓存", title: "预览视频缓存" }, { key: "jhs_other_site", text: "第三方站点缓存", title: "第三方站点资源检测结果, 如missav,123Av等" }, { key: "jhs_screenShot", text: "缩略图缓存", title: "缩略图缓存" }, { key: "jhs_translate", text: "标题翻译", title: "标题翻译" }, { key: "jhs_actress_info", text: "演员信息", title: "演员的年龄三围等数据信息" }, { key: "jhs_score_info", text: "Top250|热播 评分数据", title: "Top250及热播的评分数据" }, { key: "third_party_ttl_cache", text: "第三方TTL缓存", title: "评论、相关清单、磁力搜索、缩略图等请求缓存" }, { key: "_circuitBreaker", text: "熔断状态", title: "各站点的熔断计数和状态" }, { key: "_domainStats", text: "域名请求统计", title: "各域名的请求次数和错误统计" } ]); } getName() { return "SettingPlugin"; } /** Periodic WebDAV auto-backup based on autoBackupConfig. */ async autoBackup() { const settings = await storageManager.getSetting(); if (!shouldAutoBackup(settings.autoBackupConfig || DEFAULT_AUTO_BACKUP_CONFIG)) return; const url = settings.webDavUrl; const username = settings.webDavUsername; const password = await decryptCredential(settings.webDavPassword); if (!url || !username || !password) return; try { const payload = await encryptBackupData(JSON.stringify(await storageManager.exportData())); await new De(url, username, password).backup(this.folderName, getAutoBackupFileName(), payload); markAutoBackupDone(); clog.log("[JHS 自动备份] 完成"); } catch (err) { clog.warn("[JHS 自动备份] 失败:", err instanceof Error ? err.message : String(err)); } } async initCss() { const e = await storageManager.getSetting(); let t = (null == e ? void 0 : e.containerWidth) ?? "100"; utils.isMobileMode() && (t = "100"); let n = utils.isMobile() && window.innerWidth < 1e3 ? 1 : (null == e ? void 0 : e.containerColumns) ?? 5; window.getBeanForSetting = this.getBean.bind(this); applyImageMode().catch((e => console.error("[JHS] applyImageMode failed:", e))); applyDetailFlex().catch((e => console.error("[JHS] applyDetailFlex failed:", e))); return buildSettingCss(t, n, l, r); } async handle() { if (await storageManager.getSetting("enableClog", _) === _ && clog.show(), r) { let e = function() { $(".navbar-search").is(":hidden") ? ($(".mini-setting-box").hide(), $(".setting-box").show()) : ($(".mini-setting-box").show(), $(".setting-box").hide()); }; $("#navbar-menu-user .navbar-end").prepend('<div class="navbar-item has-dropdown is-hoverable setting-box" style="position:relative;">\n <a id="setting-btn" class="navbar-link nav-btn" style="color: #ff8400 !important;padding-right:15px !important;">\n 设置\n </a>\n <div class="simple-setting"></div>\n </div>'), utils.loopDetector((() => $("#miniHistoryBtn").length > 0), (() => { $(".miniHistoryBtnBox").before('\n <div class="navbar-item mini-setting-box" style="position:relative;margin-left: auto;">\n <a id="mini-setting-btn" class="navbar-link nav-btn" style="color: #ff8400 !important;padding-left:0 !important;padding-right:0 !important;">\n 设置\n </a>\n <div class="mini-simple-setting"></div>\n </div>\n '), e(); })), $(window).resize(e); } l && this.mountBusNavSettingBox(), $(document).off("click.jhsSettingBtn").on("click.jhsSettingBtn", "#setting-btn, #mini-setting-btn", (() => { $(".simple-setting, .mini-simple-setting").html("").hide(), clog.lowZIndex(), this.openSettingDialog("base-panel"); })), $(".main-nav, .container-fluid, .navbar, #navbar").on("mouseenter", ".setting-box", (() => { $(".simple-setting").html(buildSimpleSettingHtml()).show(), initSimpleSettingForm(this.getBean.bind(this), this.getSelector.bind(this), this.openSettingDialog.bind(this)).then(), clog.lowZIndex(); })).on("mouseleave", ".setting-box", (() => { $(".simple-setting").html("").hide(); })), $(".main-nav, .container-fluid, .navbar, #navbar").on("mouseenter", ".mini-setting-box", (() => { $(".mini-simple-setting").html(buildSimpleSettingHtml()).show(), initSimpleSettingForm(this.getBean.bind(this), this.getSelector.bind(this), this.openSettingDialog.bind(this)).then(), clog.lowZIndex(); })).on("mouseleave", ".mini-setting-box", (() => { $(".mini-simple-setting").html("").hide(); })); } /** JavBus 顶部导航「语言」下拉所在的 li (地球图标 / 语言文案) */ _findBusLangItem() { // 优先: 地球图标所在的 li (顶部导航语言 dropdown 的 toggle 本身) const $globe = $(".glyphicon-globe, .fa-globe, [class*='globe']").first(); if ($globe.length) { const $li = $globe.closest("li"); if ($li.length) return $li; } // 兜底: 导航栏内 (排除 dropdown-menu 内选项) a 文本以语言词开头的 li const langRe = /^(語言|语言|English|日本語|繁體|简体|한국어|Language)/; return $(".navbar li, .nav li, nav li").filter(((_, el) => { const $el = $(el); if ($el.closest(".dropdown-menu").length) return !1; return langRe.test(($el.children("a").first().text() || "").trim()); })).first(); } /** JavBus 顶部导航「已有磁力」所在的 li */ _findBusMagnetNavItem() { const re = /(已有磁力|有磁力|有磁鏈|有磁链)/; return $(".navbar li, .nav li, nav li").filter(((_, el) => { const $el = $(el); if ($el.closest(".dropdown-menu").length) return !1; return re.test(($el.children("a").first().text() || "").trim()); })).first(); } /** JavBus: 设置按钮插到导航栏「已有磁力」之前(无则「语言」之前), 找不到则回退 */ mountBusNavSettingBox() { const box = '\n <div id="top-right-box" style="position: relative; display: flex; align-items: center; gap: 5px; z-index: 12345679 !important;">\n <div class="setting-box" style="position: relative; display: flex; align-items: center;">\n <a id="setting-btn" class="menu-btn main-tab-btn" style="background-color:#6e685e !important; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; height: 30px; padding: 0 12px; line-height: 1; margin: 0 !important;">\n <span>设置</span>\n </a>\n <div class="simple-setting"></div>\n </div>\n </div>\n'; // 等导航项或标题任一就绪再插入 (导航栏可能异步渲染) utils.loopDetector((() => this._findBusMagnetNavItem().length > 0 || this._findBusLangItem().length > 0 || $("h3").length > 0), (() => { if ($("#top-right-box").length) return; // 防重复插入 const $magnet = this._findBusMagnetNavItem(); const $target = $magnet.length ? $magnet : this._findBusLangItem(); if ($target.length) { // 高度对齐相邻导航项, 否则 float 布局下按钮会顶到行首导致文字不在同一水平线 const h = $target.outerHeight() || 40; $target.before(`<li class="jhs-nav-setting" style="float: left; display: flex; align-items: center; justify-content: center; height: ${h}px; padding: 0 10px; margin: 0; list-style: none;">${box}</li>`); } else { $("h3").first().before(`\n <div class="container-fluid" style="margin-top:20px">${box}</div>\n `); } }), 1, 1e4, !1); } async openSettingDialog(e = "backup-panel", t) { const a = this.getBean("CoverButtonPlugin"); const s = buildSettingDialogHtml(e, this.cacheItems, a); layer.open({ type: 1, title: "设置", content: s, area: utils.getResponsiveArea([ "55%", "90%" ]), scrollbar: !1, success: (e, n) => { $(e).find(".layui-layer-content").css("position", "relative"), injectHealthPanel(), injectPluginMgmtPanel(), injectSnapshotPanel(), injectNetworkPanel(), loadSettingForm(this.getBean.bind(this)), this.bindClick(), utils.setupEscClose(n), t && t(); if (utils.isMobileMode()) { this.collapseAdvancedTabs(); } }, end: () => { this.getBean("CoverButtonPlugin").enableSvgBtn(); } }); } collapseAdvancedTabs() { const advancedPanels = [ { id: "health-panel", label: "数据体检", render: renderDataHealthPanel }, { id: "plugin-mgmt-panel", label: "插件管理", render: renderPluginMgmtPanel }, { id: "snapshot-panel", label: "恢复点", render: renderSnapshotPanel }, { id: "network-panel", label: "外部请求", render: renderNetworkPanel } ]; const sidebar = $(".jhs-mobile-sidebar"); const contentParent = $(".content-panel").parent(); if (!sidebar.length || !contentParent.length) return; advancedPanels.forEach(p => { sidebar.find(`[data-panel="${p.id}"]`).remove(); }); if (!sidebar.find('[data-panel="more-tools-panel"]').length) { sidebar.append('<div class="side-menu-item" data-panel="more-tools-panel">更多工具</div>'); } if ($("#more-tools-panel").length) return; let subTabsHtml = advancedPanels.map((p, i) => `<div class="jhs-sub-tab${i === 0 ? " active" : ""}" data-sub-panel="${p.id}">${p.label}</div>` ).join(""); let subPanelsHtml = advancedPanels.map((p, i) => `<div id="sub-${p.id}" class="jhs-sub-panel${i === 0 ? " active" : ""}" data-rendered="false"></div>` ).join(""); const wrapperHtml = ` <div id="more-tools-panel" class="content-panel" style="display:none;"> <div class="jhs-sub-tabs">${subTabsHtml}</div> <div class="jhs-sub-panels">${subPanelsHtml}</div> </div> `; contentParent.append(wrapperHtml); advancedPanels.forEach(p => { const src = $(`#${p.id}`); if (src.length) { src.children().appendTo(`#sub-${p.id}`); src.remove(); $(`#sub-${p.id}`).attr("data-rendered", "true"); } }); sidebar.on("click", '[data-panel="more-tools-panel"]', function() { $(".side-menu-item").removeClass("active"), $(this).addClass("active"), $(".content-panel").hide(); $("#more-tools-panel").show(), $("#saveBtn").show(), $("#clean-all").hide(); }); $("#more-tools-panel").on("click", ".jhs-sub-tab", function() { const target = $(this).data("sub-panel"); $("#more-tools-panel .jhs-sub-tab").removeClass("active"); $(this).addClass("active"); $("#more-tools-panel .jhs-sub-panel").removeClass("active"); $(`#sub-${target}`).addClass("active"); if ($(`#sub-${target}`).attr("data-rendered") !== "true") { $(`#sub-${target}`).attr("data-rendered", "true"); } const panel = advancedPanels.find(p => p.id === target); if (panel && panel.render) { panel.render(); } }); } bindClick() { const settingPlugin = this; $(".side-menu-item").on("click", (function() { $(".side-menu-item").removeClass("active"), $(this).addClass("active"), $(".content-panel").hide(); const e = $(this).data("panel"); $("#" + e).show(), "cache-panel" === e ? ($("#saveBtn").hide(), $("#clean-all").show()) : ($("#saveBtn").show(), $("#clean-all").hide()), "health-panel" === e && ($("#saveBtn").hide(), $("#clean-all").hide(), renderDataHealthPanel()), "plugin-mgmt-panel" === e && ($("#saveBtn").hide(), $("#clean-all").hide(), renderPluginMgmtPanel()), "hotkey-panel" === e && ($("#saveBtn").show(), $("#clean-all").hide()), "snapshot-panel" === e && ($("#saveBtn").hide(), $("#clean-all").hide(), renderSnapshotPanel()), "network-panel" === e && ($("#saveBtn").hide(), $("#clean-all").hide(), renderNetworkPanel()); })), $("#importBtn").on("click", (e => importSettingData(showDiffPreview))), $("#exportBtn").on("click", (e => exportSettingData())), $("#cleanJhsDataBtn").on("click", (e => { utils.q(null, "是否清空所有数据? 即鉴定记录、黑名单数据、配置等...<br/><span style='color: #f40'>此操作不可逆, 请备份好数据</span>", (async () => { await storageManager.importData({}); show.ok("已清空数据, 即将刷新页面..."); await utils.sleep(200); window.location.reload(); })); })), $("#webdavBackupBtn").on("click", (e => backupDataByWebDav(this.folderName))), $("#webdavBackupListBtn").on("click", (e => backupListBtnByWebDav(this.folderName, (files, client, label) => openFileListDialog(files, client, label, this.folderName, showDiffPreview)))), $("#saveBtn").on("click", (() => saveSettingForm(this.getBean.bind(this)))), $("#runHealthCheckBtn").on("click", (() => renderDataHealthPanel())), $("#repairHealthBtn").on("click", (e => { utils.q(e, "修复前会自动下载备份,是否继续?", (() => repairDataHealthWithBackup())); })), $("#pm-clear-log").on("click", (() => { unsafeWindow.pluginManager.clearErrorLog(), $("#plugin-error-log").text("无错误记录"), show.ok("错误日志已清空"); })), $("#createSnapshotBtn").on("click", (async () => { let e = loading(); try { await storageManager.createSnapshot("手动快照", "manual"), show.ok("快照创建成功"), renderSnapshotPanel(); } catch (t) { console.error(t), show.error("创建快照失败: " + t.message); } finally { e.close(); } })), $(".clean-btn").on("click", (async e => { const t = $(e.currentTarget).data("key"), n = this.cacheItems.find((e => e.key === t)); t === storageManager.third_party_cache_key ? await storageManager.clearThirdPartyCache() : "_circuitBreaker" === t ? gmHttp.resetAllCircuitBreakers() : "_domainStats" === t ? gmHttp.clearDomainStats() : localStorage.removeItem(t), show.ok(`${n.text} 清理成功`), $("#cache-data-display").hide(), "jhs_dmm_video" === t && localStorage.removeItem("jhs_other_site_dmm"); })), $("#clean-all").on("click", (async () => { this.cacheItems.forEach((e => localStorage.removeItem(e.key))), show.ok("全部缓存已清理"), $("#cache-data-display").hide(), localStorage.removeItem("jhs_other_site_dmm"), await storageManager.clearThirdPartyCache(); })), $(".view-btn").on("click", (async e => { const t = $(e.currentTarget).data("key"); let n; if (t === storageManager.third_party_cache_key) n = JSON.stringify(await storageManager.getThirdPartyCache()); else if ("_circuitBreaker" === t) n = JSON.stringify(gmHttp.getCircuitBreakerStatus()); else if ("_domainStats" === t) n = JSON.stringify(gmHttp.getDomainStats()); else n = localStorage.getItem(t); const a = $("#cache-data-display"), i = a.find("pre"); if (a.show(), n) try { const e = JSON.parse(n); i.text(JSON.stringify(e, null, 2)); } catch { i.text(n); } else i.text("无数据"); })); const e = $("#highlightedTagNumber"), t = $("#highlightedTagColor"), n = $("#highlightedTagLabel"); function a() { const a = e.val(), i = t.val(); n.css("border", `${a}px solid ${i}`); } e.on("input", a), t.on("input", a); } } const ENCRYPTION_SALT = "x7k9p3"; async function getEncryptionKey() { const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(ENCRYPTION_SALT + ".jhs.v1"), { name: "PBKDF2" }, false, [ "deriveKey" ]); return crypto.subtle.deriveKey({ name: "PBKDF2", salt: enc.encode("jhs-backup"), iterations: 1e5, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, [ "encrypt", "decrypt" ]); } function arrayBufferToBase64(e) { const t = new Uint8Array(e), n = 0x8000; let a = ""; for (let i = 0; i < t.length; i += n) a += String.fromCharCode.apply(null, t.subarray(i, i + n)); return btoa(a); } function base64ToArrayBuffer(e) { const t = atob(e), n = new Uint8Array(t.length); for (let a = 0; a < t.length; a++) n[a] = t.charCodeAt(a); return n; } async function encryptData(e) { const t = await getEncryptionKey(), n = crypto.getRandomValues(new Uint8Array(12)), a = new TextEncoder(), i = await crypto.subtle.encrypt({ name: "AES-GCM", iv: n }, t, a.encode(e)), s = new Uint8Array(n.length + i.byteLength); return s.set(n), s.set(new Uint8Array(i), n.length), arrayBufferToBase64(s); } async function decryptData(e) { const t = await getEncryptionKey(), n = base64ToArrayBuffer(e), a = n.slice(0, 12), i = n.slice(12), s = await crypto.subtle.decrypt({ name: "AES-GCM", iv: a }, t, i); return new TextDecoder().decode(s); } const CREDENTIAL_PREFIX = "AES:"; async function encryptCredential(e) { return e && !e.startsWith(CREDENTIAL_PREFIX) ? CREDENTIAL_PREFIX + await encryptData(e) : e; } async function decryptCredential(e) { return e && e.startsWith(CREDENTIAL_PREFIX) ? await decryptData(e.slice(CREDENTIAL_PREFIX.length)) : e; } /* ===== V2 password-based backup crypto (ported from Power Tools) ===== */ const BACKUP_V2_ITERATIONS = 1e5; const BACKUP_V2_SALT_BYTES = 16; const BACKUP_V2_IV_BYTES = 12; const LAST_AUTO_BACKUP_DATE_KEY = "jhs:last_auto_backup_date"; const DEFAULT_AUTO_BACKUP_CONFIG = { enabled: false, frequency: "daily" }; function _v2BytesToBase64(bytes) { let s = ""; for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]); return btoa(s); } function _v2Base64ToBytes(b64) { const bin = atob(b64), out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } async function _deriveBackupV2Key(password, salt) { const enc = new TextEncoder(); const km = await crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveKey"]); return crypto.subtle.deriveKey({ name: "PBKDF2", salt, iterations: BACKUP_V2_ITERATIONS, hash: "SHA-256" }, km, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); } async function encryptBackupV2(plaintext, password) { const enc = new TextEncoder(); const salt = crypto.getRandomValues(new Uint8Array(BACKUP_V2_SALT_BYTES)); const iv = crypto.getRandomValues(new Uint8Array(BACKUP_V2_IV_BYTES)); const key = await _deriveBackupV2Key(password, salt); const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(plaintext)); return JSON.stringify({ v: 2, alg: "AES-GCM", kdf: "PBKDF2-SHA-256", iter: BACKUP_V2_ITERATIONS, salt: _v2BytesToBase64(salt), iv: _v2BytesToBase64(iv), ct: _v2BytesToBase64(new Uint8Array(ct)), ts: new Date().toISOString() }); } async function decryptBackupV2(envelopeJson, password) { const envelope = JSON.parse(envelopeJson); if (envelope.v !== 2) throw new Error(`不支持的备份格式版本: ${envelope.v}`); const key = await _deriveBackupV2Key(password, _v2Base64ToBytes(envelope.salt)); const buf = await crypto.subtle.decrypt({ name: "AES-GCM", iv: _v2Base64ToBytes(envelope.iv) }, key, _v2Base64ToBytes(envelope.ct)); return new TextDecoder().decode(buf); } function isBackupV2(content) { try { const p = JSON.parse(content); return p && p.v === 2 && p.alg === "AES-GCM"; } catch { return false; } } /** Read the configured backup password (plaintext). */ async function getBackupPassword() { const s = await storageManager.getSetting(); return await decryptCredential(s.backupPassword) || ""; } /** Encrypt backup payload: V2 (password) if backupPassword set, else device-key AES. */ async function encryptBackupData(plaintext) { const pwd = await getBackupPassword(); return pwd ? await encryptBackupV2(plaintext, pwd) : await encryptData(plaintext); } /** Decrypt backup content: auto-detect V2 envelope, fallback to legacy device-key. */ async function decryptBackupData(content) { if (isBackupV2(content)) { const pwd = await getBackupPassword(); if (!pwd) throw new Error("该备份使用 V2 加密, 请先在数据备份面板设置备份口令"); try { return await decryptBackupV2(content, pwd); } catch { throw new Error("备份口令错误或文件已损坏"); } } return await decryptData(content); } /* ===== Auto backup helpers ===== */ const CREDENTIAL_ID_KEY = "jhs:credential_id"; /** RFC4122 v4 uuid, 带 crypto.randomUUID 缺失时的回退 */ function generateCredentialUuid() { if ("undefined" != typeof crypto && crypto.randomUUID) return crypto.randomUUID(); const b = new Uint8Array(16); crypto.getRandomValues(b), b[6] = 15 & b[6] | 64, b[8] = 63 & b[8] | 128; const h = []; b.forEach((e => h.push(e.toString(16).padStart(2, "0")))); return `${h.slice(0, 4).join("")}-${h.slice(4, 6).join("")}-${h.slice(6, 8).join("")}-${h.slice(8, 10).join("")}-${h.slice(10, 16).join("")}`; } /** 本机凭证: 首次调用时生成并持久化, 用于区分多设备的自动备份文件 */ function getCredentialId() { let id = localStorage.getItem(CREDENTIAL_ID_KEY); if (!id) id = generateCredentialUuid(), localStorage.setItem(CREDENTIAL_ID_KEY, id); return id; } function _todayStr() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function shouldAutoBackup(config) { if (!config || !config.enabled || config.frequency === "disabled") return false; if (config.frequency === "everyOpen") return true; return _todayStr() !== localStorage.getItem(LAST_AUTO_BACKUP_DATE_KEY); } function markAutoBackupDone() { localStorage.setItem(LAST_AUTO_BACKUP_DATE_KEY, _todayStr()); } /** 每台设备固定一个自动备份文件名, 覆盖式更新, 避免 WebDav 上堆积无限多份 */ function getAutoBackupFileName() { return `auto_${getCredentialId()}.json`; } class je extends X { getName() { return "BusPreviewVideoPlugin"; } async initCss() { return "\n /* 弹窗/Modal 样式 */\n .bus-preview-modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.95); \n /* 关键修改:更新 z-index */\n z-index: 12345699; \n display: flex;\n justify-content: center;\n align-items: center;\n opacity: 0; \n visibility: hidden; \n transition: opacity 0.2s ease;\n }\n .bus-preview-modal.is-open {\n opacity: 1;\n visibility: visible;\n }\n /* 垂直排列视频和按钮,并居中 */\n .bus-preview-modal-content {\n position: relative;\n max-width: 95%; \n max-height: 95%;\n display: flex; \n flex-direction: column; \n align-items: center; \n gap: 15px; \n }\n \n /* 移除 .bus-preview-close-btn 的样式 */\n\n /* 视频播放器容器 */\n .video-player-wrapper {\n /* 关键修改:更新 width 和 max-height */\n width: 80vw; \n max-height: 85vh; \n aspect-ratio: 16 / 9; \n position: relative; \n background-color: black; \n max-width: 100%; \n }\n /* 视频元素 */\n .video-player-wrapper #preview-video {\n position: absolute; \n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: block;\n }\n\n /* 画质控制盒 (底部按钮) */\n .video-control-box {\n display: flex;\n flex-direction: row; \n justify-content: center; \n flex-wrap: wrap; \n gap: 10px;\n padding: 10px 0; \n }\n\n /* 按钮样式 (保留) */\n .video-control-btn {\n min-width:80px;\n padding: 6px 12px;\n background: rgba(255,255,255,0.2);\n color: white;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 4px;\n cursor: pointer;\n text-align: center;\n font-size: 14px;\n transition: background-color 0.2s, border-color 0.2s;\n }\n .video-control-btn:hover {\n background: rgba(255,255,255,0.4);\n }\n .video-control-btn.active {\n background-color: #1890ff; \n color: white;\n font-weight: bold;\n border: 1px solid #096dd9;\n }\n "; } initModal() { if (0 === $("#bus-preview-modal").length) { $("body").append('\n <div id="bus-preview-modal" class="bus-preview-modal">\n <div class="bus-preview-modal-content">\n </div>\n </div>\n '); const e = $("#bus-preview-modal"); e.on("click", (e => { "bus-preview-modal" === e.target.id && this.closeVideoModal(); })), $(document).on("keydown", (t => { "Escape" === t.key && e.hasClass("is-open") && this.closeVideoModal(); })); } } closeVideoModal() { const e = $("#preview-video"); e.length > 0 && e[0].pause(), $("#bus-preview-modal").removeClass("is-open"); } async handle() { if (!isDetailPage) return; this.initModal(); const e = $("#sample-waterfall .sample-box .photo-frame img:first").attr("src"), t = $(`\n <a class="preview-video-container sample-box" style="cursor: pointer">\n <div class="photo-frame" style="position:relative;">\n <img src="${e}" class="video-cover" alt="">\n <div class="play-icon" style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); \n color:white; font-size:40px; text-shadow:0 0 10px rgba(0,0,0,0.5);">\n ▶\n </div>\n </div>\n </a>`); $("#sample-waterfall").prepend(t); "yes" === await storageManager.getSetting("enableLoadPreviewVideo", "yes") && ne(this.getPageInfo().carNum, !1).then(); let n = !1, a = $(".preview-video-container"); a.on("click", (async e => { if (e.preventDefault(), e.stopPropagation(), n) show.info("正在加载中, 勿重复点击"); else { n = !0; try { await this.handleVideo(); } finally { n = !1; } } })), window.location.href.includes("autoPlay=1") && a.trigger("click"); } async handleVideo() { const e = $("#bus-preview-modal"), t = e.find(".bus-preview-modal-content"); let n = $("#preview-video"); if (n.length > 0) return e.addClass("is-open"), void n[0].play().catch((e => console.warn("尝试播放失败 (可能被浏览器阻止):", e))); let a = this.getPageInfo().carNum; const i = await ne(a); i && 0 !== Object.keys(i).length ? (await this.createVideoPlayerAndControls(i, t), n = $("#preview-video"), n.length > 0 ? (e.addClass("is-open"), n[0].play().catch((e => console.warn("尝试播放失败 (可能被浏览器阻止):", e)))) : show.error("视频播放器创建失败。")) : show.error("未找到可用的视频源。"); } async createVideoPlayerAndControls(e, t) { let n = await storageManager.getSetting("videoQuality"); n = Z(Object.keys(e), n); let a = e[n]; t.html(`\n <div class="video-player-wrapper">\n <video id="preview-video" controls playsinline>\n <source src="${a}" />\n </video>\n </div>\n <div class="video-control-box">\n </div>\n `); const i = $("#preview-video"), s = i.find("source"), o = t.find(".video-control-box"); if (!i.length || !s.length) return; const r = i[0], l = localStorage.getItem("jhs_videoMuted"); r.muted = !l || "yes" === l, r.addEventListener("volumechange", (function() { localStorage.setItem("jhs_videoMuted", r.muted ? "yes" : "no"); })); let c = ""; L.forEach((t => { let a = e[t.quality]; if (a) { const e = n === t.quality; c += `\n <button class="video-control-btn${e ? " active" : ""}" \n data-quality="${t.quality}"\n data-video-src="${a}">\n ${t.text}\n </button>\n `; } })), o.html(c); const d = o.find(".video-control-btn"); o.off("click").on("click", ".video-control-btn", (async e => { try { const t = $(e.currentTarget); if (t.hasClass("active")) return; let n = t.attr("data-video-src"); s.attr("src", n); const a = r.currentTime; r.load(), r.currentTime = a, await r.play(), d.removeClass("active"), t.addClass("active"); } catch (t) { console.error("切换画质失败:", t); } })); } } class Ee extends X { constructor() { super(...arguments), i(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" } ]), i(this, "isUploading", !1); } getName() { return "SearchByImagePlugin"; } 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 \n #handle-btn, #cancel-btn {\n padding: 8px 16px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 14px;\n border: none;\n transition: opacity 0.3s;\n }\n #handle-btn {\n background-color: #2196F3;\n color: white;\n }\n #handle-btn:hover {\n opacity: 0.9;\n }\n #cancel-btn {\n background-color: #f44336;\n color: white;\n }\n #cancel-btn:hover {\n opacity: 0.9;\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 </style>\n "; } open(e) { layer.open({ type: 1, title: "以图识图", content: '\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 粘贴图片或 图片URL</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 id="url-input-container" style="margin-top: 15px;display: none;">\n <input type="text" id="image-url" placeholder="粘贴图片URL地址..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">\n </div>\n \n <div id="preview-area" style="margin-bottom: 20px; text-align: center; display: none;">\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 <div style="margin-top: 15px; display: flex; justify-content: center; gap: 10px;" id="action-btns">\n <button id="handle-btn">搜索图片</button>\n <button id="cancel-btn">取消</button>\n </div>\n \n <div id="search-results" style="display: none;">\n <p style="margin: 20px auto">请选择识图网站:<a id="openAll" style="cursor: pointer">全部打开</a></p>\n <div class="search-img-site-btns-container" id="search-img-site-btns-container"></div>\n </div>\n </div>\n \n </div>\n ', area: utils.isMobile() ? utils.getResponsiveArea() : [ "40%", "80%" ], success: async t => { this.initEventListeners(), e && e(); }, end: () => { $(document).off("paste.searchImg"); } }); } initEventListeners() { const e = $("#upload-area"), t = $("#image-file"), n = $("#select-image-btn"), a = $("#preview-area"), i = $("#preview-image"), s = $("#action-btns"), o = $("#handle-btn"), r = $("#cancel-btn"), l = $("#url-input-container"), c = $("#image-url"), d = $("#search-results"), h = $("#search-img-site-btns-container"); e.on("dragover", (t => { t.preventDefault(), e.addClass("highlight"); })).on("dragleave", (() => { e.removeClass("highlight"); })).on("drop", (t => { t.preventDefault(), e.removeClass("highlight"), t.originalEvent.dataTransfer.files && t.originalEvent.dataTransfer.files[0] && (this.handleImageFile(t.originalEvent.dataTransfer.files[0]), this.resetSearchUI()); })), n.on("click", (() => { t.trigger("click"); })), t.on("change", (e => { e.target.files && e.target.files[0] && (this.handleImageFile(e.target.files[0]), this.resetSearchUI()); })), $(document).on("paste.searchImg", (async e => { const t = e.originalEvent.clipboardData.items; for (let a = 0; a < t.length; a++) if (-1 !== t[a].type.indexOf("image")) { const e = t[a].getAsFile(); return this.handleImageFile(e), void this.resetSearchUI(); } const n = e.originalEvent.clipboardData.getData("text"); n && utils.isUrl(n) && (l.show(), c.val(n), i.attr("src", n), a.show(), this.resetSearchUI()); })), o.on("click", (async () => { const e = i.attr("src"); if (e) { if (!this.isUploading) { this.isUploading = !0; try { const t = await this.searchByImage(e); s.hide(), d.show(), h.empty(); const n = "jhs_selectedSites", a = JSON.parse(localStorage.getItem(n) || "{}"); this.siteList.forEach((e => { const n = e.url.replace("{占位符}", encodeURIComponent(t)), i = !1 !== a[e.name]; h.append(`\n <a href="${n}" class="search-img-site-btn" target="_blank" title="${e.name}">\n <input type="checkbox" \n class="site-checkbox" \n data-site-name="${e.name}" \n style="margin-right: 5px"\n ${i ? "checked" : ""}>\n <img src="${e.ico}" alt="${e.name}">\n <span>${e.name}</span>\n </a>\n `); })), h.on("change", ".site-checkbox", (function() { const e = $(this).data("site-name"); a[e] = $(this).is(":checked"), localStorage.setItem(n, JSON.stringify(a)); })), h.show(); } finally { this.isUploading = !1; } } } else show.info("请粘贴或上传图片"); })), r.on("click", (() => { a.hide(), l.hide(), t.val(""), c.val(""); })), c.on("change", (() => { utils.isUrl(c.val()) && (i.attr("src", c.val()), a.show()); })), $("#openAll").on("click", (() => { $(".search-img-site-btn").each((function() { $(this).find(".site-checkbox").is(":checked") && window.open($(this).attr("href")); })); })); } resetSearchUI() { $("#action-btns").show(), $("#search-results").hide(), $("#search-img-site-btns-container").hide().empty(); } handleImageFile(e) { const t = document.getElementById("preview-image"), n = document.getElementById("preview-area"), a = document.getElementById("url-input-container"); if (!e.type.match("image.*")) return void show.info("请选择图片文件"); const i = new FileReader; i.onload = e => { t.src = e.target.result, n.style.display = "block", a.style.display = "none", $("#handle-btn")[0].click(); }, i.readAsDataURL(e); } async searchByImage(e) { let t = loading(); try { let t = e; if (e.startsWith("data:")) { show.info("开始上传图片..."); const n = await async function(e) { var t; const n = e.match(/^data:(.+);base64,(.+)$/); if (!n || n.length < 3) throw new Error("无效的Base64图片数据"); const a = n[1], i = n[2], s = atob(i), o = new Array(s.length); for (let g = 0; g < s.length; g++) o[g] = s.charCodeAt(g); const r = new Uint8Array(o), l = new Blob([ r ], { type: a }), c = new FormData; c.append("image", l); const d = await fetch("https://api.imgur.com/3/image", { method: "POST", headers: { Authorization: "Client-ID d70305e7c3ac5c6" }, body: c }), h = await d.json(); if (h.success && h.data && h.data.link) return h.data.link; throw new Error((null == (t = h.data) ? void 0 : t.error) || "上传到Imgur失败"); }(e); if (!n) return void show.error("上传失败"); t = n; } return t; } catch (n) { show.error(`搜索失败: ${n.message}`), console.error("搜索失败:", n); } finally { t.close(); } } } class Fe extends X { getName() { return "BusNavBarPlugin"; } handle() { $("#navbar > div > div > span").append('\n <button class="btn btn-default" style="color: #0d9488" id="search-img-btn">识图</button>\n '), $("#search-img-btn").on("click", (() => { this.getBean("SearchByImagePlugin").open(); })); } } class ze extends X { constructor() { super(...arguments), i(this, "type", null); } getName() { return "WantAndWatchedVideosPlugin"; } async handle() { window.location.href.includes("/want_watch_videos") && ($("h3").append('<a class="a-primary" id="wantWatchBtn" style="padding:10px;">导入至 JHS</a>'), $("#wantWatchBtn").on("click", (e => { this.type = h, this.importWantWatchVideos(e, "是否将 想看的影片 导入到 JHS-收藏?"); }))), window.location.href.includes("/watched_videos") && ($("h3").append('<a class="a-success" id="wantWatchBtn" style="padding:10px;">导入至 JHS</a>'), $("#wantWatchBtn").on("click", (e => { this.type = g, this.importWantWatchVideos(e, "是否将 看过的影片 导入到 JHS-已下载?"); }))); } importWantWatchVideos(e, t) { utils.q(null, `${t} <br/> <span style='color: #f40'>执行此功能前请记得备份数据</span>`, (async () => { let e = loading(); try { await this.parseMovieList(); } catch (t) { console.error(t); } finally { e.close(); } })); } async parseMovieList(e) { let t, n; e ? (t = e.find(this.getSelector().itemSelector), n = e.find(".pagination-next").attr("href")) : (t = $(this.getSelector().itemSelector), n = $(".pagination-next").attr("href")); for (const i of t) { const e = $(i), t = e.find("a").attr("href"), n = e.find(".video-title strong").text().trim(), s = e.find(".meta").text().trim(); if (t && n) try { if (await storageManager.getCar(n)) { show.info(`${n} 已存在, 跳过`); continue; } await storageManager.saveCar({ carNum: n, url: t, names: null, actionType: this.type, publishTime: s }); } catch (a) { console.error(`保存失败 [${n}]:`, a); } } n ? (show.info("发现下一页,正在解析:", n), await new Promise((e => setTimeout(e, 1e3))), $.ajax({ url: n, method: "GET", success: e => { const t = new DOMParser, n = $(t.parseFromString(e, "text/html")); this.parseMovieList(n); }, error: function(e) { console.error(e), show.error("加载下一页失败:" + e.message); } })) : (show.ok("导入结束!"), window.refresh()); } } class Ue extends X { getName() { return "CoverButtonPlugin"; } async initCss() { return `\n <style>\n .box .tags {\n justify-content: space-between;\n }\n .tool-box span{\n opacity:.3\n }\n .tool-box span:hover{\n opacity:1\n }\n ${l ? ".tool-box .icon, .setting-label .icon{ height: 24px; width: 24px; }" : ""}\n .tool-box svg path {\n fill: blue;\n }\n [data-theme="dark"] .tool-box svg path {\n fill: white;\n }\n \n \n /* 鼠标移入时的弹性动画 */\n .elastic-in {\n animation: elasticIn 0.2s ease-out forwards; /* 动画名称 | 时长 | 缓动函数 | 保持最终状态 */\n }\n \n /* 鼠标移出时的弹性动画 */\n .elastic-out {\n animation: elasticOut 0.2s ease-in forwards;\n }\n /* 弹性进入动画(像果冻弹入) */\n @keyframes elasticIn {\n 0% {\n opacity: 0;\n transform: scale(0.8); /* 起始状态:80% 大小 */\n }\n 50% {\n opacity: 1;\n transform: scale(1.1); /* 弹到 110%(超调一点) */\n }\n 70% {\n transform: scale(0.95); /* 回弹到 95%(模拟弹性阻尼) */\n }\n 100% {\n opacity: 1;\n transform: scale(1); /* 最终恢复正常大小 */\n }\n }\n /* 弹性离开动画(像果冻弹出) */\n @keyframes elasticOut {\n 0% {\n opacity: 1;\n transform: scale(1); /* 起始状态:正常大小 */\n }\n 30% {\n transform: scale(1.05); /* 先弹大一点(105%) */\n }\n 100% {\n opacity: 0;\n transform: scale(0.8); /* 最终缩小并消失 */\n }\n }\n \n \n .loading {\n opacity: 0.7;\n filter: blur(1px);\n }\n .loading-spinner {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 40px;\n height: 40px;\n border: 3px solid rgba(255,255,255,.3);\n border-radius: 50%;\n border-top-color: #fff;\n animation: spin 1s ease-in-out infinite;\n z-index: 20;\n }\n @keyframes spin {\n to { transform: translate(-50%, -50%) rotate(360deg); }\n }\n </style>\n `; } handle() { window.isListPage && (this.addSvgBtn(), this.bindClick().then()); } async addSvgBtn() { const _st = await storageManager.getSetting(); // 动态生成封面按钮: 根据 coverSites 和 hiddenSites 控制显示 const _coverSites = jhsGetCoverSites(); const _hidden = jhsGetHiddenSites(); const coverBtnsHtml = (stylePrefix) => COVER_ALL_KEYS.map(key => { const cls = COVER_SITE_CLASS[key]; const show = _coverSites.includes(key) && !_hidden.includes(key); const sty = show ? "" : "display:none;"; const nameKey = "siteName" + key.charAt(0).toUpperCase() + key.slice(1); const name = _st[nameKey] || key; return `<a class="site-btn ${cls}" style="${sty}${stylePrefix}color:white !important;margin-bottom: 5px;background-color:#71bb59;"><span style="opacity: 1;${stylePrefix.includes("inline") ? "display: inline; color:white !important" : ""}">${name}</span></a>`; }).join("\n "); $(this.getSelector().itemSelector).toArray().forEach((e => { let t = $(e); if (!(t.find(".tool-box").length > 0) && (r && t.find(".tags").append(`\n <div class="tool-box" style="margin-left: auto; display: flex; align-items: center">\n <span class="screenSvg" title="长缩略图" style="margin-right: 15px;">${this.screenSvg}</span>\n \n <span class="videoSvg" title="播放视频" style="margin-right: 15px;">${this.videoSvg}</span>\n \n <div class="more-tools-container handleSvg" style="position: relative; margin-right: 15px;">\n <div title="鉴定处理" style="padding: 5px; margin: -5px;opacity:.3">${this.handleSvg}</div>\n \n <div class="more-tools" style=" position: absolute; bottom: 33px; right: -30px; display: none;\n background-color: rgba(255, 255, 255, 0);z-index: 10;">\n <a class="menu-btn hasWatchBtn" style="background-color:${S};color:white !important;margin-bottom: 5px"><span style="opacity: 1;">${k}</span></a>\n <a class="menu-btn hasDownBtn" style="background-color:${x}; color:white !important;margin-bottom: 5px"><span style="opacity: 1;">${y}</span></a>\n <a class="menu-btn favoriteBtn" style="background-color:${w}; color:white !important;margin-bottom: 5px"><span style="opacity: 1;">${v}</span></a>\n <a class="menu-btn filterBtn" style="background-color:${f}; color:white !important;margin-bottom: 5px"><span style="opacity: 1;">${m}</span></a>\n </div>\n </div>\n \n <div class="more-tools-container siteSvg" style="position: relative; margin-right: 15px;">\n <div title="第三方网站" style="padding: 5px; margin: -5px;opacity:.3">${this.siteSvg}</div>\n \n <div class="more-tools" style=" position: absolute; bottom: 33px; right: -30px; display: none;\n background-color: rgba(255, 255, 255, 0);z-index: 10;">\n ${coverBtnsHtml("")}\n </div>\n </div>\n \n <div class="more-tools-container copySvg" style="position: relative; margin-right: 15px;">\n <div title="复制按钮" style="padding: 5px; margin: -5px;opacity:.3">${this.copySvg}</div>\n \n <div class="more-tools" style="\n position: absolute;\n bottom: 20px;\n right: -10px;\n display: none;\n background: white;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n border-radius: 20px;\n padding: 10px 0;\n margin-bottom: 15px;\n z-index: 10;\n ">\n <span class="carNumSvg" title="复制番号" style="padding: 5px 10px; white-space: nowrap;">${this.carNumSvg}</span>\n <span class="titleSvg" title="复制标题" style="padding: 5px 10px; white-space: nowrap;">${this.titleSvg}</span>\n <span class="downSvg" title="下载封面" style="padding: 5px 10px; white-space: nowrap;">${this.downSvg}</span>\n </div>\n </div>\n </div>\n `), l)) { if (t.find(".avatar-box").length > 0) return; t.find(".photo-info").append(`\n <div class="tool-box" style="display: flex; align-items: center;justify-content: flex-end">\n <span class="screenSvg" title="长缩略图" style="margin-right: 15px;">${this.screenSvg}</span>\n\n <span class="videoSvg" title="播放视频" style="margin-right: 15px;">${this.videoSvg}</span>\n \n <div class="more-tools-container handleSvg" style="position: relative; margin-right: 15px;">\n <div title="鉴定处理" style="padding: 5px; margin: -5px;opacity:.3">${this.handleSvg}</div>\n \n <div class="more-tools" style=" position: absolute; bottom: 33px; right: -30px; display: none;\n background-color: rgba(255, 255, 255, 0);z-index: 10;">\n <a class="menu-btn hasWatchBtn" style="background-color:${S};color:white;margin-bottom: 5px"><span style="opacity: 1;display: inline; color:white !important">${k}</span></a>\n <a class="menu-btn hasDownBtn" style="background-color:${x}; color:white;margin-bottom: 5px"><span style="opacity: 1;display: inline; color:white !important">${y}</span></a>\n <a class="menu-btn favoriteBtn" style="background-color:${w}; color:white;margin-bottom: 5px"><span style="opacity: 1;display: inline; color:white !important">${v}</span></a>\n <a class="menu-btn filterBtn" style="background-color:${f}; color:white;margin-bottom: 5px"><span style="opacity: 1;display: inline; color:white !important">${m}</span></a>\n </div>\n </div>\n \n <div class="more-tools-container siteSvg" style="position: relative; margin-right: 15px;">\n <div title="第三方网站" style="padding: 5px; margin: -5px;opacity:.3">${this.siteSvg}</div>\n \n <div class="more-tools" style=" position: absolute; bottom: 33px; right: -30px; display: none;\n background-color: rgba(255, 255, 255, 0);z-index: 10;">\n ${coverBtnsHtml("display: inline;")}\n </div>\n </div>\n \n <div class="more-tools-container copySvg" style="position: relative;">\n <div title="复制按钮" style="padding: 5px; margin: -5px;opacity:.3">${this.copySvg}</div>\n \n <div class="more-tools" style="\n max-width: 44px;\n position: absolute;\n bottom: 20px;\n right: -10px;\n display: none;\n background: white;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n border-radius: 20px;\n padding: 10px 0;\n margin-bottom: 15px;\n z-index: 10;\n text-align: center;\n ">\n <span class="carNumSvg" title="复制番号" style="padding: 5px 10px; white-space: nowrap;display: inline">${this.carNumSvg}</span>\n <span class="titleSvg" title="复制标题" style="padding: 5px 10px; white-space: nowrap;display: inline">${this.titleSvg}</span>\n <span class="downSvg" title="下载封面" style="padding: 5px 10px; white-space: nowrap;display: inline">${this.downSvg}</span>\n </div>\n </div>\n </div>\n `); } })), this.enableSvgBtn(); } async enableSvgBtn() { const e = await storageManager.getSetting(), {enableScreenSvg: t = _, enableVideoSvg: n = _, enableHandleSvg: a = _, enableSiteSvg: i = _, enableCopySvg: s = _} = e; [ { selector: ".screenSvg", enabled: t }, { selector: ".videoSvg", enabled: n }, { selector: ".handleSvg", enabled: a }, { selector: ".siteSvg", enabled: i }, { selector: ".copySvg", enabled: s } ].forEach((({selector: e, enabled: t}) => { $(e).toggle(t === _); })); } async bindClick() { this.getSelector(); const e = this.getBean("ListPagePlugin"); $(document).on("click", ".more-tools-container", (e => { e.preventDefault(); var t = $(e.target).closest(".more-tools-container").find(".more-tools"); $(".more-tools").not(t).stop(!0, !0).removeClass("elastic-in").addClass("elastic-out").hide(), t.is(":visible") ? t.stop(!0, !0).removeClass("elastic-in").addClass("elastic-out").hide() : t.stop(!0, !0).removeClass("elastic-out").addClass("elastic-in").show(); })), $(document).on("click", (function(e) { $(e.target).closest(".more-tools-container").length || $(".more-tools").stop(!0, !0).removeClass("elastic-in").addClass("elastic-out").hide(); })), $(document).on("click", ".videoSvg", (t => { t.preventDefault(), $('.videoSvg[title!="播放视频"]').each(((t, n) => { const a = $(n); let i = a.closest(".item"), s = i.find("img"), {carNum: o} = e.findCarNumAndHref(i); this.showImg(a, s, o), a.html(this.videoSvg).attr("title", "播放视频"); })); const n = $(t.target).closest(".item"), a = n.find(".videoSvg"); if ("播放视频" === a.attr("title")) { a.html(this.recoveryVideoSvg).attr("title", "切回封面"); const {carNum: t} = e.findCarNumAndHref(n); let i = n.find("img"); if (!i.length) return void show.error("没有找到图片"); this.showVideo(a, i, t).then(); } })), $(document).on("click", ".screenSvg", (async t => { t.preventDefault(); let n = loading(); try { const a = $(t.currentTarget).closest(".item"); let {carNum: i} = e.findCarNumAndHref(a); i = i.replace("FC2-", ""); const s = await this.getBean("ScreenShotPlugin").getScreenshot(i); n.close(), showImageViewer(s); } catch (a) { console.error("图片预览出错:", a), show.error("图片预览出错:" + a); } finally { n.close(); } })), $(document).on("click", ".filterBtn, .favoriteBtn, .hasDownBtn, .hasWatchBtn", (async t => { t.preventDefault(), t.stopPropagation(); try { const n = $(t.target).closest(".menu-btn"), a = n.closest(".item"), {carNum: i, url: s, publishTime: o} = e.findCarNumAndHref(a); // 判定本次点击对应的鉴定类型 let actionType = null; if (n.hasClass("filterBtn")) actionType = d; else if (n.hasClass("favoriteBtn")) actionType = h; else if (n.hasClass("hasDownBtn")) actionType = g; else if (n.hasClass("hasWatchBtn")) actionType = p; if (!actionType) return; // 已存在且是同一鉴定 → 第二次点击, 移除该鉴定 const existing = await storageManager.getCar(i); const isSameStatus = existing && existing.status === actionType; const save = async () => { // 先保存、先刷新, 演员名后台补录(避免等 parseActressName 网络请求) await storageManager.saveCar({ carNum: i, url: s, names: null, actionType, publishTime: o }), show.ok("操作成功"), window.refresh(); // 后台异步补录演员名, 不阻塞用户操作. // 注意: updateCarInfo 找不到记录会 show.error 弹窗, 所以先确认记录还在 e.parseActressName(s).then((async names => { if (!names) return; try { if (!await storageManager.getCar(i)) return; await storageManager.updateCarInfo({ carNum: i, url: s, names, actionType }); } catch (err) { clog.error("补录演员信息失败", err); } })).catch((err => clog.error("解析演员信息失败", err))); }; const doRemove = async () => { await storageManager.removeCar(i), window.refresh(), show.ok("已取消鉴定"); }; if (isSameStatus) { // 移除鉴定. 屏蔽这类需要二次确认, 其余直接取消 if (actionType === d) { utils.q(t, `是否取消屏蔽${i}?`, doRemove); } else { await doRemove(); } } else { // 首次标记, 保持原有行为: 屏蔽需二次确认, 其余直接保存 if (actionType === d) { utils.q(t, `是否屏蔽${i}?`, save); } else { await save(); } } $(".more-tools").stop(!0, !0).removeClass("elastic-in").addClass("elastic-out").hide(); } catch (err) { console.error("按钮点击处理失败:", err); } })); const _coverSetting = await storageManager.getSetting(); const coverSelector = COVER_ALL_KEYS.map(k => "." + COVER_SITE_CLASS[k]).join(", "); $(document).on("click", coverSelector, (t => { try { t.preventDefault(), t.stopPropagation(); const o = $(t.currentTarget), rItem = o.closest(".item"), {carNum: carNum} = e.findCarNumAndHref(rItem); // 根据 class 找到对应 siteKey let siteKey = null; for (const k of COVER_ALL_KEYS) { if (o.hasClass(COVER_SITE_CLASS[k])) { siteKey = k; break; } } if (!siteKey || !carNum) return; const url = jhsBuildCoverUrl(siteKey, carNum, _coverSetting); t && (t.ctrlKey || t.metaKey) ? GM_openInTab(url, { insert: 0 }) : window.open(url); } catch (t) { console.error("站点按钮处理失败:", t); } })), $(document).on("click", ".titleSvg, .carNumSvg, .downSvg", (t => { t.preventDefault(), t.stopPropagation(); const n = $(t.currentTarget).closest(".item"), {carNum: a, title: i} = e.findCarNumAndHref(n), s = n.find(l ? ".photo-frame img" : ".cover img"); $(t.currentTarget).hasClass("titleSvg") ? utils.copyToClipboard("标题", i) : $(t.currentTarget).hasClass("carNumSvg") ? utils.copyToClipboard("番号", a) : $(t.currentTarget).hasClass("downSvg") && fetch(s.attr("src")).then((e => e.blob())).then((e => { utils.download(e, a + " " + i + ".jpg"); })); })); } showImg(e, t, n) { e.html(this.videoSvg).attr("title", "播放视频"); let a = $(`#${`${n}_preview_video`}`); a.length > 0 && (a[0].pause(), a.parent().hide()), t.show(), t.removeClass("loading"), t.next(".loading-spinner").remove(); } async showVideo(e, t, n) { const a = `${n}_preview_video`; let i = $(`#${a}`); if (i.length > 0) return i.parent().show(), i[0].play(), void t.hide(); t.addClass("loading"), t.after('<div class="loading-spinner"></div>'); const s = t.attr("src"), o = await ne(n); if (!o) return show.error("未解析到视频"), void this.showImg(e, t, n); let r = await storageManager.getSetting("videoQuality"); r = Z(Object.keys(o), r); let c = o[r], d = `\n <div style="display: flex; justify-content: center; align-items: center; position: absolute; top:0; left:0; height: 100%; width: 100%; z-index: 10; overflow: hidden">\n <video \n src="${c}" \n poster="${s}" \n id="${a}" \n controls \n loop \n muted \n playsinline\n style="max-height: 100%; max-width: 100%; object-fit: contain"\n ></video>\n </div>\n `; l && (d = `\n <div>\n <video \n src="${c}" \n poster="${s}" \n id="${a}" \n controls \n loop \n muted \n playsinline\n style="max-height: 100%; max-width: 100%; object-fit: contain"\n ></video>\n </div>\n `), t.parent().append(d), t.hide(), t.removeClass("loading"), t.next(".loading-spinner").remove(), i = $(`#${a}`); let h = i[0]; h.load(), h.muted = !1, h.play(), i.trigger("focus"); } } class Oe extends X { constructor() { super(...arguments), i(this, "$contentBox", $(".section .container")), i(this, "urlParams", new URLSearchParams(window.location.search)), i(this, "sortVal", this.urlParams.get("sort") || "release_date"), i(this, "currentPage", this.urlParams.get("page") ? parseInt(this.urlParams.get("page")) : 1), i(this, "maxPage", null), i(this, "keyword", this.urlParams.get("keyword") || null); } getName() { return "Fc2By123AvPlugin"; } async getBaseUrl() { const e = this.getBean("OtherSitePlugin"); return await e.getAv123Url() + "/ja"; } handle() { $("#navbar-menu-hero > div > div:nth-child(1) > div > a:nth-child(4)").after('<a class="navbar-item" href="/advanced_search?type=100&released_start=2099-09">123Av-Fc2</a>'), $('.tabs li:contains("FC2")').after('<li><a href="/advanced_search?type=100&released_start=2099-09"><span>123Av-Fc2</span></a></li>'), o.includes("/advanced_search?type=100") && (this.hookPage(), this.handleQuery().then()); } hookPage() { let e = $("h2.section-title"); e.contents().first().replaceWith("123Av"), e.css("marginBottom", "0"), e.append('\n <div style="margin-left: 100px; width: 400px;">\n <input id="search-123av-keyword" type="text" placeholder="搜索123Av Fc2ppv内容" style="padding: 4px 5px;margin-right: 0">\n <a id="search-123av-btn" class="a-primary" style="margin-left: 0">搜索</a>\n <a id="clear-123av-btn" class="a-info" style="margin-left: 0">重置</a>\n </div>\n '), $("#search-123av-keyword").val(this.keyword), $("#search-123av-btn").on("click", (async () => { let e = $("#search-123av-keyword").val().trim(); e && (this.keyword = e, utils.setHrefParam("keyword", e), await this.handleQuery()); })), $("#clear-123av-btn").on("click", (async () => { $("#search-123av-keyword").val(""), this.keyword = "", utils.setHrefParam("keyword", ""), $(".page-box").show(), $(".tool-box").show(), await this.handleQuery(); })), $(".empty-message").remove(), $("#foldCategoryBtn").remove(), $(".section .container .box").remove(), $("#sort-toggle-btn").remove(), this.$contentBox.append('<div class="tool-box" style="margin-top: 10px"></div>'), this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>'), this.$contentBox.append('<div class="page-box"></div>'); $(".tool-box").append('\n <div class="button-group">\n <div class="buttons has-addons" id="conditionBox">\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="release_date">发布日期</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="recent_update">最近更新</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="trending">热门</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_today">今天最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_week">本周最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_month">本月最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed">最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_favourited">最受欢迎</a>\n </div>\n </div>\n '), $(`#conditionBox a[data-sort="${this.sortVal}"]`).addClass("is-info"), utils.setHrefParam("sort", this.sortVal), utils.setHrefParam("page", this.currentPage), $("#conditionBox").on("click", "a.button", (e => { let t = $(e.target); this.sortVal = t.data("sort"), utils.setHrefParam("sort", this.sortVal), t.siblings().removeClass("is-info"), t.addClass("is-info"), this.handleQuery(); })); $(".page-box").append('\n <nav class="pagination">\n <a class="pagination-previous">上一页</a>\n <ul class="pagination-list"></ul>\n <a class="pagination-next">下一页</a>\n </nav>\n '), $(document).on("click", ".pagination-link", (e => { e.preventDefault(), this.currentPage = parseInt($(e.target).data("page")), utils.setHrefParam("page", this.currentPage), this.renderPagination(), this.handleQuery(); })), $(".pagination-previous").on("click", (e => { e.preventDefault(), this.currentPage > 1 && (this.currentPage--, utils.setHrefParam("page", this.currentPage), this.renderPagination(), this.handleQuery()); })), $(".pagination-next").on("click", (e => { e.preventDefault(), this.currentPage < this.maxPage && (this.currentPage++, utils.setHrefParam("page", this.currentPage), this.renderPagination(), this.handleQuery()); })); } renderPagination() { const e = $(".pagination-list"); e.empty(); let t = Math.max(1, this.currentPage - 2), n = Math.min(this.maxPage, this.currentPage + 2); this.currentPage <= 3 ? n = Math.min(6, this.maxPage) : this.currentPage >= this.maxPage - 2 && (t = Math.max(this.maxPage - 5, 1)), t > 1 && (e.append('<li><a class="pagination-link" data-page="1">1</a></li>'), t > 2 && e.append('<li><span class="pagination-ellipsis">…</span></li>')); for (let a = t; a <= n; a++) { const t = a === this.currentPage ? " is-current" : ""; e.append(`<li><a class="pagination-link${t}" data-page="${a}">${a}</a></li>`); } n < this.maxPage && (n < this.maxPage - 1 && e.append('<li><span class="pagination-ellipsis">…</span></li>'), e.append(`<li><a class="pagination-link" data-page="${this.maxPage}">${this.maxPage}</a></li>`)); } async handleQuery() { let e = loading(); try { let e = []; e = 1 === this.currentPage ? [ 1, 2 ] : [ 2 * this.currentPage - 1, 2 * this.currentPage ], this.keyword && (e = [ 1 ], $(".page-box").hide(), $(".tool-box").hide()); const t = await this.getBaseUrl(), n = e.map((e => { let n = `${t}/tags/fc2?sort=${this.sortVal}&page=${e}`; return this.keyword && (n = `${t}/search?keyword=${this.keyword}`), gmHttp.get(n); })), a = await Promise.all(n); let i = []; for (const o of a) { let e = $(o); if (e.find(".box-item").each(((e, n) => { const a = $(n), s = a.find("img").attr("data-src"); let o = a.find("img").attr("title"); const r = a.find(".detail a"), l = r.attr("href"), c = t + (l.startsWith("/") ? l : "/" + l), d = r.text().trim().replace(o + " - ", ""); o = o.replace("FC2-PPV", "FC2"), i.push({ imgSrc: s, carNum: o, href: c, title: d }); })), !this.maxPage) { let t, n = e.find(".page-item:not(.disabled)").last(); if (n.find("a.page-link").length) { let e = n.find("a.page-link").attr("href"); t = parseInt(e.split("page=")[1]); } else t = parseInt(n.find("span.page-link").text()); this.maxPage = Math.ceil(t / 2), this.renderPagination(); } } if (0 === i.length) { clog.log(i), show.error("无结果"); let e = `${t}/dm4/tags/fc2?sort=${this.sortVal}`; this.keyword && (e = `${t}/search?keyword=${this.keyword}`), console.error("获取数据失败!", e); } let s = this.markDataListHtml(i); $(".movie-list").html(s), await utils.smoothScrollToTop(); } catch (t) { console.error(t); } finally { e.close(); } } async open123AvFc2Dialog(e, t) { let n = ""; await storageManager.getSetting("enableLoadOtherSite", _) === _ && (n = '<div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方站点: </strong></div>'); let a = `\n <div class="movie-detail-container">\n \x3c!-- <div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>\n <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n ${n}\n \n <div style="margin: 10px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:${f}"><span>${m}</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:${w}"><span>${v}</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:${x}"><span>${y}</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:${S};"><span>${k}</span></a>\n \n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links">\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!-- </div>--\x3e\n </div>\n `; layer.open({ type: 1, title: e, content: a, area: utils.getResponsiveArea(), skin: "movie-detail-layer", scrollbar: !1, success: (n, a) => { utils.setupEscClose(a), this.loadData(e, t); let i = e.replace("FC2-", ""); $("#magnets-content").append(this.getBean("MagnetHubPlugin").createMagnetHub(i)), $("#favoriteBtn").on("click", (async n => { const a = $("#data-actress").text(), i = $("#data-publishTime").text(); await storageManager.saveCar({ carNum: e, url: t, names: a, actionType: h, publishTime: i }), window.refresh(), layer.closeAll(); })), $("#filterBtn").on("click", (n => { utils.q(n, `是否屏蔽${e}?`, (async () => { const n = $("#data-actress").text(), a = $("#data-publishTime").text(); await storageManager.saveCar({ carNum: e, url: t, names: n, actionType: d, publishTime: a }), window.refresh(), layer.closeAll(), window.location.href.includes("collection_codes?movieId") && utils.closePage(); })); })), $("#hasDownBtn").on("click", (async n => { const a = $("#data-actress").text(), i = $("#data-publishTime").text(); await storageManager.saveCar({ carNum: e, url: t, names: a, actionType: g, publishTime: i }), window.refresh(), layer.closeAll(); })), $("#hasWatchBtn").on("click", (async n => { const a = $("#data-actress").text(), i = $("#data-publishTime").text(); await storageManager.saveCar({ carNum: e, url: t, names: a, actionType: p, publishTime: i }), window.refresh(), layer.closeAll(); })), $("#search-subtitle-btn").on("click", (t => utils.openPage(`https://subtitlecat.com/index.php?search=${e}`, e, !1, t))), $("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(e))); let s = e.replace("FC2-", ""); this.getBean("OtherSitePlugin").loadOtherSite(s, e).then(); } }); } async loadData(e, t) { let n = loading(); try { const {id: n, publishDate: a, title: i, moviePoster: s} = await this.get123AvVideoInfo(t); $(".movie-info-container").html(`\n <h3 class="movie-title" style="margin-bottom: 10px"><strong class="current-title">${escapeHtml(i || "无标题")}</strong></h3>\n <div class="movie-meta" style="margin-bottom: 10px">\n <span><strong>番号: </strong>${e || "未知"}</span>\n <span><strong>年份: </strong>${a || "未知"}</span>\n <span>\n <strong>站点: </strong>\n <a href="https://fc2ppvdb.com/articles/${e.replace("FC2-", "")}" target="_blank">fc2ppvdb</a>\n <a style="margin-left: 5px;" href="https://adult.contents.fc2.com/article/${e.replace("FC2-", "")}/" target="_blank">fc2电子市场</a>\n </span>\n </div>\n <div class="movie-actors" style="margin-bottom: 10px">\n <div class="actor-list"><strong>主演: </strong></div>\n </div>\n <div class="movie-seller" style="margin-bottom: 10px">\n <span><strong>販売者: </strong></span>\n </div>\n <div class="movie-gallery" style="margin-bottom: 10px">\n <strong>剧照: </strong>\n <div class="image-list"></div>\n </div>\n \n <div id="data-publishTime" style="display: none">${a || ""}</div>\n\n `), this.getImgList(e).then(), this.getActressInfo(e).then(), this.getBean("TranslatePlugin").translate(e, !1).then(); } catch (a) { console.error(a); } finally { n.close(); } } handleLongImg(e) { utils.loopDetector((() => $(".movie-gallery .image-list").length > 0), (async () => { $(".movie-gallery .image-list").prepend(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 150px;max-width:150px; text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> '); const t = await this.getBean("ScreenShotPlugin").getScreenshot(e); t && ($(".screen-container").html(`<img src="${t}" alt="" loading="lazy" style="width: 100%;">`), $(".screen-container").on("click", (e => { e.stopPropagation(), e.preventDefault(), showImageViewer(e.currentTarget); }))); })); } async get123AvVideoInfo(e) { const t = await gmHttp.get(e), n = t.match(/v-scope="Movie\({id:\s*(\d+),/), a = n ? n[1] : null, i = utils.htmlTo$dom(t); return { id: a, publishDate: i.find('span:contains("リリース日:")').next("span").text(), title: i.find("h1").text().trim(), moviePoster: i.find("#player").attr("data-poster") }; } async getActressInfo(e) { let t = `https://fc2ppvdb.com/articles/${e.replace("FC2-", "")}`; const n = await gmHttp.get(t), a = $(n), i = a.find("div").filter((function() { return 0 === $(this).text().trim().indexOf("女優:"); })); if (0 === i.length || i.length > 1) return void show.error("解析女优信息失败"); const s = $(i[0]).find("a"); let o = "<strong>主演: </strong>"; if (s.length > 0) { let e = ""; s.each(((t, n) => { let a = $(n), i = a.text(), s = a.attr("href"); o += `<span class="actor-tag"><a href="https://fc2ppvdb.com${escapeHtml(s)}" target="_blank">${escapeHtml(i)}</a></span>`, e += i + " "; })), $("#data-actress").text(e); } else o += "<span>暂无演员信息</span>"; $(".actor-list").html(o); const r = a.find("div").filter((function() { return 0 === $(this).text().trim().indexOf("販売者:"); })); if (r.length > 0) { const e = $(r[0]).find("a"); if (e.length > 0) { const t = $(e[0]); let n = t.text(), a = t.attr("href"); $(".movie-seller").html(`<span><strong>販売者: </strong><a href="https://fc2ppvdb.com${escapeHtml(a)}" target="_blank">${escapeHtml(n)}</a></span>`); } } } async getImgList(e) { let t = e.replace("FC2-", ""), n = `https://adult.contents.fc2.com/article/${e.replace("FC2-", "")}/`; const a = await gmHttp.get(n, null, { referer: n }); let i = $(a).find(".items_article_SampleImagesArea img").map((function() { return $(this).attr("src"); })).get(), s = ""; Array.isArray(i) && i.length > 0 ? s = i.map(((e, t) => `\n <a href="${e}" data-fancybox="movie-gallery" data-caption="剧照 ${t + 1}">\n <img src="${e}" class="movie-image-thumb" loading="lazy" alt=""/>\n </a>\n `)).join("") : $(".movie-gallery").html("<h4>剧照: 暂无剧照</h4>"), $(".image-list").html(s), this.handleLongImg(t); } async getMovie(e, t) { let n = `${await this.getBaseUrl()}/ajax/v/${e}/videos`, a = loading(); try { let e = (await gmHttp.get(n)).result.watch; return e.length > 0 ? (e.forEach((e => { e.url = e.url + "?poster=" + t; })), e) : null; } catch (i) { console.error(i); } finally { a.close(); } } markDataListHtml(e) { let t = ""; return e.forEach((e => { t += `\n <div class="item">\n <a href="${e.href}" class="box" title="${e.title}">\n <div class="cover ">\n <img loading="lazy" src="${e.imgSrc.replace("/s360", "")}" alt="">\n </div>\n <div class="video-title"><strong>${e.carNum}</strong> ${e.title}</div>\n <div class="score">\n </div>\n <div class="meta">\n </div>\n <div class="tags has-addons">\n </div>\n </a>\n </div>\n `; })), t; } } /** 磁力评分:基于做种数/分辨率/字幕/新鲜度/完整性,返回 0-100 分 */ function calcMagnetScore(e) { let t = 0; const n = (e.seeders || 0); const seedersScore = n >= 50 ? 35 : n >= 10 ? 25 : n >= 1 ? 15 : 3; t += seedersScore; const a = (e.title || "").toLowerCase(); const resolutionScore = /4k|2160p/.test(a) ? 25 : /1080p/.test(a) ? 20 : /720p/.test(a) ? 15 : 5; t += resolutionScore; const subtitleScore = /-c\b|-uc\b|chinese|中字|字幕/.test(a) ? 20 : 0; t += subtitleScore; const i = e.date ? _daysSince(e.date) : 999; const freshnessScore = i <= 7 ? 15 : i <= 30 ? 12 : i <= 90 ? 8 : 3; t += freshnessScore; const completenessScore = /sample|预告|trailer/.test(a) ? -15 : 0; t += completenessScore; return { total: Math.max(0, Math.min(100, t)), seeders: seedersScore, resolution: resolutionScore, subtitle: subtitleScore, freshness: freshnessScore, completeness: completenessScore }; } function _daysSince(e) { try { const t = new Date(e); if (isNaN(t.getTime())) return 999; return Math.max(0, Math.floor((Date.now() - t.getTime()) / 864e5)); } catch (t) { return 999; } } class Re extends X { constructor() { super(...arguments), i(this, "currentEngine", null), i(this, "searchEngines", [ { name: "U9A9", id: "u9a9", url: "https://u9a9.com/?type=2&search={keyword}", targetPage: "https://u9a9.com/?type=2&search={keyword}", parseHtml: this.parseTorrentList }, { name: "U3C3", id: "u3c3", url: "https://u3c3.com/?search2=a8lr16lo&search={keyword}", targetPage: "https://u3c3.com/?search2=a8lr16lo&search={keyword}", parseHtml: this.parseTorrentList }, { name: "Sukebei", id: "Sukebei", url: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}", targetPage: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}", parseHtml: this.parseTorrentList } ]); } getName() { return "MagnetHubPlugin"; } async initCss() { return "\n <style>\n .magnet-container {\n margin: 20px auto;\n width: 100%;\n font-family: Arial, sans-serif;\n }\n .magnet-tabs {\n display: flex;\n border-bottom: 1px solid #ddd;\n margin-bottom: 15px;\n justify-content: space-between;\n }\n .magnet-tab {\n padding: 5px 12px;\n cursor: pointer;\n border: 1px solid transparent;\n border-bottom: none;\n margin-right: 5px;\n background: #f5f5f5;\n border-radius: 5px 5px 0 0;\n }\n .magnet-tab.active {\n background: #fff;\n border-color: #ddd;\n border-bottom: 1px solid #fff;\n margin-bottom: -1px;\n font-weight: bold;\n }\n .magnet-tab:hover:not(.active) {\n background: #e9e9e9;\n }\n \n .magnet-results {\n min-height: 200px;\n }\n .magnet-result {\n padding: 15px;\n border-bottom: 1px solid #eee;\n position: relative; \n }\n .magnet-result:hover {\n background-color: #f9f9f9;\n }\n .magnet-title {\n font-weight: bold;\n margin-bottom: 5px;\n white-space: nowrap;\n overflow: hidden; \n text-overflow: ellipsis;\n padding-right: 80px; \n }\n .magnet-info {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n color: #666;\n margin-bottom: 5px;\n }\n .magnet-loading {\n text-align: center;\n padding: 20px;\n }\n .magnet-error {\n color: #f44336;\n padding: 10px;\n }\n \n .magnet-copy {\n position: absolute;\n right: 15px;\n top: 12px;\n }\n .magnet-hub-btn {\n background-color: #f0f0f0;\n color: #555;\n border: 1px solid #ddd;\n padding: 3px 8px;\n border-radius: 3px;\n cursor: pointer;\n font-size: 12px;\n transition: all 0.2s;\n margin-left: 10px;\n }\n .magnet-hub-btn:hover {\n background-color: #e0e0e0;\n border-color: #ccc;\n }\n .magnet-hub-btn.copied {\n background-color: #4CAF50;\n color: white;\n border-color: #4CAF50;\n }\n </style>\n "; } createMagnetHub(e) { e = e.replace("FC2-", ""); const t = $('<div class="magnet-container"></div>'), n = $('<div class="magnet-tabs"></div>'), a = "jhs_magnetHub_selectedEngine", i = localStorage.getItem(a); let s = 0; const o = $('<div style="display: flex;"></div>'); this.searchEngines.forEach(((e, t) => { const n = $(`<div class="magnet-tab" data-engine="${e.id}">${e.name}</div>`); i && e.id === i ? (n.addClass("active"), this.currentEngine = e, s = t) : 0 !== t || i || (n.addClass("active"), this.currentEngine = e), o.append(n); })), n.append(o), n.append(`<a style="margin-right: 20px;margin-top:3px" id="targetBox" href="${this.currentEngine.targetPage.replace("{keyword}", encodeURIComponent(e))}" target="_blank">原网页</a>`), t.append(n); const r = $('<div class="magnet-results"></div>'); return t.append(r), t.on("click", ".magnet-tab", (n => { const i = $(n.target).data("engine"); this.currentEngine = this.searchEngines.find((e => e.id === i)), $("#targetBox").attr("href", this.currentEngine.targetPage.replace("{keyword}", encodeURIComponent(e))), localStorage.setItem(a, i), t.find(".magnet-tab").removeClass("active"), $(n.target).addClass("active"), this.searchEngine(r, this.currentEngine, e); })), this.searchEngine(r, this.currentEngine || this.searchEngines[s], e), t; } async searchEngine(e, t, n) { e.html(`<div class="magnet-loading">正在从 ${escapeHtml(t.name)} 搜索 "${escapeHtml(n)}"...</div>`); const a = `${t.name}_${n}`; if (t.parseHtml) try { const i = t.url.replace("{keyword}", encodeURIComponent(n)), s = await storageManager.cachedRequest(`magnet:${t.id}:${n}`, 216e5, (() => gmHttp.get(i).then((e => t.parseHtml.call(this, e, n))))); return void this.displayResults(e, s, t.name); } catch (s) { return void e.html(`<div class="magnet-error">解析 ${escapeHtml(t.name)} 结果失败: ${escapeHtml(s.message)}</div>`); } t.parseJson && await t.parseJson.call(this, e, t, n, a); } displayResults(e, t, n) { function a(e) { const t = e.text(); e.addClass("copied").text("已复制"), setTimeout((() => { e.removeClass("copied").text(t); }), 2e3); } function i(e, t) { const n = document.createElement("textarea"); n.value = e, n.style.position = "fixed", document.body.appendChild(n), n.select(); try { document.execCommand("copy"), a(t); } catch (i) { console.error("复制失败:", i), alert("复制失败,请手动复制链接"); } document.body.removeChild(n); } e.empty(), 0 !== t.length ? (t.forEach((e => { e._score = this.calcMagnetScore(e); })), t.sort(((e, t) => t._score.total - e._score.total)), t.forEach((t => { const n = t._score ? t._score.total : 0, a = n >= 80 ? "高" : n >= 60 ? "中" : "低", i = t._score ? `做种:${t._score.seeders}/35 分辨率:${t._score.resolution}/25 字幕:${t._score.subtitle}/20 新鲜度:${t._score.freshness}/15 完整性:${t._score.completeness}/5` : ""; const safeTitle = escapeHtml(t.title), safeMagnet = escapeHtml(t.magnet); $(`\n <div class="magnet-result">\n <div class="magnet-title">\n <span class="magnet-score" title="${i}" style="cursor:help;font-size:14px;margin-right:6px;">${a} ${n}</span>\n <a href="${safeMagnet}">${safeTitle}</a>\n </div>\n <div class="magnet-info">\n <span>大小: ${t.size || "未知"}</span>\n <span>做种: ${t.seeders || "—"}</span>\n <span>日期: ${t.date || "未知"}</span>\n </div>\n <div class="magnet-copy">\n <button class="magnet-hub-btn copy-btn" data-magnet="${safeMagnet}">复制链接</button>\n </div>\n </div>\n `).appendTo(e); })), e.on("click", ".copy-btn", (function() { const e = $(this), t = e.data("magnet"); navigator.clipboard ? navigator.clipboard.writeText(t).then((() => { a(e); })).catch((n => { i(t, e); })) : i(t, e); }))) : e.append('<div class="magnet-error">没有找到相关结果</div>'); } async parseBTSOW(e, t, n, a) { try { const i = await gmHttp.post(t.url, [{ search: n }, 50, 1]), s = i.data || [], o = []; for (let e = 0; e < s.length; e++) { let t = s[e]; o.push({ title: t.name, magnet: "magnet:?xt=urn:btih:" + t.hash, size: (t.size / 1073741824).toFixed(2) + " GB", date: utils.formatDate(new Date(1e3 * t.lastUpdateTime)) }); } o.length > 0 && this.displayResults(e, o, t.name); } catch (i) { e.html(`<div class="magnet-error">从 ${escapeHtml(t.name)} 获取数据失败: ${escapeHtml(String(i.message || i))}</div>`); } } parseTorrentList(e, t) { const n = utils.htmlTo$dom(e), a = []; return n.find(".torrent-list tbody tr").each(((e, n) => { const i = $(n); if (i.text().includes("置顶")) return; const s = i.find("td:nth-child(2) a").attr("title") || i.find("td:nth-child(2) a").text().trim(); if (!s.toLowerCase().includes(t.toLowerCase())) return; const o = i.find("td:nth-child(3) a[href^='magnet:']").attr("href"), r = i.find("td:nth-child(4)").text().trim(), l = i.find("td:nth-child(5)").text().trim(), c = parseInt(i.find("td:nth-child(6)").text().trim()) || 0, d = parseInt(i.find("td:nth-child(7)").text().trim()) || 0; o && a.push({ title: s, magnet: o, size: r, date: l, seeders: c, leechers: d }); })), a; } calcMagnetScore(e) { return calcMagnetScore(e); } } class Ve extends X { getName() { return "ScreenShotPlugin"; } async handle() { this.loadScreenShot().then(); } async loadScreenShot() { if (!isDetailPage) return; if ("yes" !== await storageManager.getSetting("enableLoadScreenShot", "yes")) return; let e = this.getPageInfo().carNum; r && $(".preview-images .tile-item").first().before(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 215px;text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> '), l && $("#sample-waterfall .sample-box:first").after(' <a class="sample-box screen-container" style="overflow:hidden; height: 110px; text-align:center;"><div style="margin-top: 30px;color: #000;cursor: auto">正在加载缩略图</div></a> '); try { const t = await this.getScreenshot(e); this.addImg("缩略图", t), clog.log("加载缩略图:", t); } catch (t) { this.showErrorFallback(e, t); } } async getScreenshot(e) { const t = localStorage.getItem("jhs_screenShot") ? JSON.parse(localStorage.getItem("jhs_screenShot")) : {}; if (t[e]) return clog.debug("缓存中存在缩略图:", e, t[e]), t[e]; let n; try { n = await storageManager.cachedRequest(`screenshot:${e}`, 6048e5, (() => Promise.any([ this.getJavStoreScreenShot(e) ]))); } catch (i) { throw clog.error("获取缩略图资源失败:", n, i), i; } if (!n) return this.showErrorFallback(e, null), null; const a = n.indexOf("https://"); return -1 !== a && (n = n.substring(a)), t[e] = n, clog.log("缩略图获取成功:", n), localStorage.setItem("jhs_screenShot", JSON.stringify(t)), n; } async getJavStoreScreenShot(e) { let t = `https://javstore.net/search/${e}.html`; clog.log("正在解析缩略图:", t); let n = await gmHttp.get(t); const a = utils.htmlTo$dom(n); let i = null; if (a.find("#content_news h3 span a").each((function() { if ($(this).attr("title").toLowerCase().includes(e.toLowerCase())) return i = $(this).attr("href"), !1; })), !i) return clog.error("JavStore, 查询番号失败:", t), null; let s = await gmHttp.get(i); const o = utils.htmlTo$dom(s); let r = o.find("a:contains('CLICK HERE')").attr("href") || o.find("img[src*='_s.jpg']").attr("src"); return r ? r.replace(".th", "") : (clog.error("JavStore, 解析预览图失败:", t), null); } async getJavBestScreenShot(e) { let t = `https://javbest.net/?s=${e}`; clog.log("正在解析缩略图:", t); let n = await gmHttp.get(t); const a = utils.htmlTo$dom(n), i = a.find(".app_loop_thumb a").first().attr("href"); if (!i) throw clog.error("解析JavBest搜索页失败:", t), new Error("解析JavBest搜索页失败"); const s = a.find(".app_loop_thumb a").first().attr("title"); if (!s.toLowerCase().includes(e.toLowerCase())) throw clog.error("解析JavBest搜索页失败:", s), new Error("解析JavBest搜索页失败"); const o = await gmHttp.get(i); let r = $(o).find('#content a img[src*="_t.jpg"]').attr("src"); if (!r) throw clog.error("解析JavBest缩略图失败:", t), new Error("解析JavBest缩略图失败"); return r = r.replace("_t", "").replace("http:", "https:"), r; } async getJavFreeScreenShot(e) { let t = `https://javfree.me/search/${e}/`, n = await gmHttp.get(t); const a = utils.htmlTo$dom(n).find("article h2.entry-title a"); if (!a || 0 === a.length) throw clog.error("解析JavFree搜索页失败:", t), new Error("解析JavFree搜索页失败"); let i = $(a[0]).attr("href"), s = await gmHttp.get(i); const o = utils.htmlTo$dom(s).find("#main > article > .entry-content > p img"); if (!o || 0 === o.length) throw clog.error("解析JavFree详情页失败:", i), new Error("解析JavFree详情页失败"); const r = o.filter((function() { const e = $(this).attr("src"); return e && e.toLowerCase().endsWith(".jpeg"); })).map((function() { return $(this).attr("src"); })).get(); return r.at(-1); } addImg(e, t) { t && (r && $(".screen-container").html(`<img src="${t}" alt="${e}" loading="lazy" style="width: 100%;">`), l && $(".screen-container").html(`<div class="photo-frame"><img src="${t}" style="height: inherit;width: 100%;" title="${e}" alt="${e}"></div>`), $(".screen-container").on("click", (e => { e.stopPropagation(), e.preventDefault(), showImageViewer(e.currentTarget); }))); } showErrorFallback(e, t) { var n; console.error("获取缩略图失败:", null == (n = null == t ? void 0 : t.message) ? void 0 : n.substring(0, 100)); let a = l ? "margin-top: 30px" : "margin-top: 50px"; $(".screen-container").html(`<div style="${a}; cursor:auto;color:#000;">获取缩略图失败</div><br/><a href='#' class='retry-link'>点击重试</a> 或 <a class="check-link" href='https://javstore.net/search/${e}.html' target='_blank'>前往确认</a>`).off("click", ".retry-link").off("click", ".check-link").on("click", ".retry-link", (async t => { t.stopPropagation(), t.preventDefault(), $(".screen-container").html(`<div style="${a};cursor:auto;color:#000;">正在重新加载...</div>`); try { const t = await this.getScreenshot(e); this.addImg("缩略图", t); } catch (n) { this.showErrorFallback(e, n); } })).on("click", ".check-link", (async t => { t.stopPropagation(), t.preventDefault(), window.open(`https://javstore.net/search/${e}.html`, "_blank"); })); } } class Xe extends X { getName() { return "FavoriteActressesPlugin"; } async handle() { this.bindEvent(), await this.highlightActress(), this.replaceActressAvatar(); } async highlightActress() { if (!isDetailPage) return; if (await storageManager.getSetting("enableFavoriteActresses", _) !== _) return; const e = await storageManager.getFavoriteActressList(); if (!e || 0 === e.length) return; const t = new Set; e.forEach((e => { e.starId && t.add(String(e.starId).trim()); })), 0 !== t.size && $(".female").prev().each(((e, n) => { const a = $(n), i = a.attr("href"); let s = null; if (i) { const e = (i.endsWith("/") ? i.slice(0, -1) : i).split("/"), t = e[e.length - 1]; t && (s = t.trim()); } let o = !1; s && (o = t.has(s)), o && (a.addClass("highlighted"), a.attr("title", "高亮已收藏演员, 可在设置-基础配置中关闭")); })); } async removeActorFromStorage(e) { await storageManager.removeFavoriteActress(e) && clog.log("移除演员成功"); } bindEvent() { const e = /\/actors\/(\w+)\/(collect|uncollect)/; $(document).on("confirm:complete", 'a[href*="/actors/"][href*="/uncollect"]', (async t => { const [n] = t.detail; if (!n) return; const a = $(t.currentTarget).attr("href").match(e), i = a ? a[1] : null; i && await this.removeActorFromStorage(i); })), $("#button-collect-actor").click((async t => { const n = $("#button-collect-actor").attr("href").match(e), a = n ? n[1] : null; let i = [], s = $(".actor-section-name"); s.length && s.text().trim().split(",").forEach((e => { i.push(e.trim()); })); let o = $(".section-meta:not(:contains('影片'))"); if (o.length && o.text().trim().split(",").forEach((e => { i.push(e.trim()); })), !i.length) return void clog.error("获取演员名称失败"); const r = i[0]; if (!a) return void clog.error("无法获取演员ID进行收藏操作。"); const l = ($(".avatar").first().css("background-image") || "").replace(/^url\(["']?|["']?\)$/g, ""), c = { starId: a, name: r, allName: i, avatar: l }; 1 === await storageManager.addFavoriteActressList([ c ]) ? clog.log(`收藏演员成功: ${r} (ID: ${a})`) : clog.log(`收藏演员失败: ${r} (ID: ${a})`); })), $("#button-uncollect-actor").click((async t => { const n = $("#button-uncollect-actor").attr("href").match(e), a = n ? n[1] : null; a ? await this.removeActorFromStorage(a) : clog.error("无法获取演员ID进行取消收藏操作。"); })); } async replaceActressAvatar() { const e = this.getActressId(); if (!e) return; const t = (await storageManager.getFavoriteActressList()).find((t => t.starId === e)); if (t && t.avatar) { const e = `url('${t.avatar}')`; let n = $(".avatar").first(); if (0 === n.length) { const e = '<div class="column actor-avatar"> <div class="image"> <span class="avatar"></span> </div> </div>'; $(".section-columns").prepend(e), n = $(".avatar").first(); } if (0 === n.length) return; n.css("background-image").trim().toLowerCase() !== e.trim().toLowerCase() && (n.css("background-image", e), n.css("background-size", "cover"), n.css("background-position", "top center"), n.css("background-repeat", "no-repeat")); } } } class Qe extends X { getName() { return "BusImgPlugin"; } handle() {} async getVisibleImageItems(e, t) { let n = []; const a = document.querySelectorAll(e); for (const i of a) { if (!utils.isHidden(i)) { const e = i.querySelector(t); if (!(e instanceof HTMLImageElement)) continue; e.style.removeProperty("height"); let a = e.offsetHeight; a > 0 && n.push({ element: i, imgElement: e, height: a }); } } return n; } async logImageHeightsByRow() { if (await storageManager.getSetting("enableVerticalModel", C) === _) return; // applyImageMode 已注入固定比例容器样式, 不再手动调整 img 高度 if ($("#verticalImgStyle").length) return; const e = this.getSelector().itemSelector, t = await storageManager.getSetting("containerColumns", 5), n = await this.getVisibleImageItems(e, "img"); if (0 === n.length) return; const a = []; for (let i = 0; i < n.length; i++) { const e = Math.floor(i / t); a[e] || (a[e] = []), a[e].push(n[i]); } a.forEach(((e, t) => { const n = e.map((e => e.height)); if (n.length < 2) return; const a = Math.min(...n), i = Math.max(...n); let s = 0; i - a > 50 && (s = a, e.forEach((e => { if (e.height !== s) { const t = `${s}px`; e.imgElement.style.setProperty("height", t, "important"); } }))); })); } } class Ze extends X { getName() { return "TranslatePlugin"; } async initCss() { return "\n <style> \n .translated-title {\n margin-top: 8px; \n padding: 12px; \n border-radius: 5px; \n border-left: 4px solid rgb(76, 175, 80);\n background: linear-gradient(135deg, rgb(255, 255, 255) 0%, rgb(245, 245, 245) 100%); \n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);\n font-size: 20px;\n }\n </style>\n "; } handle() { isDetailPage && this.translate(); } async translate(e, t = !0) { if (await storageManager.getSetting("translateTitle", _) !== _) return; l && (t = !1); let n = $(".origin-title"); if (n.length || (n = $(".current-title")), n.length || (n = $("h3")), !n.length) return; const a = n.text().trim(); if (!a) return void show.error("获取标题失败, 无法进行翻译"); n.after('<div class="translated-title">翻译中...</div>'); const i = n.next(".translated-title"); e || (e = this.getPageInfo().carNum); const s = localStorage.getItem("jhs_translate") ? JSON.parse(localStorage.getItem("jhs_translate")) : {}; s[e] ? i.html(t ? e + " " + s[e] : s[e]) : _e(a, "ja", "zh-CN").then((n => { i.html(t ? e + " " + n : n); })).catch((e => { console.error("翻译失败:", e), i.replaceWith(`<div class="translated-title" style="color: red;">翻译失败: ${escapeHtml(e.message)}</div>`); })); } } class et extends X { constructor() { super(...arguments), i(this, "singleTaskKey", "checkNewActressActorFilterCar"), i(this, "taskConfig", null), i(this, "storageQueue", new ve), i(this, "lastCheckFavoriteActressTimeKey", "jhs_time_checkFavoriteActress"), i(this, "lastCheckBlacklistTimeKey", "jhs_time_checkBlacklist"), i(this, "lastCheckNewVideoTimeKey", "jhs_time_checkNewVideo"); } getName() { return "TaskPlugin"; } async limitConcurrency(e, t, n, a) { this.showIsRun(); const i = [], s = e.length; let o = 0, _consecutiveFails = 0; const MAX_CONSECUTIVE_FAILS = 5; for (const r of e) { // 连续失败过多时中止, 避免 Cloudflare 拦截下浪费大量请求 if (_consecutiveFails >= MAX_CONSECUTIVE_FAILS) { clog.error(`连续失败 ${MAX_CONSECUTIVE_FAILS} 次, 提前中止任务`); show.error("检测任务: 连续失败过多, 已中止 (可能被 Cloudflare 拦截)"); break; } const e = a(r).then(() => { _consecutiveFails = 0; }).catch(() => { _consecutiveFails++; }).finally((() => { i.splice(i.indexOf(e), 1); })); if (i.push(e), o++, i.length >= t) { const e = s - o; clog.debug(`剩余任务数: <span style="color: #f40">${e}</span>`), await Promise.race(i), await utils.sleep(n); } } await Promise.all(i); } isUnnecessaryCheck(e, t) { if (!t) throw new Error("未传入checkIntervalTime"); t = parseInt(t); return utils.getHourDifference(new Date(e), new Date) < t; } handle() { this.doTask().then(); } showIsRun() { show.info("正在执行检测任务中, 请勿关闭当前窗口", { duration: 3e3 }); } async doTask() { if (isListPage) return await this.loadConfig(), this.javDbUrl = await this.getBean("OtherSitePlugin").getJavDbUrl(), // 延迟 5 秒再开始后台任务, 给页面渲染和用户交互让出优先级 await utils.sleep(5e3), navigator.locks.request(this.singleTaskKey, { ifAvailable: !0 }, (async e => { if (e) { if (isListPage && (this.taskConfig.enableCheckBlacklist === _ ? await this.checkBlacklist() : clog.warn("自动检测屏蔽黑名单-禁用"), !l)) { if (this.taskConfig.enableCheckFavoriteActress === _) { const e = localStorage.getItem(this.lastCheckFavoriteActressTimeKey), t = this.taskConfig.checkFavoriteActress_IntervalTime, n = e && this.isUnnecessaryCheck(e, t), a = $('a[href*="/users/profile"]').length > 0; n && clog.debug(`检测同步演员, 上次检测时间: ${e} 检测间隔时间: ${t}小时 未到时间`), !n && a && await this.checkFavoriteActress(); } else clog.warn("自动同步已收藏的演员-禁用"); this.taskConfig.enableCheckNewVideo === _ ? await this.checkNewVideo() : clog.warn("自动检测已收藏演员的最新作品-禁用"); } } else clog.debug("争夺任务锁失败, 跳过执行"); })).catch((e => { console.error("锁任务出现错误:", e), clog.error("锁任务出现错误:", e); })).finally((() => { setTimeout((() => { this.doTask(); }), 3e5); })); } async loadConfig() { const e = await storageManager.getSetting(); this.taskConfig = { checkConcurrencyCount: e.checkConcurrencyCount ? Number(e.checkConcurrencyCount) : 2, checkRequestSleep: e.checkRequestSleep ? Number(e.checkRequestSleep) : 100, enableCheckBlacklist: e.enableCheckBlacklist || _, checkBlacklist_intervalTime: e.checkBlacklist_intervalTime ? Number(e.checkBlacklist_intervalTime) : 12, checkBlacklist_ruleTime: e.checkBlacklist_ruleTime ? Number(e.checkBlacklist_ruleTime) : 8760, enableCheckFavoriteActress: e.enableCheckFavoriteActress || _, checkFavoriteActress_IntervalTime: e.checkFavoriteActress_IntervalTime ? Number(e.checkFavoriteActress_IntervalTime) : 24, enableCheckNewVideo: e.enableCheckNewVideo || _, checkNewVideo_intervalTime: e.checkNewVideo_intervalTime ? Number(e.checkNewVideo_intervalTime) : 12, checkNewVideo_ruleTime: e.checkNewVideo_ruleTime ? Number(e.checkNewVideo_ruleTime) : 8760 }; } async checkBlacklist(e) { let t = await storageManager.getBlacklist(); if (0 === t.length) return; t = t.sort(((e, t) => e.createTime < t.createTime ? 1 : e.createTime > t.createTime ? -1 : 0)); const n = this.taskConfig.checkConcurrencyCount, a = this.taskConfig.checkRequestSleep, i = this.taskConfig.checkBlacklist_intervalTime, s = this.taskConfig.checkBlacklist_ruleTime, o = localStorage.getItem(this.lastCheckBlacklistTimeKey); if (!e && o && this.isUnnecessaryCheck(o, i)) return void clog.debug(`检测黑名单, 上次检测时间: ${o} 检测间隔时间: ${i}小时 未到时间`); const r = [], l = []; for (const h of t) { let t = h.name, n = h.checkTime, a = h.lastPublishTime, o = h.url; if (new URL(window.location.href).hostname === new URL(o).hostname) { if (e || !n || !this.isUnnecessaryCheck(n, i)) if (!a || 0 === s || this.isUnnecessaryCheck(a, s)) r.push(h); else { let e = `检测黑名单: ${t} ${a} 停更超过${s / 24 / 365}年,跳过检测`; l.push(e), $("#checkBlacklistMsg").text(e); } } else clog.log("黑名单地址非同域名,跳过", o); } if (0 === r.length) return; l.forEach((e => { clog.log(e); })), clog.log(`<span style='color: #f40'>检测屏蔽黑名单, 总任务数: ${r.length}, 并发限制:${n}, 请求间隔时间:${a}ms</span>`); const c = this.getBean("BlacklistPlugin"); await this.limitConcurrency(r, n, a, (async e => { let {starId: t, name: n, url: a} = e; try { clog.log("正在检屏黑名单演员:", n, a), $("#checkBlacklistMsg").text(`正在检屏黑名单演员: ${n} ${a}`); const e = await gmHttp.get(a), i = utils.htmlTo$dom(e); this.storageQueue.addTask((async () => { let {lastPublishTime: e} = await c.parseAndSaveFilterInfo(i, n, t); await storageManager.updateBlacklistItem({ starId: t, name: n, checkTime: utils.getNowStr(), lastPublishTime: e }); })); } catch (i) { $("#checkBlacklistMsg").text(`检测屏蔽演员信息, 发生错误: ${a}`), clog.error("检测屏蔽演员信息, 发生错误:", a, i), show.error("检测屏蔽演员信息, 发生错误:" + i, "bottom", "right"); } })), await this.storageQueue.waitAllFinished(); const d = utils.getNowStr(); localStorage.setItem(this.lastCheckBlacklistTimeKey, d), clog.log('<span style="color: #f40">-------- END 检测屏蔽黑名单 END --------</span>'), $("#checkBlacklistMsg").text("检测屏蔽黑名单, 结束"), this.getBean("BlacklistPlugin").resetBtnTip().then(); } async checkFavoriteActress() { const e = `${this.javDbUrl}/users/collection_actors`, t = []; await this.scrapeActorInfo(e, t), clog.log("所有演员信息已收集, 总计数量:", t.length), $("#checkNewVideoMsg").text("同步完成"), t.length > 0 && (await storageManager.addFavoriteActressList(t), localStorage.setItem(this.lastCheckFavoriteActressTimeKey, utils.getNowStr()), this.getBean("NewVideoPlugin").resetBtnTip().then()); } async scrapeActorInfo(e, t, pageCount = 0, failCount = 0) { // 安全上限: 防止网站改版 / Cloudflare 拦截导致无限翻页死循环 const MAX_PAGES = 200, MAX_FAIL = 3; if (pageCount >= MAX_PAGES) { clog.warn(`同步演员: 已达最大翻页上限 ${MAX_PAGES}, 停止`); return; } clog.log(`正在抓取页面: ${e}`), $("#checkNewVideoMsg").text(`正在解析已收藏的演员: ${e}`); let nextUrl = null; try { const n = await gmHttp.get(e); // Cloudflare 验证页检测: 命中则立即中止, 避免死循环 if ("string" == typeof n && /Just a moment|challenges\.cloudflare\.com|cf-chl|_cf_chl_opt/i.test(n)) { clog.error("同步演员: 遭遇 Cloudflare 验证, 请先在浏览器手动通过验证后重试"); show.error("同步演员失败: 需先通过 JavDB 的 Cloudflare 验证"); return; } const a = utils.htmlTo$dom(n); a.find("#actors .actor-box a").each(((e, n) => { const a = $(n), i = a.attr("title"), s = a.attr("href"); if (i && s) { const e = i.split(",").map((e => e.trim())).filter((e => e.length > 0)), n = e[0] || "", o = new URL(s, this.javDbUrl).pathname.split("/").filter((e => e.length > 0)); let r = ""; o.length > 0 && (r = o[o.length - 1]); let l = D; const c = a.find("img").attr("src"), d = a.find(".info"); d.length && d.text().trim().includes("無碼") && (l = A), t.push({ starId: r, name: n, allName: e, avatar: c, actressType: l, lastCheckTime: null, lastUpdateTime: null }); } })); const i = a.find(".pagination-next").attr("href"); if (i) nextUrl = new URL(i, this.javDbUrl).href; failCount = 0; // 本页成功, 重置连续失败计数 } catch (n) { failCount++; clog.error(`抓取 ${e} 时发生错误 (连续失败 ${failCount}/${MAX_FAIL}):`, n); if (failCount >= MAX_FAIL) { clog.error("同步演员: 连续失败次数过多, 已停止 (可能是网站改版或 Cloudflare 拦截)"); show.error("同步演员失败: 网站返回异常, 已停止"); return; } const url = new URL(e), page = parseInt(url.searchParams.get("page") || "1"); if (!isNaN(page)) { url.searchParams.set("page", String(page + 1)); nextUrl = url.href; } } if (nextUrl) await this.scrapeActorInfo(nextUrl, t, pageCount + 1, failCount); } async checkNewVideo(e) { const t = await storageManager.getFavoriteActressList(), n = utils.genericSort(t, [ { key: e => { var t; return (null == (t = e.newVideoList) ? void 0 : t.length) ?? 0; }, order: "desc" }, { key: "lastPublishTime", order: "desc" } ]), a = this.taskConfig.checkConcurrencyCount, i = this.taskConfig.checkRequestSleep, s = this.taskConfig.checkNewVideo_intervalTime, o = this.taskConfig.checkNewVideo_ruleTime, r = localStorage.getItem(this.lastCheckNewVideoTimeKey); if (!e && r && this.isUnnecessaryCheck(r, s)) return void clog.debug(`检测新作品, 上次检测时间: ${r} 检测间隔时间: ${s}小时 未到时间`); const l = [], c = []; for (const m of n) { const {lastCheckTime: t, lastPublishTime: n, name: a} = m; !e && t && this.isUnnecessaryCheck(t, s) || (!n || 0 === o || this.isUnnecessaryCheck(n, o) ? l.push(m) : c.push(`检测新作品: ${a} ${n} 停更超过${o / 24 / 365}年,跳过检测`)); } if (0 === l.length) return; c.forEach((e => { clog.log(e); })), clog.log(`<span style='color: #f40'>检测最新作品, 总任务数: ${l.length}, 并发限制:${a}, 请求间隔时间:${i}ms</span>`); const d = await storageManager.getTitleFilterKeyword(), h = await storageManager.getBlacklistCarList(), g = new Set(h.map((e => e.carNum))); await this.limitConcurrency(l, a, i, (async e => { const {lastCheckTime: t, name: n, starId: a} = e; let i = `${this.javDbUrl}/actors/${a}?t=d`; try { clog.log("正在检测最新作品, 演员:", n, i), $("#checkNewVideoMsg").text(`正在检测最新作品, 演员: ${n}`); const e = await gmHttp.get(i); // Cloudflare 验证页检测: 命中则抛出让 limitConcurrency 计入连续失败 if ("string" == typeof e && /Just a moment|challenges\.cloudflare\.com|cf-chl|_cf_chl_opt/i.test(e)) { throw new Error("Cloudflare 验证拦截, 请先在浏览器手动通过验证"); } const t = utils.htmlTo$dom(e); this.storageQueue.addTask((async () => { await this.parsePage(t, a, n, d, g); })); } catch (s) { clog.error("检测新作品失败, 演员:", n, i, s), show.error(`检测新作品失败, ${n}: ${s.message || s}`, "bottom", "right"); throw s; } })), await this.storageQueue.waitAllFinished(), localStorage.setItem(this.lastCheckNewVideoTimeKey, utils.getNowStr()), clog.log('<span style="color: #f40">检测最新作品---结束</span>'), $("#checkNewVideoMsg").text("检测完毕"); const p = this.getBean("NewVideoPlugin"); p.loadData(), p.resetBtnTip().then(); } async parsePage(e, t, n, a, i) { let s, o, r = !1, l = T; if (e.text().includes(I) && (r = !0, l = I), r && e.find(".avatar-box").length > 0 && e.find(".avatar-box").parent().remove(), s = e.find(this.getSelector(l).requestDomItemSelector), o = e.find(this.getSelector(l).nextPageSelector).attr("href"), o && 0 === s.length) throw clog.error("新作品检测-解析列表失败"), show.error("新作品检测-解析列表失败"), new Error("新作品检测-解析列表失败"); let c = [], d = null; for (const m of s) { const e = $(m), {carNum: s, url: o, title: r, publishTime: l} = this.getBean("ListPagePlugin").findCarNumAndHref(e); if (!s) continue; a.find((e => r.includes(e) || s.includes(e))) || (i.has(s) || (d || (d = l), (() => { let coverUrl = e.find("img").attr("src") || ""; if (coverUrl && !coverUrl.startsWith("http")) { coverUrl = coverUrl.startsWith("/") ? this.javDbUrl + coverUrl : this.javDbUrl + "/" + coverUrl; } let url = o || ""; if (url && !url.startsWith("http")) { url = url.startsWith("/") ? this.javDbUrl + url : this.javDbUrl + "/" + url; } const scoreText = e.find(".score .value, .score").text().trim(); const score = parseFloat(scoreText) || 0; const voteMatch = scoreText.match(/由(\d+)人/); let voteCount = 0; if (voteMatch) { voteCount = parseInt(voteMatch[1]); } else { const voteText = e.find(".score .count, .meta .count").text().trim(); voteCount = parseInt(voteText.replace(/[^\d]/g, "")) || 0; } c.push({ carNum: s, coverUrl: coverUrl, title: r || "", publishTime: l || "", score: score, voteCount: voteCount, url: url }); })())); } const h = await storageManager.getCarMap(), p = c.filter((e => !h.has(e.carNum))); p.length > 0 && clog.log(`<span style='color: #f40'>检测出新作品, ${n}, 共${p.length}部</span>`), await storageManager.updateFavoriteActress({ starId: t, lastCheckTime: utils.getNowStr(), newVideoList: p, lastPublishTime: d }); } async checkOneNewVideo(e) { const t = await storageManager.getTitleFilterKeyword(), n = await storageManager.getBlacklistCarList(), a = new Set(n.map((e => e.carNum))), {lastCheckTime: i, name: s, starId: o} = e; let r = `${this.javDbUrl}/actors/${o}?t=d`; const l = $("#checkNewVideoMsg"); try { clog.log("正在检测最新作品, 演员:", s, r), l.text(`正在检测最新作品, 演员: ${s}`); const e = await gmHttp.get(r); // Cloudflare 验证页检测 if ("string" == typeof e && /Just a moment|challenges\.cloudflare\.com|cf-chl|_cf_chl_opt/i.test(e)) { show.error("检测失败: 需先通过 JavDB 的 Cloudflare 验证"); l.text("检测失败: Cloudflare 验证拦截"); return; } const n = utils.htmlTo$dom(e); await this.parsePage(n, o, s, t, a), clog.log('<span style="color: #f40">检测最新作品---结束</span>'), l.text("检测完毕"); this.getBean("NewVideoPlugin").loadData(); } catch (c) { clog.error("检测新作品失败, 演员:", s, r, c), show.error(`检测新作品失败: ${c.message || c}`, "bottom", "right"), l.text(`检测新作品失败: ${s}`); } } } const tt = [ { name: "jsDelivr (全球CDN)", json: "https://cdn.jsdelivr.net/gh/gfriends/gfriends/Filetree.json", base: "https://cdn.jsdelivr.net/gh/gfriends/gfriends/Content/" }, { name: "GitHub Raw (备用)", json: "https://raw.githubusercontent.com/gfriends/gfriends/master/Filetree.json", base: "https://raw.githubusercontent.com/gfriends/gfriends/master/Content/" } ], nt = "jhs_img_cdn_index"; let at = parseInt(localStorage.getItem(nt) || "0", 10); (at >= tt.length || at < 0) && (at = 0); let it = tt[at].json, st = tt[at].base; const ot = "filetreeStore", rt = "filetree_data", lt = { db: null, async open() { return this.db ? this.db : new Promise(((e, t) => { const n = indexedDB.open("GfriendsAvatarDB", 1); n.onupgradeneeded = e => { this.db = e.target.result, this.db.objectStoreNames.contains(ot) || this.db.createObjectStore(ot); }, n.onsuccess = t => { this.db = t.target.result, e(this.db); }, n.onerror = e => { console.error("IndexedDB open error:", e.target.errorCode), t(new Error("Failed to open IndexedDB")); }; })); }, async get(e) { return await this.open(), new Promise((t => { const n = this.db.transaction([ ot ], "readonly").objectStore(ot).get(e); n.onsuccess = () => t(n.result), n.onerror = () => t(null); })); }, async set(e, t) { return await this.open(), new Promise(((n, a) => { const i = this.db.transaction([ ot ], "readwrite").objectStore(ot).put(t, e); i.onsuccess = () => n(), i.onerror = e => { console.error("IndexedDB set error:", e.target.errorCode), a(new Error("Failed to write to IndexedDB")); }; })); } }; let ct = null, dt = null; function ht(e) { if (!e || !e.Content) return null; const t = {}, n = e.Content; for (const a in n) { const e = encodeURIComponent(a); for (const i in n[a]) { let s = i.replace(/\.jpg$/i, "").split("-")[0]; s.startsWith("AI-Fix-") && (s = s.substring(7)); const o = s.toLowerCase().trim(); if (o.length > 0) { const s = n[a][i], r = s.indexOf("?"); let l, c = ""; r > -1 ? (l = encodeURIComponent(s.substring(0, r)), c = s.substring(r)) : l = encodeURIComponent(s); const d = `${st}${e}/${l}${c}`; t[o] || (t[o] = []), t[o].includes(d) || t[o].push(d); } } } return t; } async function gt(e) { let t = loading(); try { await async function() { if (ct && dt) return ct; let e = null; try { e = await lt.get(rt); } catch (a) { console.error("读取 IndexedDB 失败:", a); } if (e && e.Content && (ct = e, dt = ht(e), dt)) return ct; show.info("正在载入头像数据源..."); const t = await fetch(it); if (!t.ok) throw new Error(`请求头像源失败: ${t.status}`); const n = await t.json(); if (n && n.Content) { ct = n, dt = ht(n); try { await lt.set(rt, n), clog.debug("载入头像数据源并写入缓存成功!"); } catch (a) { clog.error(a), show.error("头像数据源写入缓存失败,可能磁盘已满或其他权限问题。"); } return ct; } clog.error(n); throw new Error("解析头像数据源失败"); }(); } catch (i) { return show.error(i), []; } finally { t.close(); } if (!dt) return []; const n = new Set, a = e.map((e => e.toLowerCase().trim())).filter((e => e.length > 0)); if (0 === a.length) return []; for (const s of a) { const e = dt[s]; e && e.forEach((e => n.add(e))); } return Array.from(n); } class pt extends X { constructor() { super(...arguments), i(this, "currentPage", 1), i(this, "pageSize", 30), i(this, "nvCurrentPage", 1), i(this, "nvPageSize", 60), i(this, "nvFlatListCache", null), i(this, "nvSortBy", "publishTime_desc"); } getName() { return "NewVideoPlugin"; } async initCss() { return "\n <style>\n #actress-card-container {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(243px, 1fr)); /* 响应式3-5列 */\n gap: 20px;\n padding-bottom: 20px;\n padding-right: 10px;\n background: #f9f9f9;\n border-radius: 5px;\n overflow-y: auto;\n }\n .actress-card {\n background: #fff;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);\n padding: 15px;\n text-align: center;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n position: relative;\n overflow: hidden;\n }\n .actress-card:hover {\n box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);\n }\n .actress-card-name {\n font-size: 1.2em;\n font-weight: bold;\n color: #007bff;\n margin-top: 10px;\n }\n .actress-card-allname {\n font-size: 0.9em;\n color: #999;\n margin-top: 5px;\n height: 30px; /* 保证高度一致性 */\n overflow: hidden;\n white-space: nowrap; /* 防止文字换行 */\n text-overflow: ellipsis; /* 当文本溢出时,显示省略号 */\n }\n .actress-card-avatar {\n width: 100px;\n height: 100px;\n border-radius: 50%;\n object-fit: contain;\n margin: 0 auto;\n border: 4px solid #f0f0f0;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n }\n \n .card-tag {\n position: absolute;\n top: 15px; /* 调整标签距离顶部的距离 */\n right: -50px; /* 调整标签距离右侧的距离,负值让它移到外面一点 */\n \n width: 150px; /* 标签的宽度,影响斜角长度 */\n padding: 5px 0; /* 上下内边距 */\n text-align: center;\n \n background-color: #ff4757; /* 标签颜色 */\n color: white; /* 文字颜色 */\n font-size: 14px;\n font-weight: bold;\n z-index: 10; /* 确保标签在其他内容之上 */\n \n /* 3. 核心:旋转标签,使其倾斜 */\n transform: rotate(45deg); /* 45度斜角 */\n \n /* 可选:添加一些阴影或边框效果 */\n box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);\n }\n \n .card-new-count-tag {\n position: absolute;\n top: 5px;\n left: 5px;\n text-align: center;\n font-size: 14px;\n font-weight: bold;\n z-index: 10;\n }\n \n #actress-pagination {\n padding-top: 10px;\n text-align: center;\n border-top: 1px solid #ddd;\n }\n @media (max-width: 600px) {\n .page-number-btn {\n display: none !important;\n }\n }\n \n \n .card-btn {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n display: flex;\n justify-content: center;\n align-items: center;\n text-decoration: none;\n border: none;\n cursor: pointer;\n background: linear-gradient(145deg, #e0e0e0 0%, #f7f7f7 100%);\n box-shadow: 8px 8px 16px rgba(0, 0, 0, 0.08),\n -8px -8px 16px rgba(255, 255, 255, 1.0);\n transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n }\n \n .card-btn svg,\n .card-btn svg path {\n transition: fill 0.3s ease;\n }\n \n .card-btn:hover {\n box-shadow: inset 5px 5px 10px rgba(0, 0, 0, 0.1),\n inset -5px -5px 10px rgba(255, 255, 255, 0.9);\n transform: scale(0.97);\n background: #e0e0e0;\n }\n \n .btn-check-actress svg path {\n fill: #4CAF50;\n }\n .btn-check-actress:hover svg path {\n fill: #388E3C;\n }\n \n .btn-edit-actress svg path {\n fill: #FFC107;\n }\n .btn-edit-actress:hover svg path {\n fill: #FFB300;\n }\n \n .btn-delete-actress svg path {\n fill: #F44336;\n }\n .btn-delete-actress:hover svg path {\n fill: #D32F2F;\n }\n .nv-card {\n transition: transform 0.15s ease, box-shadow 0.15s ease;\n border-radius: 8px;\n overflow: hidden;\n background: #fff;\n box-shadow: 0 1px 4px rgba(0,0,0,0.08);\n }\n .nv-card:hover {\n transform: translateY(-3px);\n box-shadow: 0 6px 16px rgba(0,0,0,0.15);\n }\n .nv-cover-img {\n cursor: zoom-in;\n }\n </style>\n "; } async handle() { await this.showNewVideoCount(); } getPendingNewVideoCount(e, t) { return Array.isArray(e?.newVideoList) ? e.newVideoList.filter((e => { const n = "string" == typeof e ? e : e.carNum; return !t.has(n); })).length : 0; } async getPendingNewVideoTotal() { const e = await storageManager.getCarMap(); return (await storageManager.getFavoriteActressList()).reduce(((t, n) => t + this.getPendingNewVideoCount(n, e)), 0); } async showNewVideoCount() { const e = await this.getPendingNewVideoTotal(); $("#newVideoCount").text(`${e}`); } async resetBtnTip() { const e = this.getBean("TaskPlugin"), t = await storageManager.getSetting(), n = localStorage.getItem(e.lastCheckFavoriteActressTimeKey) || "无", a = t.checkFavoriteActress_IntervalTime, i = localStorage.getItem(e.lastCheckNewVideoTimeKey) || "无", s = t.checkNewVideo_intervalTime; $("#checkFavoriteActress").attr("data-tip", `上次自动同步时间: ${n}; 检测间隔时间: ${a}小时`), $("#checkNewVideo").attr("data-tip", `上次检测时间: ${i}; 检测间隔时间: ${s}小时`); } async openDialog() { const e = this.getBean("TaskPlugin"), t = await storageManager.getSetting(), n = localStorage.getItem(e.lastCheckFavoriteActressTimeKey) || "无", a = t.checkFavoriteActress_IntervalTime, i = localStorage.getItem(e.lastCheckNewVideoTimeKey) || "无", s = t.checkNewVideo_intervalTime; let o = `\n <div class="newVideoToolBox" style="display: flex; flex-direction: column; height: 100%; overflow: hidden; padding:10px">\n <div style="margin-bottom: 15px;display: flex; justify-content: space-between; align-items: center; gap:10px;">\n <div style="display:flex; align-items:center; flex-wrap:wrap; gap:6px; min-width:0;">\n <a class="a-danger" id="checkFavoriteActress" data-tip="上次自动同步时间: ${n}; 检测间隔时间: ${a}小时">${this.actressSvg} 手动同步演员</a>\n <a class="a-warning" id="checkNewVideo" data-tip="上次检测时间: ${i}; 检测间隔时间: ${s}小时">${this.newSvg} 手动检测最新作品</a>\n <a class="a-info" id="toSetting">${this.settingSvg} 配置</a>\n <span id="checkNewVideoMsg" style="display:inline-block;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;"></span>\n </div>\n <div style="display: flex; align-items: center; flex-wrap:wrap; gap:10px; flex:0 0 auto;">\n <select id="paramActressType" style="text-align: center; height: 32px; min-width: 150px; border: 1px solid #ddd; flex:0 0 auto;">\n <option value="all" selected>所有</option>\n <option value="uncensored">无码</option>\n <option value="censored">有码</option>\n <option value="">未知</option>\n </select>\n <select id="paramSortBy" style="text-align: center; height: 32px; min-width: 150px; border: 1px solid #ddd; flex:0 0 auto;">\n <option value="default" selected>默认排序</option>\n <optgroup label="发行时间">\n <option value="lastPublishTime_desc">发行时间 新→旧</option>\n <option value="lastPublishTime_asc">发行时间 旧→新</option>\n </optgroup>\n <optgroup label="检测时间">\n <option value="lastCheckTime_desc">检测时间 新→旧</option>\n <option value="lastCheckTime_asc">检测时间 旧→新</option>\n </optgroup>\n <optgroup label="新作品数">\n <option value="newVideoCount_desc">新作品数 多→少</option>\n <option value="newVideoCount_asc">新作品数 少→多</option>\n </optgroup>\n </select>\n <select id="nvSortBy" style="display:none; text-align:center; height:100%; min-width:150px; border:1px solid #ddd;">\n <option value="publishTime_desc" selected>发行时间 新→旧</option>\n <option value="publishTime_asc">发行时间 旧→新</option>\n <option value="voteCount_desc">评价人数 多→少</option>\n <option value="voteCount_asc">评价人数 少→多</option>\n <option value="actress_asc">演员名 A→Z</option>\n <option value="actress_desc">演员名 Z→A</option>\n <option value="carNum_asc">番号 A→Z</option>\n <option value="carNum_desc">番号 Z→A</option>\n </select>\n <a class="a-normal" id="toggleViewMode" style="margin-left: 5px;">新作品列表</a>\n <a class="a-normal" id="reLoad">${this.refreshSvg} 刷新</a>\n </div>\n\n </div>\n <div id="actress-card-container" class="jhs-scrollbar"></div>\n <div id="new-video-list-container" style="display:none; flex:1; overflow-y:auto;"></div>\n <div id="new-video-list-footer" style="display:none; padding:8px 0; border-top:1px solid #eee; font-size:13px; color:#666;"></div>\n <div id="actress-pagination"></div>\n </div>\n `; layer.open({ type: 1, title: '<span style="padding: 0 10px;" data-tip="数据来源: 女优页面首页,含磁链分类">新作品检测</span>', content: o, scrollbar: !1, area: utils.getResponsiveArea([ "80%", "90%" ]), anim: -1, success: async (e, t) => { this.loadData(), this.bindClick(), utils.setupEscClose(t); } }); } bindClick() { const e = this.getBean("TaskPlugin"); $("#reLoad").on("click", (e => { this.loadData(), $("#checkNewVideoMsg").text(""); })), $("#toSetting").on("click", (e => { this.getBean("SettingPlugin").openSettingDialog("task-panel", (() => { $("#setting-checkFavoriteActress").css({ border: "1px solid #f40" }), $("#setting-checkNewVideo").css({ border: "1px solid #f40" }); })); })); $("#checkFavoriteActress").on("click", (t => { utils.q({ clientX: t.clientX, clientY: t.clientY + 20 }, "是否手动同步演员?", (() => { navigator.locks.request(e.singleTaskKey, { ifAvailable: !0 }, (async t => { if (!t) return void show.error("当前有定时任务在后台执行中, 无法发起手动任务"); $('a[href*="/users/profile"]').length > 0 ? (await e.checkFavoriteActress(), this.loadData()) : show.error("未登录JavDb, 同步失败"); })).catch((e => { console.error("锁任务出现错误:", e), clog.error("锁任务出现错误:", e); })); })); })), $("#checkNewVideo").on("click", (t => { utils.q({ clientX: t.clientX, clientY: t.clientY + 20 }, "是否手动检测最新作品?", (() => { navigator.locks.request(e.singleTaskKey, { ifAvailable: !0 }, (async t => { t ? await e.checkNewVideo(!0) : show.error("当前有定时任务在后台执行中, 无法发起手动任务"); })).catch((e => { console.error("锁任务出现错误:", e), clog.error("锁任务出现错误:", e); })); })); })), $("#paramActressType").on("change", (e => { "list" === this._viewMode ? this.renderNewVideoList() : this.loadData(); })), $("#paramSortBy").on("change", (e => { this.loadData(); })), $("#nvSortBy").on("change", (e => { this.nvSortBy = $("#nvSortBy").val(), this.nvCurrentPage = 1, this.nvRenderPage(); })), $("#toggleViewMode").on("click", (e => { this._viewMode = "list" === this._viewMode ? "card" : "list"; const t = "list" === this._viewMode; $("#actress-card-container").toggle(!t), $("#actress-pagination").toggle(!t), $("#new-video-list-container").toggle(t), $("#new-video-list-footer").toggle(t), $("#paramSortBy").toggle(!t), $("#nvSortBy").toggle(t), $("#toggleViewMode").text(t ? "演员视图" : "新作品列表"), t ? this.renderNewVideoList() : this.loadData(); })); } loadData() { this.currentPage = 1; this.renderActressCards().catch(e => { clog.error("加载演员卡片失败:", e); show.error("加载数据失败"); }); } async renderActressCards() { const e = $("#actress-card-container"); if (!e.length) return; e.html('<div style="text-align:center; padding: 40px; color: #999;">加载中...</div>'); let t = await storageManager.getFavoriteActressList(); const n = $("#paramActressType").val(); "all" !== n && (t = t.filter((e => e.actressType === n))); const _carSet = await storageManager.getCarMap(); const _newVideoCount = e => this.getPendingNewVideoCount(e, _carSet); const sortBy = $("#paramSortBy").val(); const sortMap = { "lastPublishTime_desc": [{ key: "lastPublishTime", order: "desc" }], "lastPublishTime_asc": [{ key: "lastPublishTime", order: "asc" }], "lastCheckTime_desc": [{ key: "lastCheckTime", order: "desc" }], "lastCheckTime_asc": [{ key: "lastCheckTime", order: "asc" }], "newVideoCount_desc": [{ key: _newVideoCount, order: "desc" }], "newVideoCount_asc": [{ key: _newVideoCount, order: "asc" }] }; const defaultSort = [{ key: _newVideoCount, order: "desc" }, { key: "lastPublishTime", order: "desc" }]; const a = utils.genericSort(t, sortMap[sortBy] || defaultSort); const i = a.length, s = Math.ceil(i / this.pageSize), o = (this.currentPage - 1) * this.pageSize, r = o + this.pageSize, l = a.slice(o, r), c = await this.getBean("OtherSitePlugin").getJavDbUrl(), d = this.getBean("TaskPlugin"), h = await storageManager.getSetting("checkNewVideo_ruleTime") || 8760; if (0 === l.length) { e.html('<div style="text-align:center; padding: 40px; color: #999;">暂无数据</div>'); return void this.renderPagination(i, s); } const _escHtml = s => String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); const g = l.map((e => { const a = Array.isArray(e.allName) ? e.allName.join(",") : ""; const _a = _escHtml(a), _name = _escHtml(e.name || ""), _remark = _escHtml(e.remark || ""); const _newCount = this.getPendingNewVideoCount(e, _carSet); const _effectivePublishTime = _newCount > 0 ? (e.lastPublishTime || "") : ""; const i = `${c}/actors/${e.starId}?t=d`; let s = !1; _effectivePublishTime && (s = !d.isUnnecessaryCheck(_effectivePublishTime, h)); let o = "未知", r = "#9E9E9E"; e.actressType === A ? (o = "无码", r = "#4CAF50") : e.actressType === D && (o = "有码", r = "#FF9800"); let l = ""; return s && (l = "background: linear-gradient(145deg, #e0e0e0 0%, #cabdbd 100%);box-shadow: none"), `\n <div class="actress-card" data-starId="${e.starId}" style="${s ? "background: #d4cece; " : ""}min-height: 370px;">\n <a href="${i}" target="_blank" style="text-decoration: none; color: inherit; display: block; flex-grow: 1;">\n <img src="${e.avatar || "https://c0.jdbstatic.com/images/actor_unknow.jpg"}" alt="${_a}" class="actress-card-avatar" loading="lazy">\n </a>\n\n <div>\n <a href="${i}" target="_blank" style="text-decoration: none; color: inherit; display: block; flex-grow: 1;">\n <div class="actress-card-name">${_name}</div>\n </a>\n <div class="actress-card-allname" title="${_a}">${_a}</div>\n </div>\n\n <div style="font-size: 0.8em; margin-top: 5px;">\n <span>上次检测: ${e.lastCheckTime || ""}</span>\n </div>\n <div style="font-size: 0.8em; margin-top: 5px; min-height: 1.2em;">\n <span>${_effectivePublishTime ? "最后发行作品: " + _effectivePublishTime : (_newCount === 0 && e.lastPublishTime ? "已全部标记" : "")}</span>\n </div>\n\n <div style="font-size: 0.7em; color: #cc4444; margin-top: 5px; min-height: 18px">\n <span>${s ? "停更" + h / 24 / 365 + "年以上, 下轮任务不再进行检测" : ""}</span>\n </div>\n\n <div style="font-size: 0.8em; margin-top: 5px; color: #3765c5; min-height: 10px">\n <span>${_remark}</span>\n </div>\n\n <div style="margin-top: 10px;display: flex; justify-content:center; gap: 10px;">\n <a title="编辑" class="card-btn btn-edit-actress" style="${l}" data-starId="${e.starId}">${this.editSvg}</a>\n <a title="取消收藏" class="card-btn btn-delete-actress" style="${l}" data-starId="${e.starId}">${this.deleteSvg}</a>\n <a title="重新检测该演员" class="card-btn btn-check-actress" style="${l}" data-starId="${e.starId}">${this.checkSvg}</a>\n </div>\n\n <div class="card-tag" style="background-color:${r}">${o}</div>\n <div class="card-new-count-tag" data-tip="最新作品数量: ${_newCount}">${_newCount}</div>\n </div>\n `; })).join(""); e.html(g), $(".btn-delete-actress").off("click").on("click", (e => { e.preventDefault(); const t = $(e.currentTarget).attr("data-starId"), n = a.find((e => e.starId === t)); utils.q(e, `是否取消收藏 ${n.name}?`, (async () => { let e = `${await this.getBean("OtherSitePlugin").getJavDbUrl()}/actors/${t}/uncollect`; const n = document.querySelector("meta[name=csrf-token]").content, a = await gmHttp.post(e, null, { "x-csrf-token": n }); a.includes("removeClass") ? (await storageManager.removeFavoriteActress(t), this.loadData(), this.showNewVideoCount()) : (show.error("移除失败"), clog.error("移除失败,返回值:", a)); })); })), $(".btn-edit-actress").off("click").on("click", (e => { e.preventDefault(); const t = $(e.currentTarget).attr("data-starId"), n = a.find((e => e.starId === t)); n ? this.editActress(n) : show.error(`未找到 starId 为 ${t} 的女优记录。`); })), $(".btn-check-actress").off("click").on("click", (e => { e.preventDefault(), navigator.locks.request(d.singleTaskKey, { ifAvailable: !0 }, (async t => { if (!t) return void show.error("当前有定时任务在后台执行中, 无法发起手动任务"); const n = $(e.currentTarget).attr("data-starId"), i = a.find((e => e.starId === n)); await d.checkOneNewVideo(i); })).catch((e => { console.error("锁任务出现错误:", e), clog.error("锁任务出现错误:", e); })); })), this.renderPagination(i, s), show.ok("加载完成"); } async getNewVideoFlatList() { const e = await storageManager.getFavoriteActressList(), t = await storageManager.getCarMap(), n = $("#paramActressType").val(), a = []; for (const i of e) { if ("all" !== n && i.actressType !== n) continue; if (!Array.isArray(i.newVideoList)) continue; for (const e of i.newVideoList) { const n = "string" == typeof e ? e : e.carNum; if (t.has(n)) continue; const s = "object" == typeof e ? e : {}; a.push({ carNum: n, coverUrl: s.coverUrl || "", title: s.title || "", publishTime: s.publishTime || "", actressName: i.name || "", starId: i.starId || "", score: s.score || 0, voteCount: s.voteCount || 0, url: s.url || "" }); } } return a.sort(((e, t) => (t.publishTime || "").localeCompare(e.publishTime || ""))), a; } async loadCoverForItems(e) { const t = await this.getBean("OtherSitePlugin").getJavDbUrl(), n = {}; for (const a of e) { if (n[a.starId]) continue; if (e.every((e => e.starId !== a.starId || e.coverUrl))) continue; n[a.starId] = !0; try { const i = await gmHttp.get(`${t}/actors/${a.starId}?t=d`), s = utils.htmlTo$dom(i); s.find(".movie-list .item").each(((e, n) => { const i = $(n), o = i.find(".video-title strong").text().trim(), r = i.find("img").attr("src") || ""; if (!o || !r) return; let l = r; if (!l.startsWith("http")) { l = l.startsWith("/") ? t + l : t + "/" + l; } const c = l.replace("thumbs", "covers"), d = i.find(".video-title").text().replace(o, "").trim(); $(`.nv-card[data-car="${o}"]`).each(((e, t) => { const n = $(t), i = n.find("img"); if (i.length) { i.attr("src", c).on("error", (function() { $(this).hide().next().show(); })); } else { const e = n.find(".nv-placeholder, div[style*='加载中']"); if (e.length) { e.replaceWith(`<img src="${c}" loading="lazy" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';"><div style="display:none;align-items:center;justify-content:center;height:100%;color:#999;font-size:12px;">无封面</div>`); } } d && n.attr("title", d); })); })); } catch (i) { clog.warn("获取演员封面失败:", a.actressName, i); } } } async renderNewVideoList() { const e = $("#new-video-list-container"); if (!e.length) return; e.html('<div style="text-align:center;padding:40px;color:#999;">加载中...</div>'); let t; try { t = await this.getNewVideoFlatList(); } catch (n) { return console.error(n), void e.html(`<div style="text-align:center;padding:40px;color:#e74c3c;">加载失败: ${escapeHtml(n.message)}</div>`); } if (0 === t.length) return e.html('<div style="text-align:center;padding:40px;color:#999;">暂无待鉴定的新作品</div>'), void $("#new-video-list-footer").html(""); this.nvFlatListCache = t, this.nvCurrentPage = 1, this.nvSortBy = $("#nvSortBy").val() || "publishTime_desc"; const a = new Set; for (const i of t) a.add(i.actressName); $("#new-video-list-footer").html(`<span>共 <b>${t.length}</b> 个待鉴定番号,涉及 <b>${a.size}</b> 位演员</span>\n <a class="a-normal" id="batchMarkWatched" style="margin-left:15px;">全部标记已看</a>\n <a class="a-normal" id="batchMarkDownloaded" style="margin-left:8px;">全部标记已下载</a>`); this.nvRenderPage(), this.loadCoverForItems(t).catch((e => clog.warn("封面加载异常:", e))); $("#batchMarkWatched").off("click").on("click", (async () => { if (!this.nvFlatListCache || 0 === this.nvFlatListCache.length) return; utils.q({ clientX: 0, clientY: 0 }, `确认将 ${this.nvFlatListCache.length} 个番号全部标记为已看?`, (async () => { const e = this.nvFlatListCache.map((e => ({ carNum: e.carNum, url: `/search?q=${encodeURIComponent(e.carNum)}`, names: e.actressName, actionType: p }))); try { await storageManager.saveCarList(e), show.ok(`已标记 ${e.length} 个`), this.renderNewVideoList(), this.showNewVideoCount(); } catch (n) { show.error("标记失败: " + n.message); } })); })), $("#batchMarkDownloaded").off("click").on("click", (async () => { if (!this.nvFlatListCache || 0 === this.nvFlatListCache.length) return; utils.q({ clientX: 0, clientY: 0 }, `确认将 ${this.nvFlatListCache.length} 个番号全部标记为已下载?`, (async () => { const e = this.nvFlatListCache.map((e => ({ carNum: e.carNum, url: `/search?q=${encodeURIComponent(e.carNum)}`, names: e.actressName, actionType: g }))); try { await storageManager.saveCarList(e), show.ok(`已标记 ${e.length} 个`), this.renderNewVideoList(), this.showNewVideoCount(); } catch (n) { show.error("标记失败: " + n.message); } })); })); } nvSortList(e) { const t = this.nvSortBy || "publishTime_desc", [n, a] = t.split("_"); return e.slice().sort(((e, t) => { let i = 0; switch (n) { case "publishTime": i = (e.publishTime || "").localeCompare(t.publishTime || ""); break; case "actress": i = (e.actressName || "").localeCompare(t.actressName || ""); break; case "carNum": i = (e.carNum || "").localeCompare(t.carNum || ""); break; case "voteCount": i = (e.voteCount || 0) - (t.voteCount || 0); break; } return "desc" === a ? -i : i; })); } nvRenderPage() { const e = this.nvFlatListCache; if (!e || 0 === e.length) return; const t = this.nvSortList(e), n = this.nvPageSize, a = (this.nvCurrentPage - 1) * n, i = a + n, s = t.slice(a, i), o = Math.ceil(t.length / n), r = this.getBean("OtherSitePlugin").getJavDbUrl().then((r => { const l = $("#new-video-list-container"); let c = ""; c += '<div id="nv-grid" class="jhs-new-video-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px;padding:5px;">'; for (const n of s) { const e = escapeHtml(n.carNum), t = escapeHtml(n.title || n.carNum), a = n.coverUrl ? n.coverUrl.replace("thumbs", "covers") : "", i = n.url || `${r}/search?q=${encodeURIComponent(n.carNum)}`; let o = `番号: ${e}\\n演员: ${escapeHtml(n.actressName)}\\n发行: ${n.publishTime || "未知"}`; n.voteCount && (o += `\\n评价人数: ${n.voteCount}`); const l = n.voteCount ? `<span style="position:absolute;top:4px;right:4px;background:rgba(0,0,0,0.7);color:#fff;font-size:11px;padding:2px 5px;border-radius:3px;">${n.voteCount}人评价</span>` : ""; c += `<div class="nv-card" data-car="${e}" title="${o}">`; c += `<a href="${i}" target="_blank" style="display:block;text-decoration:none;color:inherit;">`; c += `<div style="width:100%;aspect-ratio:3/2;overflow:hidden;border-radius:6px;background:#f0f0f0;position:relative;">`; a ? c += `<img class="nv-cover-img" src="${a}" data-full="${a}" loading="lazy" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">${l}<div style="display:none;align-items:center;justify-content:center;height:100%;color:#999;font-size:12px;">无封面</div>` : c += `<div class="nv-placeholder" style="display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:12px;">加载中...</div>`; c += `</div>`; c += `<div style="padding:8px 4px;">`; c += `<div style="font-size:13px;font-weight:bold;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${e}">${e}</div>`; c += `<div style="font-size:12px;color:#666;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${escapeHtml(n.actressName)}">${escapeHtml(n.actressName)}</div>`; n.publishTime && (c += `<div style="font-size:11px;color:#999;">${n.publishTime}</div>`); c += `</div></a></div>`; } c += "</div>"; if (o > 1) { c += '<div id="nv-pagination-bar" style="padding:12px 0;text-align:center;border-top:1px solid #eee;margin-top:8px;">'; this.nvCurrentPage > 1 && (c += `<button class="pagination-btn" data-nvpage="${this.nvCurrentPage - 1}" style="padding:6px 14px;margin:0 3px;background:#f0f0f0;border:1px solid #ddd;border-radius:4px;cursor:pointer;">上一页</button>`); let e = Math.max(1, this.nvCurrentPage - 2), n = Math.min(o, e + 4); n - e < 4 && (e = Math.max(1, n - 4)); for (let t = e; t <= n; t++) c += `<button class="pagination-btn" data-nvpage="${t}" style="padding:6px 14px;margin:0 3px;border:1px solid #ddd;border-radius:4px;cursor:pointer;${t === this.nvCurrentPage ? "background:#007bff;color:white;border-color:#007bff;" : "background:#f0f0f0;"}">${t}</button>`; this.nvCurrentPage < o && (c += `<button class="pagination-btn" data-nvpage="${this.nvCurrentPage + 1}" style="padding:6px 14px;margin:0 3px;background:#f0f0f0;border:1px solid #ddd;border-radius:4px;cursor:pointer;">下一页</button>`), c += `<span style="margin-left:15px;color:#666;font-size:13px;">第 ${this.nvCurrentPage}/${o} 页,共 ${t.length} 条</span>`, c += "</div>"; } l.html(c), l.find(".pagination-btn").off("click").on("click", (e => { const n = parseInt($(e.currentTarget).data("nvpage")); n >= 1 && n <= o && n !== this.nvCurrentPage && (this.nvCurrentPage = n, this.nvRenderPage(), l.scrollTop(0)); })), window.imageHoverPreviewObj ? window.imageHoverPreviewObj.bindEvents() : window.imageHoverPreviewObj = new ImageHoverPreview({ selector: ".nv-cover-img", dataAttribute: "data-full" }); })); } async editActress(e) { const t = e.name, n = e.avatar, a = e.remark || "", i = Array.isArray(e.allName) ? e.allName.join(",") : "", s = Array.isArray(e.newVideoList) ? e.newVideoList.map((e => "string" == typeof e ? e : e.carNum)).join(",") : "", o = e.starId, r = "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; min-height: 60px; overflow-y: hidden;", l = e.actressType || "", c = `\n <div style="padding: 20px;">\n <div style="margin-bottom: 15px; text-align: center;">\n <img id="edit-avatar-preview" src="${n}" alt="Avatar Preview" \n style="width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; border: 2px solid #ddd;">\n <div style="text-align: left">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">头像链接:</label>\n <input type="text" id="edit-actress-avatar" value="${n}" \n style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">\n <div style="display: flex; gap: 5px; margin-top: 5px;">\n <button type="button" id="search-avatar-btn" \n style="flex-grow: 1; padding: 8px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">\n 搜索头像\n </button>\n <button type="button" id="select-cdn-btn" \n style="width: 100px; padding: 8px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">\n 选择 CDN 源\n </button>\n </div>\n </div>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">主名称:</label>\n <input type="text" id="edit-actress-name" value="${t}" \n style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">所有别名(用逗号隔开):</label>\n <textarea id="edit-actress-allname" style="${r}">${i}</textarea>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">演员类别:</label>\n <select id="actressType" style="width: 100%; padding: 10px; border: 1px solid #ddd;">\n <option value="" ${"" === l ? "selected" : ""}>未知</option>\n <option value="censored" ${"censored" === l ? "selected" : ""}>有码</option>\n <option value="uncensored" ${"uncensored" === l ? "selected" : ""}>无码</option>\n </select>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">最新作品(用逗号隔开):</label>\n <textarea id="edit-actress-newvideolist" style="${r}">${s}</textarea>\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display: block; margin-bottom: 5px; font-weight: bold;">备注:</label>\n <textarea id="edit-remark" style="${r}">${a}</textarea>\n </div>\n </div>\n `; layer.open({ type: 1, title: `编辑女优: ${t} (${o})`, area: utils.getResponsiveArea(["500px", "750px"]), content: c, btn: [ "保存", "取消" ], success: (e, t) => { const n = e => { e.css("height", "auto"), e.css("height", e[0].scrollHeight + 15 + "px"); }; $("#edit-actress-avatar").on("input", (function() { const e = $(this).val(); $("#edit-avatar-preview").attr("src", e); })); const a = $("#edit-actress-allname"); a.on("input", (function() { n($(this)); })), n(a); const i = $("#edit-actress-newvideolist"); i.on("input", (function() { n($(this)); })), n(i), $("#search-avatar-btn").on("click", (async () => { await this.searchAvatar(); })), $("#select-cdn-btn").on("click", (async () => { await async function() { const e = at, t = tt.map(((t, n) => `\n <div style="margin-bottom: 10px;">\n <input type="radio" id="cdn-${n}" name="cdn-source" value="${n}" ${n === e ? "checked" : ""} style="margin-right: 10px;">\n <label for="cdn-${n}">${t.name} ${t.json.includes("jsdelivr") ? "(推荐)" : ""}</label>\n </div>\n `)).join(""), n = `\n <div style="padding: 20px;">\n <p style="margin-bottom: 15px; font-weight: bold; color: #333;">请选择头像数据源 (当前: ${tt[e].name}):</p>\n ${t}\n <p style="margin-top: 20px; color: #555; font-size: 12px;">切换源会清除本地缓存的数据,并在下次搜索时重新加载。</p>\n </div>\n `; layer.open({ type: 1, title: "选择 CDN 源", area: utils.getResponsiveArea([ "400px", "auto" ]), content: n, btn: [ "确定", "取消" ], success: (e, t) => { utils.setupEscClose(t); }, yes: async e => { const t = $('input[name="cdn-source"]:checked').val(), n = parseInt(t, 10); if (n !== at) { at = n, localStorage.setItem(nt, n.toString()), it = tt[n].json, st = tt[n].base, ct = null, dt = null; try { await lt.set(rt, null); } catch (a) { clog.error("清除 IndexedDB 缓存失败:", a); } show.ok(`CDN 源已切换为: ${tt[n].name}`), layer.close(e); } else layer.close(e); } }); }(); })), utils.setupEscClose(t); }, yes: async t => { const n = $("#edit-actress-avatar").val().trim(), a = $("#edit-actress-name").val().trim(), i = $("#edit-actress-allname").val().trim(), s = $("#edit-actress-newvideolist").val().trim(), o = $("#edit-remark").val().trim(), r = $("#actressType").val(); if (!a) return show.error("主名称不能为空"), !1; const l = i.split(/[\uff0c,]/).map((e => e.trim())).filter((e => e.length > 0)), c = s.split(/[\uff0c,]/).map((e => e.trim())).filter((e => e.length > 0)); e.avatar = n, e.name = a, e.allName = l, e.newVideoList = c, e.actressType = r, e.remark = o; try { await storageManager.updateFavoriteActress(e); this.renderActressCards().then(); this.showNewVideoCount(); show.ok(`女优 ${a} 信息已更新`); layer.close(t); } catch(err) { show.error("修改失败: " + (err.message || err)); } } }); } renderPagination(e, t) { const n = this.currentPage; let a = ""; const i = $("#actress-pagination"); if (0 === t) return a = '<span style="color: #666;">共 0 条记录</span>', void i.html(a); n > 1 && t > 5 && (a += '<button class="pagination-btn" data-page="1" style="padding: 8px 12px; margin: 0 5px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;">首页</button>'), n > 1 && (a += `<button class="pagination-btn" data-page="${n - 1}" style="padding: 8px 12px; margin: 0 5px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;">上一页</button>`); let s = Math.max(1, n - Math.floor(2.5)), o = Math.min(t, s + 5 - 1); o - s < 4 && (s = Math.max(1, o - 5 + 1)); for (let r = s; r <= o; r++) { a += `<button class="pagination-btn page-number-btn ${r === n ? "active" : ""}" data-page="${r}" style="padding: 8px 12px; margin: 0 3px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; ${r === n ? "background: #007bff; color: white; border-color: #007bff;" : ""}">${r}</button>`; } n < t && (a += `<button class="pagination-btn" data-page="${n + 1}" style="padding: 8px 12px; margin: 0 5px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;">下一页</button>`), n < t && t > 5 && (a += `<button class="pagination-btn" data-page="${t}" style="padding: 8px 12px; margin: 0 5px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;">尾页</button>`), a += `<span style="margin-left: 20px; color: #666;">共 ${e} 条记录 (第 ${n}/${t} 页)</span>`, i.html(a), i.find(".pagination-btn").off("click").on("click", (e => { if ($(e.currentTarget).is("[disabled]")) return; const n = parseInt($(e.currentTarget).data("page")); n >= 1 && n <= t && n !== this.currentPage && (this.currentPage = n, this.renderActressCards()); })); } async searchAvatar() { const e = $("#edit-actress-name"), t = $("#edit-actress-allname"), n = e.val().trim(), a = t.val().trim().split(/[\uff0c,]/).map((e => e.trim())).filter((e => e.length > 0)); if (n && a.unshift(n), 0 === a.length) return void show.error("请先填写女优主名称或别名进行搜索。"); const i = loading("正在搜索头像..."); let s = []; try { s = await gt(a); } catch (c) { return void show.error(`头像数据加载或搜索失败: ${c.message || c}`); } finally { i.close(); } if (0 === s.length) return void show.error(`未找到与 '${a.join("、")}' 相关的头像。请检查名称。`); const o = s.map(((e, t) => `\n <div id="wrapper-${t}" class="gfriends-image-item-wrapper">\n <img alt="" src="${e}" data-url="${e}" class="gfriends-selectable-img" data-wrapper-id="wrapper-${t}" >\n <div class="gfriends-size-tag" data-size-for="wrapper-${t}">...</div> \n </div>\n `)).join(""), r = `\n <style>\n /* 保持上一个回答的美化样式 */\n #gfriends-image-list-container { padding: 15px; height: 100%; box-sizing: border-box; background-color: #f8f9fa; }\n #gfriends-prompt { color: #555; font-weight: 500; border-bottom: 1px solid #eee; padding-bottom: 10px; }\n #gfriends-image-list { display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; }\n .gfriends-image-item-wrapper {\n width: 160px; height: 225px; /* 增加高度以容纳尺寸标签 */\n overflow: hidden; border-radius: 6px;\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); transition: transform 0.2s ease, box-shadow 0.2s ease;\n cursor: pointer; position: relative; \n padding-bottom: 25px; /* 为尺寸标签留出空间 */\n }\n .gfriends-selectable-img {\n width: 100%; height: 200px; /* 固定图片高度 */\n object-fit: cover; border: 3px solid transparent; \n border-radius: 6px; transition: border 0.2s ease;\n }\n .gfriends-image-item-wrapper:hover {\n transform: translateY(-4px) scale(1.02);\n box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);\n }\n .gfriends-selectable-img.is-selected {\n border-color: #ff6347;\n box-shadow: 0 0 0 3px #ff6347;\n }\n /* 新增:尺寸标签样式 */\n .gfriends-size-tag {\n position: absolute;\n bottom: 0; /* 定位到图片容器底部 */\n left: 0;\n right: 0;\n height: 25px;\n line-height: 25px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.7); /* 半透明背景 */\n color: #fff;\n font-size: 11px;\n font-weight: bold;\n border-bottom-left-radius: 6px;\n border-bottom-right-radius: 6px;\n user-select: none;\n }\n </style>\n \n <div id="gfriends-image-list-container">\n <p id="gfriends-prompt" style="text-align: center; font-size: 15px; margin-bottom: 15px;">\n 点击图片即可选择(初始共 ${s.length} 张)\n </p>\n <div style="overflow-y: auto; height: calc(100% - 40px);">\n <div id="gfriends-image-list">\n ${o}\n </div>\n </div>\n </div>\n `; let l = 0; layer.open({ type: 1, title: `选择女优头像 (${s.length} 张)`, area: utils.getResponsiveArea([ "900px", "85%" ]), content: r, btn: [ "关闭" ], success: (e, t) => { const n = $(e), a = n.find(".gfriends-selectable-img"), i = n.find("#gfriends-prompt"); a.each((function() { const e = $(this), a = e.data("wrapper-id"), o = n.find(`#${a}`), r = n.find(`.gfriends-size-tag[data-size-for="${a}"]`); e.on("load", (function() { const e = this.naturalWidth, t = this.naturalHeight; r.text(`${e} x ${t}`); })), e.on("error", (function() { o.remove(), l++; const e = s.length - l; i.text(`点击图片即可选择(已移除 ${l} 张错误图片,剩余 ${e} 张)`), 0 === e && (show.error("所有搜索到的头像链接均已失效,无法选择。"), layer.close(t)); })), this.complete && (this.naturalWidth > 0 ? e.trigger("load") : e.trigger("error")); })), a.on("click", (function() { const e = $(this), n = e.data("url"); $("#edit-actress-avatar").val(n), $("#edit-avatar-preview").attr("src", n), a.removeClass("is-selected"), e.addClass("is-selected"), setTimeout((() => { layer.close(t); }), 150); })), utils.setupEscClose(t); } }); } } class mt extends X { getName() { return "LocalPlugin"; } async handle() { if (r && !window.location.href.includes("/actors/")) { this.baseUrl = "http://127.0.0.1:7890", this.canRun = !1; try { const result = await utils.pingLocalService(this.baseUrl, 3e4); this.canRun = result && result.ok; } catch (e) { this.canRun = !1, console.error("本地服务连通性检查失败:", e); } this.canRun && isListPage && utils.loopDetector((() => $("#addBlacklistBtn").length), (() => { this.createBtn(); }), 1, 1e4, !1); } } createBtn() { $("#addBlacklistBtn").last().after('\n <a id="archiveBtn" class="menu-btn main-tab-btn" style="background-color:#39babe !important;margin-left: 20px!important;"><span>视频归档</span></a>\n <a id="checkSubtitleBtn" class="menu-btn main-tab-btn" style="background-color:#d08736 !important;"><span>检查字幕</span></a>\n '), $("#archiveBtn").on("click", (e => { this.archiveFile().then(); })), $("#checkSubtitleBtn").on("click", (e => { this.checkSubTitle().then(); })); } async archiveFile() { let e = await storageManager.getCarList(); const t = await gmHttp.post(this.baseUrl + "/archiveFile", { carList: e }); let n = t.dataList, a = t.updateHasDownCarNumList; if (a && a.length) { const t = new Set(a), n = Array.from(t); for (const a of n) { const t = e.find((e => e.carNum === a)); t && (await storageManager.saveCar({ carNum: a, url: t.url, actionType: g }), show.ok(`归档成功, ${a}标记为已下载`)); } } n.length > 0 ? layer.open({ type: 1, title: "归档信息", shadeClose: !0, scrollbar: !1, content: '\n <div style="height: 100%;overflow:hidden;"> \n <div id="archive-container" style="height: 100%;"></div>\n </div>\n ', anim: -1, area: utils.getResponsiveArea([ "50%", "70%" ]), success: e => { new Tabulator("#archive-container", { layout: "fitColumns", placeholder: "暂无数据", virtualDom: !0, data: n, responsiveLayout: "collapse", responsiveLayoutCollapse: !0, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, columns: [ { title: "信息", field: "msg", headerSort: !1, formatter: (e, t, n) => { const a = e.getData(); return "ok" === a.type ? `<span style="color:#58ad67">${a.msg}</span>` : `<span style="color:#c52323">${a.msg}</span>`; } }, { title: "操作", headerSort: !1, width: 200, formatter: (e, t, n) => { const a = e.getData(); return n((() => { var t; null == (t = e.getElement().querySelector(".a-primary")) || t.addEventListener("click", (e => { gmHttp.get(this.baseUrl + "/openFilePath", { filePath: a.file ? String(a.file).replace(/\.\./g, "") : "" }); })); })), '<a class="a-primary">打开路径</a>'; } } ], locale: "zh-cn", langs: { "zh-cn": { pagination: { first: "首页", first_title: "首页", last: "尾页", last_title: "尾页", prev: "上一页", prev_title: "上一页", next: "下一页", next_title: "下一页", all: "所有", page_size: "每页行数" } } } }); }, end() { window.refresh && window.refresh(); } }) : show.info("没有可归档文件"); } async checkSubTitle() { let e = await storageManager.getCarList(); let t = (await gmHttp.post(this.baseUrl + "/checkSubTitle", { dataList: e })).data; 0 !== t.length ? layer.open({ type: 1, title: "检测缺失字幕", shadeClose: !0, scrollbar: !1, content: '\n <div style="height: 100%;overflow:hidden;"> \n <div id="checkSubTitle-table-container" style="height: 100%;padding-bottom: 10px"></div>\n </div>\n ', anim: -1, area: utils.getResponsiveArea([ "70%", "70%" ]), success: e => { new Tabulator("#checkSubTitle-table-container", { layout: "fitColumns", placeholder: "暂无数据", virtualDom: !0, data: t, responsiveLayout: "collapse", responsiveLayoutCollapse: !0, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, columns: [ { title: "番号", width: 150, field: "carNum", headerSort: !1, formatter: (e, t, n) => { const a = e.getData(), i = a.type; return a.msg, "error" === i ? `<span style="color: #f40">${a.msg}</span>` : a.carNum; } }, { title: "文件路径", field: "filePath", headerSort: !1, formatter: (e, t, n) => e.getData().filePath }, { title: "操作", headerSort: !1, responsive: 0, formatter: (e, t, n) => { const a = e.getData(); return n((() => { var t, n, i, s, o; null == (t = e.getElement().querySelector(".a-success")) || t.addEventListener("click", (e => { gmHttp.get(this.baseUrl + "/openFilePath", { filePath: a.filePath ? String(a.filePath).replace(/\.\./g, "") : "" }); })), null == (n = e.getElement().querySelector(".a-info")) || n.addEventListener("click", (e => { let t = a.carNum, n = a.url; if (n) if (t.includes("FC2-")) { let e = this.parseMovieId(n); this.getBean("Fc2Plugin").openFc2Dialog(e, t, n); } else utils.openPage(n, t, !0, e); else show.error("没有找到url"); })), null == (i = e.getElement().querySelector(".a-warning")) || i.addEventListener("click", (e => { this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(a.carNum); })), null == (s = e.getElement().querySelector(".a-primary")) || s.addEventListener("click", (e => { utils.openPage("" + ("https://subtitlecat.com/index.php?search=" + a.carNum.replace("FC2-", "")), a.carNum.replace("FC2-", ""), !0, e); })), null == (o = e.getElement().querySelector(".a-danger")) || o.addEventListener("click", (e => { const t = a.filePath.split("<br/>").filter((e => "" !== e.trim())); utils.q(e, `是否调用AI程序生成字幕,共${t.length}个视频文件`, (() => { this.aiSubtitle(t); })); })); })), '\n <a class="a-success">打开路径</a>\n <a class="a-info">详情页</a>\n <a class="a-warning">迅雷字幕</a>\n <a class="a-primary">SubTitleCat字幕</a>\n <a class="a-danger">AI字幕</a>\n '; } } ], locale: "zh-cn", langs: { "zh-cn": { pagination: { first: "首页", first_title: "首页", last: "尾页", last_title: "尾页", prev: "上一页", prev_title: "上一页", next: "下一页", next_title: "下一页", all: "所有", page_size: "每页行数" } } } }); }, end() { window.refresh && window.refresh(); } }) : show.info("视频字幕完整"); } async aiSubtitle(e) { const t = await gmHttp.post(this.baseUrl + "/aiSubtitle", { fileList: e }); 200 === t.code ? show.info("已调用后台程序, 请自行确认") : show.error(t.msg); } checkHasDown() { this.allowRepeatDown = !1; $("#enable-magnets-filter").after('<a id="allowRepeatDown" class="menu-btn" style="background-color:#b8d747;margin-left: 5px"><span>关闭重复下载检验</span></a>'), $("#allowRepeatDown").on("click", (e => { this.allowRepeatDown = !this.allowRepeatDown, $("#allowRepeatDown span").text(this.allowRepeatDown ? "开启重复下载检验" : "关闭重复下载检验"); })); let e = this.findJavDbCarNum(), t = !1; $("#magnets-content .item a").on("click", (n => { let a = $(n.target).closest("a, button")[0] || n.target; if (t) t = !1; else { if (n.preventDefault(), this.allowRepeatDown) return t = !0, void a.click(); gmHttp.get(baseUrl + "/checkHasDown?carNum=" + e).then((e => { "no" === e.data ? (t = !0, a.click()) : show.info(e.msg, { icon: 2 }); })); } })); } } const searchFiles115 = async (search_value, offset = 0, limit = 30) => { const url = `https://webapi.115.com/files/search?search_value=${encodeURIComponent(search_value)}&offset=${offset}&limit=${limit}`; return await gmHttp.get(url); }; const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends X { constructor() { super(...arguments); i(this, "loginStatus", _WangPan115MatchPlugin.LoginStatus.UNCHECKED); } getName() { return "WangPan115MatchPlugin"; } async initCss() { return "\n <style>\n [class^='jhs-match-'] {\n padding: 1px 2px;\n margin-left: 0;\n margin-right: 5px;\n }\n \n .jhs-match-detail {\n display: block;\n width: 100%;\n z-index: 1000;\n background: #fffdfa;\n border: 1px solid #e6ddd3;\n border-radius: 7px;\n padding: 12px 16px;\n overflow-y: auto;\n box-sizing: border-box;\n margin-bottom: 0;\n box-shadow: 0 8px 24px rgba(68,49,31,.07);\n }\n .jhs-match-detail.isListPage{\n position: absolute;\n box-shadow: 0 4px 16px rgba(0,0,0,0.15);\n width: 600px;\n max-width: 90vw;\n }\n .jhs-match-detail table {\n width: 100%;\n border-collapse: collapse;\n }\n .jhs-match-detail th, .jhs-match-detail td {\n padding: 8px 12px;\n border: 1px solid #eee7df;\n text-align: left;\n font-size: 13px;\n color: #172033;\n }\n .jhs-match-detail th {\n background-color: #fffbf6;\n font-weight: 600;\n color: #172033;\n }\n .jhs-match-detail tr:hover {\n background-color: #fff9f1;\n }\n .jhs-match-detail th:nth-child(1),\n .jhs-match-detail td:nth-child(1) {\n min-width: 180px;\n max-width: 400px;\n word-break: break-all;\n }\n .jhs-match-detail th:nth-child(2),\n .jhs-match-detail td:nth-child(2) {\n width: 100px;\n white-space: nowrap;\n }\n .jhs-match-detail th:nth-child(3),\n .jhs-match-detail td:nth-child(3) {\n width: 140px;\n white-space: nowrap;\n }\n .jhs-match-detail th:nth-child(4),\n .jhs-match-detail td:nth-child(4) {\n width: 70px;\n text-align: center;\n }\n .jhs-match-detail .jhs-match-title-row th {\n background-color: #fffbf6;\n font-size: 16px;\n font-weight: 720;\n color: #172033;\n padding: 10px 12px;\n border-bottom: 1px solid #e6ddd3;\n }\n </style>\n "; } async handle() { $(document).on("click", ".jhs-match-no-login-btn", (async event => { event.preventDefault(); event.stopPropagation(); await this.handleLoginRedirect(); })); $(document).on("click", ".jhs-match-btn", (event => { event.preventDefault(); event.stopImmediatePropagation(); this.showMatchDetail(event.currentTarget); })); $(document).on("click", ".jhs-match-error-btn", (async event => { event.preventDefault(); event.stopPropagation(); await this.retryMatch(event.currentTarget); })); await this.matchDetailPage(); await this.matchListPage(); $(document).on("click", ".jhs-match-detail-error-btn", (async event => { event.preventDefault(); event.stopPropagation(); $(event.currentTarget).replaceWith("<a class='jhs-match-btn' title=\"匹配中...\">匹配中...</a>"); try { const carNum2 = this.getPageInfo().carNum, matchList = await this.searchFiles(carNum2); $(".jhs-115-match-detail").remove(); await this.matchDetailPage(matchList); } catch (error) { console.error(`重新匹配失败:`, error); } })); } async matchDetailPage(matchList) { if (!window.isDetailPage) return; if (await storageManager.getSetting("enable115Match", C) === C) return; const $detail = $('\n <div class="jhs-match-detail jhs-115-match-detail" id="115-match-table">\n <table>\n <thead>\n <tr class="jhs-match-title-row">\n <th colspan="4">115匹配</th>\n </tr>\n <tr>\n <th>名称</th>\n <th>大小</th>\n <th>时间</th>\n <th>播放</th>\n </tr>\n </thead>\n <tbody>\n </tbody>\n </table>\n </div>\n '), $tbody = $detail.find("tbody"); try { const carNum2 = this.getPageInfo().carNum; matchList || (matchList = await this.searchFiles(carNum2)); await this.checkLoginStatus(); if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-no-login-btn a-info'\n data-keyword="${carNum2}"\n title="未登录115网盘">未登录</a>\n </td></tr>`); else if (matchList.length > 0) { const rowsHtml = matchList.map((match => `\n <tr>\n <td>${match.name}</td>\n <td>${this.formatSize(match.size)}</td>\n <td>${match.createTime}</td>\n <td>\n <a href="https://115vod.com/?pickcode=${match.videoId}&share_id=0"\n target="_blank"\n class="a-success"\n title="播放">播放</a>\n </td>\n </tr>\n `)).join(""); $tbody.append(rowsHtml); } else return; // 未匹配: 不显示115匹配框 } catch (error) { $tbody.append(`<tr><td colspan="4">\n <a class="a-danger jhs-match-detail-error-btn" title="${error.message || "加载失败"}">加载失败,请重试</a>\n </td></tr>`); console.error("加载文件列表时发生错误:", error); } if (r) { if ($("#all-match-box").length) $("#all-match-box").append($detail); else { $("#tabs-container").before("<div id='all-match-box' style='width: 100%;'></div>"); $("#all-match-box").append($detail); } // 只在「磁力」标签激活时显示; 若当前是短评/相关清单, 先隐藏 const magnetsPane = document.getElementById("magnets"); if (magnetsPane && magnetsPane.classList.contains("is-hidden")) $detail[0].style.display = "none"; } else $("#mag-submit-show").before($detail); } async matchListPage() { if (!window.isListPage) return; if (await storageManager.getSetting("enable115Match", C) === C) return; try { const items = $(this.getSelector().itemSelector).toArray(); if (items.length) await this.matchMovieList(items); } catch (error) { clog.error("115列表页匹配失败", error); } } async matchMovieList(movieListElement) { if (await storageManager.getSetting("enable115Match", C) !== C) { await this.checkLoginStatus(); await this.processMovieElements(movieListElement); } else $(".video-title [class^='jhs-match-']").remove(); } showMatchDetail(buttonElement) { const $el = $(buttonElement), matchData = $el.attr("data-match"); $(".jhs-match-detail").remove(); const matches = this.parseMatchData(matchData); if (0 === matches.length) return; if (1 === matches.length) { const pickcode = matches[0].videoId; window.open(`https://115vod.com/?pickcode=${pickcode}&share_id=0`, "_blank"); return; } const $detail = this.createMatchDetailElement(matches); this.positionDetailElement($detail, $el); this.addOutsideClickHandler($detail); $detail.on("click", (e => { e.stopPropagation(); })); } parseMatchData(matchData) { try { return JSON.parse(matchData) || []; } catch (e) { console.error("解析匹配数据失败:", e); return []; } } createMatchDetailElement(matches) { const $detail = $(`\n <div class="jhs-match-detail isListPage">\n <table>\n <thead>\n <tr>\n <th>名称</th>\n <th>大小</th>\n <th>时间</th>\n <th>播放</th>\n </tr>\n </thead>\n <tbody>\n ${matches.map((match => `\n <tr>\n <td>${match.name}</td>\n <td>${this.formatSize(match.size)}</td>\n <td>${match.createTime}</td>\n <td>\n <a href="https://115vod.com/?pickcode=${match.videoId}&share_id=0" \n target="_blank" \n class="a-success"\n title="播放">播放</a>\n </td>\n </tr>\n `)).join("")}\n </tbody>\n </table>\n </div>\n `); $("body").append($detail); return $detail; } positionDetailElement($detail, $trigger) { const offset = $trigger.offset(), winW = $(window).width(), detailW = $detail.outerWidth(), detailH = $detail.outerHeight(); let left = offset.left, top = offset.top - detailH + 20; left + detailW > winW - 10 && (left = winW - detailW - 10); left < 0 && (left = 10); top < 0 && (top = offset.top + $trigger.outerHeight() + 5); $detail.css({ top, left }); } addOutsideClickHandler($detail) { setTimeout((() => { $(document).on("click.jhs-match-detail", (e => { if (!$detail.is(e.target) && 0 === $detail.has(e.target).length) { $detail.remove(); $(document).off("click.jhs-match-detail"); } })); }), 100); } async retryMatch(buttonElement) { const $el = $(buttonElement), $box2 = $el.closest(".movie-box, .item"), carNum2 = $el.attr("data-keyword"); $el.replaceWith("<a class='jhs-match-btn' title=\"匹配中...\">匹配中...</a>"); try { const matchList = await this.searchFiles(carNum2); this.updateMatchStatus($box2, carNum2, matchList); } catch (error) { console.error(`重新匹配失败 [${carNum2}]:`, error); this.showMatchError($box2, carNum2, error); } } updateMatchStatus($box2, carNum2, matchList) { matchList.length > 0 ? $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="点击查看匹配详情">匹配${matchList.length}个</a>`) : $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn a-info' data-keyword="${carNum2}" \n title="点击重新尝试匹配">未匹配</a>`); } async handleLoginRedirect() { window.open("https://115.com"); } async searchFiles(carNum2) { var _a2; let searchKeyword = carNum2.toLowerCase().replace("fc2-", ""); return (null == (_a2 = (await searchFiles115(searchKeyword)).data) ? void 0 : _a2.map((data => ({ folderId: data.fid, videoId: data.pc, name: data.n, createTime: utils.formatDate(new Date(1e3 * data.te)), size: data.s, isVideo: [ ".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv" ].some((ext => { var _a3; return null == (_a3 = data.n) ? void 0 : _a3.toLowerCase().endsWith(ext); })) }))).filter((x => x.folderId && x.isVideo && x.name.toLowerCase().includes(searchKeyword)))) || []; } showMatchError($box2, carNum2, error) { $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn' data-keyword="${carNum2}" \n title="匹配失败,点击重试">匹配失败</a>`); show.error(`${carNum2} 匹配失败: ${error.message || "网络错误"}`); } async checkLoginStatus() { var _a2; if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.UNCHECKED) try { const testResult = await searchFiles115("test"); this.loginStatus = (null == (_a2 = testResult.error) ? void 0 : _a2.includes("登录")) ? _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT : _WangPan115MatchPlugin.LoginStatus.LOGGED_IN; } catch { this.loginStatus = _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT; } } async processMovieElements(movieListElement) { const promises = Array.from(movieListElement).filter((ele => !utils.isHidden(ele))).filter((ele => !(l && $(ele).find(".avatar-box").length > 0))).map((ele => this.processSingleMovieElement(ele))); await Promise.allSettled(promises); } async processSingleMovieElement(element) { const $box2 = $(element); let carNum2; try { carNum2 = this.getBoxCarInfo($box2).carNum; } catch (error) { return; } if (!carNum2) return; if (!($box2.find("[class^='jhs-match-']").length > 0)) if (this.loginStatus !== _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) try { const matchList = await this.searchFiles(carNum2); this.addTag($box2, carNum2, matchList); } catch (error) { console.error(`搜索失败 [${carNum2}]:`, error); this.addTag($box2, carNum2, []); } else this.addTag($box2, carNum2, []); } addTag($box2, carNum2, matchList) { if (!($box2.find("[class^='jhs-match-']").length > 0)) if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $box2.find(".video-title").prepend(`<a class='jhs-match-no-login-btn a-info' \n data-keyword="${carNum2}" \n title="未登录115网盘">未登录</a>`); else if (matchList.length > 0) { const title = 1 === matchList.length ? "点击直接播放" : `点击查看${matchList.length}个匹配结果`; $box2.find(".video-title").prepend(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="${title}">匹配${matchList.length}个</a>`); } else $box2.find(".video-title").prepend(`<a class='jhs-match-error-btn a-info' \n data-keyword="${carNum2}" \n title="未匹配,点击重试">未匹配</a>`); } formatSize(bytes) { if (!bytes) return "-"; const units = [ "B", "KB", "MB", "GB", "TB" ]; let size = parseFloat(bytes), unit = 0; for (;size >= 1024 && unit < units.length - 1; ) { size /= 1024; unit++; } return `${size.toFixed(2)} ${units[unit]}`; } }; i(_WangPan115MatchPlugin, "LoginStatus", { UNCHECKED: -1, LOGGED_OUT: 0, LOGGED_IN: 1 }); let WangPan115MatchPlugin = _WangPan115MatchPlugin; class StatsPlugin extends X { getName() { return "StatsPlugin"; } async handle() { window.isListPage && this.createBtn(); } createBtn() { const e = '<a id="statsBtn" class="menu-btn main-tab-btn" style="background-color:#6c5ce7 !important;"><span>统计</span></a>'; r ? $("#newVideoBtn").after(e) : l && $("#newVideoBtn").after(e); $("#statsBtn").on("click", (() => { this.openDialog(); })); } async openDialog() { const e = await storageManager.getCarList(), t = await storageManager.getFavoriteActressList(), n = await storageManager.getBlacklist(), a = e.length, m = await storageManager.getStatusMap(), i = { filter: m[d].size, favorite: m[h].size, hasDown: m[g].size, hasWatch: m[p].size }, s = {}; e.forEach((e => { e.names && e.names.split(" ").forEach((e => { e && (s[e] = (s[e] || 0) + 1); })); })); const o = Object.entries(s).sort(((e, t) => t[1] - e[1])).slice(0, 10), r = o.length > 0 ? o[0][1] : 1, l = a - i.filter - i.favorite - i.hasDown - i.hasWatch; let c = 0; const pendingCarSet = await storageManager.getCarMap(), pendingCounter = this.getBean("NewVideoPlugin"); t.forEach((e => { if (pendingCounter) c += pendingCounter.getPendingNewVideoCount(e, pendingCarSet); })); const barPct = e => '<span style="display:inline-block;height:18px;width:' + Math.round(e / r * 100) + '%;background:#6c5ce7;border-radius:3px;min-width:2px"></span>', barRow = (e, t, r) => { const c = a > 0 ? Math.round(e / a * 100) : 0; return '<div style="display:flex;align-items:center;margin-bottom:6px;gap:8px"><span style="width:60px;font-size:13px;text-align:right">' + t + '</span><span style="flex:1;background:#eee;border-radius:3px;height:18px"><span style="display:inline-block;height:18px;width:' + c + '%;background:' + r + ';border-radius:3px;min-width:2px"></span></span><span style="width:50px;font-size:12px;color:#888">' + e + ' (' + c + '%)</span></div>'; }, dialogHtml = '<div style="padding:10px 20px;height:100%;overflow:auto" class="jhs-scrollbar"><div style="display:flex;gap:10px;margin-bottom:15px;flex-wrap:wrap"><div style="flex:1;min-width:100px;background:#f8f9fa;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#333">' + a + '</div><div style="font-size:12px;color:#888">总数</div></div><div style="flex:1;min-width:100px;background:#f0f7ff;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#25b1dc">' + (i.favorite || 0) + '</div><div style="font-size:12px;color:#888">已收藏</div></div><div style="flex:1;min-width:100px;background:#f0fff4;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#7bc73b">' + (i.hasDown || 0) + '</div><div style="font-size:12px;color:#888">已下载</div></div><div style="flex:1;min-width:100px;background:#fff9f0;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#d7a80c">' + (i.hasWatch || 0) + '</div><div style="font-size:12px;color:#888">已观看</div></div><div style="flex:1;min-width:100px;background:#fff5f5;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#de3333">' + (i.filter || 0) + '</div><div style="font-size:12px;color:#888">已屏蔽</div></div><div style="flex:1;min-width:100px;background:#f5f5f5;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#666">' + l + '</div><div style="font-size:12px;color:#888">待鉴定</div></div></div><div style="display:flex;gap:10px;margin-bottom:15px;flex-wrap:wrap"><div style="flex:1;min-width:100px;background:#f8f0ff;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#6c5ce7">' + t.length + '</div><div style="font-size:12px;color:#888">收藏演员</div></div><div style="flex:1;min-width:100px;background:#fff0f0;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#b22222">' + n.length + '</div><div style="font-size:12px;color:#888">黑名单演员</div></div><div style="flex:1;min-width:100px;background:#fffef0;border-radius:8px;padding:12px;text-align:center"><div style="font-size:24px;font-weight:bold;color:#e8ab39">' + c + '</div><div style="font-size:12px;color:#888">新作品待看</div></div></div><div style="font-size:14px;font-weight:bold;margin-bottom:8px;color:#333">状态分布</div>' + barRow(i.favorite || 0, "已收藏", "#25b1dc") + barRow(i.hasDown || 0, "已下载", "#7bc73b") + barRow(i.hasWatch || 0, "已观看", "#d7a80c") + barRow(i.filter || 0, "已屏蔽", "#de3333") + barRow(l, "待鉴定", "#888") + (o.length > 0 ? '<div style="font-size:14px;font-weight:bold;margin:12px 0 8px;color:#333">Top10 演员</div>' + o.map(((e, t) => '<div style="display:flex;align-items:center;margin-bottom:4px;gap:8px"><span style="width:24px;font-size:12px;color:#888;text-align:right">' + (t + 1) + '</span><span style="width:100px;font-size:13px;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml(e[0]) + '">' + escapeHtml(e[0]) + '</span>' + barPct(e[1]) + '<span style="width:36px;font-size:12px;color:#888">' + e[1] + '</span></div>')).join("") : "") + '</div>'; layer.open({ type: 1, title: "收藏统计", content: dialogHtml, scrollbar: !1, area: utils.getResponsiveArea(["60%", "80%"]), anim: -1, success: (e, t) => { utils.setupEscClose(t); } }); } } class MobileBottomBarPlugin extends X { constructor() { super(...arguments); this._fabGeneration = 0; } getName() { return "MobileBottomBarPlugin"; } shouldSkipOnMobile() { return false; // this plugin only runs on mobile } async initCss() { return ` /* FAB 浮动操作按钮 */ #jhs-fab { position: fixed; bottom: calc(24px + env(safe-area-inset-bottom, 0px)); right: 20px; width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #3b82f6, #1e40af); color: white; font-size: 26px; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10002; box-shadow: 0 4px 16px rgba(59,130,246,0.4); transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1), box-shadow 0.3s; -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none; } #jhs-fab:active { transform: scale(0.9); } #jhs-fab.jhs-fab-open { transform: rotate(135deg); background: linear-gradient(135deg, #ef4444, #b91c1c); box-shadow: 0 4px 16px rgba(239,68,68,0.4); } /* FAB 遮罩 */ .jhs-fab-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.25); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); z-index: 10000; opacity: 0; pointer-events: none; transition: opacity 0.3s; } .jhs-fab-backdrop.jhs-fab-backdrop-visible { opacity: 1; pointer-events: auto; } /* FAB 菜单 */ .jhs-fab-menu { position: fixed; bottom: calc(92px + env(safe-area-inset-bottom, 0px)); right: 16px; z-index: 10001; display: flex; flex-direction: column; gap: 10px; align-items: flex-end; opacity: 0; transform: translateY(16px) scale(0.92); pointer-events: none; transition: opacity 0.25s, transform 0.25s cubic-bezier(0.32, 0.72, 0, 1); } .jhs-fab-menu.jhs-fab-menu-open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; } /* FAB 菜单分组 */ .jhs-fab-group { display: flex; flex-direction: column; gap: 10px; align-items: flex-end; } .jhs-fab-divider { width: 32px; height: 2px; background: rgba(0,0,0,0.08); border-radius: 1px; align-self: flex-end; margin: 2px 0; } /* FAB 菜单项 */ .jhs-fab-menu-item { display: flex; align-items: center; gap: 10px; padding: 10px 16px; background: white; border-radius: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); cursor: pointer; white-space: nowrap; min-height: 44px; font-size: 14px; font-weight: 500; color: #333; opacity: 0; transform: translateY(8px) scale(0.92); transition: opacity 0.2s, transform 0.2s cubic-bezier(0.32, 0.72, 0, 1), background 0.15s; -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none; } .jhs-fab-menu-item.jhs-fab-item-visible { opacity: 1; transform: translateY(0) scale(1); transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.32, 0.72, 0, 1), background 0.15s; } .jhs-fab-menu-item:active { transform: scale(0.95); background: #f3f4f6; } /* FAB 状态色块 */ .jhs-fab-status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: #e5e7eb; } @media (min-width: 769px) { #jhs-fab, .jhs-fab-menu, .jhs-fab-backdrop { display: none !important; } } `; } async handle() { if (!utils.isMobileMode()) return; // 添加遮罩 const backdrop = $('<div class="jhs-fab-backdrop"></div>').appendTo("body"); // 添加菜单 const menu = this.createMenu(); $("body").append(menu); // 添加 FAB 按钮 const fab = $('<div id="jhs-fab">+</div>').appendTo("body"); this.bindEvents(fab, backdrop); } /** 获取详情页番号 */ getCarNum() { try { const basePlugin = this.getBean("DetailPageButtonPlugin"); if (basePlugin?.parseMovieId) return basePlugin.parseMovieId(location.href); const el = document.querySelector(".header, #video_id, .video-id"); if (el) return el.textContent.trim(); } catch (e) {} return null; } createMenu() { const isList = window.isListPage; const isDetail = window.isDetailPage; let items = ""; if (isList) { const sortMethod = localStorage.getItem("jhs_sortMethod") || "default"; const sortLabel = { default: "默认", rateCount: "评价人数", date: "时间" }[sortMethod]; items = ` <div class="jhs-fab-group"> <div class="jhs-fab-menu-item" data-action="check">待鉴定</div> <div class="jhs-fab-menu-item" data-action="newVideo">新作品</div> <div class="jhs-fab-menu-item" data-action="blacklist">黑名单</div> <div class="jhs-fab-menu-item" data-action="sort">排序: ${sortLabel}</div> </div> <div class="jhs-fab-divider"></div> <div class="jhs-fab-group"> <div class="jhs-fab-menu-item" data-action="setting">设置</div> </div> `; } else if (isDetail) { const statusDefs = [ { action: "filter", icon: m, label: "屏蔽", key: "filter" }, { action: "fav", icon: v, label: "收藏", key: "fav" }, { action: "down", icon: y, label: "已下载", key: "down" }, { action: "watch", icon: k, label: "已观看", key: "watch" } ]; items = ` <div class="jhs-fab-group"> ${statusDefs.map(d => `<div class="jhs-fab-menu-item" data-action="${d.action}" data-label="${d.label}"><span class="jhs-fab-status-dot" data-status-key="${d.key}"></span>${d.icon}</div>`).join("")} </div> <div class="jhs-fab-divider"></div> <div class="jhs-fab-group"> <div class="jhs-fab-menu-item" data-action="magnetFilter">磁力过滤</div> <div class="jhs-fab-menu-item" data-action="magnet">磁力搜索</div> <div class="jhs-fab-menu-item" data-action="subtitle">字幕</div> </div> <div class="jhs-fab-divider"></div> <div class="jhs-fab-group"> <div class="jhs-fab-menu-item" data-action="setting">设置</div> </div> `; } else { items = ` <div class="jhs-fab-group"> <div class="jhs-fab-menu-item" data-action="setting">设置</div> </div> `; } return $(`<div class="jhs-fab-menu">${items}</div>`); } /** 刷新详情页菜单的状态指示 */ async refreshDetailStatus() { try { const carNum = this.getCarNum(); if (!carNum) return; const car = await storageManager.getCar(carNum); const menu = $(".jhs-fab-menu"); const colors = { filter: "#ef4444", fav: "#3b82f6", down: "#22c55e", watch: "#a855f7" }; const activeKey = { [d]: "filter", [h]: "fav", [g]: "down", [p]: "watch" }; let activeStatus = null; if (car?.status && activeKey[car.status]) activeStatus = activeKey[car.status]; menu.find(".jhs-fab-status-dot").each(function () { const key = $(this).data("status-key"); const item = $(this).closest(".jhs-fab-menu-item"); if (key === activeStatus) { $(this).css({ background: colors[key] || "#ccc" }); } else { $(this).css({ background: "#e5e7eb" }); } }); } catch (e) {} } bindEvents(fab, backdrop) { const menu = $(".jhs-fab-menu"); const closeMenu = () => { this._fabGeneration++; fab.removeClass("jhs-fab-open"); menu.removeClass("jhs-fab-menu-open"); backdrop.removeClass("jhs-fab-backdrop-visible"); menu.find(".jhs-fab-menu-item").removeClass("jhs-fab-item-visible"); }; const toggleMenu = () => { const isOpen = fab.hasClass("jhs-fab-open"); if (isOpen) { closeMenu(); } else { fab.addClass("jhs-fab-open"); menu.addClass("jhs-fab-menu-open"); backdrop.addClass("jhs-fab-backdrop-visible"); // 刷新排序标签 if (window.isListPage) { const sortMethod = localStorage.getItem("jhs_sortMethod") || "default"; const sortLabel = { default: "默认", rateCount: "评价人数", date: "时间" }[sortMethod]; menu.find('[data-action="sort"]').html(`排序: ${sortLabel}`); } // 刷新详情页状态 if (window.isDetailPage) this.refreshDetailStatus(); // stagger 动画:依次显示菜单项(generation counter 防竞态) const gen = ++this._fabGeneration; const self = this; const items = menu.find(".jhs-fab-menu-item"); items.each(function (i) { const el = $(this); setTimeout(() => { if (gen === self._fabGeneration) el.addClass("jhs-fab-item-visible"); }, 30 + i * 35); }); } }; // FAB 点击切换 fab.on("click", toggleMenu); // 遮罩点击关闭 backdrop.on("click", closeMenu); // 菜单项点击 menu.on("click", ".jhs-fab-menu-item", (e) => { const action = $(e.currentTarget).data("action"); this.handleAction(action); closeMenu(); }); } handleAction(action) { switch (action) { // 列表页操作 case "check": $("#waitCheckBtn").length && $("#waitCheckBtn").click(); break; case "newVideo": this.getBean("NewVideoPlugin")?.openDialog(); break; case "blacklist": this.getBean("BlacklistPlugin")?.openBlacklistDialog(); break; case "sort": { const cur = localStorage.getItem("jhs_sortMethod") || "default"; const next = cur === "default" ? "rateCount" : cur === "rateCount" ? "date" : "default"; localStorage.setItem("jhs_sortMethod", next); const btnPlugin = this.getBean("ListPageButtonPlugin"); btnPlugin?.sortItems?.(); const label = { default: "默认", rateCount: "评价人数", date: "时间" }[next]; show.info(`排序: ${label}`); break; } // 详情页操作 case "filter": $("#filterBtn").length && $("#filterBtn").click(); break; case "fav": $("#favoriteBtn").length && $("#favoriteBtn").click(); break; case "down": $("#hasDownBtn").length && $("#hasDownBtn").click(); break; case "watch": $("#hasWatchBtn").length && $("#hasWatchBtn").click(); break; case "magnetFilter": $("#enable-magnets-filter").length && $("#enable-magnets-filter").click(); break; case "magnet": $("#magnetSearchBtn").length && $("#magnetSearchBtn").click(); break; case "subtitle": $("#search-subtitle-btn").length && $("#search-subtitle-btn").click(); break; // 通用 case "setting": this.getBean("SettingPlugin")?.openSettingDialog(); break; } } } const DEFAULT_JAVDB_PLUGINS = [ Ie, Be, le, de, Ce, xe, Ae, fe, pe, ue, Ee, Ue, Oe, Q, ye, ce, ae, ke, he, be, Ze, ze, Re, Ve, Se, Xe, pt, et, mt, StatsPlugin, MobileBottomBarPlugin, WangPan115MatchPlugin, NativeMagnetPanelPlugin, NativeReviewPanelPlugin, NativeListPanelPlugin ]; const DEFAULT_JAVBUS_PLUGINS = [ Ie, Ce, Ae, xe, Be, Ee, Fe, Ue, Qe, we, ye, ke, ce, je, Re, Ve, he, be, Ze, Se, et, StatsPlugin, MobileBottomBarPlugin, WangPan115MatchPlugin, NativeMagnetPanelPlugin ]; const DEFAULT_SHARED_PLUGIN_RULES = [ { shouldRegister: hostname => hostname.includes("javtrailers"), plugins: [ oe ] }, { shouldRegister: hostname => hostname.includes("subtitlecat"), plugins: [ re ] } ]; function registerPluginGroup(pluginManager, plugins) { plugins.forEach((pluginClass => pluginManager.register(pluginClass))); } function registerSitePlugins(pluginManager, hostname = window.location.hostname) { DEFAULT_SHARED_PLUGIN_RULES.forEach((rule => { rule.shouldRegister(hostname) && registerPluginGroup(pluginManager, rule.plugins); })); r && registerPluginGroup(pluginManager, DEFAULT_JAVDB_PLUGINS); l && registerPluginGroup(pluginManager, DEFAULT_JAVBUS_PLUGINS); } const ut = layer.close; layer.close = function(e) { const t = ut.call(this, e); return function(e = 10) { setTimeout((() => { const e = document.querySelectorAll(".layui-layer-shade").length; document.documentElement.style.overflow = e > 0 ? "hidden" : ""; }), e); }(), t; }; const ft = layer.open; layer.open = function(e) { const t = (e = e || {}).success; return e.success = function(e, n) { "function" == typeof t && t.call(this, e, n), utils.setupEscClose(n); }, ft.call(this, e); }, utils.importResource("https://cdn.jsdelivr.net/npm/[email protected]/layer.min.css"), utils.importResource("https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css"), utils.importResource("https://cdn.jsdelivr.net/npm/[email protected]/dist/viewer.min.css"), utils.importResource("https://cdn.jsdelivr.net/npm/[email protected]/dist/css/tabulator_semanticui.min.css"); /** 插件注册中心: 按站点注册所有插件, 暴露到 unsafeWindow.pluginManager */ const vt = function() { const e = new Y; unsafeWindow.pluginManager = e; registerSitePlugins(e); return e; }(); vt.processCss().catch((e => console.error("[JHS] processCss failed:", e))), async function() { window.isDetailPage = function() { let e = window.location.href; return r ? e.split("?")[0].includes("/v/") : !!l && $("#magnet-table").length > 0; }(), window.isListPage = function() { let e = window.location.href; return r ? $(".movie-list").length > 0 || e.includes("advanced_search") : !!l && $(".masonry > div .item").length > 0; }(), window.isFc2Page = function() { let e = window.location.href; return e.includes("advanced_search?type=3") || e.includes("advanced_search?type=100"); }(), await (async () => { const e = await storageManager.getDataVersion(); e < CURRENT_DATA_VERSION && (await storageManager.merge_table_name(), await storageManager.clean_no_url_blacklist(), await storageManager.async_merge_other(), await storageManager.merge_blacklist(), await storageManager.merge_favoriteActress(), await storageManager.merge_tow_car_list_table(), await storageManager.setDataVersion(CURRENT_DATA_VERSION)); })(), r && /(^|;)\s*locale\s*=\s*en\s*($|;)/i.test(document.cookie) && show.error("请切换到中文语言下才可正常使用本脚本", { duration: -1 }), vt.processPlugins().then(() => { // 自动备份: 插件就绪后按配置的频率备份到 WebDav if (!r) return; const settingPlugin = vt.getBean("SettingPlugin"); if (settingPlugin && settingPlugin.autoBackup) settingPlugin.autoBackup().catch((e => clog.error("自动备份失败", e))); }).catch((e => console.error("[JHS] processPlugins failed:", e))); // 悬浮大图: 独立于 ListPagePlugin, 确保即使列表页插件被禁用也能生效 storageManager.getSetting("hoverBigImg", C).then((e => { if (e === _) { const sel = r ? ".cover img" : l ? ".masonry .movie-box .photo-frame img" : null; if (sel) window.imageHoverPreviewObj ? window.imageHoverPreviewObj.bindEvents() : window.imageHoverPreviewObj = new ImageHoverPreview({ selector: sel }); } })); // 悬浮图片保存快捷键 (列表页悬浮大图 + 详情页封面) storageManager.getSetting("saveHoverImageHotKey", "q").then((key => { if (key && HotkeyManager.isHotkeyFormat(key)) { HotkeyManager.registerHotkey(key, (() => { const ae = document.activeElement; if ("INPUT" === ae.tagName || "TEXTAREA" === ae.tagName || ae.isContentEditable) return; // 优先: 悬浮预览图 if (window.imageHoverPreviewObj && window.imageHoverPreviewObj.currentTarget) { window.imageHoverPreviewObj.saveCurrentImage(); return; } // 详情页: 保存封面大图 if (window.isDetailPage) { let coverSrc = ""; if (r) coverSrc = $(".column-video-cover img").attr("src") || ""; if (l) coverSrc = $(".movie .bigImage img, .screencap img").first().attr("src") || ""; if (!coverSrc) return; // 提取番号命名 let carNum = ""; const $clip = $("a[data-clipboard-text]").first(); if ($clip.length) carNum = ($clip.attr("data-clipboard-text") || "").trim(); if (!carNum) { const seg = location.href.split("?")[0].split("#")[0].split("/").filter(Boolean).pop() || ""; if (/^[A-Za-z0-9]+-?\d+/.test(seg)) carNum = seg; } if (!carNum) { const $id = $("span.header:contains('識別碼'), span.header:contains('识别码')").first().parent(); if ($id.length) carNum = $id.text().replace(/.*[::]/, "").trim(); } carNum = carNum ? carNum.toUpperCase().replace(/[\\/:*?"<>|]/g, "_") : ""; if (typeof GM_xmlhttpRequest !== "undefined") { GM_xmlhttpRequest({ method: "GET", url: coverSrc, responseType: "arraybuffer", headers: { Referer: window.location.href }, onload: (res) => { if (!res.response || res.response.byteLength < 100) { show.error && show.error("封面下载失败"); return; } const arr = new Uint8Array(res.response); let ext = "jpg", mime = "image/jpeg"; if (arr[0] === 0x89 && arr[1] === 0x50) { ext = "png"; mime = "image/png"; } else if (arr[0] === 0x47 && arr[1] === 0x49) { ext = "gif"; mime = "image/gif"; } else if (arr[0] === 0x52 && arr[1] === 0x49 && arr[8] === 0x57 && arr[9] === 0x45) { ext = "webp"; mime = "image/webp"; } const name = (carNum || "cover") + "." + ext; const blob = new Blob([res.response], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = name; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1e3); show.ok && show.ok("封面已保存: " + name); }, onerror: () => { show.error && show.error("封面下载失败"); } }); } } })); } })); }();