// ==UserScript== // @name 视频收藏助手 - 多站联合版 // @namespace https://gitee.com/xiaobin90/jVideoCollector // @version 1.0.7 // @description 视频收藏助手 - 多站联合版,支持低端影视、厂长资源、嘀嗒影视等影视站点收藏工具 // @author jackMa // @match *://www.didahd.xyz/* // @match *://www.4kcz.com/* // @match *://czzy.top/* // @match *://www.cz4k.com/* // @match *://hantv.net/* // @match *://*.hantv.net/* // @match *://ddys.app/* // @match *://*.ddys.app/* // @match *://ddys.ai/* // @match *://*.ddys.ai/* // @match *://libvio.host/* // @match *://*.libvio.host/* // @match *://www.libvio.mov/* // @match *://*.libvio.mov/* // @match *://www.libvios.com/* // @match *://*.libvios.com/* // @match *://www.libhd.com/* // @match *://*.libhd.com/* // @match *://libviobd.com/* // @match *://*.libviobd.com/* // @match *://www.libvio.in/* // @match *://*.libvio.in/* // @match *://xl02.com.de/* // @match *://*.xl02.com.de/* // @match *://v.xl.in.ua/* // @match *://*.v.xl.in.ua/* // @match *://v.xl01.eu.cc/* // @match *://*.v.xl01.eu.cc/* // @match *://v.xl02.eu.cc/* // @match *://*.v.xl02.eu.cc/* // @match *://v.xl01.cc.ua/* // @match *://*.v.xl01.cc.ua/* // @match *://www.duse2.com/* // @match *://*.duse2.com/* // @match *://www.duse3.com/* // @match *://*.duse3.com/* // @match *://www.duse4.com/* // @match *://*.duse4.com/* // @match *://www.hanju.com/* // @match *://hanju.com/* // @noframes true // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @run-at document-start // ==/UserScript== (() => { // src/services/drama/sites/dida.js var dida_default = { id: "dida", name: "\u5600\u55D2\u5F71\u89C6", domains: ["www.didahd.xyz"], matches: ["*://www.didahd.xyz/*"], // 跨站跳转搜索路径(域名无关) searchPath: (kw) => "/search/-------------.html?wd=" + encodeURIComponent(kw), pageType() { const p = location.pathname; if (p.startsWith("/detail/")) return "detail"; if (p.startsWith("/play/")) return "play"; if (p.startsWith("/search/")) return "search"; if (document.querySelector(".myui-vodlist")) return "list"; return "other"; }, // 列表页 + 搜索页卡片悬浮收藏 listCardSelector: ".myui-vodlist__box, #searchList li", getListCards() { const cards = []; const seen = /* @__PURE__ */ new Set(); document.querySelectorAll(".myui-vodlist__box").forEach((box) => { var _a; if (seen.has(box)) return; const a = box.querySelector("a.myui-vodlist__thumb"); if (!a) return; const name = (a.getAttribute("title") || "").trim(); const cover = a.getAttribute("data-original") || ((_a = a.querySelector("img")) == null ? void 0 : _a.src) || ""; if (name) { seen.add(box); cards.push({ element: box, name, cover, path: a.pathname }); } }); document.querySelectorAll("#searchList li").forEach((li) => { var _a; if (seen.has(li)) return; const a = li.querySelector("a.myui-vodlist__thumb"); if (!a) return; const sk = li.querySelector("a.searchkey"); const name = ((sk == null ? void 0 : sk.textContent) || a.getAttribute("title") || "").trim(); const cover = a.getAttribute("data-original") || ((_a = a.querySelector("img")) == null ? void 0 : _a.src) || ""; if (name) { seen.add(li); cards.push({ element: li, name, cover, path: a.pathname }); } }); return cards; }, // 详情页:名称从 .myui-content__detail .title,图片从 img.src,跳转链接从 a.href(播放链接) getDetailInfo() { const img = document.querySelector(".myui-content__thumb img"); const titleEl = document.querySelector(".myui-content__detail .title"); const a = document.querySelector(".myui-content__thumb a"); const name = ((titleEl == null ? void 0 : titleEl.textContent) || "").trim(); if (!name) return null; const cover = (img == null ? void 0 : img.src) || ""; const path = (a == null ? void 0 : a.getAttribute("href")) || location.pathname; return { name, cover, path }; }, // 播放页:名称从 #player-sidebar h3.title(去掉集数 small),链接取当前 url getPlayInfo() { const h3 = document.querySelector("#player-sidebar .myui-panel__head h3.title"); let name = ""; if (h3) { const clone = h3.cloneNode(true); clone.querySelectorAll("small").forEach((s) => s.remove()); name = clone.textContent.trim(); } if (!name) return null; return { name, path: location.pathname }; } }; // src/services/drama/sites/czzy.js var czzy_default = { id: "czzy", name: "\u5382\u957F\u8D44\u6E90", domains: ["www.4kcz.com", "czzy.top", "www.cz4k.com"], matches: ["*://www.4kcz.com/*", "*://czzy.top/*", "*://www.cz4k.com/*"], searchPath: (kw) => "/#vc-search=" + encodeURIComponent(kw), // 首页加载后检测 hash 中的搜索关键词,从搜索表单提取 action 直达搜索页。 // 表单为标准 GET(action 即搜索路由、参数名为输入框 name),拼 URL 等价于提交表单 onPageReady() { const m = location.hash.match(/vc-search=([^&]+)/); if (!m) return; const kw = decodeURIComponent(m[1]); history.replaceState(null, "", location.pathname + location.search); let attempts = 0; const trySearch = () => { if (attempts++ > 10) return; const form = document.querySelector("#sosuobox form, form.w-search-form"); const input = form == null ? void 0 : form.querySelector("input[name]"); const action = form == null ? void 0 : form.getAttribute("action"); if (form && input && action) { const url = new URL(action, location.href); url.searchParams.set(input.getAttribute("name"), kw); location.href = url.href; return; } setTimeout(trySearch, 300); }; trySearch(); }, pageType() { const p = location.pathname; if (p.startsWith("/movie/")) return "detail"; if (p.startsWith("/v_play/")) return "play"; if (/^\/(boss1O1|nimasile)\b/.test(p)) return "search"; if (document.querySelector(".bt_img, .bt_image")) return "list"; return "other"; }, // 列表页 + 搜索页卡片悬浮收藏 listCardSelector: ".bt_img li, .bt_image li", getListCards() { const cards = []; const seen = /* @__PURE__ */ new Set(); document.querySelectorAll(".bt_img li, .bt_image li").forEach((li) => { if (seen.has(li)) return; const a = li.querySelector("a"); const img = li.querySelector("img"); const dytit = li.querySelector(".dytit"); if (!a) return; const name = ((dytit == null ? void 0 : dytit.textContent) || (img == null ? void 0 : img.alt) || "").trim(); const cover = (img == null ? void 0 : img.getAttribute("data-original")) || (img == null ? void 0 : img.src) || ""; if (name) { seen.add(li); cards.push({ element: li, name, cover, path: a.pathname }); } }); return cards; }, // 详情页:名称从 .dytext h1,图片从 .dyimg img.src,跳转链接从 .paly_list_btn a(播放链接) getDetailInfo() { const img = document.querySelector(".dyimg img"); const h1 = document.querySelector(".dytext h1"); const playA = document.querySelector(".paly_list_btn a"); const name = ((h1 == null ? void 0 : h1.textContent) || "").trim(); if (!name) return null; const cover = (img == null ? void 0 : img.src) || ""; const path = (playA == null ? void 0 : playA.getAttribute("href")) || location.pathname; return { name, cover, path }; }, // 播放页:名称从 .ptit a,链接从 a.href getPlayInfo() { const a = document.querySelector(".ptit a"); if (!a) return null; const name = (a.textContent || "").trim(); if (!name) return null; return { name, path: a.pathname }; } }; // src/base/formatHelper.js function interpolate(template, data) { return template.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] ?? ""); } function formatDate(ts) { if (!ts) return ""; const d = new Date(ts); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } function formatDuration(seconds) { if (!seconds || seconds <= 0) return ""; const s = Math.floor(seconds); const pad = (n) => String(n).padStart(2, "0"); return `${pad(Math.floor(s / 3600))}:${pad(Math.floor(s % 3600 / 60))}:${pad(s % 60)}`; } function stripParens(s) { return (s || "").replace(/\s*[((][^))]*[))]\s*/g, "").trim(); } function parseBgImage(style) { const m = (style || "").match(/background-image:\s*url\(["']?([^"')]+)["']?\)/i); return m ? m[1] : ""; } function isPlaceholderImage(src) { return !src || /load\.gif|loading\.gif|placeholder|^data:image|\.svg$/i.test(src); } // src/services/drama/sites/hantv.js var hantv_default = { id: "hantv", name: "\u97E9\u5267tv", domains: ["hantv.net", "www.hantv.net"], matches: ["*://hantv.net/*", "*://*.hantv.net/*"], searchPath: (kw) => "/vodsearch.html?wd=" + encodeURIComponent(kw), pageType() { const p = location.pathname; if (p.startsWith("/nf-detail/")) return "detail"; if (p.startsWith("/nf-play/")) return "play"; if (p.startsWith("/vodsearch")) return "search"; if (document.querySelector(".module-items")) return "list"; return "other"; }, // 列表页(module-poster-item) + 搜索页(module-card-item) listCardSelector: "a.module-poster-item, .module-card-item", getListCards() { const cards = []; const seen = /* @__PURE__ */ new Set(); document.querySelectorAll("a.module-poster-item").forEach((a) => { var _a; if (seen.has(a)) return; const name = (((_a = a.querySelector(".module-poster-item-title")) == null ? void 0 : _a.textContent) || "").trim(); const img = a.querySelector("img"); const cover = (img == null ? void 0 : img.getAttribute("data-original")) || (img == null ? void 0 : img.src) || ""; if (name) { seen.add(a); cards.push({ element: a, name, cover, path: a.pathname }); } }); document.querySelectorAll(".module-card-item").forEach((item) => { var _a; if (seen.has(item)) return; const name = (((_a = item.querySelector(".module-card-item-title")) == null ? void 0 : _a.textContent) || "").trim(); const img = item.querySelector("img"); const a = item.querySelector("a.module-card-item-poster"); const cover = (img == null ? void 0 : img.getAttribute("data-original")) || (img == null ? void 0 : img.src) || ""; if (name && a) { seen.add(item); cards.push({ element: item, name, cover, path: a.pathname }); } }); return cards; }, // 详情页:名称从 .module-info-heading h1,图片从 img.src,链接取当前 url getDetailInfo() { const img = document.querySelector(".module-info-poster img"); const h1 = document.querySelector(".module-info-heading h1"); const name = ((h1 == null ? void 0 : h1.textContent) || "").trim(); if (!name) return null; const cover = (img == null ? void 0 : img.src) || (img == null ? void 0 : img.getAttribute("data-original")) || ""; return { name, cover, path: location.pathname }; }, // 播放页:名称从 .module-player-side .module-info-heading a,链接从 a.href getPlayInfo() { const a = document.querySelector(".module-player-side .module-info-heading a"); if (!a) return null; const name = (a.getAttribute("title") || a.textContent || "").trim(); if (!name) return null; return { name, path: a.pathname }; } }; // src/services/drama/sites/ddys.js var ddys_default = { id: "ddys", name: "\u4F4E\u7AEF\u5F71\u89C6", domains: ["ddys.app", "ddys.ai"], matches: ["*://ddys.app/*", "*://*.ddys.app/*", "*://ddys.ai/*", "*://*.ddys.ai/*"], searchPath: (kw) => "/?s=" + encodeURIComponent(kw) + "&post_type=post", // 详情页即播放页:虽 pageType 返回 detail,仍需在详情页上启用播放记录 trackPlaybackOnDetail: true, pageType() { if (document.querySelector(".post-title")) return "detail"; if (document.querySelector(".post-box")) return "list"; return "other"; }, // 列表/搜索页卡片 listCardSelector: ".post-box", getListCards() { const cards = []; document.querySelectorAll(".post-box").forEach((box) => { const a = box.querySelector(".post-box-title a") || box.querySelector("a"); if (!a) return; const titleEl = box.querySelector(".post-box-title"); const name = stripParens((titleEl == null ? void 0 : titleEl.textContent) || ""); const imgEl = box.querySelector(".post-box-image"); const cover = parseBgImage((imgEl == null ? void 0 : imgEl.getAttribute("style")) || ""); if (name) { cards.push({ element: box, name, cover, path: a.pathname }); if (cover && !box.dataset.ddysCoverBound) { box.dataset.ddysCoverBound = "1"; box.addEventListener("click", () => { try { sessionStorage.setItem("ddys_cover_" + name, cover); } catch (e) { } }); } } }); return cards; }, // 详情页(=播放页):名称从 .post-title(去括号),图片优先用列表页缓存,否则取最后一个 doulist-item 的 img getDetailInfo() { const titleEl = document.querySelector(".post-title"); const name = stripParens((titleEl == null ? void 0 : titleEl.textContent) || ""); if (!name) return null; let cover = ""; try { cover = sessionStorage.getItem("ddys_cover_" + name) || ""; } catch (e) { } if (!cover) { const imgs = document.querySelectorAll(".doulist-item img"); if (imgs.length) cover = imgs[imgs.length - 1].src || ""; } return { name, cover, path: location.pathname }; } // 无 getPlayInfo:详情页即播放页 }; // src/services/drama/sites/libvio.js var libvio_default = { id: "libvio", name: "LIBVIO", domains: ["libvio.host", "www.libvio.mov", "www.libvios.com", "www.libhd.com", "libviobd.com", "www.libvio.in"], matches: [ "*://libvio.host/*", "*://*.libvio.host/*", "*://www.libvio.mov/*", "*://*.libvio.mov/*", "*://www.libvios.com/*", "*://*.libvios.com/*", "*://www.libhd.com/*", "*://*.libhd.com/*", "*://libviobd.com/*", "*://*.libviobd.com/*", "*://www.libvio.in/*", "*://*.libvio.in/*" ], searchPath: (kw) => "/search/-------------.html?wd=" + encodeURIComponent(kw), pageType() { const p = location.pathname; if (p.startsWith("/detail/")) return "detail"; if (p.startsWith("/w/")) return "play"; if (p.startsWith("/search/")) return "search"; if (document.querySelector(".stui-vodlist")) return "list"; return "other"; }, // 列表/搜索页卡片 listCardSelector: ".stui-vodlist__box", getListCards() { const cards = []; document.querySelectorAll(".stui-vodlist__box").forEach((box) => { const a = box.querySelector("a.stui-vodlist__thumb"); const titleEl = box.querySelector(".stui-vodlist__detail .title"); if (!a) return; const name = ((titleEl == null ? void 0 : titleEl.textContent) || a.getAttribute("title") || "").trim(); const cover = a.getAttribute("data-original") || ""; if (name) cards.push({ element: box, name, cover, path: a.pathname }); }); return cards; }, // 详情页:名称从 .vod-info .title,图片优先 data-original(懒加载真实地址)并过滤占位图, // 否则懒加载未触发时 src 可能是 load.gif;链接从 .vod-info a(播放链接) getDetailInfo() { const img = document.querySelector(".vod-poster img"); const titleEl = document.querySelector(".vod-info .title"); const playA = document.querySelector(".vod-info a"); const name = ((titleEl == null ? void 0 : titleEl.textContent) || "").trim(); if (!name) return null; let cover = (img == null ? void 0 : img.getAttribute("data-original")) || ""; if (isPlaceholderImage(cover)) cover = (img == null ? void 0 : img.src) || ""; if (isPlaceholderImage(cover)) cover = ""; const path = (playA == null ? void 0 : playA.getAttribute("href")) || location.pathname; return { name, cover, path }; }, // 播放页:名称从 .stui-player__detail .title,链接取当前 url getPlayInfo() { const titleEl = document.querySelector(".stui-player__detail .title"); const name = ((titleEl == null ? void 0 : titleEl.textContent) || "").trim(); if (!name) return null; return { name, path: location.pathname }; } }; // src/services/drama/sites/xiuluo.js var xiuluo_default = { id: "xiuluo", name: "\u4FEE\u7F57\u5F71\u89C6", domains: ["xl02.com.de", "v.xl.in.ua", "v.xl01.eu.cc", "v.xl02.eu.cc", "v.xl01.cc.ua"], matches: [ "*://xl02.com.de/*", "*://*.xl02.com.de/*", "*://v.xl.in.ua/*", "*://*.v.xl.in.ua/*", "*://v.xl01.eu.cc/*", "*://*.v.xl01.eu.cc/*", "*://v.xl02.eu.cc/*", "*://*.v.xl02.eu.cc/*", "*://v.xl01.cc.ua/*", "*://*.v.xl01.cc.ua/*" ], searchPath: (kw) => "/search/" + encodeURIComponent(kw), pageType() { const p = location.pathname; if (p.includes("/play/")) return "play"; if (p.startsWith("/search/")) return "search"; if (document.querySelector(".movie-header, .movie-title")) return "detail"; if (document.querySelector(".movie-card")) return "list"; return "other"; }, // 列表页卡片 listCardSelector: ".movie-card", getListCards() { const cards = []; document.querySelectorAll(".movie-card").forEach((card) => { const a = card.querySelector("a.card-img"); const img = card.querySelector("img"); const h4 = card.querySelector(".card-info h4"); if (!a) return; const name = ((h4 == null ? void 0 : h4.textContent) || "").trim(); const cover = (img == null ? void 0 : img.getAttribute("data-src")) || (img == null ? void 0 : img.src) || ""; if (name) cards.push({ element: card, name, cover, path: a.pathname }); }); return cards; }, // 详情页:名称从 .movie-title(去括号),图片从 .movie-poster img.src,链接取当前 url getDetailInfo() { const img = document.querySelector(".movie-poster img"); const titleEl = document.querySelector(".movie-title"); const name = stripParens((titleEl == null ? void 0 : titleEl.textContent) || ""); if (!name) return null; const cover = (img == null ? void 0 : img.src) || ""; return { name, cover, path: location.pathname }; }, // 播放页:名称从 .player-title a,链接从 a.href getPlayInfo() { const a = document.querySelector(".player-title a"); if (!a) return null; const name = (a.textContent || a.getAttribute("title") || "").trim(); if (!name) return null; return { name, path: a.pathname }; } }; // src/services/drama/sites/duse.js function pickCover(imgs) { if (!imgs.length) return ""; const real = imgs.filter((img) => img.id !== "noneCoverImg" && !/placeholder/i.test(img.src || "")); const pick = real.length ? real[real.length - 1] : imgs[imgs.length - 1]; return pick.src || pick.getAttribute("data-src") || pick.getAttribute("data-original") || ""; } var duse_default = { id: "duse", name: "\u6BD2\u86C7\u7535\u5F71", domains: ["www.duse2.com", "www.duse3.com", "www.duse4.com"], matches: [ "*://www.duse2.com/*", "*://*.duse2.com/*", "*://www.duse3.com/*", "*://*.duse3.com/*", "*://www.duse4.com/*", "*://*.duse4.com/*" ], // 跨站直达:跳转首页 + hash 传递关键词(站点有 cdndefend 保护 + 动态 t token, // 无法跨域直接构造搜索 URL,由 onPageReady 在首页提取 t 后跳转搜索页) searchPath: (kw) => "/#vc-search=" + encodeURIComponent(kw), // 判断 URL 是否指向播放页(详情页点击播放链接时重新缓存图片) isPlayUrl(href) { return /\/play\//.test(href || ""); }, // 首页加载后检测 hash 中的搜索关键词,从 DOM 提取 t token 后直达搜索页 // 兜底:若无法提取 t,则填入搜索框并模拟提交 onPageReady() { const m = location.hash.match(/vc-search=([^&]+)/); if (!m) return; const kw = decodeURIComponent(m[1]); history.replaceState(null, "", location.pathname + location.search); let attempts = 0; const trySearch = () => { if (attempts++ > 30) return; const tInput = document.querySelector('input[name="t"], input#t'); if (tInput && tInput.value) { location.href = "/search?t=" + encodeURIComponent(tInput.value) + "&k=" + encodeURIComponent(kw); return; } const scripts = document.querySelectorAll("script:not([src])"); for (const s of scripts) { const tm = s.textContent.match(/["']?t["']?\s*[:=]\s*["']([A-Za-z0-9+/=]{16,})["']/); if (tm) { location.href = "/search?t=" + encodeURIComponent(tm[1]) + "&k=" + encodeURIComponent(kw); return; } } let input = document.querySelector('input[type="search"], input[placeholder*="\u641C"], input[placeholder*="Search" i], input[name="wd"], input[name="search"], input[name="q"], input[name="k"]'); if (!input) { input = Array.from(document.querySelectorAll('input[type="text"]')).find( (i) => /search|搜索|搜|片名/.test(i.placeholder || "") || /search|wd|keyword|q|k/.test(i.name || "") ); } if (!input) { const icon = document.querySelector('[class*="search" i] i, [class*="search" i] svg, .search-icon, .icon-search, [aria-label*="search" i]'); if (icon) icon.click(); setTimeout(trySearch, 300); return; } const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; setter.call(input, kw); input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); input.focus(); setTimeout(() => { input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true })); input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true })); const btn = document.querySelector('button[type="submit"], .search-btn, [class*="search" i] button'); if (btn) btn.click(); const form = input.closest("form"); if (form) form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); }, 200); }; trySearch(); }, pageType() { const p = location.pathname; if (p.startsWith("/detail/")) return "detail"; if (p.startsWith("/play/")) return "play"; if (p.startsWith("/search")) return "search"; if (document.querySelector("a.v-item")) return "list"; return "other"; }, // 列表页卡片 listCardSelector: "a.v-item", getListCards() { const cards = []; document.querySelectorAll("a.v-item").forEach((a) => { let name = ""; a.querySelectorAll(".v-item-footer .v-item-title").forEach((t) => { if (name) return; if (t.style.display !== "none") name = t.textContent.trim(); }); if (!name) return; const imgs = Array.from(a.querySelectorAll(".v-item-cover img")); const cover = pickCover(imgs); cards.push({ element: a, name, cover, path: a.pathname }); }); return cards; }, // 详情页:名称从 img.alt,图片从 img.src(过滤占位图,兜底 data-src),链接取当前 url getDetailInfo() { const img = document.querySelector(".detail-pic img"); const name = ((img == null ? void 0 : img.getAttribute("alt")) || "").trim(); if (!name) return null; const src = (img == null ? void 0 : img.src) || ""; const cover = src && !/placeholder/i.test(src) ? src : (img == null ? void 0 : img.getAttribute("data-src")) || (img == null ? void 0 : img.getAttribute("data-original")) || src || ""; return { name, cover, path: location.pathname }; }, // 播放页:名称从 .detail-title a,链接从 a.href getPlayInfo() { const a = document.querySelector(".detail-title a"); if (!a) return null; const name = (a.textContent || "").trim(); if (!name) return null; return { name, path: a.pathname }; } }; // src/services/drama/sites/hanju.js var hanju_default = { id: "hanju", name: "\u97E9\u5267\u7F51", domains: ["www.hanju.com", "hanju.com"], matches: ["*://www.hanju.com/*", "*://hanju.com/*"], // 仅作为跨站直达的起点,不出现在其他站点的跨站直达菜单中 crossSiteTarget: false, pageType() { if (location.pathname.startsWith("/series/")) return "detail"; return "other"; }, // 详情页:名称从 .series-title,图片从 .series-image //(封面为带 sign/t 的签名 CDN URL,可能有过期时间),链接取当前 url getDetailInfo() { const h1 = document.querySelector(".series-title"); const img = document.querySelector(".series-image"); const name = ((h1 == null ? void 0 : h1.textContent) || "").trim(); if (!name) return null; return { name, cover: (img == null ? void 0 : img.getAttribute("src")) || (img == null ? void 0 : img.src) || "", path: location.pathname }; } }; // src/config/appConfig.js var PAGE_SIZE = 12; // src/config/dramaConfig.js var STORAGE_KEY = "video_collector_drama_data"; var POSITION_KEY = "video_collector_drama_position"; var COLLAPSE_KEY = "video_collector_drama_collapsed"; var XJUMP_POSITION_KEY = "video_collector_drama_xjump_pos"; var IMAGE_CACHE_KEY = "video_collector_drama_img_cache"; var LAST_DOMAIN_KEY = "video_collector_drama_last_domain"; var PLAYBACK_KEY = "video_collector_drama_playback"; // src/services/drama/domains.js function normalizeHost(host) { return (host || "").replace(/^www\./, ""); } var _lastDomainMap = null; function loadMap() { if (_lastDomainMap === null) { try { _lastDomainMap = JSON.parse(GM_getValue(LAST_DOMAIN_KEY, "{}")); } catch (e) { _lastDomainMap = {}; } } return _lastDomainMap; } function recordWorkingDomain(siteId, host) { const map = loadMap(); if (map[siteId] === host) return; map[siteId] = host; GM_setValue(LAST_DOMAIN_KEY, JSON.stringify(map)); } function getLastWorking(siteId) { return loadMap()[siteId] || ""; } function pickDomain(site) { if (!site || !site.domains || !site.domains.length) return ""; const last = getLastWorking(site.id); if (last && site.domains.some((d) => normalizeHost(d) === normalizeHost(last))) return last; return site.domains[0]; } // src/services/drama/sites/index.js var sites = [dida_default, czzy_default, hantv_default, ddys_default, libvio_default, xiuluo_default, duse_default, hanju_default]; var _currentSite = void 0; function getCurrentSite() { if (_currentSite !== void 0) return _currentSite; const host = location.hostname; _currentSite = sites.find((s) => s.domains.some((d) => normalizeHost(d) === normalizeHost(host))) || null; if (_currentSite) recordWorkingDomain(_currentSite.id, host); return _currentSite; } // src/services/drama/state.js var state = { currentTab: "want", currentPage: { want: 1, watched: 1, history: 1 }, renderedTabs: /* @__PURE__ */ new Set(), searchKeyword: "", siteFilter: "" // 网站标签筛选:'' 为全部,否则为站点 id }; // src/base/storageHelper.js var MODULE_LABELS = { jav: "JAV \u89C6\u9891", drama: "\u5F71\u89C6" }; function createStorageHelper({ storageKey, backupFileName, moduleId, itemIdField, showAlert: showAlert2, chooseImportStrategy, historyLoadFn, historySaveFn }) { function loadData2() { try { const data = GM_getValue(storageKey, ""); return data ? JSON.parse(data) : { want: [], watched: [] }; } catch (e) { return { want: [], watched: [] }; } } function saveData2(data) { GM_setValue(storageKey, JSON.stringify(data)); } function exportData2() { const data = loadData2(); const payload = { module: moduleId, want: data.want, watched: data.watched }; if (historyLoadFn) payload.history = historyLoadFn(); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${backupFileName}_${(/* @__PURE__ */ new Date()).toLocaleDateString().replace(/\//g, "-")}.json`; a.click(); URL.revokeObjectURL(url); } function isValidItem(item) { return !!item && typeof item === "object" && item[itemIdField] !== void 0 && item[itemIdField] !== null && item[itemIdField] !== ""; } function validateImported(imported) { if (!imported || !Array.isArray(imported.want) || !Array.isArray(imported.watched)) { return "\u6587\u4EF6\u683C\u5F0F\u4E0D\u6B63\u786E"; } if (imported.module && imported.module !== moduleId) { return `\u8BE5\u5907\u4EFD\u5C5E\u4E8E\u300C${MODULE_LABELS[imported.module] || imported.module}\u300D\u6A21\u5757\uFF0C\u4E0D\u80FD\u5BFC\u5165\u5230\u5F53\u524D\u6A21\u5757`; } const sample = imported.want[0] || imported.watched[0]; if (sample && !(itemIdField in sample)) { return "\u5907\u4EFD\u6570\u636E\u4E0E\u5F53\u524D\u6A21\u5757\u4E0D\u5339\u914D\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6"; } return ""; } function mergeList(localList, importList, strategy) { const result = [...localList]; const indexById = new Map(result.map((item, i) => [item[itemIdField], i])); for (const item of importList) { if (!isValidItem(item)) continue; const id = item[itemIdField]; if (indexById.has(id)) { if (strategy === "import") result[indexById.get(id)] = item; } else { result.push(item); indexById.set(id, result.length - 1); } } return result; } function buildMergedData(localData, importedData, strategy) { const stats = { added: 0, duplicates: 0, errors: 0 }; if (strategy === "replace") { const clean = (list) => list.filter((item) => isValidItem(item) ? (stats.added++, true) : (stats.errors++, false)); return { data: { want: clean(importedData.want), watched: clean(importedData.watched) }, stats }; } const mergeOne = (localList, importList) => { const validImports = []; for (const item of importList) { if (isValidItem(item)) validImports.push(item); else stats.errors++; } const result = mergeList(localList, validImports, strategy); const addedCount = result.length - localList.length; stats.added += addedCount; stats.duplicates += validImports.length - addedCount; return result; }; return { data: { want: mergeOne(localData.want, importedData.want), watched: mergeOne(localData.watched, importedData.watched) }, stats }; } function formatResult(stats, strategy) { if (strategy === "replace") { return `\u5BFC\u5165\u5B8C\u6210\uFF1A\u5BFC\u5165 ${stats.added} \u6761\uFF0C\u8DF3\u8FC7\u9519\u8BEF ${stats.errors} \u6761`; } return `\u5BFC\u5165\u5B8C\u6210\uFF1A\u65B0\u589E ${stats.added} \u6761\uFF0C\u91CD\u590D ${stats.duplicates} \u6761\uFF0C\u9519\u8BEF ${stats.errors} \u6761`; } function importData2(callback) { const input = document.createElement("input"); input.type = "file"; input.accept = "application/json"; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (ev) => { try { const imported = JSON.parse(ev.target.result); const invalidReason = validateImported(imported); if (invalidReason) { showAlert2(invalidReason); return; } const strategy = await chooseImportStrategy(); if (!strategy) return; const local = loadData2(); const { data, stats } = buildMergedData(local, imported, strategy); saveData2(data); if (historyLoadFn && historySaveFn) { const importedHistory = Array.isArray(imported.history) ? imported.history : []; const mergedHistory = strategy === "replace" ? importedHistory.filter(isValidItem) : mergeList(historyLoadFn(), importedHistory, strategy); historySaveFn(mergedHistory); } if (callback) callback(); showAlert2(formatResult(stats, strategy)); } catch (err) { showAlert2("\u6587\u4EF6\u89E3\u6790\u5931\u8D25"); } }; reader.readAsText(file); }; input.click(); } return { loadData: loadData2, saveData: saveData2, exportData: exportData2, importData: importData2 }; } // src/assets/templates/confirm.html var confirm_default = '
\n
\n
\n \n \n
\n
'; // src/assets/templates/import-dialog.html var import_dialog_default = '
\n
\u9009\u62E9\u5BFC\u5165\u65B9\u5F0F
\n
\n \n \n \n
\n
\n \n
\n
\n'; // src/components/ConfirmModal.js function createModal() { const mask = document.createElement("div"); mask.className = "vc-confirm-mask"; mask.innerHTML = confirm_default; document.body.appendChild(mask); requestAnimationFrame(() => mask.classList.add("show")); function close() { mask.classList.remove("show"); setTimeout(() => mask.remove(), 150); } return { mask, close }; } function showConfirm(text2, { danger = false } = {}) { return new Promise((resolve) => { const { mask, close } = createModal(); mask.querySelector(".vc-confirm-text").textContent = text2; if (danger) mask.querySelector(".vc-confirm-ok").classList.add("danger"); function finish(result) { close(); resolve(result); } mask.querySelector(".vc-confirm-cancel").onclick = () => finish(false); mask.querySelector(".vc-confirm-ok").onclick = () => finish(true); mask.onclick = (e) => { if (e.target === mask) finish(false); }; }); } function showAlert(text2) { return new Promise((resolve) => { const { mask, close } = createModal(); mask.querySelector(".vc-confirm-text").textContent = text2; mask.querySelector(".vc-confirm-cancel").style.display = "none"; mask.querySelector(".vc-confirm-ok").onclick = () => { close(); resolve(); }; mask.onclick = (e) => { if (e.target === mask) { close(); resolve(); } }; }); } function showImportStrategyDialog() { return new Promise((resolve) => { const mask = document.createElement("div"); mask.className = "vc-confirm-mask"; mask.innerHTML = import_dialog_default; document.body.appendChild(mask); requestAnimationFrame(() => mask.classList.add("show")); function close(result) { mask.classList.remove("show"); setTimeout(() => mask.remove(), 150); resolve(result); } mask.querySelectorAll(".vc-import-option").forEach((btn) => { btn.onclick = () => close(btn.dataset.strategy); }); mask.querySelector(".vc-confirm-cancel").onclick = () => close(null); mask.onclick = (e) => { if (e.target === mask) close(null); }; }); } // src/services/drama/storage.js var { loadData, saveData, exportData, importData } = createStorageHelper({ storageKey: STORAGE_KEY, backupFileName: "\u89C6\u9891\u6536\u85CF\u5907\u4EFD", moduleId: "drama", itemIdField: "name", showAlert, chooseImportStrategy: showImportStrategyDialog, historyLoadFn: loadPlaybackExportList, historySaveFn: savePlaybackList }); function findItem(name, list) { return list.findIndex((item) => item.name === name); } function loadImageCache() { try { return JSON.parse(GM_getValue(IMAGE_CACHE_KEY, "{}")); } catch (e) { return {}; } } function setImageCache(name, cover) { if (!name || isPlaceholderImage(cover)) return; const cache = loadImageCache(); cache[name] = cover; GM_setValue(IMAGE_CACHE_KEY, JSON.stringify(cache)); } function readImageCache(name) { const cache = loadImageCache(); const entry = cache[name]; if (!entry) return ""; return typeof entry === "string" ? entry : entry.cover || ""; } function getCoverFromExisting(name) { const data = loadData(); for (const type of ["want", "watched"]) { const idx = findItem(name, data[type]); if (idx > -1) { const cover2 = data[type][idx].cover || ""; if (cover2 && !isPlaceholderImage(cover2)) return cover2; } } const record = getPlaybackRecord(name); const cover = (record == null ? void 0 : record.cover) || ""; return cover && !isPlaceholderImage(cover) ? cover : ""; } var PLAY_TABS_KEY = "vc_drama_play_tabs_alive"; function loadPlayTabsAlive() { try { return JSON.parse(GM_getValue(PLAY_TABS_KEY, "{}")); } catch (e) { return {}; } } function markPlayTabAlive(name) { if (!name) return; const alive = loadPlayTabsAlive(); alive[name] = Date.now(); GM_setValue(PLAY_TABS_KEY, JSON.stringify(alive)); } function expirePlayTabAlive(name) { if (!name) return; const alive = loadPlayTabsAlive(); alive[name] = 0; GM_setValue(PLAY_TABS_KEY, JSON.stringify(alive)); } function handlePageCache(currentName = "") { const alive = loadPlayTabsAlive(); const cache = loadImageCache(); let changed = false; Object.keys(cache).forEach((name) => { if (name === currentName) return; if (alive[name] === 0) { delete cache[name]; changed = true; } }); if (changed) GM_setValue(IMAGE_CACHE_KEY, JSON.stringify(cache)); } function loadPlaybackRecords() { try { return JSON.parse(GM_getValue(PLAYBACK_KEY, "{}")); } catch (e) { return {}; } } function getPlaybackRecord(name) { return loadPlaybackRecords()[name] || null; } function savePlaybackRecord(name, { episode = "", time = 0, cover = "", path = "", siteId = "" } = {}) { if (!name) return; const records = loadPlaybackRecords(); records[name] = { episode, time, cover, path, siteId, updateTime: Date.now() }; GM_setValue(PLAYBACK_KEY, JSON.stringify(records)); } function deletePlaybackRecord(name) { const records = loadPlaybackRecords(); if (!(name in records)) return; delete records[name]; GM_setValue(PLAYBACK_KEY, JSON.stringify(records)); } function loadPlaybackList() { const records = loadPlaybackRecords(); const data = loadData(); const coverByName = /* @__PURE__ */ new Map(); data.want.forEach((item) => coverByName.set(item.name, item.cover)); data.watched.forEach((item) => coverByName.set(item.name, item.cover)); return Object.entries(records).map(([name, record]) => ({ name, ...record, cover: record.cover || coverByName.get(name) || "" })).sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0)); } function loadPlaybackExportList() { return Object.entries(loadPlaybackRecords()).map(([name, record]) => ({ name, ...record })); } function savePlaybackList(list = []) { const records = {}; list.forEach((record) => { if (record && record.name) { records[record.name] = { episode: record.episode || "", time: record.time || 0, cover: record.cover || "", path: record.path || "", siteId: record.siteId || "", updateTime: record.updateTime || Date.now() }; } }); GM_setValue(PLAYBACK_KEY, JSON.stringify(records)); } // src/components/Toast.js var PLAY_ICON_SVG = ''; function showToast(text2, { duration = 6e3, icon = "", variant = "" } = {}) { let toast = document.querySelector(".vc-toast"); if (!toast) { toast = document.createElement("div"); toast.className = "vc-toast"; toast.innerHTML = ` ${PLAY_ICON_SVG} `; toast.querySelector(".vc-toast-close").addEventListener("click", () => { toast.classList.remove("show"); }); document.body.appendChild(toast); } toast.classList.remove("show"); void toast.offsetWidth; toast.classList.toggle("variant-primary", variant === "primary"); toast.querySelector(".vc-toast-icon").style.display = icon === "play" ? "" : "none"; toast.querySelector(".vc-toast-text").textContent = text2; toast.style.setProperty("--vc-toast-duration", `${duration}ms`); toast.classList.add("show"); clearTimeout(toast._timer); toast._timer = setTimeout(() => toast.classList.remove("show"), duration); } // src/services/drama/playback.js function text(selector) { const el = document.querySelector(selector); return el ? el.textContent.trim() : ""; } function numberToEpisode(str) { const m = (str || "").trim().match(/^第?\s*(\d+)\s*集?$/); return m ? `\u7B2C${m[1]}\u96C6` : ""; } var EPISODE_FROM_DOM = { czzy: () => text(".mikd .ptit span"), duse: () => text(".episode-list .episode-item-active span"), dida: () => numberToEpisode(text(".myui-content__list.playlist li.active a")), // 低端影视:集数取"正在播放"项,季节前缀取激活的季 tab ddys: () => { const episode = text(".ddys-playlist-item.ddys-playlist-playing .ddys-playlist-item-title"); if (!episode) return ""; const season = text(".ddys-season-tab.is-active"); return (season ? `${season} ` : "") + `\u7B2C${episode.trim()}\u96C6`; }, hantv: () => text(".module-play-list-link.active span"), // 修罗:播放标题含多余文字(如"- 第2集(国语)"),仅提取第N集 xiuluo: () => { var _a, _b; const m = (_b = (_a = document.querySelector(".player-title")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.match(/第\s*(\d+)\s*集/); return m ? `\u7B2C${m[1]}\u96C6` : ""; }, libvio: () => text(".stui-play__list li.active a") }; var EPISODE_FROM_URL = { ddys: () => new URLSearchParams(location.search).get("ep") || "", dida: () => lastEpisodeFromPath(location.pathname), hantv: () => lastEpisodeFromPath(location.pathname), libvio: () => lastEpisodeFromPath(location.pathname), xiuluo: () => lastEpisodeFromPath(location.pathname) }; function lastEpisodeFromPath(path) { const m = path.match(/(\d+)(?:\.html?)?$/); return m ? `\u7B2C${m[1]}\u96C6` : ""; } function resolveEpisode(site) { if (typeof site.getPlaybackInfo === "function") { const info = site.getPlaybackInfo(); if (info && info.episode) return info.episode; } const fromDom = EPISODE_FROM_DOM[site.id]; if (fromDom) { const episode = fromDom(); if (episode) return episode; } const fromUrl = EPISODE_FROM_URL[site.id]; if (fromUrl) { const episode = fromUrl(); if (episode) return episode; } return ""; } function findPlayerEl() { var _a; const direct = document.querySelector("video"); if (direct) return direct; for (const frame of document.querySelectorAll("iframe")) { try { const video = (_a = frame.contentDocument) == null ? void 0 : _a.querySelector("video"); if (video) return video; } catch (e) { } } return null; } var PLAY_COVER_SELECTORS = [ ".vod-poster__wrap img", ".stui-player__detail img", ".myui-content__thumb img", ".module-info-poster img", ".movie-poster img", ".detail-pic img", ".dyimg img" ]; var WATCHING_CTX_KEY = "vc_drama_watching_context"; function isSameWatchingContext(name) { try { const same = sessionStorage.getItem(WATCHING_CTX_KEY) === name; sessionStorage.setItem(WATCHING_CTX_KEY, name); return same; } catch (e) { return false; } } function coverFromPage() { for (const selector of PLAY_COVER_SELECTORS) { const el = document.querySelector(selector); if (!el) continue; const src = el.getAttribute("data-original") || el.src || ""; if (!isPlaceholderImage(src)) return src; } return ""; } function initPlayback(site, name) { if (!name) return; markPlayTabAlive(name); document.addEventListener("pagehide", () => expirePlayTabAlive(name)); document.addEventListener("visibilitychange", () => { if (!document.hidden) markPlayTabAlive(name); }); const playerEl = findPlayerEl(); let episode = resolveEpisode(site); const record = getPlaybackRecord(name); const oldCover = (record == null ? void 0 : record.cover) && !isPlaceholderImage(record.cover) ? record.cover : ""; const cover = oldCover || getCoverFromExisting(name) || readImageCache(name) || coverFromPage(); if (!isSameWatchingContext(name) && record) { if (record.time > 30) { const position = formatDuration(record.time); const episodeText = record.episode ? `\uFF08${record.episode}\uFF09` : ""; showToast(`\u4E0A\u6B21\u64AD\u653E\u5230 ${position}${episodeText}`, { duration: 1e4, icon: "play", variant: "primary" }); } else if (record.episode) { showToast(`\u4E0A\u6B21\u89C2\u770B\u5230\uFF08${record.episode}\uFF09`, { duration: 1e4, icon: "play", variant: "primary" }); } } if (playerEl) { setInterval(() => { if (playerEl.ended) return; const currentEpisode = resolveEpisode(site) || episode; if (currentEpisode && currentEpisode !== episode) episode = currentEpisode; if (playerEl.currentTime > 5) { savePlaybackRecord(name, { episode, time: playerEl.currentTime, cover, path: location.pathname, siteId: site.id }); } }, 6e4); } else { setInterval(() => { const currentEpisode = resolveEpisode(site) || episode; if (!currentEpisode || currentEpisode === episode) return; episode = currentEpisode; savePlaybackRecord(name, { episode: currentEpisode, time: 0, cover, path: location.pathname, siteId: site.id }); }, 6e4); } } function formatPlayback(record) { if (!record) return ""; const time = formatDuration(record.time); return [record.episode, time].filter(Boolean).join(" "); } // src/services/drama/collection.js function getCurrentName() { const site = getCurrentSite(); if (!site) return ""; const t = site.pageType(); if (t === "detail" && site.getDetailInfo) return (site.getDetailInfo() || {}).name || ""; if (t === "play" && site.getPlayInfo) return (site.getPlayInfo() || {}).name || ""; return ""; } function cacheCurrentDetailImage() { const site = getCurrentSite(); if (!site || site.pageType() !== "detail" || !site.getDetailInfo) return; const info = site.getDetailInfo(); if (info && info.name && info.cover) setImageCache(info.name, info.cover); } function isFavorited(name, type) { const data = loadData(); return findItem(name, data[type]) > -1; } function toggleFavorite(drama, type) { const data = loadData(); const otherType = type === "want" ? "watched" : "want"; const idx = findItem(drama.name, data[type]); const otherIdx = findItem(drama.name, data[otherType]); if (otherIdx > -1) data[otherType].splice(otherIdx, 1); if (idx > -1) { data[type].splice(idx, 1); } else { drama.updateTime = Date.now(); data[type].unshift(drama); } saveData(data); state.renderedTabs.clear(); } function toggleDrama(type) { const site = getCurrentSite(); if (!site) return; const t = site.pageType(); let info = null; if (t === "detail") info = site.getDetailInfo ? site.getDetailInfo() : null; else if (t === "play") info = site.getPlayInfo ? site.getPlayInfo() : null; if (!info || !info.name) return; const name = info.name; const data = loadData(); const idx = findItem(name, data[type]); if (idx > -1) { data[type].splice(idx, 1); saveData(data); } else { let cover = ""; if (t === "detail") { cover = info.cover || getCoverFromExisting(name) || readImageCache(name); setImageCache(name, cover); } else if (t === "play") { cover = getCoverFromExisting(name) || readImageCache(name) || info.cover; } const otherType = type === "want" ? "watched" : "want"; const otherIdx = findItem(name, data[otherType]); if (otherIdx > -1) data[otherType].splice(otherIdx, 1); data[type].unshift({ name, cover, path: info.path || location.pathname, siteId: site.id, createTime: Date.now(), updateTime: Date.now() }); saveData(data); } state.renderedTabs.clear(); updateButtonState(); } function toggleWant() { toggleDrama("want"); } function toggleWatched() { toggleDrama("watched"); } function deleteItem(name, type) { if (type === "history") { deletePlaybackRecord(name); state.renderedTabs.clear(); renderActiveList(); return; } const data = loadData(); const idx = findItem(name, data[type]); if (idx > -1) { data[type].splice(idx, 1); saveData(data); state.renderedTabs.clear(); renderActiveList(); updateButtonState(); } } function moveItem(name, fromType, toType) { const data = loadData(); const idx = findItem(name, data[fromType]); if (idx === -1) return; const item = data[fromType].splice(idx, 1)[0]; item.updateTime = Date.now(); data[toType].unshift(item); saveData(data); state.renderedTabs.clear(); renderActiveList(); updateButtonState(); } function isDetailOrPlay() { const site = getCurrentSite(); if (!site) return false; const t = site.pageType(); return t === "detail" || t === "play"; } function batchMoveItems(names, fromType, toType) { const data = loadData(); const moved = []; for (const name of names) { const idx = findItem(name, data[fromType]); if (idx > -1) { const item = data[fromType].splice(idx, 1)[0]; item.updateTime = Date.now(); moved.push(item); } } data[toType].unshift(...moved); saveData(data); state.renderedTabs.clear(); renderActiveList(); updateButtonState(); } function batchDeleteItems(names, type) { if (type === "history") { names.forEach((name) => deletePlaybackRecord(name)); state.renderedTabs.clear(); renderActiveList(); return; } const data = loadData(); for (const name of names) { const idx = findItem(name, data[type]); if (idx > -1) data[type].splice(idx, 1); } saveData(data); state.renderedTabs.clear(); renderActiveList(); updateButtonState(); } // src/base/domHelper.js function openImagePreview(src) { let preview = document.querySelector(".vc-image-preview"); if (!preview) { preview = document.createElement("div"); preview.className = "vc-image-preview"; const img = document.createElement("img"); img.alt = "\u5C01\u9762\u9884\u89C8"; preview.appendChild(img); preview.addEventListener("click", () => preview.classList.remove("show")); document.body.appendChild(preview); } preview.querySelector("img").src = src; preview.classList.add("show"); } function copyText(text2) { if (!text2) return false; const fallback = () => { const ta = document.createElement("textarea"); ta.value = text2; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); let ok = false; try { ok = document.execCommand("copy"); } catch (e) { ok = false; } document.body.removeChild(ta); return ok; }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text2).catch(() => fallback()); return true; } return fallback(); } // src/components/PanelController.js function createPanelController(config) { const { state: state2, // { currentTab, currentPage, renderedTabs, searchKeyword } pageSize, loadFn, // () => data object filterFn, // (list, keyword) => filtered list getId, // (item) => unique id string getTitle, // (item) => title string getUrl, // (item) => url string getSiteName, // (item) => site name string getTime, // (item) => timestamp for display getAvcode, // (item) => avcode string,可选,有番号的卡片标题旁显示复制按钮 moveFn, // (id, fromType, toType) => void deleteFn, // (id, type) => void batchMoveFn, // (ids[], fromType, toType) => void batchDeleteFn, // (ids[], type) => void exportFn, // () => void importFn, // (callback) => void updateButtonStateFn, // () => void getExtraInfo, // (item) => string,可选,卡片附加信息(如播放进度) hasHistory, // boolean,可选,是否显示"观看记录"标签 emptyText, // string templates // { panelTemplate, cardTemplate, coverImageTemplate, defaultCoverTemplate } } = config; const TAB_LABELS = { want: "\u60F3\u770B", watched: "\u5DF2\u770B", history: "\u89C2\u770B\u8BB0\u5F55" }; let panelEl = null; let isBatchMode = false; const batchSelected = /* @__PURE__ */ new Set(); function createPanel() { const mask = document.createElement("div"); mask.className = "vc-mask"; mask.onclick = (e) => { if (e.target === mask) closePanel2(); }; const panel2 = document.createElement("div"); panel2.className = "vc-panel"; panel2.innerHTML = templates.panelTemplate; if (!hasHistory) { const tab = panel2.querySelector('.vc-tab[data-type="history"]'); const wrap = panel2.querySelector('.vc-list-wrap[data-type="history"]'); if (tab) tab.style.display = "none"; if (wrap) wrap.style.display = "none"; } mask.appendChild(panel2); document.body.appendChild(mask); panelEl = mask; bindPanelEvents(panel2); } function bindPanelEvents(panel2) { panel2.querySelector(".vc-close").onclick = closePanel2; panel2.querySelectorAll(".vc-tab").forEach((tab) => { tab.onclick = () => { if (isBatchMode) exitBatchMode(); panel2.querySelectorAll(".vc-tab").forEach((t) => t.classList.remove("active")); tab.classList.add("active"); state2.currentTab = tab.dataset.type; switchTab(tab.dataset.type); }; }); const searchInput = panel2.querySelector(".vc-search"); searchInput.oninput = (e) => { state2.searchKeyword = e.target.value.trim(); state2.currentPage[state2.currentTab] = 1; state2.renderedTabs.clear(); renderActiveList2(); }; panel2.querySelector("#vc-refresh").onclick = () => { state2.renderedTabs.clear(); renderActiveList2(); }; panel2.querySelector("#vc-export").onclick = exportFn; panel2.querySelector("#vc-import").onclick = () => { importFn(() => { state2.renderedTabs.clear(); renderActiveList2(); if (updateButtonStateFn) updateButtonStateFn(); }); }; panel2.querySelector("#vc-first").onclick = () => jumpToPage(1); panel2.querySelector("#vc-prev").onclick = () => jumpToPage(state2.currentPage[state2.currentTab] - 1); panel2.querySelector("#vc-next").onclick = () => jumpToPage(state2.currentPage[state2.currentTab] + 1); panel2.querySelector("#vc-last").onclick = () => jumpToPage(getTotalPages(state2.currentTab)); const siteSelect = panel2.querySelector("#vc-site-select"); siteSelect.addEventListener("change", () => { state2.siteFilter = siteSelect.value; state2.currentPage[state2.currentTab] = 1; state2.renderedTabs.clear(); renderActiveList2(); }); panel2.querySelector("#vc-batch-toggle").onclick = () => { if (isBatchMode) exitBatchMode(); else enterBatchMode(); }; panel2.querySelector("#vc-batch-selectall").onchange = (e) => { const gridEl = panel2.querySelector(`#vc-grid-${state2.currentTab}`); const cards = gridEl ? gridEl.querySelectorAll(".vc-card") : []; cards.forEach((card) => { const id = card.dataset.id; const check = card.querySelector(".vc-card-check"); if (e.target.checked) { batchSelected.add(id); card.classList.add("selected"); if (check) check.checked = true; } else { batchSelected.delete(id); card.classList.remove("selected"); if (check) check.checked = false; } }); updateBatchUI(); }; panel2.querySelector("#vc-batch-move").onclick = async () => { if (state2.currentTab === "history") return; if (batchSelected.size === 0) return; const type = state2.currentTab; const oppositeType = type === "want" ? "watched" : "want"; const targetText = type === "want" ? "\u5DF2\u770B" : "\u60F3\u770B"; const ok = await showConfirm(`\u786E\u5B9A\u5C06\u9009\u4E2D\u7684 ${batchSelected.size} \u6761\u79FB\u5230${targetText}\u5417\uFF1F`); if (!ok) return; batchMoveFn(Array.from(batchSelected), type, oppositeType); exitBatchMode(); }; panel2.querySelector("#vc-batch-delete").onclick = async () => { if (batchSelected.size === 0) return; const type = state2.currentTab; const deleteLabel = type === "history" ? "\u89C2\u770B\u8BB0\u5F55" : "\u6536\u85CF"; const ok = await showConfirm(`\u786E\u5B9A\u5220\u9664\u9009\u4E2D\u7684 ${batchSelected.size} \u6761${deleteLabel}\u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002`, { danger: true }); if (!ok) return; batchDeleteFn(Array.from(batchSelected), type); exitBatchMode(); }; panel2.querySelector("#vc-batch-cancel").onclick = () => exitBatchMode(); } function switchTab(type) { panelEl.querySelectorAll(".vc-list-wrap").forEach((wrap) => { wrap.classList.toggle("active", wrap.dataset.type === type); }); if (!state2.renderedTabs.has(type)) { renderList(type); state2.renderedTabs.add(type); } updatePagination(); } function renderActiveList2() { renderList(state2.currentTab); state2.renderedTabs.add(state2.currentTab); updatePagination(); if (isBatchMode) updateBatchUI(); } function collectSiteFilters(rawList) { const map = /* @__PURE__ */ new Map(); rawList.forEach((item) => { const id = item.siteId || ""; if (!id || map.has(id)) return; map.set(id, getSiteName(item) || id); }); return Array.from(map, ([id, name]) => ({ id, name })); } function renderSiteSelect(siteList) { const select = panelEl.querySelector("#vc-site-select"); if (!select) return; const items = [{ id: "", name: "\u5168\u90E8" }, ...siteList]; select.innerHTML = items.map( (s) => `` ).join(""); if (!siteList.some((s) => s.id === state2.siteFilter)) state2.siteFilter = ""; select.value = state2.siteFilter; } function applyFilters(rawList) { let list = filterFn(rawList, state2.searchKeyword); if (state2.siteFilter) list = list.filter((item) => (item.siteId || "") === state2.siteFilter); return list; } function getTotalPages(type) { const data = loadFn(); const list = applyFilters(data[type] || []); return Math.ceil(list.length / pageSize) || 1; } function jumpToPage(page) { const totalPages = getTotalPages(state2.currentTab); const target = Math.max(1, Math.min(page, totalPages)); if (target === state2.currentPage[state2.currentTab]) return; state2.currentPage[state2.currentTab] = target; state2.renderedTabs.clear(); renderActiveList2(); } function renderList(type) { const data = loadFn(); const rawList = data[type] || []; renderSiteSelect(collectSiteFilters(rawList)); const fullList = applyFilters(rawList); const page = state2.currentPage[type]; const start = (page - 1) * pageSize; const pageList = fullList.slice(start, start + pageSize); const gridEl = panelEl.querySelector(`#vc-grid-${type}`); const oppositeType = type === "want" ? "watched" : "want"; const oppositeText = type === "want" ? "\u6807\u8BB0\u5DF2\u770B" : "\u79FB\u56DE\u60F3\u770B"; if (fullList.length === 0) { gridEl.innerHTML = ""; const empty = document.createElement("div"); empty.className = "vc-empty"; empty.textContent = emptyText; gridEl.appendChild(empty); return; } gridEl.innerHTML = pageList.map((item) => { const coverHtml = item.cover ? interpolate(templates.coverImageTemplate, { cover: item.cover, title: getTitle(item) }) : templates.defaultCoverTemplate; const avcode = getAvcode ? getAvcode(item) : item.avcode || ""; const copyBtnHtml = avcode ? `` : ""; return interpolate(templates.cardTemplate, { id: getId(item), title: getTitle(item), url: getUrl(item), siteName: getSiteName(item), dateStr: formatDate(getTime(item)), lastPlayed: getExtraInfo ? getExtraInfo(item) : "", oppositeType, oppositeText, coverHtml, copyBtnHtml }); }).join(""); bindCardEvents(gridEl, data, type); } function bindCardEvents(gridEl, data, type) { gridEl.querySelectorAll(".vc-card").forEach((card) => { const id = card.dataset.id; const url = card.dataset.url; const item = data[type].find((i) => getId(i) === id); if (isBatchMode) { const check = card.querySelector(".vc-card-check"); card.addEventListener("click", (e) => { if (e.target.closest(".vc-card-checkbox")) return; toggleCardSelected(card, id); }); if (check) { check.addEventListener("change", () => { toggleCardSelected(card, id, check.checked); }); } if (batchSelected.has(id)) { card.classList.add("selected"); if (check) check.checked = true; } } else { card.addEventListener("click", (e) => { if (e.target.closest(".vc-card-actions")) return; if (e.target.tagName === "IMG" && (item == null ? void 0 : item.cover)) { openImagePreview(item.cover); return; } if (url) window.open(url, "_blank"); }); const moveBtn = card.querySelector(".vc-card-move-btn"); if (type === "history") { moveBtn.style.display = "none"; } else { moveBtn.addEventListener("click", (e) => { e.stopPropagation(); moveFn(id, type, e.target.dataset.to); }); } const copyBtn = card.querySelector(".vc-card-copy"); if (copyBtn) { copyBtn.addEventListener("click", (e) => { e.stopPropagation(); if (copyText(copyBtn.dataset.avcode || "")) { copyBtn.textContent = "\u2705"; clearTimeout(copyBtn._copyTimer); copyBtn._copyTimer = setTimeout(() => { copyBtn.textContent = "\u{1F4CB}"; }, 3e3); } }); } const deleteText = type === "history" ? "\u786E\u5B9A\u5220\u9664\u8FD9\u6761\u89C2\u770B\u8BB0\u5F55\u5417\uFF1F" : "\u786E\u5B9A\u5220\u9664\u8FD9\u6761\u6536\u85CF\u5417\uFF1F"; card.querySelector(".vc-card-delete-btn").addEventListener("click", (e) => { e.stopPropagation(); showConfirm(deleteText, { danger: true }).then((ok) => { if (ok) deleteFn(id, type); }); }); } }); gridEl.querySelectorAll(".vc-card-cover img").forEach((img) => { img.addEventListener("error", function() { this.onerror = null; this.parentElement.innerHTML = templates.defaultCoverTemplate; }); }); } function toggleCardSelected(card, id, forceState) { const check = card.querySelector(".vc-card-check"); const isSelected = forceState !== void 0 ? forceState : !batchSelected.has(id); if (isSelected) { batchSelected.add(id); card.classList.add("selected"); if (check) check.checked = true; } else { batchSelected.delete(id); card.classList.remove("selected"); if (check) check.checked = false; } updateBatchUI(); } function enterBatchMode() { isBatchMode = true; batchSelected.clear(); panelEl.querySelector(".vc-panel").classList.add("batch-mode"); panelEl.querySelector("#vc-batch-bar").style.display = ""; state2.renderedTabs.clear(); renderActiveList2(); } function exitBatchMode() { isBatchMode = false; batchSelected.clear(); panelEl.querySelector(".vc-panel").classList.remove("batch-mode"); panelEl.querySelector("#vc-batch-bar").style.display = "none"; panelEl.querySelector("#vc-batch-selectall").checked = false; state2.renderedTabs.clear(); renderActiveList2(); } function updateBatchUI() { const type = state2.currentTab; const gridEl = panelEl.querySelector(`#vc-grid-${type}`); const cards = gridEl ? gridEl.querySelectorAll(".vc-card") : []; const totalCount = cards.length; let selectedOnPage = 0; cards.forEach((card) => { if (batchSelected.has(card.dataset.id)) selectedOnPage++; }); panelEl.querySelector("#vc-batch-count").textContent = `\u5DF2\u9009 ${batchSelected.size} \u6761`; const selectAll = panelEl.querySelector("#vc-batch-selectall"); selectAll.checked = totalCount > 0 && selectedOnPage === totalCount; const hasSelection = batchSelected.size > 0; panelEl.querySelector("#vc-batch-delete").disabled = !hasSelection; const moveBtn = panelEl.querySelector("#vc-batch-move"); if (type === "history") { moveBtn.style.display = "none"; } else { moveBtn.style.display = ""; moveBtn.disabled = !hasSelection; moveBtn.textContent = type === "want" ? "\u79FB\u5230\u5DF2\u770B" : "\u79FB\u5230\u60F3\u770B"; } } function updatePagination() { const data = loadFn(); const list = applyFilters(data[state2.currentTab] || []); const total = list.length; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const currentPage = state2.currentPage[state2.currentTab]; const tabLabel = TAB_LABELS[state2.currentTab] || ""; panelEl.querySelector("#vc-count-info").textContent = `\u5171 ${total} \u6761${tabLabel}`; panelEl.querySelector("#vc-page-info").textContent = `${currentPage} / ${totalPages}`; panelEl.querySelector("#vc-first").disabled = currentPage <= 1; panelEl.querySelector("#vc-prev").disabled = currentPage <= 1; panelEl.querySelector("#vc-next").disabled = currentPage >= totalPages; panelEl.querySelector("#vc-last").disabled = currentPage >= totalPages; } function openPanel2() { if (!panelEl) createPanel(); panelEl.classList.add("show"); renderActiveList2(); } function closePanel2() { if (isBatchMode) exitBatchMode(); panelEl.classList.remove("show"); } function getPanelEl() { return panelEl; } return { createPanel, openPanel: openPanel2, closePanel: closePanel2, renderActiveList: renderActiveList2, getPanelEl }; } // src/base/dragHelper.js function initFloatDrag(handle, btnGroup, getPositionKey2, lockAxis = null) { let isDragging = false, hasMoved = false, startX, startY, startRight, startBottom; let dragW = 0, dragH = 0, rafId = null, pendingRight = 0, pendingBottom = 0, lock = null; handle.addEventListener("mousedown", (e) => { isDragging = true; hasMoved = false; startX = e.clientX; startY = e.clientY; startRight = parseFloat(btnGroup.style.right) || 0; startBottom = parseFloat(btnGroup.style.bottom) || 0; dragW = btnGroup.offsetWidth; dragH = btnGroup.offsetHeight; pendingRight = startRight; pendingBottom = startBottom; lock = typeof lockAxis === "function" ? lockAxis() : lockAxis; btnGroup.style.transition = "none"; document.body.style.cursor = "grabbing"; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const dx = e.clientX - startX, dy = e.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true; if (lock !== "vertical") { pendingRight = Math.max(0, Math.min(window.innerWidth - dragW, startRight - dx)); } if (lock !== "horizontal") { pendingBottom = Math.max(0, Math.min(window.innerHeight - dragH, startBottom - dy)); } if (rafId === null) { rafId = requestAnimationFrame(() => { rafId = null; btnGroup.style.right = pendingRight + "px"; btnGroup.style.bottom = pendingBottom + "px"; }); } }); document.addEventListener("mouseup", () => { if (!isDragging) return; isDragging = false; document.body.style.cursor = ""; if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } btnGroup.style.right = pendingRight + "px"; btnGroup.style.bottom = pendingBottom + "px"; void btnGroup.offsetWidth; btnGroup.style.transition = ""; GM_setValue(getPositionKey2(), { right: parseFloat(btnGroup.style.right) || 0, bottom: parseFloat(btnGroup.style.bottom) || 0 }); }); handle.addEventListener("click", (e) => { if (hasMoved) { e.preventDefault(); e.stopPropagation(); hasMoved = false; } }, true); } function initXjumpDrag(handle, xjumpEl, getXjumpPositionKey2) { let isDragging = false, hasMoved = false, startX, startY, startRight, startTop; let dragW = 0, dragH = 0, rafId = null, pendingRight = 0, pendingTop = 0; handle.addEventListener("mousedown", (e) => { if (!xjumpEl) return; isDragging = true; hasMoved = false; startX = e.clientX; startY = e.clientY; startRight = parseFloat(xjumpEl.style.right) || 0; startTop = parseFloat(xjumpEl.style.top) || 0; dragW = xjumpEl.offsetWidth; dragH = xjumpEl.offsetHeight; xjumpEl.classList.add("dragging"); document.body.style.cursor = "grabbing"; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const dx = e.clientX - startX, dy = e.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true; pendingRight = Math.max(0, Math.min(window.innerWidth - dragW, startRight - dx)); pendingTop = Math.max(0, Math.min(window.innerHeight - dragH, startTop + dy)); if (rafId === null) { rafId = requestAnimationFrame(() => { rafId = null; xjumpEl.style.right = pendingRight + "px"; xjumpEl.style.top = pendingTop + "px"; }); } }); document.addEventListener("mouseup", () => { if (!isDragging) return; isDragging = false; document.body.style.cursor = ""; if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; xjumpEl.style.right = pendingRight + "px"; xjumpEl.style.top = pendingTop + "px"; } xjumpEl.classList.remove("dragging"); GM_setValue(getXjumpPositionKey2(), { right: parseFloat(xjumpEl.style.right) || 0, top: parseFloat(xjumpEl.style.top) || 0 }); }); handle.addEventListener("click", (e) => { if (hasMoved) { e.preventDefault(); e.stopPropagation(); hasMoved = false; } }, true); } // src/components/FloatButtonGroup.js function createFloatButtonGroup(config) { const { positionKeyFn, // () => storage key string collapseKeyFn, // () => GM key for collapsed state(按站点区分) collapseText, // e.g. '⋮⋮ 收藏工具' listBtnText, // e.g. '📋 收藏列表' onWant, // callback for want button click onWatched, // callback for watched button click onOpenPanel, // callback for list button click isDetailPageFn, // () => boolean updateButtonStateFn, // () => void, refresh active state onVisibilityChange // () => void, optional, refresh mini buttons } = config; let btnGroup, collapseBtn, wantBtn, watchedBtn, listBtn; let collapseKey = ""; let collapseTimer = null; function create() { collapseKey = collapseKeyFn ? collapseKeyFn() : ""; btnGroup = document.createElement("div"); btnGroup.className = "vc-btn-group"; const savedPos = GM_getValue(positionKeyFn(), null); if (savedPos) { btnGroup.style.right = savedPos.right + "px"; btnGroup.style.bottom = savedPos.bottom + "px"; } else { btnGroup.style.right = "20px"; btnGroup.style.bottom = "20px"; } collapseBtn = document.createElement("button"); collapseBtn.className = "vc-btn vc-collapse-btn"; collapseBtn.title = "\u62D6\u52A8\u53EF\u79FB\u52A8\u4F4D\u7F6E\uFF0C\u70B9\u51FB\u6536\u8D77/\u5C55\u5F00"; collapseBtn.innerHTML = `${collapseText}`; wantBtn = document.createElement("button"); wantBtn.className = "vc-btn vc-btn-want"; wantBtn.textContent = "\u2B50 \u60F3\u770B"; wantBtn.onclick = onWant; watchedBtn = document.createElement("button"); watchedBtn.className = "vc-btn vc-btn-watched"; watchedBtn.textContent = "\u2713 \u5DF2\u770B"; watchedBtn.onclick = onWatched; listBtn = document.createElement("button"); listBtn.className = "vc-btn vc-btn-list"; listBtn.textContent = listBtnText; listBtn.onclick = onOpenPanel; btnGroup.appendChild(collapseBtn); btnGroup.appendChild(wantBtn); btnGroup.appendChild(watchedBtn); btnGroup.appendChild(listBtn); document.body.appendChild(btnGroup); initFloatDrag(collapseBtn, btnGroup, positionKeyFn, () => btnGroup.classList.contains("vc-collapsed") ? "vertical" : null); collapseBtn.addEventListener("click", () => { const isCollapsed = !GM_getValue(collapseKey, false); GM_setValue(collapseKey, isCollapsed); updateVisibility(); }); updateButtonStateFn(); updateVisibility(); document.addEventListener("visibilitychange", () => { if (document.hidden) return; updateButtonStateFn(); const savedPos2 = GM_getValue(positionKeyFn(), null); if (savedPos2) { btnGroup.style.right = savedPos2.right + "px"; btnGroup.style.bottom = savedPos2.bottom + "px"; } updateVisibility(); if (onVisibilityChange) onVisibilityChange(); }); window.addEventListener("resize", () => { if (!btnGroup) return; if (GM_getValue(collapseKey, false)) snapToEdge(); else clampInViewport(); }); } function updateVisibility() { if (!wantBtn || !watchedBtn) return; const isCollapsed = GM_getValue(collapseKey, false); btnGroup.classList.toggle("vc-collapsed", isCollapsed); if (isCollapsed) { wantBtn.style.display = "none"; watchedBtn.style.display = "none"; listBtn.style.display = "none"; snapToEdge(); clearTimeout(collapseTimer); collapseTimer = setTimeout(() => { if (!GM_getValue(collapseKey, false)) return; snapToEdge(); }, 260); } else { clearTimeout(collapseTimer); if (!collapseBtn.querySelector(".vc-collapse-icon")) { collapseBtn.innerHTML = `${collapseText}`; } if (!isDetailPageFn()) { wantBtn.style.display = "none"; watchedBtn.style.display = "none"; listBtn.style.display = ""; } else { wantBtn.style.display = ""; watchedBtn.style.display = ""; listBtn.style.display = ""; } } if (!isCollapsed) clampInViewport(); } function snapToEdge() { if (!btnGroup) return; const w = btnGroup.offsetWidth; if (!w) return; const right = parseFloat(btnGroup.style.right) || 0; const distRight = right; const distLeft = window.innerWidth - right - w; const snappedRight = distRight <= distLeft ? 0 : Math.max(0, window.innerWidth - w); btnGroup.style.right = snappedRight + "px"; collapseBtn.classList.toggle("vc-collapse-btn-left", snappedRight > 0); GM_setValue(positionKeyFn(), { right: snappedRight, bottom: parseFloat(btnGroup.style.bottom) || 0 }); } function clampInViewport() { if (!btnGroup) return; const w = btnGroup.offsetWidth; const h = btnGroup.offsetHeight; if (!w || !h) return; let right = parseFloat(btnGroup.style.right) || 0; let bottom = parseFloat(btnGroup.style.bottom) || 0; right = Math.max(0, Math.min(right, window.innerWidth - w)); bottom = Math.max(0, Math.min(bottom, window.innerHeight - h)); btnGroup.style.right = right + "px"; btnGroup.style.bottom = bottom + "px"; GM_setValue(positionKeyFn(), { right, bottom }); } return { create, updateVisibility, getWantBtn: () => wantBtn, getWatchedBtn: () => watchedBtn }; } // src/components/MiniButtonInjector.js function createMiniButtonInjector(config) { const { getSiteFn, // () => site object buildItemFn, // (cardData, site) => item object 或 null(null 表示跳过) getIdFn, // (item) => 唯一标识 string isFavoritedFn, // (id, type) => boolean toggleFavoriteFn, // (item, type) => void refreshItemFn, // (item, site, btn) => void,可选,从 DOM 刷新数据 afterToggleFn // () => void,可选,toggle 后回调 } = config; let observer = null; let timer = null; let isMouseBound = false; function inject() { injectCards(); [500, 1500, 3e3].forEach((delay) => setTimeout(injectCards, delay)); if (!observer) { observer = new MutationObserver(() => { if (timer) return; timer = setTimeout(() => { timer = null; injectCards(); }, 150); }); observer.observe(document.body, { childList: true, subtree: true }); } if (!isMouseBound) { isMouseBound = true; document.addEventListener("mouseover", (e) => { const site = getSiteFn(); if (!site || !site.listCardSelector) return; const card = e.target.closest(site.listCardSelector); if (!card || card.querySelector(".vc-mini-fav")) return; injectCards(); }); } } function injectCards() { const site = getSiteFn(); if (!site || !site.getListCards) return; let hasInjected = false; site.getListCards().forEach((cardData) => { const { element } = cardData; if (!element || element.querySelector(".vc-mini-fav")) return; const item = buildItemFn(cardData, site); if (!item) return; if (getComputedStyle(element).position === "static") { element.style.position = "relative"; } element.classList.add("vc-fav-host"); const group = document.createElement("div"); group.className = "vc-mini-fav"; const refreshFns = []; group.appendChild(makeBtn("vc-mini-want", "\u2B50", "\u60F3\u770B", "want", item, refreshFns)); group.appendChild(makeBtn("vc-mini-watched", "\u2713", "\u5DF2\u770B", "watched", item, refreshFns)); element.appendChild(group); hasInjected = true; }); if (hasInjected) refreshAll(); } function makeBtn(btnClass, icon, title, type, item, refreshFns) { const btn = document.createElement("button"); btn.type = "button"; btn.className = `vc-mini-btn ${btnClass}`; btn.title = title; btn.textContent = icon; btn.dataset.vcId = getIdFn(item); btn.dataset.vcType = type; const refresh = () => btn.classList.toggle("active", isFavoritedFn(getIdFn(item), type)); refresh(); refreshFns.push(refresh); btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (refreshItemFn) refreshItemFn(item, getSiteFn(), btn); toggleFavoriteFn(item, type); refreshAll(); if (afterToggleFn) afterToggleFn(); }); return btn; } function refreshAll() { document.querySelectorAll(".vc-mini-btn[data-vc-id]").forEach((btn) => { const id = btn.dataset.vcId; const type = btn.dataset.vcType; if (id && type) btn.classList.toggle("active", isFavoritedFn(id, type)); }); } return { inject, refreshAll }; } // src/components/CrossSiteJump.js function createCrossSiteJump(config) { const { getSiteFn, // () => 当前站点对象 getKeywordFn, // () => 搜索关键词 string 或 null getPositionKeyFn, // () => 位置存储 key getAllSitesFn, // () => 所有站点数组 pickDomainFn, // (site) => 域名 string siteIdField // 'name' 或 'id',用于过滤当前站点 } = config; let xjumpEl = null; function clampInViewport() { if (!xjumpEl) return; const w = xjumpEl.offsetWidth; const h = xjumpEl.offsetHeight; if (!w || !h) return; let right = parseFloat(xjumpEl.style.right) || 0; let top = parseFloat(xjumpEl.style.top) || 0; right = Math.max(0, Math.min(right, window.innerWidth - w)); top = Math.max(0, Math.min(top, window.innerHeight - h)); xjumpEl.style.right = right + "px"; xjumpEl.style.top = top + "px"; GM_setValue(getPositionKeyFn(), { right, top }); } if (!window.__vcXjumpResizeBound) { window.__vcXjumpResizeBound = true; window.addEventListener("resize", clampInViewport); } function inject() { const site = getSiteFn(); if (!site) return; const keyword = getKeywordFn(); if (!keyword) return; if (xjumpEl && xjumpEl.dataset.keyword === keyword) return; if (xjumpEl) xjumpEl.remove(); xjumpEl = document.createElement("div"); xjumpEl.className = "vc-xjump"; xjumpEl.dataset.keyword = keyword; const savedPos = GM_getValue(getPositionKeyFn(), null); if (savedPos) { xjumpEl.style.right = savedPos.right + "px"; xjumpEl.style.top = savedPos.top + "px"; } else { xjumpEl.style.right = "16px"; xjumpEl.style.top = "16px"; } const toggle = document.createElement("button"); toggle.className = "vc-xjump-toggle"; toggle.textContent = "\u{1F517} \u8DE8\u7AD9\u76F4\u8FBE"; const menu = document.createElement("div"); menu.className = "vc-xjump-menu"; const allSites = getAllSitesFn(); allSites.filter((s) => s[siteIdField] !== site[siteIdField] && s.crossSiteTarget !== false).forEach((target) => { const a = document.createElement("a"); a.className = "vc-xjump-btn"; a.textContent = target.name; a.target = "_blank"; a.rel = "noopener noreferrer"; a.href = "https://" + pickDomainFn(target) + target.searchPath(keyword); menu.appendChild(a); }); xjumpEl.appendChild(toggle); xjumpEl.appendChild(menu); document.body.appendChild(xjumpEl); initXjumpDrag(toggle, xjumpEl, getPositionKeyFn); const el = xjumpEl; let hideTimer = null; el.addEventListener("mouseenter", () => { if (el.classList.contains("dragging")) return; if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } el.classList.add("show"); requestAnimationFrame(() => { const r = toggle.getBoundingClientRect(); const spaceBelow = window.innerHeight - r.bottom; const menuH = menu.offsetHeight; el.classList.toggle("up", spaceBelow < menuH + 8); }); }); el.addEventListener("mouseleave", () => { if (el.classList.contains("dragging")) return; if (hideTimer) clearTimeout(hideTimer); hideTimer = setTimeout(() => el.classList.remove("show"), 150); }); } function getXjumpEl() { return xjumpEl; } return { inject, getXjumpEl }; } // src/assets/adblock.css var adblock_default = '/* ============================================================\n \u89C6\u9891\u6536\u85CF\u52A9\u624B - \u5E7F\u544A\u5C4F\u853D\u89C4\u5219\n \u6309\u7AD9\u70B9\u5206\u7EC4\uFF0C\u9009\u62E9\u5668\u4F9D\u636E \u5E7F\u544Ahtml/ \u76EE\u5F55\u4E0B\u5404\u7AD9\u5E7F\u544A\u6A21\u677F\u7ED3\u6784\u7F16\u5199\n \u89C4\u5219\u7EDF\u4E00\u4F7F\u7528 !important\uFF1A\u5BF9\u6297\u7AD9\u70B9\u5185\u8054\u6837\u5F0F\u4E0E\u811A\u672C\u52A8\u6001\u8BBE\u7F6E\u7684 display\n \u6CE8\u5165\u65B9\u5F0F\u89C1 src/base/adblock.js\uFF08\u63D2\u5165 \u672B\u5C3E\uFF0C\u6700\u540E\u5B9A\u4E49\u4F18\u5148\uFF09\n ============================================================ */\n\n/* ===== JavBus ===== */\n/* \u9876\u90E8/\u9875\u811A\u8F6E\u64AD\u5E7F\u544A\u5BB9\u5668\uFF08\u6A21\u677F\u6CE8\u91CA\uFF1A.bcpic2 \u4E5F\u8981\u9690\u85CF\uFF09 */\n.ad-box,\n.bcpic2 {\n display: none !important;\n}\n\n/* juicy \u5E7F\u544A iframe\uFF08poweredby.jads.co / adshow.php \u6295\u653E\uFF09 */\niframe[src*="jads.co"],\niframe[src*="adshow.php"] {\n display: none !important;\n}\n\n/* ===== JavLibrary ===== */\n/* \u9876\u90E8\u6A2A\u5E45 728x115 */\n.topbanner1,\n#topbanner11 {\n display: none !important;\n}\n\n/* \u53F3\u4FA7\u8FB9\u680F 160x600 */\n.sidebanner1,\n#sidebanner11 {\n display: none !important;\n}\n\n/* \u4E2D\u90E8\u6A2A\u5E45 + \u4E2D\u90E8\u6587\u5B57\u5E7F\u544A */\n#middlebanner11,\n#middletextbanner {\n display: none !important;\n}\n\n/* ===== \u4FEE\u7F57\u5F71\u89C6 ===== */\n/* \u53F3\u4E0B\u89D2\u6D6E\u52A8\u5E7F\u544A */\n#float-ad {\n display: none !important;\n}\n\n/* \u9996\u9875\u9876\u90E8\u901A\u680F\u5E7F\u544A */\n#fuck-u {\n display: none !important;\n}\n\n/* \u64AD\u653E\u5668\u524D\u8D34\u7247\u5E7F\u544A\uFF08artplayer \u63D2\u4EF6\u5BB9\u5668\uFF09 */\n.artplayer-plugin-ads {\n display: none !important;\n}\n\n/* ===== LIBVIO ===== */\n/* \u901A\u680F\u56FE\u7247\u5E7F\u544A\uFF08\u542B"\u5E7F\u544A"\u6807\u8BC6 .a_ms\uFF09\u53CA\u5176\u5916\u5C42\u5305\u88C5 */\n.t-img-box,\n#iaujwnefhw {\n display: none !important;\n}\n\n/* ===== \u5382\u957F\u8D44\u6E90 ===== */\n/* \u5E7F\u544A\u5757\uFF1A\u516C\u5171\u7C7B .mikd/.mi_cont/.mi_btcon \u4E0D\u53EF\u4FEE\u6539\uFF08\u6A21\u677F\u6CE8\u91CA\uFF09\uFF0C\n \u4EC5\u547D\u4E2D\u5E26\u5E7F\u544A\u6807\u5FD7\u7684\u7EC4\u5408\u4E0E\u5E7F\u544A\u4E13\u7528\u7C7B */\n.mi_btcon.ad,\n.module-adslist,\n.ads_w,\n.footerad,\n.right-ad,\n.close-ads,\n.gg-icon {\n display: none !important;\n}\n'; // src/base/adblock.js var ADBLOCK_KEY = "adBlockEnabled"; function isAdBlockEnabled() { return GM_getValue(ADBLOCK_KEY, true); } function applyAdBlock() { const id = "vc-adblock-style"; if (!isAdBlockEnabled()) { const style2 = document.getElementById(id); if (style2) style2.remove(); return; } if (document.getElementById(id)) return; const style = document.createElement("style"); style.id = id; style.textContent = adblock_default; const tryInject = () => { const head = document.head || document.documentElement; if (head) { head.appendChild(style); return true; } return false; }; if (!tryInject()) { const observer = new MutationObserver(() => { if (tryInject()) observer.disconnect(); }); try { observer.observe(document, { childList: true, subtree: true }); } catch (e) { requestAnimationFrame(tryInject); } setTimeout(() => observer.disconnect(), 3e3); } } function setAdBlockEnabled(enabled) { GM_setValue(ADBLOCK_KEY, !!enabled); applyAdBlock(); } // src/components/SettingsPanel.js function createSettingsPanel() { let mask = null; function open() { if (!mask) build(); mask.querySelector("#vc-settings-adblock").checked = isAdBlockEnabled(); mask.classList.add("show"); } function close() { if (mask) mask.classList.remove("show"); } function build() { mask = document.createElement("div"); mask.className = "vc-settings-mask"; mask.innerHTML = ` `; document.body.appendChild(mask); mask.querySelector(".vc-settings-close").addEventListener("click", close); mask.addEventListener("click", (e) => { if (e.target === mask) close(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && mask.classList.contains("show")) close(); }); const adBlockInput = mask.querySelector("#vc-settings-adblock"); adBlockInput.addEventListener("change", () => { setAdBlockEnabled(adBlockInput.checked); }); } return { open, close }; } // src/assets/style.css var style_default = `/* \u4E3B\u9898\u53D8\u91CF\uFF08\u539F THEME \u5BF9\u8C61\uFF0C\u73B0\u4EE5 CSS \u53D8\u91CF\u7EDF\u4E00\u7BA1\u7406\uFF09 */\r :root {\r /* \u4E3B\u8272\uFF1A\u67D4\u548C\u975B\u84DD\uFF0C\u4F4E\u9971\u548C\u4E0D\u523A\u773C */\r --vc-primary: #6366f1;\r --vc-primary-light: #a5b4fc;\r --vc-primary-lighter: #e0e7ff;\r --vc-primary-dark: #4338ca;\r \r /* \u60F3\u770B - \u6696\u6A58\u8C03\u6DE1\u96C5\u7248\uFF08\u53BB\u6389\u9AD8\u9971\u548C\u523A\u773C\u6A59\uFF09 */\r --vc-want: #fb923c;\r --vc-want-light: #fdba74;\r --vc-want-lighter: #fff7ed;\r --vc-want-dark: #c2410c;\r \r /* \u5DF2\u770B - \u6E05\u900F\u8584\u8377\u7EFF */\r --vc-watched: #34d399;\r --vc-watched-light: #6ee7b7;\r --vc-watched-lighter: #f0fdf4;\r --vc-watched-dark: #047857;\r \r /* \u5371\u9669/\u5220\u9664 */\r --vc-danger: #f87171;\r --vc-danger-light: #fca5a5;\r --vc-danger-lighter: #fef2f2;\r --vc-danger-dark: #b91c1c;\r \r /* \u4E2D\u6027\u5E95\u8272\u4F53\u7CFB\uFF08\u6E05\u65B0\u6DE1\u96C5\u7070\u767D\u68AF\u5EA6\uFF09 */\r --vc-bg: #ffffff;\r --vc-bg-soft: #fafafa;\r --vc-bg-muted: #f5f5f5;\r \r /* \u6587\u5B57\u68AF\u5EA6 */\r --vc-text: #18181b;\r --vc-text-medium: #3f3f46;\r --vc-text-secondary: #71717a;\r --vc-text-placeholder: #a1a1aa;\r \r /* \u8FB9\u6846\u68AF\u5EA6 */\r --vc-border: #e4e4e7;\r --vc-border-light: #f4f4f5;\r --vc-border-dark: #d4d4d8;\r \r /* hover \u4EA4\u4E92\u5E95\u8272 */\r --vc-hover: #f4f4f5;\r --vc-hover-strong: #e4e4e7;\r \r /* \u9634\u5F71\uFF08\u8F7B\u91CF\u5316\uFF0C\u67D4\u548C\u4E0D\u539A\u91CD\uFF09 */\r --vc-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);\r --vc-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.07);\r --vc-shadow-lg: 0 12px 36px rgba(0, 0, 0, 0.10);\r }\r \r /* \u60AC\u6D6E\u6309\u94AE\u7EC4 */\r .vc-btn-group {\r position: fixed;\r z-index: 99999;\r display: flex;\r flex-direction: column;\r gap: 8px;\r font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r user-select: none;\r /* \u4F4D\u7F6E + \u6536\u8D77\u52A8\u753B\uFF08\u7F29\u5C0F/\u6DE1\u5316\uFF09\u7EDF\u4E00\u8FC7\u6E21\uFF1A\u62D6\u62FD\u4E2D\u7531\u811A\u672C\u4E34\u65F6\u7981\u7528 transition */\r transition: right 0.25s ease, bottom 0.25s ease, transform 0.25s ease, opacity 0.25s ease;\r }\r \r /* \u6536\u8D77\u72B6\u6001\uFF1A\u534A\u5706\u5F62\u6001\u5DF2\u4F53\u73B0\u6536\u7F29\uFF0C\u4FDD\u7559\u6DE1\u5316\uFF1B\u4F4D\u7F6E/\u52A8\u753B\u8FC7\u6E21\u7EDF\u4E00 */\r .vc-btn-group.vc-collapsed {\r opacity: 0.85;\r }\r \r .vc-btn {\r padding: 6px 10px;\r border: 1px solid var(--vc-border);\r border-radius: 10px;\r font-size: 13px;\r font-weight: 500;\r cursor: pointer;\r transition: all 0.2s ease;\r background: rgba(255, 255, 255, 0.95);\r backdrop-filter: blur(10px);\r box-shadow: var(--vc-shadow);\r color: var(--vc-text-secondary);\r white-space: nowrap;\r }\r \r .vc-btn:hover {\r transform: translateY(-1px);\r color: var(--vc-text);\r }\r \r .vc-btn-want.active {\r background: var(--vc-want);\r color: white;\r border-color: var(--vc-want);\r }\r \r .vc-btn-watched.active {\r background: var(--vc-watched);\r color: white;\r border-color: var(--vc-watched);\r }\r \r .vc-btn-list:hover {\r background: var(--vc-primary);\r color: white;\r border-color: var(--vc-primary);\r }\r \r .vc-collapse-btn {\r display: flex;\r align-items: center;\r justify-content: center;\r gap: 4px;\r padding: 6px 10px;\r height: 32px;\r max-width: 200px;\r box-sizing: border-box;\r overflow: hidden;\r font-size: 12px;\r text-align: center;\r cursor: grab;\r /* \u5C55\u5F00/\u6536\u8D77\u5F62\u6001\u8FC7\u6E21\uFF1A\u5BBD\u5EA6(max-width)\u4E0E\u9AD8\u5EA6\u5747\u7528\u786E\u5B9A\u503C\uFF0C\u53EF\u5E73\u6ED1\u52A8\u753B */\r transition: height 0.25s ease, max-width 0.25s ease, border-radius 0.25s ease,\r padding 0.25s ease, opacity 0.25s ease;\r }\r \r /* \u6536\u8D77/\u5C55\u5F00\u6309\u94AE\u7684\u56FE\u6807\uFF1A\u4EC5\u56FE\u6807\uFF0C\u65CB\u8F6C 90\xB0 \u7AD6\u6392\uFF0C\u5C45\u4E2D */\r .vc-collapse-icon {\r display: inline-block;\r transform: rotate(90deg);\r line-height: 1;\r font-style: normal;\r }\r \r /* \u6536\u8D77\u72B6\u6001\uFF1Acollapse \u6309\u94AE\u53D8\u4E3A\u8D34\u8FB9\u534A\u5706\uFF0C\u4EC5\u663E\u793A\u56FE\u6807\r \u4E0D\u8BBE width\uFF1A\u7531 max-width \u8FC7\u6E21\u5E73\u6ED1\u538B\u7F29\u5BBD\u5EA6\uFF08min-width \u4FDD\u8BC1\u534A\u5706\u6700\u5C0F\u5BBD\u5EA6\uFF09 */\r .vc-btn-group.vc-collapsed .vc-collapse-btn {\r min-width: 30px;\r max-width: 30px;\r height: 40px;\r padding: 0;\r border-radius: 50% 0 0 50%; /* \u9ED8\u8BA4\u8D34\u53F3\u7F18\uFF1A\u5DE6\u534A\u5706\u51F8\u5411\u9875\u9762 */\r font-size: 16px;\r \r border: none;\r outline: none;\r background: var(--vc-primary-light);\r }\r .vc-btn-group.vc-collapsed .vc-collapse-icon {\r transform: rotate(0deg);\r }\r \r /* \u8D34\u5DE6\u7F18\u65F6\uFF1A\u53F3\u534A\u5706\u51F8\u5411\u9875\u9762 */\r .vc-btn-group.vc-collapsed .vc-collapse-btn.vc-collapse-btn-left {\r min-width: 40px;\r border-radius: 0 50% 50% 0;\r }\r \r /* \u906E\u7F69 - \u5168\u5C40\u5C45\u4E2D */\r .vc-mask {\r position: fixed;\r inset: 0;\r width: 100vw;\r height: 100vh;\r background: rgba(0, 0, 0, 0.3);\r z-index: 100000;\r display: none;\r align-items: center;\r justify-content: center;\r backdrop-filter: blur(2px);\r margin: 0;\r padding: 0;\r }\r \r .vc-mask.show {\r display: flex !important;\r }\r \r /* \u8BBE\u7F6E\u9762\u677F */\r .vc-settings-mask {\r position: fixed;\r inset: 0;\r background: rgba(0, 0, 0, 0.3);\r z-index: 100001;\r display: none;\r align-items: center;\r justify-content: center;\r backdrop-filter: blur(2px);\r }\r \r .vc-settings-mask.show {\r display: flex;\r }\r \r .vc-settings-panel {\r width: 420px;\r max-width: 92vw;\r background: var(--vc-bg);\r border-radius: 16px;\r box-shadow: var(--vc-shadow-lg);\r overflow: hidden;\r animation: vcFadeIn 0.2s ease;\r }\r \r .vc-settings-header {\r display: flex;\r align-items: center;\r justify-content: space-between;\r padding: 16px 20px;\r border-bottom: 1px solid var(--vc-border);\r }\r \r .vc-settings-title {\r font-size: 15px;\r font-weight: 600;\r color: var(--vc-text);\r }\r \r .vc-settings-close {\r width: 28px;\r height: 28px;\r border: none;\r border-radius: 8px;\r background: transparent;\r color: var(--vc-text-secondary);\r font-size: 18px;\r line-height: 1;\r cursor: pointer;\r transition: background 0.15s ease, color 0.15s ease;\r }\r \r .vc-settings-close:hover {\r background: var(--vc-bg-soft);\r color: var(--vc-text);\r }\r \r .vc-settings-body {\r padding: 12px 20px 20px;\r }\r \r .vc-settings-item {\r display: flex;\r align-items: center;\r gap: 12px;\r padding: 12px;\r border-radius: 10px;\r cursor: pointer;\r transition: background 0.15s ease;\r }\r \r .vc-settings-item:hover {\r background: var(--vc-bg-soft);\r }\r \r .vc-settings-info {\r display: flex;\r flex-direction: column;\r gap: 2px;\r flex: 1;\r min-width: 0;\r }\r \r .vc-settings-label {\r font-size: 14px;\r font-weight: 500;\r color: var(--vc-text);\r }\r \r .vc-settings-desc {\r font-size: 12px;\r color: var(--vc-text-secondary);\r }\r \r /* \u81EA\u5B9A\u4E49\u590D\u9009\u6846\uFF1A\u52FE\u9009/\u672A\u52FE\u9009\u89C6\u89C9\u53CD\u9988 */\r .vc-settings-item input[type="checkbox"] {\r appearance: none;\r -webkit-appearance: none;\r width: 20px;\r height: 20px;\r margin: 0;\r border: 2px solid var(--vc-border);\r border-radius: 6px;\r background: var(--vc-bg);\r cursor: pointer;\r position: relative;\r flex-shrink: 0;\r transition: background 0.15s ease, border-color 0.15s ease;\r }\r \r .vc-settings-item input[type="checkbox"]:checked {\r background: var(--vc-primary);\r border-color: var(--vc-primary);\r }\r \r .vc-settings-item input[type="checkbox"]:checked::after {\r content: '\u2713';\r position: absolute;\r inset: 0;\r display: flex;\r align-items: center;\r justify-content: center;\r color: #fff;\r font-size: 13px;\r font-weight: 700;\r line-height: 1;\r }\r \r .vc-settings-item input[type="checkbox"]:focus-visible {\r outline: 2px solid var(--vc-primary);\r outline-offset: 2px;\r }\r \r /* \u6536\u85CF\u9762\u677F - \u56FA\u5B9A\u9AD8\u5EA6\u5E03\u5C40 */\r .vc-panel {\r width: 980px;\r max-width: 92vw;\r height: 88vh;\r min-height: 400px;\r background: var(--vc-bg);\r border-radius: 16px;\r box-shadow: var(--vc-shadow-lg);\r display: flex;\r flex-direction: column;\r overflow: hidden;\r animation: vcFadeIn 0.2s ease;\r position: relative;\r border: 1px solid var(--vc-border);\r }\r \r @keyframes vcFadeIn {\r from { opacity: 0; transform: translateY(10px); }\r to { opacity: 1; transform: translateY(0); }\r }\r \r /* \u5934\u90E8 */\r .vc-panel-header {\r padding: 20px 56px 16px 24px;\r border-bottom: 1px solid var(--vc-border);\r flex-shrink: 0;\r }\r \r .vc-panel-title-row {\r display: flex;\r align-items: center;\r justify-content: space-between;\r margin-bottom: 16px;\r }\r \r .vc-panel-title {\r font-size: 18px;\r font-weight: 600;\r color: var(--vc-text);\r margin: 0;\r }\r \r .vc-header-actions {\r display: flex;\r gap: 8px;\r }\r \r .vc-icon-btn-sm {\r padding: 6px 12px;\r border: 1px solid var(--vc-border);\r background: white;\r border-radius: 8px;\r font-size: 12px;\r cursor: pointer;\r color: var(--vc-text-secondary);\r transition: all 0.15s;\r }\r \r .vc-icon-btn-sm:hover {\r border-color: var(--vc-primary);\r color: var(--vc-primary);\r }\r \r .vc-tabs {\r display: flex;\r gap: 24px;\r margin-bottom: 12px;\r }\r \r .vc-tab {\r padding: 6px 0;\r font-size: 14px;\r color: var(--vc-text-secondary);\r cursor: pointer;\r border-bottom: 2px solid transparent;\r transition: all 0.2s;\r }\r \r .vc-tab.active {\r color: var(--vc-primary);\r border-bottom-color: var(--vc-primary);\r font-weight: 500;\r }\r \r .vc-search {\r flex: 1;\r min-width: 0;\r width: auto;\r padding: 8px 12px;\r border: 1px solid var(--vc-border);\r border-radius: 8px;\r font-size: 13px;\r outline: none;\r transition: border-color 0.2s;\r box-sizing: border-box;\r background: var(--vc-bg);\r }\r \r .vc-search:focus {\r border-color: var(--vc-primary);\r }\r \r .vc-search-row {\r display: flex;\r align-items: center;\r gap: 8px;\r margin-top: 10px;\r }\r \r /* \u7F51\u7AD9\u7B5B\u9009\u4E0B\u62C9\uFF1A\u653E\u5728\u641C\u7D22\u6846\u5DE6\u4FA7\uFF0C\u4E0E\u641C\u7D22\u6846\u540C\u4E00\u884C */\r .vc-site-select {\r flex-shrink: 0;\r max-width: 140px;\r padding: 8px 6px;\r border: 1px solid var(--vc-border);\r border-radius: 8px;\r font-size: 12px;\r color: var(--vc-text);\r background: var(--vc-bg);\r outline: none;\r cursor: pointer;\r transition: border-color 0.2s;\r box-sizing: border-box;\r }\r \r .vc-site-select:focus {\r border-color: var(--vc-primary);\r }\r \r /* \u5217\u8868\u5BB9\u5668 */\r .vc-list-wrap {\r flex: 1;\r overflow-y: auto;\r padding: 20px 24px;\r display: none;\r box-sizing: border-box;\r }\r \r .vc-list-wrap.active {\r display: block;\r }\r \r /* \u7F51\u683C\u5E03\u5C40 - \u56FA\u5B9A3\u884C4\u5217 */\r .vc-grid {\r display: grid;\r grid-template-columns: repeat(4, 1fr);\r grid-template-rows: repeat(3, minmax(160px, 1fr));\r gap: 16px;\r height: 100%;\r width: 100%;\r }\r \r /* \u5361\u7247 */\r .vc-card {\r position: relative;\r border-radius: 10px;\r overflow: hidden;\r border: 1px solid var(--vc-border);\r background: white;\r transition: all 0.2s ease;\r cursor: pointer;\r display: flex;\r flex-direction: column;\r max-height: 260px;\r }\r \r .vc-card:hover {\r box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);\r transform: translateY(-2px);\r }\r \r /* \u5C01\u9762\u533A\u57DF */\r .vc-card-cover {\r flex: 1;\r min-height: 0;\r background: #f3f4f6;\r display: flex;\r align-items: center;\r justify-content: center;\r overflow: hidden;\r }\r \r .vc-card-cover img {\r width: 100%;\r height: 100%;\r object-fit: cover;\r object-position: center top;\r cursor: zoom-in;\r transition: transform 0.2s;\r }\r \r .vc-card:hover .vc-card-cover img {\r transform: scale(1.05);\r }\r \r .vc-card-cover .vc-default-cover {\r color: #9ca3af;\r font-size: 28px;\r }\r \r /* \u5361\u7247\u64CD\u4F5C\u6309\u94AE */\r .vc-card-actions {\r position: absolute;\r top: 8px;\r right: 8px;\r display: flex;\r gap: 4px;\r opacity: 0;\r transition: opacity 0.2s ease;\r z-index: 2;\r }\r \r .vc-card:hover .vc-card-actions {\r opacity: 1;\r }\r \r .vc-card-action-btn {\r padding: 4px 8px;\r border: none;\r background: rgba(255, 255, 255, 0.95);\r border-radius: 6px;\r font-size: 11px;\r cursor: pointer;\r color: var(--vc-text-secondary);\r backdrop-filter: blur(4px);\r transition: all 0.15s;\r }\r \r .vc-card-action-btn:hover {\r background: var(--vc-primary);\r color: white;\r }\r \r .vc-card-delete-btn:hover {\r background: var(--vc-danger);\r color: white;\r }\r \r /* \u5361\u7247\u4FE1\u606F\u533A */\r .vc-card-info {\r padding: 8px 10px 10px;\r flex-shrink: 0;\r }\r \r .vc-card-title {\r font-size: 12px;\r line-height: 1.4;\r color: var(--vc-text);\r display: -webkit-box;\r -webkit-line-clamp: 2;\r -webkit-box-orient: vertical;\r overflow: hidden;\r font-weight: 500;\r }\r \r /* \u590D\u5236\u756A\u53F7\u6309\u94AE\uFF08\u7AD9\u70B9\u6807\u7B7E\u540E\u3001\u6807\u9898\u524D\uFF0C\u968F\u6807\u9898\u5185\u8054\uFF09 */\r .vc-card-copy {\r display: inline-block;\r border: none;\r background: transparent;\r cursor: pointer;\r font-size: 11px;\r line-height: 1;\r padding: 0;\r margin: 0;\r margin-top: -2px;\r border-radius: 4px;\r vertical-align: middle;\r opacity: 0.55;\r transition: opacity 0.15s, background 0.15s;\r }\r \r .vc-card-copy:hover {\r opacity: 1;\r background: rgba(79, 70, 229, 0.1);\r }\r \r .vc-card-site {\r display: inline-block;\r font-size: 11px;\r color: var(--vc-primary);\r background: rgba(79, 70, 229, 0.1);\r padding: 1px 6px;\r border-radius: 4px;\r white-space: nowrap;\r vertical-align: middle;\r margin-right: 4px;\r }\r \r /* \u7AD9\u70B9\u6807\u8BC6\u4E3A\u7A7A\u65F6\u4E0D\u5360\u4F4D\uFF08\u89C6\u9891\u6A21\u5757\u65E0\u7AD9\u70B9\u6765\u6E90\uFF09 */\r .vc-card-site:empty {\r display: none;\r }\r \r /* \u5361\u7247\u4FE1\u606F\u533A\u5E95\u90E8\uFF1A\u65F6\u95F4\u9760\u5DE6\u3001\u64AD\u653E\u8BB0\u5F55\u9760\u53F3\uFF08\u540C\u4E00\u884C\uFF09 */\r .vc-card-meta {\r display: flex;\r align-items: center;\r justify-content: space-between;\r gap: 6px;\r margin-top: 4px;\r min-width: 0;\r }\r \r .vc-card-time {\r font-size: 11px;\r color: var(--vc-text-secondary);\r white-space: nowrap;\r overflow: hidden;\r text-overflow: ellipsis;\r min-width: 0;\r }\r \r /* \u4E0A\u6B21\u64AD\u653E\u4F4D\u7F6E\uFF08\u5982 S03E02 00:10:30\uFF09\uFF0C\u65E0\u8BB0\u5F55\u65F6\u9690\u85CF */\r .vc-card-last-played {\r font-size: 11px;\r color: var(--vc-want);\r white-space: nowrap;\r overflow: hidden;\r text-overflow: ellipsis;\r min-width: 0;\r }\r \r .vc-card-last-played:empty {\r display: none;\r }\r \r .vc-empty {\r grid-column: 1 / -1;\r grid-row: 1 / -1;\r display: flex;\r align-items: center;\r justify-content: center;\r color: var(--vc-text-secondary);\r font-size: 14px;\r }\r \r /* \u5206\u9875\u5668 */\r .vc-pagination {\r padding: 12px 24px;\r border-top: 1px solid var(--vc-border);\r display: flex;\r align-items: center;\r justify-content: space-between;\r font-size: 12px;\r color: var(--vc-text-secondary);\r flex-shrink: 0;\r }\r \r .vc-pagination-btns {\r display: flex;\r align-items: center;\r gap: 8px;\r }\r \r .vc-page-btn {\r padding: 4px 10px;\r border: 1px solid var(--vc-border);\r background: white;\r border-radius: 6px;\r font-size: 12px;\r cursor: pointer;\r color: var(--vc-text-secondary);\r transition: all 0.15s;\r }\r \r .vc-page-btn:hover:not(:disabled) {\r border-color: var(--vc-primary);\r color: var(--vc-primary);\r }\r \r .vc-page-btn:disabled {\r opacity: 0.4;\r cursor: not-allowed;\r }\r \r /* \u5173\u95ED\u6309\u94AE */\r .vc-close {\r position: absolute;\r top: 16px;\r right: 20px;\r background: none;\r border: none;\r font-size: 20px;\r color: var(--vc-text-secondary);\r cursor: pointer;\r line-height: 1;\r padding: 4px;\r z-index: 2;\r }\r \r .vc-close:hover {\r color: var(--vc-text);\r }\r \r /* \u56FE\u7247\u9884\u89C8 - \u5168\u5C40\u5C45\u4E2D */\r .vc-image-preview {\r position: fixed;\r inset: 0;\r width: 100vw;\r height: 100vh;\r background: rgba(0, 0, 0, 0.85);\r z-index: 100001;\r display: none;\r align-items: center;\r justify-content: center;\r cursor: zoom-out;\r margin: 0;\r padding: 0;\r }\r \r .vc-image-preview.show {\r display: flex !important;\r }\r \r .vc-image-preview img {\r max-width: 92vw;\r max-height: 92vh;\r object-fit: contain;\r border-radius: 4px;\r }\r \r /* ===== \u5217\u8868\u9875\u5361\u7247\u8FF7\u4F60\u6536\u85CF\u6309\u94AE ===== */\r .vc-mini-fav {\r position: absolute;\r top: 6px;\r left: 6px;\r display: flex;\r gap: 4px;\r opacity: 0;\r pointer-events: none;\r transition: opacity 0.15s ease;\r z-index: 10;\r font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r }\r \r /* \u5361\u7247\u60AC\u6D6E\u6216\u6309\u94AE\u81EA\u8EAB\u60AC\u6D6E\u65F6\u663E\u793A\u5E76\u53EF\u70B9\u51FB */\r .vc-fav-host:hover .vc-mini-fav,\r .vc-mini-fav:hover {\r opacity: 1;\r pointer-events: auto;\r }\r \r .vc-mini-btn {\r width: 26px;\r height: 26px;\r padding: 0;\r border: none;\r border-radius: 6px;\r background: rgba(255, 255, 255, 0.92);\r color: var(--vc-text-secondary);\r font-size: 13px;\r line-height: 1;\r cursor: pointer;\r display: flex;\r align-items: center;\r justify-content: center;\r backdrop-filter: blur(4px);\r box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);\r transition: transform 0.15s, background 0.15s, color 0.15s;\r }\r \r .vc-mini-btn:hover {\r transform: scale(1.12);\r }\r \r .vc-mini-want:hover {\r color: var(--vc-want);\r }\r \r .vc-mini-watched:hover {\r color: var(--vc-watched);\r }\r \r .vc-mini-want.active {\r background: var(--vc-want);\r color: #fff;\r }\r \r .vc-mini-watched.active {\r background: var(--vc-watched);\r color: #fff;\r }\r \r /* ===== \u8DE8\u7AD9\u76F4\u8FBE widget ===== */\r .vc-xjump {\r position: fixed;\r z-index: 99999;\r font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r }\r \r .vc-xjump-toggle {\r padding: 8px 14px;\r border: 1px solid var(--vc-border);\r border-radius: 8px;\r background: rgba(255, 255, 255, 0.95);\r backdrop-filter: blur(10px);\r box-shadow: var(--vc-shadow);\r color: var(--vc-text-secondary);\r font-size: 13px;\r font-weight: 500;\r cursor: grab;\r white-space: nowrap;\r user-select: none;\r transition: transform 0.15s;\r }\r \r .vc-xjump-toggle:hover {\r transform: translateY(-1px);\r }\r \r .vc-xjump-menu {\r position: absolute;\r top: calc(100% + 6px);\r right: 0;\r display: none;\r flex-direction: column;\r gap: 4px;\r background: rgba(255, 255, 255, 0.98);\r border: 1px solid var(--vc-border);\r border-radius: 10px;\r box-shadow: var(--vc-shadow-lg);\r padding: 6px;\r max-height: 60vh;\r overflow: auto;\r min-width: 102px;\r box-sizing: border-box;\r }\r \r .vc-xjump.up .vc-xjump-menu {\r top: auto;\r bottom: calc(100% + 6px);\r }\r \r .vc-xjump.show .vc-xjump-menu {\r display: flex;\r }\r \r .vc-xjump-btn {\r padding: 6px 12px;\r border: none;\r border-radius: 6px;\r background: transparent;\r color: var(--vc-text-secondary);\r font-size: 12px;\r font-weight: 500;\r text-decoration: none;\r cursor: pointer;\r text-align: left;\r white-space: nowrap;\r transition: background 0.15s, color 0.15s;\r }\r \r .vc-xjump-btn:hover {\r background: var(--vc-primary);\r color: #fff;\r }\r \r /* ===== JavLibrary \u8BE6\u60C5\u9875\u8DF3\u8F6C JavBus \u6309\u94AE ===== */\r .vc-jb-jump {\r display: inline-block;\r margin-left: 8px;\r padding: 2px 10px;\r font-size: 12px;\r font-weight: 500;\r color: #fff !important;\r background: var(--vc-primary);\r border-radius: 6px;\r text-decoration: none;\r cursor: pointer;\r vertical-align: middle;\r transition: opacity 0.15s, transform 0.15s;\r }\r \r .vc-jb-jump:hover {\r opacity: 0.88;\r transform: translateY(-1px);\r }\r \r /* ===== \u6279\u91CF\u6A21\u5F0F ===== */\r /* \u5361\u7247\u9009\u62E9\u6846 */\r .vc-card-checkbox {\r position: absolute;\r top: 6px;\r left: 6px;\r z-index: 3;\r width: 20px;\r height: 20px;\r display: flex;\r align-items: center;\r justify-content: center;\r }\r \r .vc-card-checkbox input {\r width: 16px;\r height: 16px;\r cursor: pointer;\r accent-color: var(--vc-primary);\r }\r \r .vc-panel.batch-mode .vc-card-checkbox {\r display: flex !important;\r }\r \r .vc-panel.batch-mode .vc-card-actions {\r display: none !important;\r }\r \r .vc-panel.batch-mode .vc-card-copy {\r display: none !important;\r }\r \r .vc-panel.batch-mode .vc-card {\r cursor: default;\r }\r \r .vc-card.selected {\r box-shadow: 0 0 0 2px var(--vc-primary);\r }\r \r /* \u6279\u91CF\u5DE5\u5177\u680F */\r .vc-batch-bar {\r padding: 12px 24px;\r border-top: 1px solid var(--vc-border);\r display: flex;\r align-items: center;\r gap: 12px;\r font-size: 13px;\r color: var(--vc-text-secondary);\r flex-shrink: 0;\r font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r background: var(--vc-hover);\r }\r \r .vc-batch-select-all {\r display: flex;\r align-items: center;\r gap: 4px;\r cursor: pointer;\r white-space: nowrap;\r font-weight: 500;\r }\r \r .vc-batch-select-all input {\r width: 16px;\r height: 16px;\r cursor: pointer;\r accent-color: var(--vc-primary);\r }\r \r .vc-batch-count {\r color: var(--vc-primary);\r font-weight: 600;\r white-space: nowrap;\r }\r \r .vc-batch-actions {\r margin-left: auto;\r display: flex;\r gap: 6px;\r }\r \r .vc-batch-btn {\r padding: 4px 12px;\r border: 1px solid var(--vc-border);\r background: white;\r border-radius: 6px;\r font-size: 12px;\r font-weight: 500;\r cursor: pointer;\r color: var(--vc-text-secondary);\r transition: all 0.15s;\r font-family: inherit;\r }\r \r .vc-batch-btn:hover:not(:disabled) {\r border-color: var(--vc-primary);\r color: var(--vc-primary);\r }\r \r .vc-batch-btn:disabled {\r opacity: 0.4;\r cursor: not-allowed;\r }\r \r .vc-batch-btn-danger:hover:not(:disabled) {\r border-color: var(--vc-danger);\r color: var(--vc-danger);\r }\r \r .vc-batch-btn-ghost {\r border-color: transparent;\r background: transparent;\r }\r \r .vc-batch-btn-ghost:hover {\r color: var(--vc-text) !important;\r border-color: var(--vc-border) !important;\r }\r \r /* \u6279\u91CF\u6A21\u5F0F\u65F6\u9690\u85CF\u5206\u9875\u5668 */\r .vc-panel.batch-mode .vc-pagination {\r display: none;\r }\r \r /* ===== \u786E\u8BA4\u5F39\u6846 ===== */\r .vc-confirm-mask {\r position: fixed;\r inset: 0;\r z-index: 100002;\r background: rgba(0, 0, 0, 0.35);\r display: none;\r align-items: center;\r justify-content: center;\r backdrop-filter: blur(2px);\r }\r \r .vc-confirm-mask.show {\r display: flex !important;\r }\r \r .vc-confirm-dialog {\r width: 375px;\r max-width: 90vw;\r background: white;\r border-radius: 12px;\r box-shadow: var(--vc-shadow-lg);\r padding: 24px;\r text-align: center;\r animation: vcFadeIn 0.15s ease;\r font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r }\r \r .vc-confirm-text {\r font-size: 15px;\r font-weight: 500;\r color: var(--vc-text);\r line-height: 1.5;\r margin-bottom: 25px;\r }\r \r .vc-confirm-actions {\r display: flex;\r gap: 20px;\r justify-content: center;\r }\r \r .vc-confirm-btn {\r padding: 7px 20px;\r border: none;\r border-radius: 8px;\r font-size: 14px;\r font-weight: 500;\r cursor: pointer;\r transition: all 0.15s;\r font-family: inherit;\r }\r \r .vc-confirm-cancel {\r background: var(--vc-hover);\r color: var(--vc-text-secondary);\r border: 1px solid var(--vc-border);\r }\r \r .vc-confirm-cancel:hover {\r background: var(--vc-border);\r color: var(--vc-text);\r }\r \r .vc-confirm-ok {\r background: var(--vc-primary);\r color: white;\r }\r \r .vc-confirm-ok:hover {\r opacity: 0.9;\r }\r \r .vc-confirm-ok.danger {\r background: var(--vc-danger);\r }\r \r /* ===== \u5BFC\u5165\u7B56\u7565\u9009\u62E9\u5F39\u6846 ===== */\r .vc-import-dialog {\r width: 420px;\r max-width: 92vw;\r background: white;\r border-radius: 12px;\r box-shadow: var(--vc-shadow-lg);\r padding: 24px;\r animation: vcFadeIn 0.15s ease;\r font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r }\r \r .vc-import-title {\r font-size: 16px;\r font-weight: 600;\r color: var(--vc-text);\r margin-bottom: 16px;\r }\r \r .vc-import-options {\r display: flex;\r flex-direction: column;\r gap: 10px;\r margin-bottom: 20px;\r }\r \r .vc-import-option {\r display: flex;\r flex-direction: column;\r align-items: flex-start;\r gap: 2px;\r padding: 10px 14px;\r border: 1px solid var(--vc-border);\r border-radius: 8px;\r background: #fff;\r cursor: pointer;\r text-align: left;\r transition: border-color 0.15s, background 0.15s;\r font-family: inherit;\r }\r \r .vc-import-option:hover {\r border-color: var(--vc-primary);\r background: rgba(79, 70, 229, 0.04);\r }\r \r .vc-import-option-name {\r font-size: 14px;\r font-weight: 500;\r color: var(--vc-text);\r }\r \r .vc-import-option-desc {\r font-size: 12px;\r color: var(--vc-text-secondary);\r line-height: 1.4;\r }\r \r .vc-import-actions {\r display: flex;\r justify-content: center;\r }\r \r /* ===== \u65E0\u6253\u6270\u60AC\u6D6E\u63D0\u793A\uFF08toast\uFF09\uFF1A\u9876\u90E8\u767D\u5E95\u3001\u53EF\u624B\u52A8\u5173\u95ED =====\r \u52A8\u6548\uFF1A\u8FDB\u5165\u5F39\u8DF3\u6ED1\u5165\uFF08vc-toast-pop\uFF09\u3001\u5E95\u90E8\u5012\u8BA1\u65F6\u8FDB\u5EA6\u6761\uFF08\u4E0E duration \u540C\u6B65\uFF09\u3001\r \u64AD\u653E\u63D0\u793A\u7528 variant-primary \u5F3A\u8C03\uFF08\u4E3B\u8272\u56FE\u6807\u5706\u6807 + \u8FDB\u5EA6\u6761\uFF09 */\r .vc-toast {\r position: fixed;\r top: 5vh;\r left: 50%;\r transform: translateX(-50%) translateY(-8px);\r z-index: 100010;\r display: flex;\r align-items: center;\r gap: 8px;\r max-width: 80vw;\r padding: 10px 14px 12px; /* \u5E95\u90E8\u7559\u51FA\u8FDB\u5EA6\u6761\u7A7A\u95F4 */\r background: #fff;\r color: var(--vc-text);\r font-size: 13px;\r font-weight: 500;\r border-radius: 10px;\r box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);\r border: 1px solid var(--vc-border);\r font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r opacity: 0;\r pointer-events: none;\r overflow: hidden;\r transition: opacity 0.3s ease, transform 0.3s ease;\r }\r \r .vc-toast.show {\r opacity: 1;\r transform: translateX(-50%) translateY(0);\r pointer-events: auto;\r /* \u8FDB\u5165\uFF1A\u4E0A\u6ED1\u6DE1\u5165 + \u8F7B\u5FAE\u56DE\u5F39\uFF0C\u5438\u5F15\u6CE8\u610F */\r animation: vc-toast-pop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);\r }\r \r @keyframes vc-toast-pop {\r 0% {\r opacity: 0;\r transform: translateX(-50%) translateY(-16px) scale(0.96);\r }\r 70% {\r opacity: 1;\r transform: translateX(-50%) translateY(3px) scale(1.02);\r }\r 100% {\r opacity: 1;\r transform: translateX(-50%) translateY(0) scale(1);\r }\r }\r \r .vc-toast-text {\r white-space: nowrap;\r overflow: hidden;\r text-overflow: ellipsis;\r }\r \r /* \u7C7B\u578B\u56FE\u6807\uFF08\u5982\u64AD\u653E\uFF09\u5706\u5F62\u5E95\u6807 */\r .vc-toast-icon {\r flex-shrink: 0;\r display: inline-flex;\r align-items: center;\r justify-content: center;\r width: 20px;\r height: 20px;\r border-radius: 50%;\r background: var(--vc-primary-light);\r color: #fff;\r }\r \r /* \u5E95\u90E8\u5012\u8BA1\u65F6\u8FDB\u5EA6\u6761\uFF1A\u65F6\u957F\u7531 JS \u5199\u5165 --vc-toast-duration */\r .vc-toast-bar {\r position: absolute;\r left: 0;\r right: 0;\r bottom: 0;\r height: 3px;\r background: var(--vc-primary-light);\r transform-origin: left center;\r opacity: 0;\r }\r \r .vc-toast.show .vc-toast-bar {\r opacity: 1;\r animation: vc-toast-progress var(--vc-toast-duration, 6000ms) linear forwards;\r }\r \r @keyframes vc-toast-progress {\r to {\r transform: scaleX(0);\r }\r }\r \r /* \u5F3A\u8C03\u53D8\u4F53\uFF1A\u64AD\u653E/\u7EED\u64AD\u7B49\u5173\u952E\u63D0\u793A\uFF0C\u4E3B\u8272\u8FB9\u6846 + \u5149\u6655\uFF0C\u66F4\u9192\u76EE */\r .vc-toast.variant-primary {\r border-color: var(--vc-primary-light);\r box-shadow: 0 8px 30px rgba(79, 70, 229, 0.28);\r }\r \r .vc-toast-close {\r flex-shrink: 0;\r padding: 0 2px;\r border: none;\r background: none;\r color: var(--vc-text-secondary);\r font-size: 16px;\r line-height: 1;\r cursor: pointer;\r }\r \r .vc-toast-close:hover {\r color: var(--vc-text);\r }\r `; // src/assets/templates/panel.html var panel_default = '\n
\n
\n
\u6211\u7684\u6536\u85CF
\n
\n \n \n \n \n
\n
\n
\n
\u60F3\u770B
\n
\u5DF2\u770B
\n
\u89C2\u770B\u8BB0\u5F55
\n
\n
\n \n \n
\n
\n\n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n\n\n
\n \u5171 0 \u6761\u6536\u85CF\n
\n \n \n 1 / 1\n \n \n
\n
'; // src/assets/templates/card.html var card_default = '
\n \n
{{coverHtml}}
\n
\n \n \n
\n
\n
{{siteName}}{{copyBtnHtml}}{{title}}
\n
\n {{dateStr}}\n {{lastPlayed}}\n
\n
\n
'; // src/assets/templates/cover-image.html var cover_image_default = '{{title}}\n'; // src/assets/templates/default-cover.html var default_cover_default = '
\u{1F5BC}\uFE0F
\n'; // src/services/drama/ui.js var TEMPLATES = { panelTemplate: panel_default, cardTemplate: card_default, coverImageTemplate: cover_image_default, defaultCoverTemplate: default_cover_default }; var floatBtns; var settingsPanel = createSettingsPanel(); var _renderActiveList = null; function renderActiveList() { if (_renderActiveList) _renderActiveList(); } var panel = createPanelController({ state, pageSize: PAGE_SIZE, loadFn: () => ({ ...loadData(), history: loadPlaybackList() }), hasHistory: true, filterFn: (list, keyword) => { if (!keyword) return list; const kw = keyword.toLowerCase(); return list.filter((item) => (item.name || "").toLowerCase().includes(kw)); }, getId: (item) => item.name, getTitle: (item) => item.name, getUrl: (item) => buildDramaUrl(item), getSiteName: (item) => { if (!item.siteId) return ""; return (sites.find((s) => s.id === item.siteId) || {}).name || ""; }, getTime: (item) => item.updateTime || item.createTime, moveFn: moveItem, deleteFn: deleteItem, batchMoveFn: batchMoveItems, batchDeleteFn: batchDeleteItems, exportFn: exportData, importFn: importData, updateButtonStateFn: () => updateButtonState(), getExtraInfo: (item) => formatPlayback(getPlaybackRecord(item.name)), emptyText: "\u6682\u65E0\u6536\u85CF\u5185\u5BB9", templates: TEMPLATES }); _renderActiveList = panel.renderActiveList; var openPanel = panel.openPanel; var closePanel = panel.closePanel; function injectStyles() { const style = document.createElement("style"); style.textContent = style_default; document.head.appendChild(style); } function initDrama() { injectStyles(); createFloatButtons(); enhancePage(); GM_registerMenuCommand("\u6536\u85CF\u5217\u8868", openPanel); GM_registerMenuCommand("\u8BBE\u7F6E", () => settingsPanel.open()); let lastUrl = location.href; setInterval(() => { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(() => { enhancePage(); updateButtonState(); updateButtonVisibility(); }, 500); } }, 1e3); document.addEventListener("keydown", (e) => { var _a, _b; if (e.key !== "Escape") return; const preview = document.querySelector(".vc-image-preview"); if (preview == null ? void 0 : preview.classList.contains("show")) { preview.classList.remove("show"); return; } if ((_a = xjump.getXjumpEl()) == null ? void 0 : _a.classList.contains("show")) { xjump.getXjumpEl().classList.remove("show"); return; } if ((_b = panel.getPanelEl()) == null ? void 0 : _b.classList.contains("show")) closePanel(); }); } function enhancePage() { const site = getCurrentSite(); if (!site) return; const t = site.pageType(); const isPlayPage = t === "play" || t === "detail" && site.trackPlaybackOnDetail; handlePageCache(getCurrentName()); if (typeof site.onPageReady === "function") site.onPageReady(); if (t === "detail") { cacheCurrentDetailImage(); if (site.isPlayUrl && !document.body.dataset.dramaPlayClickBound) { document.body.dataset.dramaPlayClickBound = "1"; document.addEventListener("click", (e) => { const a = e.target.closest("a"); if (a && site.isPlayUrl(a.href)) cacheCurrentDetailImage(); }); } } if (isPlayPage && !document.body.dataset.dramaPlaybackBound) { document.body.dataset.dramaPlaybackBound = "1"; initPlayback(site, getCurrentName()); } if (t === "detail" || t === "play") injectCrossSiteJump(); injectListFavoriteButtons(); } function getPositionKey() { const site = getCurrentSite(); return site ? `${POSITION_KEY}_${site.id}` : POSITION_KEY; } function getCollapseKey() { const site = getCurrentSite(); return site ? `${COLLAPSE_KEY}_${site.id}` : COLLAPSE_KEY; } function createFloatButtons() { floatBtns = createFloatButtonGroup({ positionKeyFn: getPositionKey, collapseKeyFn: getCollapseKey, collapseText: "\u22EE\u22EE", listBtnText: "\u{1F4CB} \u6536\u85CF\u5217\u8868", onWant: toggleWant, onWatched: toggleWatched, onOpenPanel: openPanel, isDetailPageFn: isDetailOrPlay, updateButtonStateFn: updateButtonState, onVisibilityChange: refreshAllMiniButtons }); floatBtns.create(); } function updateButtonVisibility() { if (floatBtns) floatBtns.updateVisibility(); } function updateButtonState() { const wantBtn = floatBtns == null ? void 0 : floatBtns.getWantBtn(); const watchedBtn = floatBtns == null ? void 0 : floatBtns.getWatchedBtn(); if (!wantBtn || !watchedBtn) return; const name = getCurrentName(); if (!name) { wantBtn.classList.remove("active"); watchedBtn.classList.remove("active"); return; } wantBtn.classList.toggle("active", isFavorited(name, "want")); watchedBtn.classList.toggle("active", isFavorited(name, "watched")); } function buildDramaUrl(item) { if (!item.siteId) return ""; const site = sites.find((s) => s.id === item.siteId); if (!site) return ""; const domain = pickDomain(site); if (!domain) return ""; return "https://" + domain + (item.path || ""); } var miniBtns = createMiniButtonInjector({ getSiteFn: getCurrentSite, buildItemFn: (cardData, site) => { const { element, name, cover, path } = cardData; if (!name) return null; return { name, cover, path, siteId: site.id, createTime: Date.now(), updateTime: Date.now(), element }; }, getIdFn: (item) => item.name, isFavoritedFn: isFavorited, toggleFavoriteFn: toggleFavorite, refreshItemFn: (item, site, btn) => { if (!item.element || !(site == null ? void 0 : site.getListCards)) return; const fresh = site.getListCards().find((c) => c.element === item.element); if (fresh && fresh.cover) item.cover = fresh.cover; }, afterToggleFn: updateButtonState }); function injectListFavoriteButtons() { miniBtns.inject(); } function refreshAllMiniButtons() { miniBtns.refreshAll(); } function getXjumpPositionKey() { const site = getCurrentSite(); return site ? `${XJUMP_POSITION_KEY}_${site.id}` : XJUMP_POSITION_KEY; } var xjump = createCrossSiteJump({ getSiteFn: getCurrentSite, getKeywordFn: () => { const site = getCurrentSite(); if (!site) return null; const t = site.pageType(); if (t === "detail" && site.getDetailInfo) return (site.getDetailInfo() || {}).name || null; if (t === "play" && site.getPlayInfo) return (site.getPlayInfo() || {}).name || null; return null; }, getPositionKeyFn: getXjumpPositionKey, getAllSitesFn: () => sites, pickDomainFn: pickDomain, siteIdField: "id" }); function injectCrossSiteJump() { xjump.inject(); } // src/drama.js applyAdBlock(); function init() { if (!getCurrentSite()) return; initDrama(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();