// ==UserScript== // @name 视频收藏助手 - 多站点视频收藏用户脚本 // @namespace jVideoCollector // @version 1.0.1 // @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/* // @noframes true // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @run-at document-end // ==/UserScript== (() => { // src/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) => { 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") || ""; if (name) { seen.add(box); cards.push({ element: box, name, cover, path: a.pathname }); } }); document.querySelectorAll("#searchList li").forEach((li) => { 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") || ""; 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/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) => "/boss1O1?q=" + encodeURIComponent(kw), pageType() { const p = location.pathname; if (p.startsWith("/movie/")) return "detail"; if (p.startsWith("/v_play/")) return "play"; if (p.startsWith("/boss1O1")) 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/drama/util.js 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] : ""; } // src/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/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() { 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("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/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,图片从 .vod-poster img.src,链接从 .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; const cover = (img == null ? void 0 : img.src) || (img == null ? void 0 : img.getAttribute("data-original")) || ""; 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/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.startsWith("/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/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/drama/config.js var DRAMA_STORAGE_KEY = "video_collector_drama_data"; var DRAMA_IMAGE_CACHE_KEY = "video_collector_drama_img_cache"; var DRAMA_LAST_DOMAIN_KEY = "video_collector_drama_last_domain"; var DRAMA_POSITION_KEY = "video_collector_drama_position"; var DRAMA_COLLAPSE_KEY = "video_collector_drama_collapsed"; var IMAGE_CACHE_TTL = 30 * 60 * 1e3; var PAGE_SIZE = 12; // src/drama/domains.js function norm(host) { return (host || "").replace(/^www\./, ""); } var _lastDomainMap = null; function loadMap() { if (_lastDomainMap === null) { try { _lastDomainMap = JSON.parse(GM_getValue(DRAMA_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(DRAMA_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) => norm(d) === norm(last))) return last; return site.domains[0]; } // src/drama/sites/index.js var dramaSites = [dida_default, czzy_default, hantv_default, ddys_default, libvio_default, xiuluo_default, duse_default]; var _currentSite = void 0; function getCurrentDramaSite() { if (_currentSite !== void 0) return _currentSite; const host = location.hostname; _currentSite = dramaSites.find((s) => s.domains.some((d) => norm(d) === norm(host))) || null; if (_currentSite) recordWorkingDomain(_currentSite.id, host); return _currentSite; } // src/drama/state.js var dramaState = { currentTab: "want", currentPage: { want: 1, watched: 1 }, renderedTabs: /* @__PURE__ */ new Set(), searchKeyword: "" }; // src/drama/storage.js function loadDramaData() { try { const data = GM_getValue(DRAMA_STORAGE_KEY, ""); return data ? JSON.parse(data) : { want: [], watched: [] }; } catch (e) { return { want: [], watched: [] }; } } function saveDramaData(data) { GM_setValue(DRAMA_STORAGE_KEY, JSON.stringify(data)); } function findDrama(name, list) { return list.findIndex((item) => item.name === name); } function prune(cache) { const now = Date.now(); for (const k in cache) { if (!cache[k] || typeof cache[k].ts !== "number" || now - cache[k].ts > IMAGE_CACHE_TTL) { delete cache[k]; } } return cache; } function loadImageCache() { try { return prune(JSON.parse(GM_getValue(DRAMA_IMAGE_CACHE_KEY, "{}"))); } catch (e) { return {}; } } function setImageCache(name, cover) { if (!name || !cover) return; const cache = prune(loadImageCache()); cache[name] = { cover, ts: Date.now() }; GM_setValue(DRAMA_IMAGE_CACHE_KEY, JSON.stringify(cache)); } function consumeImageCache(name) { const cache = prune(loadImageCache()); const entry = cache[name]; if (!entry) return ""; const cover = entry.cover; delete cache[name]; GM_setValue(DRAMA_IMAGE_CACHE_KEY, JSON.stringify(cache)); return cover; } function exportDramaData() { const data = loadDramaData(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `\u5F71\u89C6\u6536\u85CF\u5907\u4EFD_${(/* @__PURE__ */ new Date()).toLocaleDateString().replace(/\//g, "-")}.json`; a.click(); URL.revokeObjectURL(url); } function importDramaData(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 = (ev) => { try { const data = JSON.parse(ev.target.result); if (data.want && data.watched) { if (confirm("\u5BFC\u5165\u5C06\u8986\u76D6\u73B0\u6709\u5F71\u89C6\u6536\u85CF\u6570\u636E\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F")) { saveDramaData(data); callback && callback(); alert("\u5BFC\u5165\u6210\u529F\uFF01"); } } else { alert("\u6587\u4EF6\u683C\u5F0F\u4E0D\u6B63\u786E"); } } catch (err) { alert("\u6587\u4EF6\u89E3\u6790\u5931\u8D25"); } }; reader.readAsText(file); }; input.click(); } // src/drama/collection.js function getCurrentDramaName() { const site = getCurrentDramaSite(); 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 = getCurrentDramaSite(); if (!site || site.pageType() !== "detail" || !site.getDetailInfo) return; const info = site.getDetailInfo(); if (info && info.name && info.cover) setImageCache(info.name, info.cover); } function isDramaFavorited(name, type) { const data = loadDramaData(); return findDrama(name, data[type]) > -1; } function toggleDramaFavorite(drama, type) { const data = loadDramaData(); const otherType = type === "want" ? "watched" : "want"; const idx = findDrama(drama.name, data[type]); const otherIdx = findDrama(drama.name, data[otherType]); if (otherIdx > -1) data[otherType].splice(otherIdx, 1); if (idx > -1) { data[type].splice(idx, 1); } else { data[type].unshift(drama); } saveDramaData(data); dramaState.renderedTabs.clear(); } function toggleDrama(type) { const site = getCurrentDramaSite(); 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 = loadDramaData(); const idx = findDrama(name, data[type]); if (idx > -1) { data[type].splice(idx, 1); saveDramaData(data); } else { let cover = info.cover || ""; if (t === "detail") setImageCache(name, cover); if (t === "play") cover = consumeImageCache(name) || cover; const otherType = type === "want" ? "watched" : "want"; const otherIdx = findDrama(name, data[otherType]); if (otherIdx > -1) data[otherType].splice(otherIdx, 1); data[type].unshift({ name, cover, path: info.path || location.pathname, siteId: site.id, addTime: Date.now() }); saveDramaData(data); } dramaState.renderedTabs.clear(); updateDramaButtonState(); } function deleteDrama(name, type) { const data = loadDramaData(); const idx = findDrama(name, data[type]); if (idx > -1) { data[type].splice(idx, 1); saveDramaData(data); dramaState.renderedTabs.clear(); renderDramaActiveList(); updateDramaButtonState(); } } function moveDrama(name, fromType, toType) { const data = loadDramaData(); const idx = findDrama(name, data[fromType]); if (idx === -1) return; const item = data[fromType].splice(idx, 1)[0]; data[toType].unshift(item); saveDramaData(data); dramaState.renderedTabs.clear(); renderDramaActiveList(); updateDramaButtonState(); } function isDramaDetailOrPlay() { const site = getCurrentDramaSite(); if (!site) return false; const t = site.pageType(); return t === "detail" || t === "play"; } // src/common/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\n:root {\r\n --vc-primary: #4f46e5;\r\n --vc-want: #f97316;\r\n --vc-watched: #10b981;\r\n --vc-danger: #ef4444;\r\n --vc-bg: #ffffff;\r\n --vc-text: #1f2937;\r\n --vc-text-secondary: #6b7280;\r\n --vc-border: #e5e7eb;\r\n --vc-hover: #f9fafb;\r\n --vc-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\r\n --vc-shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.15);\r\n}\r\n\r\n/* \u60AC\u6D6E\u6309\u94AE\u7EC4 */\r\n.vc-btn-group {\r\n position: fixed;\r\n z-index: 99999;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r\n user-select: none;\r\n transition: right 0.2s, bottom 0.2s;\r\n}\r\n\r\n.vc-btn {\r\n padding: 6px 10px;\r\n border: 1px solid var(--vc-border);\r\n border-radius: 10px;\r\n font-size: 13px;\r\n font-weight: 500;\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n background: rgba(255, 255, 255, 0.95);\r\n backdrop-filter: blur(10px);\r\n box-shadow: var(--vc-shadow);\r\n color: var(--vc-text-secondary);\r\n white-space: nowrap;\r\n}\r\n\r\n.vc-btn:hover {\r\n transform: translateY(-1px);\r\n color: var(--vc-text);\r\n}\r\n\r\n.vc-btn.want.active {\r\n background: var(--vc-want);\r\n color: white;\r\n border-color: var(--vc-want);\r\n}\r\n\r\n.vc-btn.watched.active {\r\n background: var(--vc-watched);\r\n color: white;\r\n border-color: var(--vc-watched);\r\n}\r\n\r\n.vc-btn.list:hover {\r\n background: var(--vc-primary);\r\n color: white;\r\n border-color: var(--vc-primary);\r\n}\r\n\r\n.vc-collapse-btn {\r\n padding: 6px 10px;\r\n font-size: 12px;\r\n text-align: center;\r\n cursor: grab;\r\n}\r\n\r\n/* \u906E\u7F69 - \u5168\u5C40\u5C45\u4E2D */\r\n.vc-mask {\r\n position: fixed;\r\n inset: 0;\r\n width: 100vw;\r\n height: 100vh;\r\n background: rgba(0, 0, 0, 0.3);\r\n z-index: 100000;\r\n display: none;\r\n align-items: center;\r\n justify-content: center;\r\n backdrop-filter: blur(2px);\r\n margin: 0;\r\n padding: 0;\r\n}\r\n\r\n.vc-mask.show {\r\n display: flex !important;\r\n}\r\n\r\n/* \u6536\u85CF\u9762\u677F - \u56FA\u5B9A\u9AD8\u5EA6\u5E03\u5C40 */\r\n.vc-panel {\r\n width: 880px;\r\n max-width: 92vw;\r\n height: 80vh;\r\n background: var(--vc-bg);\r\n border-radius: 16px;\r\n box-shadow: var(--vc-shadow-lg);\r\n display: flex;\r\n flex-direction: column;\r\n overflow: hidden;\r\n animation: vcFadeIn 0.2s ease;\r\n position: relative;\r\n border: 1px solid var(--vc-border);\r\n}\r\n\r\n@keyframes vcFadeIn {\r\n from { opacity: 0; transform: translateY(10px); }\r\n to { opacity: 1; transform: translateY(0); }\r\n}\r\n\r\n/* \u5934\u90E8 */\r\n.vc-panel-header {\r\n padding: 20px 56px 16px 24px;\r\n border-bottom: 1px solid var(--vc-border);\r\n flex-shrink: 0;\r\n}\r\n\r\n.vc-panel-title-row {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n margin-bottom: 16px;\r\n}\r\n\r\n.vc-panel-title {\r\n font-size: 18px;\r\n font-weight: 600;\r\n color: var(--vc-text);\r\n margin: 0;\r\n}\r\n\r\n.vc-header-actions {\r\n display: flex;\r\n gap: 8px;\r\n}\r\n\r\n.vc-icon-btn-sm {\r\n padding: 6px 12px;\r\n border: 1px solid var(--vc-border);\r\n background: white;\r\n border-radius: 8px;\r\n font-size: 12px;\r\n cursor: pointer;\r\n color: var(--vc-text-secondary);\r\n transition: all 0.15s;\r\n}\r\n\r\n.vc-icon-btn-sm:hover {\r\n border-color: var(--vc-primary);\r\n color: var(--vc-primary);\r\n}\r\n\r\n.vc-tabs {\r\n display: flex;\r\n gap: 24px;\r\n margin-bottom: 12px;\r\n}\r\n\r\n.vc-tab {\r\n padding: 6px 0;\r\n font-size: 14px;\r\n color: var(--vc-text-secondary);\r\n cursor: pointer;\r\n border-bottom: 2px solid transparent;\r\n transition: all 0.2s;\r\n}\r\n\r\n.vc-tab.active {\r\n color: var(--vc-primary);\r\n border-bottom-color: var(--vc-primary);\r\n font-weight: 500;\r\n}\r\n\r\n.vc-search {\r\n width: 100%;\r\n padding: 8px 12px;\r\n border: 1px solid var(--vc-border);\r\n border-radius: 8px;\r\n font-size: 13px;\r\n outline: none;\r\n transition: border-color 0.2s;\r\n box-sizing: border-box;\r\n background: var(--vc-bg);\r\n}\r\n\r\n.vc-search:focus {\r\n border-color: var(--vc-primary);\r\n}\r\n\r\n/* \u5217\u8868\u5BB9\u5668 */\r\n.vc-list-wrap {\r\n flex: 1;\r\n overflow: hidden;\r\n padding: 20px 24px;\r\n display: none;\r\n box-sizing: border-box;\r\n}\r\n\r\n.vc-list-wrap.active {\r\n display: block;\r\n}\r\n\r\n/* \u7F51\u683C\u5E03\u5C40 - \u56FA\u5B9A3\u884C4\u5217 */\r\n.vc-grid {\r\n display: grid;\r\n grid-template-columns: repeat(4, 1fr);\r\n grid-template-rows: repeat(3, 1fr);\r\n gap: 16px;\r\n height: 100%;\r\n width: 100%;\r\n}\r\n\r\n/* \u5361\u7247 */\r\n.vc-card {\r\n position: relative;\r\n border-radius: 10px;\r\n overflow: hidden;\r\n border: 1px solid var(--vc-border);\r\n background: white;\r\n transition: all 0.2s ease;\r\n cursor: pointer;\r\n display: flex;\r\n flex-direction: column;\r\n max-height: 260px;\r\n}\r\n\r\n.vc-card:hover {\r\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);\r\n transform: translateY(-2px);\r\n}\r\n\r\n/* \u5C01\u9762\u533A\u57DF */\r\n.vc-card-cover {\r\n flex: 1;\r\n min-height: 0;\r\n background: #f3f4f6;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n overflow: hidden;\r\n}\r\n\r\n.vc-card-cover img {\r\n width: 100%;\r\n height: 100%;\r\n object-fit: cover;\r\n cursor: zoom-in;\r\n transition: transform 0.2s;\r\n}\r\n\r\n.vc-card:hover .vc-card-cover img {\r\n transform: scale(1.05);\r\n}\r\n\r\n.vc-card-cover .default-cover {\r\n color: #9ca3af;\r\n font-size: 28px;\r\n}\r\n\r\n/* \u5361\u7247\u64CD\u4F5C\u6309\u94AE */\r\n.vc-card-actions {\r\n position: absolute;\r\n top: 8px;\r\n right: 8px;\r\n display: flex;\r\n gap: 4px;\r\n opacity: 0;\r\n transition: opacity 0.2s ease;\r\n z-index: 2;\r\n}\r\n\r\n.vc-card:hover .vc-card-actions {\r\n opacity: 1;\r\n}\r\n\r\n.vc-card-action-btn {\r\n padding: 4px 8px;\r\n border: none;\r\n background: rgba(255, 255, 255, 0.95);\r\n border-radius: 6px;\r\n font-size: 11px;\r\n cursor: pointer;\r\n color: var(--vc-text-secondary);\r\n backdrop-filter: blur(4px);\r\n transition: all 0.15s;\r\n}\r\n\r\n.vc-card-action-btn:hover {\r\n background: var(--vc-primary);\r\n color: white;\r\n}\r\n\r\n.vc-card-action-btn.delete:hover {\r\n background: var(--vc-danger);\r\n color: white;\r\n}\r\n\r\n/* \u5361\u7247\u4FE1\u606F\u533A */\r\n.vc-card-info {\r\n padding: 8px 10px 10px;\r\n flex-shrink: 0;\r\n}\r\n\r\n.vc-card-title {\r\n font-size: 12px;\r\n line-height: 1.4;\r\n color: var(--vc-text);\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n overflow: hidden;\r\n font-weight: 500;\r\n}\r\n\r\n.vc-card-site {\r\n display: inline-block;\r\n font-size: 11px;\r\n color: var(--vc-primary);\r\n background: rgba(79, 70, 229, 0.1);\r\n padding: 1px 6px;\r\n border-radius: 4px;\r\n white-space: nowrap;\r\n vertical-align: middle;\r\n margin-right: 4px;\r\n}\r\n\r\n/* \u7AD9\u70B9\u6807\u8BC6\u4E3A\u7A7A\u65F6\u4E0D\u5360\u4F4D\uFF08\u89C6\u9891\u6A21\u5757\u65E0\u7AD9\u70B9\u6765\u6E90\uFF09 */\r\n.vc-card-site:empty {\r\n display: none;\r\n}\r\n\r\n.vc-card-time {\r\n font-size: 11px;\r\n color: var(--vc-text-secondary);\r\n margin-top: 4px;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n min-width: 0;\r\n}\r\n\r\n.vc-empty {\r\n grid-column: 1 / -1;\r\n grid-row: 1 / -1;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--vc-text-secondary);\r\n font-size: 14px;\r\n}\r\n\r\n/* \u5206\u9875\u5668 */\r\n.vc-pagination {\r\n padding: 12px 24px;\r\n border-top: 1px solid var(--vc-border);\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n font-size: 12px;\r\n color: var(--vc-text-secondary);\r\n flex-shrink: 0;\r\n}\r\n\r\n.vc-pagination-btns {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n\r\n.vc-page-btn {\r\n padding: 4px 10px;\r\n border: 1px solid var(--vc-border);\r\n background: white;\r\n border-radius: 6px;\r\n font-size: 12px;\r\n cursor: pointer;\r\n color: var(--vc-text-secondary);\r\n transition: all 0.15s;\r\n}\r\n\r\n.vc-page-btn:hover:not(:disabled) {\r\n border-color: var(--vc-primary);\r\n color: var(--vc-primary);\r\n}\r\n\r\n.vc-page-btn:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n/* \u5173\u95ED\u6309\u94AE */\r\n.vc-close {\r\n position: absolute;\r\n top: 16px;\r\n right: 20px;\r\n background: none;\r\n border: none;\r\n font-size: 20px;\r\n color: var(--vc-text-secondary);\r\n cursor: pointer;\r\n line-height: 1;\r\n padding: 4px;\r\n z-index: 2;\r\n}\r\n\r\n.vc-close:hover {\r\n color: var(--vc-text);\r\n}\r\n\r\n/* \u56FE\u7247\u9884\u89C8 - \u5168\u5C40\u5C45\u4E2D */\r\n.vc-image-preview {\r\n position: fixed;\r\n inset: 0;\r\n width: 100vw;\r\n height: 100vh;\r\n background: rgba(0, 0, 0, 0.85);\r\n z-index: 100001;\r\n display: none;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: zoom-out;\r\n margin: 0;\r\n padding: 0;\r\n}\r\n\r\n.vc-image-preview.show {\r\n display: flex !important;\r\n}\r\n\r\n.vc-image-preview img {\r\n max-width: 92vw;\r\n max-height: 92vh;\r\n object-fit: contain;\r\n border-radius: 4px;\r\n}\r\n\r\n/* ===== \u5217\u8868\u9875\u5361\u7247\u8FF7\u4F60\u6536\u85CF\u6309\u94AE ===== */\r\n.vc-mini-fav {\r\n position: absolute;\r\n top: 6px;\r\n left: 6px;\r\n display: flex;\r\n gap: 4px;\r\n opacity: 0;\r\n pointer-events: none;\r\n transition: opacity 0.15s ease;\r\n z-index: 10;\r\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r\n}\r\n\r\n/* \u5361\u7247\u60AC\u6D6E\u6216\u6309\u94AE\u81EA\u8EAB\u60AC\u6D6E\u65F6\u663E\u793A\u5E76\u53EF\u70B9\u51FB */\r\n.vc-fav-host:hover .vc-mini-fav,\r\n.vc-mini-fav:hover {\r\n opacity: 1;\r\n pointer-events: auto;\r\n}\r\n\r\n.vc-mini-btn {\r\n width: 26px;\r\n height: 26px;\r\n padding: 0;\r\n border: none;\r\n border-radius: 6px;\r\n background: rgba(255, 255, 255, 0.92);\r\n color: var(--vc-text-secondary);\r\n font-size: 13px;\r\n line-height: 1;\r\n cursor: pointer;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n backdrop-filter: blur(4px);\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);\r\n transition: transform 0.15s, background 0.15s, color 0.15s;\r\n}\r\n\r\n.vc-mini-btn:hover {\r\n transform: scale(1.12);\r\n}\r\n\r\n.vc-mini-want:hover {\r\n color: var(--vc-want);\r\n}\r\n\r\n.vc-mini-watched:hover {\r\n color: var(--vc-watched);\r\n}\r\n\r\n.vc-mini-want.active {\r\n background: var(--vc-want);\r\n color: #fff;\r\n}\r\n\r\n.vc-mini-watched.active {\r\n background: var(--vc-watched);\r\n color: #fff;\r\n}\r\n\r\n/* ===== JavLibrary \u8BE6\u60C5\u9875\u8DF3\u8F6C JavBus \u6309\u94AE ===== */\r\n.vc-jb-jump {\r\n display: inline-block;\r\n margin-left: 8px;\r\n padding: 2px 10px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n color: #fff !important;\r\n background: var(--vc-primary);\r\n border-radius: 6px;\r\n text-decoration: none;\r\n cursor: pointer;\r\n vertical-align: middle;\r\n transition: opacity 0.15s, transform 0.15s;\r\n}\r\n\r\n.vc-jb-jump:hover {\r\n opacity: 0.88;\r\n transform: translateY(-1px);\r\n}\r\n'; // src/drama/style.css var style_default2 = '/* \u5F71\u89C6\u6A21\u5757\u4E13\u5C5E\u6837\u5F0F\uFF08\u5171\u4EAB\u6837\u5F0F\u89C1 ui/style.css\uFF0C\u6B64\u5904\u4EC5\u8DE8\u7AD9\u76F4\u8FBE widget\uFF09 */\n\n.vc-xjump {\n position: fixed;\n z-index: 99999;\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\n}\n\n.vc-xjump-toggle {\n padding: 8px 14px;\n border: 1px solid var(--vc-border);\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.95);\n backdrop-filter: blur(10px);\n box-shadow: var(--vc-shadow);\n color: var(--vc-text);\n font-size: 13px;\n font-weight: 500;\n cursor: grab;\n white-space: nowrap;\n user-select: none;\n transition: transform 0.15s;\n}\n\n.vc-xjump-toggle:hover {\n transform: translateY(-1px);\n}\n\n.vc-xjump-menu {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n display: none;\n flex-direction: column;\n gap: 6px;\n background: rgba(255, 255, 255, 0.98);\n border: 1px solid var(--vc-border);\n border-radius: 10px;\n box-shadow: var(--vc-shadow-lg);\n padding: 8px;\n max-height: 60vh;\n overflow: auto;\n min-width: 102px;\n box-sizing: border-box;\n}\n\n/* \u5E95\u90E8\u7A7A\u95F4\u4E0D\u591F\u65F6\u83DC\u5355\u5411\u4E0A\u5C55\u5F00 */\n.vc-xjump.up .vc-xjump-menu {\n top: auto;\n bottom: calc(100% + 6px);\n}\n\n.vc-xjump.show .vc-xjump-menu {\n display: flex;\n}\n\n.vc-xjump-btn {\n padding: 6px 12px;\n border: none;\n border-radius: 8px;\n background: var(--vc-hover);\n color: var(--vc-text);\n font-size: 12px;\n text-decoration: none;\n cursor: pointer;\n text-align: left;\n white-space: nowrap;\n transition: background 0.15s, color 0.15s;\n}\n\n.vc-xjump-btn:hover {\n background: var(--vc-primary);\n color: #fff;\n}\n'; // src/common/templates/panel.html var panel_default = '\n