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