// ==UserScript==
// @name 视频收藏助手 - 多站联合版
// @namespace https://gitee.com/xiaobin90/jVideoCollector
// @version 1.0.5
// @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) => {
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/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 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 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(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) => norm(d) === norm(last))) return last;
return site.domains[0];
}
// src/drama/sites/index.js
var sites = [dida_default, czzy_default, hantv_default, ddys_default, libvio_default, xiuluo_default, duse_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) => norm(d) === norm(host))) || null;
if (_currentSite) recordWorkingDomain(_currentSite.id, host);
return _currentSite;
}
// src/drama/state.js
var state = {
currentTab: "want",
currentPage: { want: 1, watched: 1 },
renderedTabs: /* @__PURE__ */ new Set(),
searchKeyword: ""
};
// src/drama/storage.js
function loadData() {
try {
const data = GM_getValue(STORAGE_KEY, "");
return data ? JSON.parse(data) : { want: [], watched: [] };
} catch (e) {
return { want: [], watched: [] };
}
}
function saveData(data) {
GM_setValue(STORAGE_KEY, JSON.stringify(data));
}
function findItem(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(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(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(IMAGE_CACHE_KEY, JSON.stringify(cache));
return cover;
}
function exportData() {
const data = loadData();
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 = `\u89C6\u9891\u6536\u85CF\u5907\u4EFD_${(/* @__PURE__ */ new Date()).toLocaleDateString().replace(/\//g, "-")}.json`;
a.click();
URL.revokeObjectURL(url);
}
function importData(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")) {
saveData(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 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 {
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 = info.cover || "";
if (t === "detail") setImageCache(name, cover);
if (t === "play") cover = consumeImageCache(name) || 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) {
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) {
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/common/templates/confirm.html
var confirm_default = '
\n
\n
\n \n \n
\n
';
// src/common/utils.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 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 showConfirm(text, { danger = false } = {}) {
return new Promise((resolve) => {
const mask = document.createElement("div");
mask.className = "vc-confirm-mask";
mask.innerHTML = confirm_default;
mask.querySelector(".vc-confirm-text").textContent = text;
if (danger) mask.querySelector(".vc-confirm-ok").classList.add("danger");
document.body.appendChild(mask);
requestAnimationFrame(() => mask.classList.add("show"));
function close(result) {
mask.classList.remove("show");
setTimeout(() => mask.remove(), 150);
resolve(result);
}
mask.querySelector(".vc-confirm-cancel").onclick = () => close(false);
mask.querySelector(".vc-confirm-ok").onclick = () => close(true);
mask.onclick = (e) => {
if (e.target === mask) close(false);
};
});
}
// src/common/panel.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
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
emptyText,
// string
templates
// { panelTemplate, cardTemplate, coverImageTemplate, defaultCoverTemplate }
} = config;
let panelEl = null;
let batchMode = 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;
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 (batchMode) 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-prev").onclick = () => {
if (state2.currentPage[state2.currentTab] > 1) {
state2.currentPage[state2.currentTab]--;
state2.renderedTabs.clear();
renderActiveList2();
}
};
panel2.querySelector("#vc-next").onclick = () => {
const data = loadFn();
const list = filterFn(data[state2.currentTab], state2.searchKeyword);
const totalPages = Math.ceil(list.length / pageSize) || 1;
if (state2.currentPage[state2.currentTab] < totalPages) {
state2.currentPage[state2.currentTab]++;
state2.renderedTabs.clear();
renderActiveList2();
}
};
panel2.querySelector("#vc-batch-toggle").onclick = () => {
if (batchMode) 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 (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 ok = await showConfirm(`\u786E\u5B9A\u5220\u9664\u9009\u4E2D\u7684 ${batchSelected.size} \u6761\u6536\u85CF\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 (batchMode) updateBatchUI();
}
function renderList(type) {
const data = loadFn();
const fullList = filterFn(data[type], state2.searchKeyword);
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;
return interpolate(templates.cardTemplate, {
id: getId(item),
title: getTitle(item),
url: getUrl(item),
siteName: getSiteName(item),
dateStr: formatDate(getTime(item)),
oppositeType,
oppositeText,
coverHtml
});
}).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 (batchMode) {
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");
});
card.querySelector(".move-btn").addEventListener("click", (e) => {
e.stopPropagation();
moveFn(id, type, e.target.dataset.to);
});
card.querySelector(".delete").addEventListener("click", (e) => {
e.stopPropagation();
showConfirm("\u786E\u5B9A\u5220\u9664\u8FD9\u6761\u6536\u85CF\u5417\uFF1F", { 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 selected = forceState !== void 0 ? forceState : !batchSelected.has(id);
if (selected) {
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() {
batchMode = true;
batchSelected.clear();
panelEl.querySelector(".vc-panel").classList.add("batch-mode");
panelEl.querySelector("#vc-batch-bar").style.display = "";
state2.renderedTabs.clear();
renderActiveList2();
}
function exitBatchMode() {
batchMode = 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-move").disabled = !hasSelection;
panelEl.querySelector("#vc-batch-delete").disabled = !hasSelection;
panelEl.querySelector("#vc-batch-move").textContent = type === "want" ? "\u79FB\u5230\u5DF2\u770B" : "\u79FB\u5230\u60F3\u770B";
}
function updatePagination() {
const data = loadFn();
const list = filterFn(data[state2.currentTab], state2.searchKeyword);
const total = list.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = state2.currentPage[state2.currentTab];
const tabLabel = state2.currentTab === "want" ? "\u60F3\u770B" : "\u5DF2\u770B";
panelEl.querySelector("#vc-count-info").textContent = `\u5171 ${total} \u6761${tabLabel}`;
panelEl.querySelector("#vc-page-info").textContent = `${currentPage} / ${totalPages}`;
panelEl.querySelector("#vc-prev").disabled = currentPage <= 1;
panelEl.querySelector("#vc-next").disabled = currentPage >= totalPages;
}
function openPanel2() {
if (!panelEl) createPanel();
panelEl.classList.add("show");
renderActiveList2();
}
function closePanel2() {
if (batchMode) exitBatchMode();
panelEl.classList.remove("show");
}
function getPanelEl() {
return panelEl;
}
return { createPanel, openPanel: openPanel2, closePanel: closePanel2, renderActiveList: renderActiveList2, getPanelEl };
}
// src/common/drag.js
function initFloatDrag(handle, btnGroup, getPositionKey2) {
let isDragging = false, hasMoved = false, startX, startY, startRight, startBottom;
let dragW = 0, dragH = 0, rafId = null, pendingRight = 0, pendingBottom = 0;
handle.addEventListener("mousedown", (e) => {
isDragging = true;
hasMoved = false;
startX = e.clientX;
startY = e.clientY;
const rect = btnGroup.getBoundingClientRect();
startRight = window.innerWidth - rect.right;
startBottom = window.innerHeight - rect.bottom;
dragW = btnGroup.offsetWidth;
dragH = btnGroup.offsetHeight;
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));
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";
}
GM_setValue(getPositionKey2(), {
right: parseInt(btnGroup.style.right),
bottom: parseInt(btnGroup.style.bottom)
});
});
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;
const rect = xjumpEl.getBoundingClientRect();
startRight = window.innerWidth - rect.right;
startTop = rect.top;
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: parseInt(xjumpEl.style.right),
top: parseInt(xjumpEl.style.top)
});
});
handle.addEventListener("click", (e) => {
if (hasMoved) {
e.preventDefault();
e.stopPropagation();
hasMoved = false;
}
}, true);
}
// src/common/float-btn.js
function createFloatButtonGroup(config) {
const {
positionKeyFn,
// () => storage key string
collapseKey,
// 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, wantBtn, watchedBtn, listBtn;
function create() {
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";
}
const collapseBtn = document.createElement("button");
collapseBtn.className = "vc-btn vc-collapse-btn";
collapseBtn.textContent = collapseText;
collapseBtn.title = "\u62D6\u52A8\u53EF\u79FB\u52A8\u4F4D\u7F6E\uFF0C\u70B9\u51FB\u6536\u8D77/\u5C55\u5F00";
wantBtn = document.createElement("button");
wantBtn.className = "vc-btn want";
wantBtn.textContent = "\u2B50 \u60F3\u770B";
wantBtn.onclick = onWant;
watchedBtn = document.createElement("button");
watchedBtn.className = "vc-btn watched";
watchedBtn.textContent = "\u2713 \u5DF2\u770B";
watchedBtn.onclick = onWatched;
listBtn = document.createElement("button");
listBtn.className = "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);
collapseBtn.addEventListener("click", () => {
const collapsed = !GM_getValue(collapseKey, false);
GM_setValue(collapseKey, collapsed);
updateVisibility();
});
updateButtonStateFn();
updateVisibility();
document.addEventListener("visibilitychange", () => {
if (document.hidden) return;
updateButtonStateFn();
if (onVisibilityChange) onVisibilityChange();
});
}
function updateVisibility() {
if (!wantBtn || !watchedBtn) return;
const collapsed = GM_getValue(collapseKey, false);
if (collapsed) {
wantBtn.style.display = "none";
watchedBtn.style.display = "none";
listBtn.style.display = "none";
} else if (!isDetailPageFn()) {
wantBtn.style.display = "none";
watchedBtn.style.display = "none";
listBtn.style.display = "";
} else {
wantBtn.style.display = "";
watchedBtn.style.display = "";
listBtn.style.display = "";
}
if (!collapsed) clampInViewport();
}
function clampInViewport() {
if (!btnGroup) return;
const w = btnGroup.offsetWidth;
const h = btnGroup.offsetHeight;
if (!w || !h) return;
let right = parseInt(btnGroup.style.right) || 0;
let bottom = parseInt(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/common/mini-btn.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 mouseBound = 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 (!mouseBound) {
mouseBound = 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 injected = 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);
injected = true;
});
if (injected) refreshAll();
}
function makeBtn(cls, icon, title, type, item, refreshFns) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = `vc-mini-btn ${cls}`;
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();
item.updateTime = Date.now();
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/common/xjump.js
function createCrossSiteJump(config) {
const {
getSiteFn,
// () => 当前站点对象
getKeywordFn,
// () => 搜索关键词 string 或 null
getPositionKeyFn,
// () => 位置存储 key
getAllSitesFn,
// () => 所有站点数组
pickDomainFn,
// (site) => 域名 string
siteIdField
// 'name' 或 'id',用于过滤当前站点
} = config;
let xjumpEl = null;
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]).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/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 min-height: 400px;\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-y: auto;\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, minmax(160px, 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/* ===== \u8DE8\u7AD9\u76F4\u8FBE widget ===== */\r\n.vc-xjump {\r\n position: fixed;\r\n z-index: 99999;\r\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r\n}\r\n\r\n.vc-xjump-toggle {\r\n padding: 8px 14px;\r\n border: 1px solid var(--vc-border);\r\n border-radius: 8px;\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 font-size: 13px;\r\n font-weight: 500;\r\n cursor: grab;\r\n white-space: nowrap;\r\n user-select: none;\r\n transition: transform 0.15s;\r\n}\r\n\r\n.vc-xjump-toggle:hover {\r\n transform: translateY(-1px);\r\n}\r\n\r\n.vc-xjump-menu {\r\n position: absolute;\r\n top: calc(100% + 6px);\r\n right: 0;\r\n display: none;\r\n flex-direction: column;\r\n gap: 4px;\r\n background: rgba(255, 255, 255, 0.98);\r\n border: 1px solid var(--vc-border);\r\n border-radius: 10px;\r\n box-shadow: var(--vc-shadow-lg);\r\n padding: 6px;\r\n max-height: 60vh;\r\n overflow: auto;\r\n min-width: 102px;\r\n box-sizing: border-box;\r\n}\r\n\r\n.vc-xjump.up .vc-xjump-menu {\r\n top: auto;\r\n bottom: calc(100% + 6px);\r\n}\r\n\r\n.vc-xjump.show .vc-xjump-menu {\r\n display: flex;\r\n}\r\n\r\n.vc-xjump-btn {\r\n padding: 6px 12px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--vc-text-secondary);\r\n font-size: 12px;\r\n font-weight: 500;\r\n text-decoration: none;\r\n cursor: pointer;\r\n text-align: left;\r\n white-space: nowrap;\r\n transition: background 0.15s, color 0.15s;\r\n}\r\n\r\n.vc-xjump-btn:hover {\r\n background: var(--vc-primary);\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\r\n/* ===== \u6279\u91CF\u6A21\u5F0F ===== */\r\n/* \u5361\u7247\u9009\u62E9\u6846 */\r\n.vc-card-checkbox {\r\n position: absolute;\r\n top: 6px;\r\n left: 6px;\r\n z-index: 3;\r\n width: 20px;\r\n height: 20px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.vc-card-checkbox input {\r\n width: 16px;\r\n height: 16px;\r\n cursor: pointer;\r\n accent-color: var(--vc-primary);\r\n}\r\n\r\n.vc-panel.batch-mode .vc-card-checkbox {\r\n display: flex !important;\r\n}\r\n\r\n.vc-panel.batch-mode .vc-card-actions {\r\n display: none !important;\r\n}\r\n\r\n.vc-panel.batch-mode .vc-card {\r\n cursor: default;\r\n}\r\n\r\n.vc-card.selected {\r\n box-shadow: 0 0 0 2px var(--vc-primary);\r\n}\r\n\r\n/* \u6279\u91CF\u5DE5\u5177\u680F */\r\n.vc-batch-bar {\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 gap: 12px;\r\n font-size: 13px;\r\n color: var(--vc-text-secondary);\r\n flex-shrink: 0;\r\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r\n background: var(--vc-hover);\r\n}\r\n\r\n.vc-batch-select-all {\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n cursor: pointer;\r\n white-space: nowrap;\r\n font-weight: 500;\r\n}\r\n\r\n.vc-batch-select-all input {\r\n width: 16px;\r\n height: 16px;\r\n cursor: pointer;\r\n accent-color: var(--vc-primary);\r\n}\r\n\r\n.vc-batch-count {\r\n color: var(--vc-primary);\r\n font-weight: 600;\r\n white-space: nowrap;\r\n}\r\n\r\n.vc-batch-actions {\r\n margin-left: auto;\r\n display: flex;\r\n gap: 6px;\r\n}\r\n\r\n.vc-batch-btn {\r\n padding: 4px 12px;\r\n border: 1px solid var(--vc-border);\r\n background: white;\r\n border-radius: 6px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n cursor: pointer;\r\n color: var(--vc-text-secondary);\r\n transition: all 0.15s;\r\n font-family: inherit;\r\n}\r\n\r\n.vc-batch-btn:hover:not(:disabled) {\r\n border-color: var(--vc-primary);\r\n color: var(--vc-primary);\r\n}\r\n\r\n.vc-batch-btn:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n.vc-batch-btn-danger:hover:not(:disabled) {\r\n border-color: var(--vc-danger);\r\n color: var(--vc-danger);\r\n}\r\n\r\n.vc-batch-btn-ghost {\r\n border-color: transparent;\r\n background: transparent;\r\n}\r\n\r\n.vc-batch-btn-ghost:hover {\r\n color: var(--vc-text) !important;\r\n border-color: var(--vc-border) !important;\r\n}\r\n\r\n/* \u6279\u91CF\u6A21\u5F0F\u65F6\u9690\u85CF\u5206\u9875\u5668 */\r\n.vc-panel.batch-mode .vc-pagination {\r\n display: none;\r\n}\r\n\r\n/* ===== \u786E\u8BA4\u5F39\u6846 ===== */\r\n.vc-confirm-mask {\r\n position: fixed;\r\n inset: 0;\r\n z-index: 100002;\r\n background: rgba(0, 0, 0, 0.35);\r\n display: none;\r\n align-items: center;\r\n justify-content: center;\r\n backdrop-filter: blur(2px);\r\n}\r\n\r\n.vc-confirm-mask.show {\r\n display: flex !important;\r\n}\r\n\r\n.vc-confirm-dialog {\r\n width: 375px;\r\n max-width: 90vw;\r\n background: white;\r\n border-radius: 12px;\r\n box-shadow: var(--vc-shadow-lg);\r\n padding: 24px;\r\n text-align: center;\r\n animation: vcFadeIn 0.15s ease;\r\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\r\n}\r\n\r\n.vc-confirm-text {\r\n font-size: 15px;\r\n font-weight: 500;\r\n color: var(--vc-text);\r\n line-height: 1.5;\r\n margin-bottom: 25px;\r\n}\r\n\r\n.vc-confirm-actions {\r\n display: flex;\r\n gap: 20px;\r\n justify-content: center;\r\n}\r\n\r\n.vc-confirm-btn {\r\n padding: 7px 20px;\r\n border: none;\r\n border-radius: 8px;\r\n font-size: 14px;\r\n font-weight: 500;\r\n cursor: pointer;\r\n transition: all 0.15s;\r\n font-family: inherit;\r\n}\r\n\r\n.vc-confirm-cancel {\r\n background: var(--vc-hover);\r\n color: var(--vc-text-secondary);\r\n border: 1px solid var(--vc-border);\r\n}\r\n\r\n.vc-confirm-cancel:hover {\r\n background: var(--vc-border);\r\n color: var(--vc-text);\r\n}\r\n\r\n.vc-confirm-ok {\r\n background: var(--vc-primary);\r\n color: white;\r\n}\r\n\r\n.vc-confirm-ok:hover {\r\n opacity: 0.9;\r\n}\r\n\r\n.vc-confirm-ok.danger {\r\n background: var(--vc-danger);\r\n}\r\n';
// src/common/templates/panel.html
var panel_default = '\n\n\n\n\n\n\n
\n
\u5DF2\u9009 0 \u6761\n
\n \n \n \n
\n
\n\n';
// src/common/templates/card.html
var card_default = '\n
\n
{{coverHtml}}
\n
\n \n \n
\n
\n
{{siteName}}{{title}}
\n
{{dateStr}}
\n
\n
';
// src/common/templates/cover-image.html
var cover_image_default = '
\n';
// src/common/templates/default-cover.html
var default_cover_default = '\u{1F5BC}\uFE0F
\n';
// src/drama/ui.js
var TEMPLATES = { panelTemplate: panel_default, cardTemplate: card_default, coverImageTemplate: cover_image_default, defaultCoverTemplate: default_cover_default };
var floatBtns;
var _renderActiveList = null;
function renderActiveList() {
if (_renderActiveList) _renderActiveList();
}
var panel = createPanelController({
state,
pageSize: PAGE_SIZE,
loadFn: loadData,
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(),
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);
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();
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 (t === "detail" || t === "play") injectCrossSiteJump();
injectListFavoriteButtons();
}
function getPositionKey() {
const site = getCurrentSite();
return site ? `${POSITION_KEY}_${site.id}` : POSITION_KEY;
}
function createFloatButtons() {
floatBtns = createFloatButtonGroup({
positionKeyFn: getPositionKey,
collapseKey: COLLAPSE_KEY,
collapseText: "\u22EE\u22EE \u6536\u85CF\u5DE5\u5177",
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/index.js
function init() {
if (!getCurrentSite()) return;
initDrama();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();