// ==UserScript== // @name 腾果影视 // @namespace https://scriptcat.org/zh-CN/ // @version 1.1.2 // @description 爱看纯净版:爱看1/爱看2 双站聚合搜索,m3u8 直链解析,多线路切换 + 实时测速,自动连播,统一观看历史,红果详情页一键搜索,腾讯片库海报一键搜爱看(弹窗播放)。仅限个人学习使用。https://v.qq.com/tv-series-list/v-index.html // @author 失辛向南 // @license MIT // @run-at document-start // @noframes // @match *://www.imoviebot.com/* // @match *://hongguoduanju.com/* // @match *://*.hongguoduanju.com/* // @match *://ikanbot.eu.org/* // @match *://*.ikanbot.eu.org/* // @match *://www1.ikanbot.com/* // @match *://*.www1.ikanbot.com/* // @match *://v.qq.com/tv-series-list/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_info // @grant unsafeWindow // @connect www.hongniuzy2.com // @connect bfzyapi.com // @connect cj.ffzyapi.com // @connect cj.lziapi.com // @connect ikunzyapi.com // @connect api.guangsuapi.com // @connect suoniapi.com // @connect sdzyapi.com // @connect collect.wolongzyw.com // @connect api.apibdzy.com // @connect cj.yayazy.net // @connect jyzyapi.com // @connect api.wujinapi.me // @connect www.huyaapi.com // @connect caiji.dyttzyapi.com // @connect p2100.net // @connect zuidazy.me // @connect api.zuidapi.com // @connect caiji.moduapi.cc // @connect www.mdzyapi.com // @connect wolongzyw.com // @connect api.xinlangapi.com // @connect iqiyizyapi.com // @connect caiji.dbzy5.com // @connect tyyszy.com // @connect cj.rycjapi.com // @connect jinyingzy.com // @connect jszyapi.com // @connect wjzyapi.com // @connect apiyutu.com // @connect hongguoduanju.com // @connect *.hongguoduanju.com // @connect ikanbot.eu.org // @connect *.ikanbot.eu.org // @connect www1.ikanbot.com // @connect *.www1.ikanbot.com // @require https://cdnjs.cloudflare.com/ajax/libs/artplayer/5.1.0/artplayer.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.9/vue.min.js // ==/UserScript== (function () { 'use strict'; // ==================== 站点识别 ==================== const HOSTNAME = location.hostname; const PATHNAME = location.pathname; const HREF = location.href; const isDuanjuDomain = HOSTNAME.indexOf('hongguoduanju') !== -1; const isImoviebotDomain = HOSTNAME.indexOf('ikanbot') !== -1 || HOSTNAME.indexOf('imoviebot') !== -1; // 腾讯片库:仅 v.qq.com 的 tv-series-list 频道列表页启用频道整合卡片墙 const isQQDomain = HOSTNAME === 'v.qq.com'; const isTencentList = isQQDomain && PATHNAME.indexOf('/tv-series-list/') !== -1; // 站点代号:dj=红果短剧, im=爱看影视, tx=腾讯片库;其他页面兜底为 db(无专门适配) const CURRENT_SITE = isTencentList ? 'tx' : (isDuanjuDomain ? 'dj' : (isImoviebotDomain ? 'im' : 'db')); // 统一布局判断:宽度≥769 或横屏 → PC 版;否则移动版 const useDesktopLayout = () => window.innerWidth >= 769 || window.innerWidth > window.innerHeight; // 红果短剧广告屏蔽 if (isDuanjuDomain) { const adCss = '.mobile-guide-container-Cz30NR,.m-container-HjIuqt,.m-container-GjpJgU,.rp-hanger-EfEun1,.pc-hero-btn-download-CHc_Va,.pc-rp-wrap-lhRmt9,.pc-episode-item-7KBv6v,.pc-episode-toggle-AX4DVV{display:none!important;}'; const addStyle = () => { const s = document.createElement('style'); s.textContent = adCss; (document.head || document.documentElement).appendChild(s); }; if (document.head) addStyle(); else document.addEventListener('DOMContentLoaded', addStyle); } // 跨脚本通信契约:找不到"加载更多"按钮的重试计数(供播放层判断) try { window._noButtonRetryCount = 0; } catch (e) {} // ==================== 诊断日志系统 ==================== const __diag = (() => { const MAX = 800; const lines = []; const scriptName = '豆果影视'; // 版本号单一来源:优先取 GM 元数据中的实际安装版本,避免头注版本与内部版本不一致(ISS-001) const version = (() => { try { if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) { return String(GM_info.script.version); } } catch (e) {} return '7.4.18'; })(); const nowStr = () => { try { return new Date().toLocaleString('zh-CN', { hour12: false }); } catch (e) { return new Date().toString(); } }; const safeStr = (o) => { try { if (o instanceof Error) return o.name + ': ' + o.message + (o.stack ? '\n' + o.stack.split('\n').slice(0, 6).join('\n') : ''); if (typeof o === 'string') return o; return JSON.stringify(o); } catch (e) { return String(o); } }; const push = (level, msg) => { try { let s = safeStr(msg); if (s.length > 600) s = s.slice(0, 600) + '…'; lines.push('[' + nowStr() + '][' + level + '] ' + s); if (lines.length > MAX) lines.shift(); } catch (e) {} }; const wrapConsole = () => { ['log', 'info', 'warn', 'error', 'debug'].forEach((m) => { const orig = console[m]; if (typeof orig !== 'function') return; try { console[m] = function () { try { const args = Array.prototype.slice.call(arguments); if (m === 'error' || m === 'warn') { push(m.toUpperCase(), args.map((a) => typeof a === 'string' ? a : safeStr(a)).join(' ')); } else { // log/info 仅收集以 '[' 开头的结构化日志:先预判首个参数,避免对无关调用做 join 处理 const first = args[0]; if (typeof first === 'string' && first.charAt(0) === '[') { push(m.toUpperCase(), args.map((a) => typeof a === 'string' ? a : safeStr(a)).join(' ')); } } } catch (e) {} return orig.apply(console, arguments); }; } catch (e) {} }); }; const hookErrors = () => { window.addEventListener('error', (ev) => { try { // 资源加载失败(img/script/link 等):单独记录具体资源 URL const t = ev.target; if (t && t.tagName && !(ev.message)) { const src = t.src || t.currentSrc || t.href || ''; push('ERROR', '资源加载失败: <' + String(t.tagName).toLowerCase() + '> ' + (src || '(无URL)')); return; } let msg = ev.message || ''; const loc = ((ev.filename || '') + ':' + (ev.lineno || '') + ':' + (ev.colno || '')).replace(/^::/, ''); if (ev.error && ev.error.message) msg = ev.error.message; // 过滤无 message、无位置、无 error 对象的空错误事件(多为扩展/无关资源注入,无排障价值) if (!msg && !loc && !ev.error) return; let extra = ''; if (ev.error && ev.error.stack) { extra = '\n' + ev.error.stack.split('\n').slice(0, 5).join('\n'); } push('ERROR', 'window.onerror: ' + msg + ' @ ' + loc + extra); } catch (e2) {} }, true); window.addEventListener('unhandledrejection', (ev) => { const r = ev.reason; push('ERROR', 'unhandledrejection: ' + (r && r.message ? r.message : safeStr(r))); }); }; const buildReport = () => { const out = []; out.push('========== ' + scriptName + ' 诊断日志 =========='); out.push('版本: ' + version); out.push('生成时间: ' + nowStr()); out.push('站点: ' + CURRENT_SITE); out.push('页面: ' + location.href); out.push('UA: ' + navigator.userAgent); out.push('视口: ' + (window.innerWidth || '') + 'x' + (window.innerHeight || '')); try { out.push('自动连播: ' + GM_getValue(AUTO_NEXT_ENABLED_KEY, true)); out.push('主题: ' + getTheme()); const allKeys = GM_listValues(); const stats = {}; allKeys.forEach((k) => { const prefix = k.split('_')[0]; stats[prefix] = (stats[prefix] || 0) + 1; }); out.push('GM存储键统计: ' + safeStr(stats) + ' (共' + allKeys.length + '个键)'); } catch (e) { out.push('存储状态读取失败: ' + safeStr(e)); } out.push('-------- 日志记录 (' + lines.length + ' 条) --------'); for (let i = 0; i < lines.length; i++) out.push(lines[i]); return out.join('\n'); }; const download = () => { try { const text = buildReport(); const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const d = new Date(), pad = (n) => n < 10 ? '0' + n : '' + n; a.download = scriptName + '-诊断日志-' + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '-' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => { try { URL.revokeObjectURL(url); } catch (e2) {} }, 5000); return true; } catch (e) { try { prompt('无法自动下载日志,请复制以下内容:', buildReport()); } catch (e2) {} return false; } }; wrapConsole(); hookErrors(); return { push, download, buildReport, info: (m) => push('INFO', m), warn: (m) => push('WARN', m), error: (m) => push('ERROR', m) }; })(); __diag.info('脚本已加载,版本 ' + __diag.version); // ==================== 阶段一:document-start(红果根跳转)==================== let didRedirect = false; // 红果短剧主域名直接跳转到分类页 if (isDuanjuDomain && PATHNAME === '/' && !location.search) { location.replace('https://hongguoduanju.com/category?sort_type=1'); didRedirect = true; } // 重定向已触发则当前页面即将跳转,不再注入任何内容 if (didRedirect) return; // ==================== Artplayer 工具 ==================== // ISS-003:外部库缺失守卫。@require CDN 失败/被替换时不再整脚本崩溃: // $ 退化为标准 querySelector、isMobile 退化为 UA 嗅探;Artplayer 正常时解构结果与原来完全一致。 const ArtplayerReady = !!(typeof Artplayer !== 'undefined' && Artplayer && Artplayer.utils); const { query: $ = (sel, root) => (root || document).querySelector(sel), isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) } = (ArtplayerReady ? Artplayer.utils : {}); // ==================== 配置区 ==================== const ANTI_BLOCK_CONFIG = { maxConcurrent: 3, requestJitterMin: 100, requestJitterMax: 300, }; // 搜索早停(默认关):命中 ≥EARLY_EXIT_MIN_SUCCESS 个成功源后,未开始的源不再发请求。 // 默认关闭以保持搜索结果集与原版完全一致;如需省请求可置为 true。 const SEARCH_EARLY_EXIT_ENABLED = false; const EARLY_EXIT_MIN_SUCCESS = 3; const CACHE_TTL = 6 * 60 * 60 * 1000; // 成功搜索缓存 6 小时 const SEARCH_FAILURE_CACHE_TTL = 10 * 60 * 1000; // 失败搜索只短缓存 10 分钟:瞬时故障不长时间锁死重试 const MAX_CACHE_ITEMS_PER_MOVIE = 50; const MAX_HISTORY_ITEMS = 80; // 测速结果会话缓存:同页面生命周期内 10 分钟内复用,避免重复下载 ts 分片测速 const SPEED_TEST_CACHE_TTL = 10 * 60 * 1000; const speedTestSessionCache = new Map(); // sourceName -> { speed, time } const AUTO_NEXT_ENABLED_KEY = 'autoPlayNextEnabled'; const UNIFIED_HISTORY_KEY = 'unified_watch_history'; // 红果详情页开关:true=点海报进详情页,由详情页"一键搜索"触发搜索;false(默认)=点海报直接弹窗搜索片名 const HONGUO_DETAIL_KEY = 'hgDetailPageEnabled'; const PROGRESS_PREFIX = 'play_'; // play_{site}_{id} const TIP_SHOWN_PREFIX = 'tip_shown_'; // tip_shown_{uid},手机端一次性提示标记 const MIGRATION_KEY = 'v7_migration_done'; const SEARCH_HISTORY_KEY = 'search_keyword_history'; const THEME_KEY = 'app_theme'; // 'dark' | 'light' const MAX_SEARCH_HISTORY = 12; // 搜索关键词历史 const getSearchHistory = () => { try { const list = GM_getValue(SEARCH_HISTORY_KEY, []) || []; console.log('[阿豆影视] 读取搜索历史:', list.length, '条'); return list; } catch (e) { console.warn('[阿豆影视] 读取搜索历史失败:', e); return []; } }; const addSearchHistory = (keyword) => { if (!keyword) return; let list = getSearchHistory(); list = list.filter(k => k !== keyword); list.unshift(keyword); list = list.slice(0, MAX_SEARCH_HISTORY); try { GM_setValue(SEARCH_HISTORY_KEY, list); console.log('[阿豆影视] 保存搜索历史:', keyword, '共', list.length, '条'); } catch (e) { console.warn('[阿豆影视] 保存搜索历史失败:', e); } }; const clearSearchHistory = () => GM_setValue(SEARCH_HISTORY_KEY, []); // 清空搜索相关数据(历史关键词),弹窗"清除"共用 const clearSearchRelatedData = () => { let count = 0; if (GM_getValue(SEARCH_HISTORY_KEY, undefined) !== undefined) { GM_deleteValue(SEARCH_HISTORY_KEY); count++; } return count; }; // 主题管理 const getTheme = () => GM_getValue(THEME_KEY, 'light'); const setTheme = (theme) => { GM_setValue(THEME_KEY, theme); document.documentElement.setAttribute('data-adys-theme', theme); }; const initTheme = () => { document.documentElement.setAttribute('data-adys-theme', getTheme()); }; // 尽早初始化主题,避免闪烁 initTheme(); // ==================== 影片数据提取(站点感知)==================== let videoDataBase = null; function getVideoDataOnce() { if (videoDataBase) return videoDataBase; const data = { videoName: '未知', videoYear: '', videoFirstActor: '', posterUrl: '', site: CURRENT_SITE, id: 'unknown_' + Date.now(), subjectUrl: HREF, }; try { if (CURRENT_SITE === 'dj') { // 红果有两种详情页 URL 格式,都需要提取 series_id: // 1. /detail?series_id=7650505082215091224 → 直接从查询参数提取 // 2. /series/toushizhiyan1133208616 → 从路径末尾的数字提取 // 3. /player/7678593905394191422 → 从路径提取(新版详情/播放页) const currentHref = location.href; const m = currentHref.match(/series_id=(\d+)/); if (m) { data.id = m[1]; } else { const m2 = currentHref.match(/\/series\/[^/]*?(\d+)(?:[/?#]|$)/); if (m2) { data.id = m2[1]; } else { const m3 = currentHref.match(/\/player\/(\d+)/); if (m3) data.id = m3[1]; } } data.subjectUrl = currentHref; // 红果短剧标题提取:页面标题格式 "红果短剧 | XXX全集免费观看" const titleMatch = document.title.match(/^红果短剧\s*\|\s*(.*?)全集免费观看/); if (titleMatch && titleMatch[1]) data.videoName = titleMatch[1].trim(); else { // 移动端标题可能不同,尝试更多匹配模式 const titleMatch2 = document.title.match(/红果短剧\s*[-|]\s*(.+)/); const h1 = document.querySelector('h1, [class*="drama-title"], [class*="series-name"], [class*="title-text"]'); if (h1) { // 排除脚本自身注入的按钮文本(一键搜索/搜索中…等),避免标题污染搜索关键词与历史记录 const clone = h1.cloneNode(true); clone.querySelectorAll('[class*="adys-"], .liu-btn, .play-btn').forEach(n => n.remove()); data.videoName = (clone.innerText || h1.innerText).trim(); } else if (titleMatch2 && titleMatch2[1]) data.videoName = titleMatch2[1].replace(/全集免费观看$/, '').trim(); else data.videoName = document.title.replace(/^红果短剧\s*[-|]\s*/, '').replace(/全集免费观看$/, '').trim() || document.title.trim(); } // 红果短剧海报提取:优先从 _ROUTER_DATA SSR 数据获取(最可靠) // 策略0:从 window._ROUTER_DATA.loaderData 中查找 seriesDetail //(新版页面 loaderData 键为 player_(series_id)/page 等动态键,需遍历查找) try { const rd = window._ROUTER_DATA; if (rd && rd.loaderData) { let sd = null; if (rd.loaderData.detail_page && rd.loaderData.detail_page.seriesDetail) { sd = rd.loaderData.detail_page.seriesDetail; } else { for (const k in rd.loaderData) { const v = rd.loaderData[k]; if (v && typeof v === 'object' && v.seriesDetail) { sd = v.seriesDetail; break; } } } if (sd) { if (sd.series_cover) data.posterUrl = sd.series_cover; if (sd.series_name && data.videoName === '未知') data.videoName = sd.series_name; } } } catch(e) {} // 策略1:精确匹配主封面容器内的 img(排除 logo、头像、推荐列表封面) if (!data.posterUrl) { const mainPosterImg = document.querySelector('.pc-img-y1TjVL img, [class*="img-box"] > [class*="img-vB9HMs"] img, [class*="m-img"] img[src*="byteimg"], [class*="cover"] > img[src*="byteimg"]'); if (mainPosterImg && mainPosterImg.src) { data.posterUrl = mainPosterImg.src; } } // 策略2:匹配来自 byteimg.com 的大尺寸封面图(width >= 600),排除头像(200px)和 logo if (!data.posterUrl) { const byteImgs = document.querySelectorAll('img[src*="novel.byteimg.com"]'); let firstBigImg = ''; for (const img of byteImgs) { if (!img.src || !(img.naturalWidth >= 600 || img.width >= 600)) continue; // 记录第一张大图作兜底 if (!firstBigImg) firstBigImg = img.src; // 排除推荐列表封面(parentCls 包含 pc-img-Lpi5Zs) const parentCls = img.parentElement ? img.parentElement.className : ''; if (parentCls.indexOf('pc-img-Lpi5Zs') === -1 && parentCls.indexOf('m-img-MqFAvn') === -1) { firstBigImg = img.src; break; } } data.posterUrl = firstBigImg; } // 策略3:回退到 og:image if (!data.posterUrl) data.posterUrl = getOgImage(); } videoDataBase = data; return data; } catch (e) { tip(CURRENT_SITE === 'dj' ? '短剧数据读取失败' : '影片数据读取失败'); return null; } } const getUid = () => { const d = getVideoDataOnce(); return d ? `${d.site}_${d.id}` : 'unknown_' + Date.now(); }; // 播放进度按"片名"存取(而非 site_id),保证同一片名从红果详情/腾讯片库/搜索弹窗/历史 // 任意入口进入都续播同一集、同一源。 // 归一化:去噪声词 + 去标点空白,并单独提取结尾年份。 // 匹配规则:基础名一致才算同一部;年份都存在但不同(不同季/翻拍)→ 不合并,避免进度串台。 const titleParts = (s) => { if (!s) return { base: '', year: '' }; let r = removeNoiseWords(s); let year = ''; const ym = r.match(/[((\[【]?\s*((?:19|20)\d{2})\s*[))\]】]?\s*$/); if (ym) { year = ym[1]; r = r.slice(0, ym.index).trim(); } r = r.replace(/[\s\-—–::,,.。!!??'"“”‘’·、]/g, ''); return { base: r.toLowerCase(), year }; }; // 进度存储键:有年份带年份,无年份不带(带年份片名与腾讯"X"通过 matchTitle 桥接) const titleKey = (s) => { const { base, year } = titleParts(s); return base + (year ? '|' + year : ''); }; // 同名片匹配:基础名一致,且年份不冲突(两侧都有年份但不同 → 视为不同季/翻拍,不合并) const matchTitle = (a, b) => { const pa = titleParts(a), pb = titleParts(b); if (!pa.base || !pb.base || pa.base !== pb.base) return false; if (pa.year && pb.year && pa.year !== pb.year) return false; return true; }; const getProgressKey = () => { const d = getVideoDataOnce(); const name = d && d.videoName ? titleKey(d.videoName) : ''; if (name) return PROGRESS_PREFIX + 't_' + name; return PROGRESS_PREFIX + getUid(); }; const savePlayHistory = (data) => { try { const key = getProgressKey(); const newData = { ...GM_getValue(key, {}), ...data, updateTime: Date.now() }; GM_setValue(key, newData); updateUnifiedHistoryPlayData(newData); } catch (e) {} }; const getPlayHistory = () => { try { const local = GM_getValue(getProgressKey(), {}); // 跨入口:从统一历史条目按片名补回已存的进度(选集/选集源/时间点); // matchTitle 容忍带年份/不带年份的片名,但拒绝不同年份的翻拍与季 const d = getVideoDataOnce(); if (d && d.videoName) { const hist = getUnifiedHistory(); const hit = hist.find(it => it && matchTitle(it.movieName || '', d.videoName)); if (hit && hit.playHistory) return { ...hit.playHistory, ...local }; } return local; } catch (e) { return {}; } }; // ==================== 统一历史管理 ==================== // 删除被挤出历史条目的孤儿键(播放进度 + 一次性提示标记),避免 GM 存储长期堆积 const pruneOrphanHistoryKeys = (dropped, keptIds) => { if (!dropped || dropped.length === 0) return; dropped.forEach(item => { if (item && item.movieId && !keptIds.has(item.movieId)) { GM_deleteValue(PROGRESS_PREFIX + item.movieId); GM_deleteValue(TIP_SHOWN_PREFIX + item.movieId); } }); }; const getUnifiedHistory = () => { try { let history = GM_getValue(UNIFIED_HISTORY_KEY, []); if (!Array.isArray(history)) history = []; const trimmed = history.slice(0, MAX_HISTORY_ITEMS); if (trimmed.length !== history.length) { // 防御:旧版本残留的超限数据,读取时一并截断并清理孤儿键 const keptIds = new Set(trimmed.filter(item => item && item.movieId).map(item => item.movieId)); pruneOrphanHistoryKeys(history.slice(MAX_HISTORY_ITEMS), keptIds); saveUnifiedHistory(trimmed); } return trimmed; } catch (e) { return []; } }; const saveUnifiedHistory = (history) => { try { const trimmed = history.slice(0, MAX_HISTORY_ITEMS); GM_setValue(UNIFIED_HISTORY_KEY, trimmed); // 历史被自动截断(超出 MAX_HISTORY_ITEMS)时,被挤出条目的播放进度键与 // 一次性提示标记一并物理删除,避免孤儿键在 GM 存储中长期堆积 if (trimmed.length !== history.length) { const keptIds = new Set(trimmed.filter(item => item && item.movieId).map(item => item.movieId)); pruneOrphanHistoryKeys(history.slice(MAX_HISTORY_ITEMS), keptIds); } } catch (e) {} }; const addToUnifiedHistory = (uid, name, subjectUrl, playData = {}, poster = '', sourceData = null) => { if (!uid || !name) return; let history = getUnifiedHistory(); // 同一影视去重:同 movieId 或同片名(matchTitle:基础名一致且年份不冲突)只保留一条,命中则更新到最前。 // 不同年份的翻拍/季(如 1997 版 vs 2003 版)不合并,避免历史与进度串台。 const idx = history.findIndex(item => item.movieId === uid || matchTitle(item.movieName || '', name) ); // 优先使用传入的海报,否则从当前页面获取 let posterUrl = poster; if (!posterUrl) { const videoData = getVideoDataOnce(); posterUrl = (videoData && videoData.posterUrl) || ''; } const entry = { movieId: uid, movieName: name, subjectUrl, posterUrl, timestamp: Date.now(), playHistory: playData, // 存播放源:历史点击时直接从源唤起播放器,不再重新搜索 sourceData: sourceData && sourceData.sources && sourceData.sources.length > 0 ? { title: sourceData.title || name, poster: sourceData.poster || posterUrl, sources: sourceData.sources } : null }; if (idx !== -1) { const old = history[idx]; // 合并:保留旧条目更完整的海报/链接,更新播放进度与时间戳;源优先补新; // playHistory 合并而非整体替换,避免刚开播只写了 selectedSource 就抹掉旧的选集/时间点 history[idx] = { ...old, ...entry, posterUrl: old.posterUrl || posterUrl, playHistory: { ...old.playHistory, ...entry.playHistory }, timestamp: Date.now() }; if (!history[idx].sourceData) history[idx].sourceData = entry.sourceData; } else history.unshift(entry); history.sort((a, b) => b.timestamp - a.timestamp); saveUnifiedHistory(history); }; // 统一历史播放数据节流写入:进度小幅推进时跳过整数组读改写(每 30s 或跳变 ≥30s / duration 变化才写) const lastUnifiedPlayWriteMap = new Map(); // 规范化片名 -> {time, currentTime, duration} const updateUnifiedHistoryPlayData = (playData) => { const now = Date.now(); const d = getVideoDataOnce(); const curName = d && d.videoName ? d.videoName : ''; const last = curName ? lastUnifiedPlayWriteMap.get(curName) : null; if (last) { const dT = (playData.currentTime || 0) - last.currentTime; if (now - last.time < 30000 && Math.abs(dT) < 30 && playData.duration === last.duration) { return; // 明细进度键仍由调用方周期写入,此处仅跳过全历史数组的读改写 } } if (curName) lastUnifiedPlayWriteMap.set(curName, { time: now, currentTime: playData.currentTime || 0, duration: playData.duration }); let history = getUnifiedHistory(); const idx = curName ? history.findIndex(item => item && matchTitle(item.movieName || '', curName)) : -1; if (idx !== -1) { history[idx].playHistory = { ...history[idx].playHistory, ...playData, updateTime: Date.now() }; history[idx].timestamp = Date.now(); saveUnifiedHistory(history); } }; const deleteUnifiedHistoryItem = (uid) => { let history = getUnifiedHistory(); const entry = history.find(item => item.movieId == uid); const newHistory = history.filter(item => item.movieId != uid); if (newHistory.length === history.length) return false; saveUnifiedHistory(newHistory); GM_deleteValue(PROGRESS_PREFIX + uid); GM_deleteValue(TIP_SHOWN_PREFIX + uid); // 同时清理按片名存取的进度键与提示标记(带年份与不带年份两个可能键都清) if (entry && entry.movieName) { const parts = titleParts(entry.movieName); GM_deleteValue(PROGRESS_PREFIX + 't_' + parts.base); GM_deleteValue(TIP_SHOWN_PREFIX + 't_' + parts.base); if (parts.year) { GM_deleteValue(PROGRESS_PREFIX + 't_' + parts.base + '|' + parts.year); GM_deleteValue(TIP_SHOWN_PREFIX + 't_' + parts.base + '|' + parts.year); } } return true; }; const clearAllUnifiedHistory = () => { saveUnifiedHistory([]); const allKeys = GM_listValues(); allKeys.forEach(key => { if (key.startsWith(PROGRESS_PREFIX) || key.startsWith(TIP_SHOWN_PREFIX)) GM_deleteValue(key); }); tip('所有观看记录已清除'); }; // ==================== 旧数据迁移(一次性)==================== function migrateLegacyData() { if (GM_getValue(MIGRATION_KEY, false)) return; try { // 1) 统一历史:合并旧短剧历史 + 为旧条目补站点前缀 let merged = GM_getValue(UNIFIED_HISTORY_KEY, []); if (!Array.isArray(merged)) merged = []; const oldDj = GM_getValue('dj_unified_watch_history', []); if (Array.isArray(oldDj) && oldDj.length) { const exist = new Set(merged.map(i => i.movieId)); oldDj.forEach(item => { if (item.movieId) item.movieId = 'dj_' + item.movieId; if (item.movieId && !exist.has(item.movieId)) merged.push(item); }); merged.sort((a, b) => b.timestamp - a.timestamp); GM_deleteValue('dj_unified_watch_history'); } merged.forEach(item => { if (item.movieId && !/^(db|dj|im)_/.test(item.movieId)) { if (item.subjectUrl && (item.subjectUrl.indexOf('hongguoduanju') !== -1 || item.subjectUrl.indexOf('duanjubaike') !== -1)) item.movieId = 'dj_' + item.movieId; else if (item.subjectUrl && item.subjectUrl.indexOf('imoviebot') !== -1) item.movieId = 'im_' + item.movieId; else item.movieId = 'db_' + item.movieId; } }); saveUnifiedHistory(merged); // 2) 进度键迁移 + 旧搜索缓存清理 const allKeys = GM_listValues(); allKeys.forEach(key => { let m = key.match(/^douban_movie_(.+)$/); if (m) { const d = GM_getValue(key, null); if (d) { GM_setValue('play_db_' + m[1], d); } GM_deleteValue(key); return; } m = key.match(/^dj_drama_(.+)$/); if (m) { const d = GM_getValue(key, null); if (d) { GM_setValue('play_dj_' + m[1], d); } GM_deleteValue(key); return; } m = key.match(/^dj_search_(.+)$/); if (m) { GM_deleteValue(key); return; } // 旧短剧搜索缓存直接丢弃 m = key.match(/^search_(\d+)_/); if (m) { GM_deleteValue(key); return; } // 旧搜索缓存直接丢弃 }); // 3) 设置项合并 const djAuto = GM_getValue('dj_autoPlayNextEnabled', undefined); if (djAuto !== undefined && GM_getValue(AUTO_NEXT_ENABLED_KEY, undefined) === undefined) GM_setValue(AUTO_NEXT_ENABLED_KEY, djAuto); GM_deleteValue('dj_autoPlayNextEnabled'); GM_setValue(MIGRATION_KEY, true); } catch (e) { console.warn('[阿豆整合] 数据迁移失败', e); } } // 预加载播放数据(从搜索结果直接启动播放器时使用) let cmsPlayData = null; // ==================== CMS 搜索协调器(单飞 + 取消 + 订阅) ==================== // 目标:同一关键词同时只发一路 CMS 聚合搜索;弹窗/播放器/历史播放可订阅进度; // 订阅者全部退出后延迟取消底层请求,避免"弹窗关闭后仍打站"与"两路并发限流"。 const tip = (message, duration = 3000) => { try { const old = document.getElementById('custom-tip'); if (old) old.remove(); const el = document.createElement('div'); el.id = 'custom-tip'; el.className = 'adys-tip'; el.innerText = message; document.body.appendChild(el); setTimeout(() => el.remove(), duration); } catch (e) { alert(message); } }; const escapeHtml = (str) => { if (!str) return ''; return str.replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); }; const htmlToElement = (html) => { try { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstChild; } catch (e) { tip('界面生成失败,请刷新页面重试'); return null; } }; const get = (detail, retryCount = 0, timeoutMs = 10000, signal = null) => { const maxRetry = 2; const backoff = (r) => Math.pow(2, r) * 1000; return new Promise((resolve) => { if (signal && signal.aborted) { resolve({ r: false, errorMsg: '请求已取消' }); return; } const timer = setTimeout(() => resolve({ r: false, errorMsg: '请求超时' }), timeoutMs); const abortHandler = () => { clearTimeout(timer); resolve({ r: false, errorMsg: '请求已取消' }); }; if (signal) signal.addEventListener('abort', abortHandler, { once: true }); const cfg = { method: 'GET', timeout: timeoutMs, headers: { 'User-Agent': navigator.userAgent, 'Accept': '*/*', 'Cache-Control': 'no-cache' }, anonymous: true, followRedirects: true, onload: (r) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (r.status >= 400) { resolve({ r: false, errorMsg: `请求失败(${r.status})` }); return; } resolve({ r: true, content: r.response, status: r.status, errorMsg: '请求成功' }); }, onerror: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (retryCount < maxRetry) setTimeout(() => resolve(get(detail, retryCount + 1, timeoutMs, signal)), backoff(retryCount)); else resolve({ r: false, errorMsg: '网络异常' }); }, onabort: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); resolve({ r: false, errorMsg: '请求中止' }); }, ontimeout: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (retryCount < maxRetry) setTimeout(() => resolve(get(detail, retryCount + 1, timeoutMs, signal)), backoff(retryCount)); else resolve({ r: false, errorMsg: '请求超时' }); }, }; GM_xmlhttpRequest(Object.assign(cfg, detail)); }); }; const fetchWithTimeout = (url, timeout = 15000, signal = null) => { // get() 内部已通过 AbortSignal 处理超时/中止并返回 {r:false,errorMsg:...},这里不再叠加一层 Promise.race, // 避免外层先超时后内层 GM 请求仍在后台运行,造成连接浪费与站点限流。 // signal: 可选 AbortSignal,用于在搜索被取消(弹窗关闭/换词/播放让路)时中止未完成请求。 return get({ url: encodeURI(url), responseType: 'json', overrideMimeType: 'application/json' }, 0, timeout, signal) .catch(err => ({ r: false, errorMsg: err && err.message ? err.message : String(err) })); }; // ==================== 通用小工具(多处复用,避免重复实现)==================== // og:image 海报回退 const getOgImage = () => { const og = document.querySelector('meta[property="og:image"]'); return (og && og.content) ? og.content : ''; }; // 播放进度百分比(历史浮窗与播放器历史共用) const calcProgressPercent = (playHistory) => { if (!playHistory) return 0; const ct = playHistory.currentTime || 0; const dur = playHistory.duration || 0; return dur > 0 ? Math.min(Math.round((ct / dur) * 100), 100) : 0; }; // 清除全部观看历史并提示(菜单命令与设置面板共用) const doClearHistory = () => { clearAllUnifiedHistory(); tip('所有观看历史已清除'); }; // 重置"一键搜索"按钮外观(电脑版/手机版共用) const resetBtnEl = (btn) => { if (!btn) return; btn.innerText = '一键搜索'; btn.style.backgroundColor = ''; btn.disabled = false; btn.style.cursor = 'pointer'; }; // 防抖 MutationObserver 工厂:高频 DOM 变动合并为一次回调,避免同步遍历热点 const observeDebounced = (target, callback, { delay = 300, options } = {}) => { let timer = null; const mo = new MutationObserver(() => { if (timer) return; timer = setTimeout(() => { timer = null; try { callback(); } catch (e) {} }, delay); }); mo.observe(target, options || { childList: true, subtree: true }); return mo; }; // ==================== 匹配算法 ==================== const _noiseCache = new Map(); const removeNoiseWords = (str) => { if (!str) return ''; if (_noiseCache.has(str)) return _noiseCache.get(str); let r = str.replace(/[((【\[][^)\])】]*?(全集|高清|4K|8K|蓝光|修复版|国语|中字|BD|HD)[^)\])】]*?[))】\]]/gi, '').trim(); r = r.replace(/(全集|高清|4K|8K|蓝光|修复版|国语|中字|中文字幕|无水印|无删减|完整版|加长版|导演剪辑版|未删减|BD|HD|WEB-DL|WEBRip|BluRay|DVDRip)$/gi, '').trim(); if (_noiseCache.size > 500) _noiseCache.clear(); _noiseCache.set(str, r); return r; }; // ==================== 爱看搜索(双站:爱看1 / 爱看2) ==================== // base: 站点根地址,如 https://www1.ikanbot.com 或 https://ikanbot.eu.org // label: 结果标签,如 爱看1 / 爱看2 // ISS-004:默认 https(防中间人/混合内容),站点仅支持 http 时回退一次明文请求,功能不中断 const searchAikan = async (keyword, base, label) => { base = base || 'https://ikanbot.eu.org'; label = label || '爱看'; const encQ = encodeURIComponent(keyword); let resp = await get({ url: `${base}/search?q=${encQ}`, responseType: 'text', anonymous: false }, 2, 15000); if (!resp.r) { const httpBase = base.replace(/^https/, 'http'); const httpResp = await get({ url: `${httpBase}/search?q=${encQ}`, responseType: 'text', anonymous: false }, 1, 15000); if (httpResp.r) { resp = httpResp; base = httpBase; } } if (!resp.r) return []; const html = resp.content; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const items = doc.querySelectorAll('.media'); const results = []; items.forEach(item => { const linkEl = item.querySelector('.media-left a.cover-link'); const imgEl = item.querySelector('.media-left img'); const titleEl = item.querySelector('.media-body h5 a.title-text'); const labelEl = item.querySelector('.media-body .label'); const smallEls = item.querySelectorAll('.media-body .small'); if (!titleEl || !linkEl) return; const title = titleEl.textContent.trim(); const urlPath = linkEl.getAttribute('href'); if (!urlPath) return; const fullUrl = /^https?:\/\//i.test(urlPath) ? urlPath : `${base}${urlPath}`; let poster = imgEl ? (imgEl.getAttribute('data-src') || imgEl.getAttribute('src') || '') : ''; if (poster && poster.startsWith('data:image')) poster = ''; let lineCount = ''; if (labelEl) { const text = labelEl.textContent.trim(); const match = text.match(/\[(\d+)条线路可播放\]/); if (match) lineCount = match[1] + '条线路'; } let abstract = ''; if (smallEls.length > 0) { abstract = smallEls[0].textContent.trim(); if (smallEls.length > 1) { abstract += ' ' + smallEls[1].textContent.trim(); } } results.push({ title: title, url: fullUrl, poster: poster, abstract: abstract, source: 'aikan', siteLabel: label, lineCount: lineCount, }); }); return results.slice(0, 50); }; // 爱看:从播放页 HTML 中提取 current_id / e_token / mtype const parseAikanPlayPage = (html) => { const doc = new DOMParser().parseFromString(html, 'text/html'); const g = (id) => { const el = doc.getElementById(id); return el ? el.value : ''; }; return { id: g('current_id'), token: g('e_token'), mtype: g('mtype') || '1' }; }; // 爱看:按播放页 get_tks 算法计算接口 token(与站方 play_new.js 一致) const calcAikanTks = (videoId, eToken) => { if (!videoId || !eToken) return ''; const tail = videoId.substring(videoId.length - 4, videoId.length); let tok = eToken; const parts = []; for (let i = 0; i < tail.length; i++) { const digit = parseInt(tail[i], 10); if (isNaN(digit)) continue; const pos = digit % 3 + 1; parts.push(tok.substring(pos, pos + 8)); tok = tok.substring(pos + 8, tok.length); } return parts.join(''); }; // 爱看:把 /api/getResN 返回的线路列表构造为内置播放器 sources const buildAikanSources = (resJson, title) => { const list = (resJson && resJson.data && resJson.data.list) || []; const sources = []; list.forEach((item, idx) => { let resArr; try { resArr = typeof item.resData === 'string' ? JSON.parse(item.resData) : item.resData; } catch (e) { return; } if (!Array.isArray(resArr)) return; const playList = []; resArr.forEach(res => { const raw = (res && res.url) || ''; raw.split('#').forEach(seg => { const i = seg.indexOf('$'); if (i < 0) return; const name = seg.slice(0, i).trim(); const url = seg.slice(i + 1).trim(); if (url && /\.m3u8($|\?)/i.test(url)) { playList.push({ name: name || ('第' + (playList.length + 1) + '集'), url, speed: -1 }); } }); }); if (playList.length > 0) sources.push({ name: '线路' + (idx + 1), playList, vod_name: title, vod_pic: '' }); }); return sources; }; // 爱看搜索结果 → 拉取播放页线路 → 塞进内置播放器(上方播放器 + 下方选集/线路) async function playAikanInPlayer(r) { const playUrl = r.url; if (!playUrl) { tip('该结果无可播放地址'); return; } let origin = ''; try { origin = new URL(playUrl).origin; } catch (e) { tip('链接解析失败'); return; } const tipEl = showLoadingTip('正在获取爱看线路...', 0, 0, 0); try { // 1) 拉播放页 HTML,拿 current_id / e_token / mtype const pageResp = await get({ url: playUrl, responseType: 'text', anonymous: false }, 2, 15000); if (!pageResp.r || !pageResp.content) throw new Error('播放页加载失败'); const meta = parseAikanPlayPage(pageResp.content); if (!meta.id || !meta.token) throw new Error('未找到播放参数'); // 2) 计算 token 并请求线路接口 const tks = calcAikanTks(meta.id, meta.token); const apiUrl = `${origin}/api/getResN?videoId=${encodeURIComponent(meta.id)}&mtype=${encodeURIComponent(meta.mtype)}&token=${encodeURIComponent(tks)}`; const apiResp = await get({ url: apiUrl, responseType: 'text', anonymous: false, headers: { 'Referer': playUrl } }, 2, 15000); if (!apiResp.r || !apiResp.content) throw new Error('线路接口请求失败'); let resJson; try { resJson = JSON.parse(apiResp.content); } catch (e) { throw new Error('线路数据解析失败'); } if (resJson.state !== 1) throw new Error('线路获取失败(' + resJson.state + ')'); // 3) 构造 sources 并启动内置播放器 const sources = buildAikanSources(resJson, r.title); if (sources.length === 0) throw new Error('未解析到有效 m3u8 线路'); hideLoadingTip(); const uid = 'im_' + meta.id; addToUnifiedHistory(uid, r.title, playUrl, {}, r.poster || ''); cmsPlayData = { title: r.title, poster: r.poster || '', sources: sources, uid: uid }; try { initVue(); } catch (err) { tip('播放器启动失败: ' + err.message); } } catch (err) { hideLoadingTip(); tip(err.message || '加载失败'); } }; // ==================== 播放辅助 ==================== const playM3u8 = (video, url, art) => { try { if (Hls.isSupported()) { if (art.hls) art.hls.destroy(); const hls = new Hls({ maxBufferLength: 60, maxMaxBufferLength: 120, startLevel: -1, enableWorker: true, backBufferLength: 30 }); hls.loadSource(url); hls.attachMedia(video); art.hls = hls; art.on('destroy', () => hls.destroy()); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url; } else { art.notice.show = '不支持的播放格式: m3u8'; } } catch (e) { tip('视频播放初始化失败,请重试'); } }; const downloadtsList = async (url, signal = null) => { if (!url) return { r: false }; try { const baseUrl = new URL(url); const result = await get({ url: encodeURI(url) }, 0, 10000, signal); if (!result.r) return { r: false }; const content = result.content.trim().replace(/^\uFEFF/, ''); if (!content.includes('#EXTM3U')) return { r: false }; if (content.includes('#EXT-X-STREAM-INF')) { const lines = content.split('\n'); const list = []; let lastBw = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#EXT-X-STREAM-INF')) { const m = line.match(/BANDWIDTH=(\d+)/); lastBw = m ? parseInt(m[1]) : 0; } else if (line && !line.startsWith('#') && line.includes('.m3u8')) list.push({ url: new URL(line, baseUrl).href, bandwidth: lastBw }); } if (list.length > 0) { list.sort((a, b) => b.bandwidth - a.bandwidth); return await downloadtsList(list[0].url, signal); } } if (content.includes('.ts')) { const tsList = []; for (const item of content.split('\n')) { const t = item.trim(); if (/^[#\s]/.test(t) || t === '') continue; tsList.push(new URL(t, baseUrl).href); } return { r: true, content: tsList }; } return { r: false }; } catch (e) { return { r: false }; } }; const showLoadingTip = (text, successCount = 0, totalCount = 0, failedCount = 0) => { try { const old = $('#loading-tip'); if (old) old.remove(); let progressHtml = ''; if (totalCount > 0) { const completed = successCount + failedCount; const pct = Math.round((completed / totalCount) * 100); progressHtml = `
成功:${successCount} / 失败:${failedCount} / 共 ${totalCount}
`; } const el = htmlToElement(`
${text}${progressHtml}
`); document.body.appendChild(el); return el; } catch (e) { return null; } }; const hideLoadingTip = () => { try { const el = $('#loading-tip'); if (el) el.remove(); } catch (e) {} }; const resetPlayBtn = () => { // 重置电脑版一键搜索按钮 resetBtnEl(window.doubanPlayBtn); // 手机版浮动按钮组 const mobileGroup = document.getElementById('mobile-action-buttons'); if (mobileGroup && isMobile) { mobileGroup.style.display = 'flex'; resetBtnEl(mobileGroup.querySelector('.play-btn')); } }; const asyncPool = async (limit, array, iteratorFn) => { const ret = []; const executing = []; for (const item of array) { const p = Promise.resolve().then(() => iteratorFn(item, array)); ret.push(p); if (limit <= array.length) { const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); if (executing.length >= limit) { await Promise.race(executing); const jitter = Math.floor(Math.random() * (ANTI_BLOCK_CONFIG.requestJitterMax - ANTI_BLOCK_CONFIG.requestJitterMin + 1)) + ANTI_BLOCK_CONFIG.requestJitterMin; await new Promise(r => setTimeout(r, jitter)); } } } return Promise.all(ret); }; /* ==================== Part 2:界面、逻辑与初始化 ==================== */ // ==================== 1. CSS 样式(播放器界面)==================== GM_addStyle(` /* ================= 设计系统 · 主题令牌 ================= */ :root { /* 字阶体系(断崖式) */ --adys-fs-xs: 11px; --adys-fs-sm: 13px; --adys-fs-md: 15px; --adys-fs-lg: 18px; --adys-fs-xl: 22px; /* 圆角角色 */ --adys-r-pill: 999px; --adys-r-md: 8px; --adys-r-lg: 12px; --adys-r-xl: 16px; /* 动效 */ --adys-ease: cubic-bezier(.2,.8,.2,1); --adys-fast: .16s; --adys-med: .24s; } /* 主题变量 - 深色(默认);播放器层 .liu-playContainer 始终强制深色,不随浅色主题漂白 */ :root, [data-adys-theme="dark"], .liu-playContainer { --adys-bg: #1b1f24; --adys-bg-deep: #13161b; --adys-bg-elev: #222830; --adys-bg-hover: #2a323c; --adys-border: #333c47; --adys-text: #f5f7fa; --adys-text-dim: #9aa4b2; --adys-text-sub: #6c7786; --adys-accent: #00b96b; --adys-accent-hover: #00d67c; --adys-accent-light: #4dd794; --adys-accent-soft: rgba(0,185,107,.15); --adys-danger: #ff6b6b; --adys-warn: #ffb020; --adys-ring: rgba(0,185,107,.35); --adys-overlay: rgba(7,9,13,.72); } /* 主题变量 - 浅色 */ [data-adys-theme="light"] { --adys-bg: #f6f7f9; --adys-bg-deep: #ffffff; --adys-bg-elev: #ffffff; --adys-bg-hover: #edeff3; --adys-border: #e3e6eb; --adys-text: #1b1f24; --adys-text-dim: #5f6a78; --adys-text-sub: #9aa4b2; --adys-accent: #009a5a; --adys-accent-hover: #00b96b; --adys-accent-light: #17b574; --adys-accent-soft: rgba(0,154,90,.12); --adys-danger: #e5484d; --adys-warn: #d98b00; --adys-ring: rgba(0,154,90,.3); --adys-overlay: rgba(24,28,34,.42); } :root::-webkit-scrollbar, .liu-playContainer::-webkit-scrollbar, .series-contianer::-webkit-scrollbar { display: none; } .TalionNav{ z-index:10; } /* Artplayer 自定义控制按钮样式 */ .art-control.art-control-prev-episode, .art-control.art-control-next-episode { font-size: 20px !important; padding: 0 8px !important; cursor: pointer !important; opacity: 0.85; transition: opacity 0.2s; } .art-control.art-control-prev-episode:hover, .art-control.art-control-next-episode:hover { opacity: 1; } /* 确保 Artplayer 播放器层级高于底部搜索栏,防止控制栏被遮挡 */ .art-video-player { z-index: 1000000 !important; } /* 播放器打开时:隐藏手机浮动按钮(一键搜索/爱看影视),用 body 类 + !important 确保生效 */ body.adys-playing #mobile-action-buttons { display: none !important; } /* Artplayer 网页全屏时隐藏底部搜索栏,防止遮挡控制栏(CSS 方案,兼容 :has) */ body:has(.art-video-player.art-fullscreen-web) .search-container, body.adys-fullscreen-web .search-container { display: none !important; } /* 手机版:播放器容器内 artplayer-app 层级高于底部搜索栏 */ @media screen and (max-width: 768px) { .player-panel { position: relative; z-index: 1000001; } /* 确保 Artplayer 控制栏不被容器裁剪 */ .artplayer-app { overflow: visible !important; } .artplayer-app .art-video-player { overflow: hidden !important; border-radius: 6px; } } .speed-slow{ color:#9e9e9e; } .speed-fast{ color:var(--adys-accent-light); } .speed-testing{ color:#ff4d4d !important; font-weight:500; } .failed-status{ color:#F76965 !important; font-weight: 500; } .mannul{ margin:16px 0px 16px 14px; font-size:16px; display:flex; flex-wrap:wrap; } .authoralert{ font-size:16px; margin-left:14px; color:#F76965; } .liu-btn{ cursor:pointer; font-size:17px; padding: 12px 18px; border: 1px solid transparent; border-radius: 6px; max-height:60px; box-sizing: border-box; } /* 按钮容器:确保多个按钮在同一水平线 */ .adys-btn-group { display: inline-flex !important; align-items: center; gap: 12px; flex-wrap: wrap; } .play-btn { border-radius: 8px; cursor: pointer; font-weight: bolder; background-color:#e8f5e9; color:#1a1a1a !important; border: none; display: inline-flex !important; align-items: center; justify-content: center; } .play-btn:hover { background-color:#c8e6c9; } .play-btn:active{ background-color: #81c784; } .play-btn:disabled { background-color:#9e9e9e; cursor:not-allowed; } /* 电脑版红果详情:顶替原生"播放正片"按钮的样式 */ .dj-hero-play { height:49px !important; padding:10px 40px !important; font-size:18px !important; font-weight:500 !important; letter-spacing:1px; background:linear-gradient(105.2deg,#ff7e0d .62%,#ff9233) !important; color:#fff !important; border-radius:12px !important; width:auto !important; min-width:200px; flex:0 0 auto; margin:0 !important; } .dj-hero-play:hover { background:linear-gradient(105.2deg,#ff7e0d .62%,#ff9233) !important; filter:brightness(1.06); } .dj-hero-play:active { background:linear-gradient(105.2deg,#ff7e0d .62%,#ff9233) !important; filter:brightness(0.96); } /* 手机版独立按钮组 - 固定在右下角圆形按钮上方 */ .mobile-action-buttons { position: fixed; bottom: calc(30px + 48px + 12px + env(safe-area-inset-bottom, 0px)); right: 20px; z-index: 10000000; display: flex; gap: 12px; flex-direction: row; justify-content: flex-end; } .mobile-action-buttons .play-btn { margin: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } .liu-closePlayer{ position: absolute; top: 10px; right: 10px; z-index: 99999; border-radius: 50%; background-color: rgba(0,0,0,0.7) !important; color: #ffffff !important; width: 36px; height: 36px; line-height: 36px; padding: 0; margin: 0; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0,0,0,0.5); transition: all 0.2s ease; } .liu-closePlayer:hover{ background-color: rgba(0,112,17,0.9) !important; transform: scale(1.05); } .source-selector{ width: 130px; height: 56px; padding: 6px 8px; margin: 0 10px 10px 0; border-radius: var(--adys-r-lg); flex-shrink: 0; background-color: var(--adys-bg-deep) !important; color: var(--adys-text-dim) !important; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; line-height: 1.2; box-sizing: border-box; border: 2px solid transparent; position: relative; transition: border-color var(--adys-fast) var(--adys-ease), background-color var(--adys-fast) var(--adys-ease), transform var(--adys-fast) var(--adys-ease); } .source-selector:hover:not(:disabled) { background-color: var(--adys-bg-hover) !important; transform: translateY(-1px); } .source-selector.selected { border-color: var(--adys-accent); background-color: var(--adys-accent-soft) !important; } .source-selector.selected::after { content: '✓'; position: absolute; top: 4px; right: 8px; color: var(--adys-accent-light); font-size: 13px; font-weight: 700; line-height: 1; } .source-name { font-size: 14px; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .source-status { font-size: 11px; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; } .source-status.speed-fast { color: var(--adys-accent-light); } .source-status.speed-slow { color: #9e9e9e; } .source-status.speed-testing { color: #ff4d4d !important; } .pc-source-list { display: flex; flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; padding: 5px 0; margin: 0; scroll-behavior: smooth; width: 100%; box-sizing: border-box; } .pc-source-list::-webkit-scrollbar { height: 4px; } .pc-source-list::-webkit-scrollbar-thumb { background-color: var(--adys-accent); border-radius: 4px; } .mobile-only .sourceButtonList { display: grid; grid-template-columns: repeat(3, 1fr); grid-column-gap: 10px; grid-row-gap: 10px; padding:0 16px; box-sizing: border-box; overflow-y: auto; max-height: 200px; } .mobile-only .sourceButtonList .source-selector { width: 100%; margin: 0; height: 48px; padding: 4px 4px; } .mobile-only .sourceButtonList .source-selector .source-name { font-size: 12px; } .mobile-only .sourceButtonList .source-selector .source-status { font-size: 10px; } .series-selector{ background-color: var(--adys-bg-elev) !important; border-radius: var(--adys-r-md); color: var(--adys-text-dim) !important; font-size: var(--adys-fs-sm); padding: 10px 8px; box-sizing: border-box; border: 2px solid var(--adys-border); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: border-color var(--adys-fast) var(--adys-ease), background-color var(--adys-fast) var(--adys-ease), color var(--adys-fast) var(--adys-ease); } .series-selector:hover{ background-color: var(--adys-accent-soft) !important; box-sizing: border-box; color: var(--adys-text) !important; } .playing{ border:2px solid var(--adys-accent); background-color: var(--adys-accent-soft) !important; color: var(--adys-accent-light) !important; box-sizing: border-box; } .love-support{ color:var(--adys-text-dim); background-color:transparent; margin-right:32px; } .liu-playContainer a:visited{ color:var(--adys-text-dim); } .liu-playContainer a:hover{ font-weight:bold; color:#A8DB39; background:none; } .series-contianer{ display:grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); grid-auto-rows:44px; grid-column-gap:10px; grid-row-gap:10px; margin-top:16px; height:400px; overflow-y:auto; scroll-behavior: smooth; position: relative; box-sizing: border-box; padding-right: 4px; } .series-wrapper { position: relative; } .series-wrapper.has-more::after { content: "▼"; position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%); color: var(--adys-text-dim); font-size: 14px; z-index: 2; animation: bounce 2s ease-in-out 8; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; } .series-wrapper.has-more::before { content: ""; position: absolute; bottom: 0; left: 0; right: 0; height: 30px; background: linear-gradient(transparent, var(--adys-bg-deep)); z-index: 1; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; } .series-wrapper.has-more::after, .series-wrapper.has-more::before { opacity: 1; } @keyframes bounce { 0%, 100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(4px); } } @keyframes scrollTip { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } .liu-playContainer{ width: 100vw !important; width: 100dvw !important; height: 100vh !important; height: 100dvh !important; background-color:var(--adys-bg); position: fixed !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; z-index: 999999 !important; overflow: auto; padding-bottom: 60px; box-sizing: border-box; padding-top: 40px; margin: 0 !important; font-size: 16px; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .liu-playContainer * { box-sizing: border-box; } .seletor-title{ height:50px; line-height:50px; background-color: var(--adys-bg-deep); color:var(--adys-text); font-size: var(--adys-fs-lg); padding:0 16px; border-radius:6px 6px 0 0; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; } .title-text { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .title-right-actions { display: flex; align-items: center; gap: 12px; flex-shrink: 0; } .history-btn { background: rgba(74, 161, 80, 0.15) !important; border: 1.5px solid var(--adys-accent-light) !important; color: var(--adys-accent-light) !important; padding: 4px 14px !important; font-size: var(--adys-fs-lg) !important; line-height: 1.2 !important; border-radius: 24px !important; transition: all 0.2s ease; max-height: 38px; display: inline-flex; align-items: center; font-weight: 500; } .history-btn:hover { background: var(--adys-accent-light) !important; color: #fff !important; transform: scale(1.02); } .speed-test-btn { background-color: var(--adys-accent) !important; color: white !important; margin: 0; width: 100%; max-width: 200px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; gap: 8px; border-radius: var(--adys-r-md); white-space: nowrap; /* 文字在"开始测速/测速中 X/Y"间切换时不换行,高度恒定,避免上下边距抖动收缩 */ min-height: 44px; box-sizing: border-box; } .speed-test-btn:disabled { background-color: #9e9e9e !important; cursor: not-allowed; } .speed-test-btn:hover:not(:disabled) { background-color: var(--adys-accent-hover) !important; } /* 测速中:仅保留文字进度反馈(“测速中 X/Y”),不叠加额外动画元素 */ .control-btn-group { display: flex; align-items: center; gap: 0; margin-bottom:12.8px; } .mobile-btn-group { display: flex; justify-content: center; align-items: center; gap:16px; padding:0 16px; } .pc-only { display: none; } .mobile-only { display: block; } .search-container { position: fixed; bottom: 0; left: 0; width: 100%; /* 播放器内固定深色:不随浅色主题漂白,保证与深色播放器一体 */ background-color: #14171c; padding:16px; box-sizing: border-box; box-shadow: 0 -2px 10px rgba(0,0,0,0.5); display: flex; gap:8px; z-index: 999999; } .search-input { flex: 1; padding:12.8px 16px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.14); background-color: rgba(255,255,255,0.07); color: #e8eaed; font-size:16px; outline: none; } .search-input::placeholder { color: rgba(255,255,255,0.42); } .search-input:focus { border-color: var(--adys-accent); } .search-btn { background-color: var(--adys-accent); color: white; white-space: nowrap; flex-shrink: 0; } .search-btn:hover:not(:disabled) { background-color: var(--adys-accent-hover); } .search-btn:disabled { background-color: #9e9e9e; cursor: not-allowed; } .loading-tip { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--adys-bg-deep); color: var(--adys-text); padding:19.2px 32px; border-radius: 12px; z-index: 1000000; font-size:16px; text-align: center; line-height: 1.5; max-width: 80vw; border: 1px solid var(--adys-border); box-shadow: 0 8px 30px rgba(0,0,0,0.5); } .loading-tip .rate-success { color: var(--adys-accent-light); font-weight: bold; } .loading-tip .rate-failed { color: #F76965; font-weight: bold; } .loading-tip .loading-progress-wrap { margin-top: 10px; width: 200px; height: 6px; background: var(--adys-border); border-radius: 3px; overflow: hidden; } .loading-tip .loading-progress-bar { height: 100%; background: linear-gradient(90deg, var(--adys-accent), var(--adys-accent-light)); border-radius: 3px; transition: width 0.4s ease; width: 0%; } .loading-tip .loading-stats { margin-top: 8px; font-size: var(--adys-fs-sm); color: var(--adys-text-dim); } /* Toast 提示 */ .adys-tip { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--adys-bg-deep); color: var(--adys-text); padding: 12px 24px; border-radius: var(--adys-r-lg); z-index: 9999999; font-size: var(--adys-fs-md); line-height: 1.5; text-align: center; max-width: 80vw; pointer-events: none; box-shadow: 0 8px 30px rgba(0,0,0,.35); border: 1px solid var(--adys-border); animation: adysPopIn var(--adys-fast) var(--adys-ease); } /* 弹窗统一动效 */ @keyframes adysFadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes adysPopIn { from { opacity: 0; transform: translateY(14px) scale(.965); } to { opacity: 1; transform: translateY(0) scale(1); } } .history-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--adys-overlay); z-index: 10000000; display: flex; align-items: center; justify-content: center; animation: adysFadeIn var(--adys-med) var(--adys-ease); } .history-modal-content { background: var(--adys-bg); border-radius: var(--adys-r-xl); width: 90%; max-width: 480px; max-height: 70vh; display: flex; flex-direction: column; box-shadow: 0 12px 40px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.15); border: 1px solid var(--adys-border); animation: adysPopIn var(--adys-med) var(--adys-ease); font-size: 16px; line-height: 1.5; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .history-modal-content * { box-sizing: border-box; } .history-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--adys-border); font-size: var(--adys-fs-lg); font-weight: 500; color: var(--adys-text); } .history-modal-close { background: none; border: none; color: var(--adys-text-dim); font-size: 28px; cursor: pointer; line-height: 1; padding: 0; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .history-modal-close:hover { background: var(--adys-bg-hover); color: var(--adys-text); } .history-modal-body { flex: 1; overflow-y: auto; padding: 8px 0; } @media (min-width: 1025px) { .history-modal-body { scrollbar-width: thin; scrollbar-color: var(--adys-accent-light) var(--adys-bg-hover); } .history-modal-body::-webkit-scrollbar { width: 6px; height: 6px; } .history-modal-body::-webkit-scrollbar-track { background: var(--adys-bg-hover); border-radius: 4px; } .history-modal-body::-webkit-scrollbar-thumb { background: var(--adys-accent-light); border-radius: 4px; } .history-modal-body::-webkit-scrollbar-thumb:hover { background: var(--adys-accent-hover); } } @media (max-width: 1024px) { .history-modal-body { scrollbar-width: none; -ms-overflow-style: none; } .history-modal-body::-webkit-scrollbar { display: none; } } .history-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; border-bottom: 1px solid var(--adys-bg-hover); transition: background 0.2s; gap: 12px; } .history-item:hover { background: var(--adys-bg-hover); } .history-item-poster { width: 48px; height: 60px; border-radius: 6px; object-fit: cover; flex-shrink: 0; background: var(--adys-bg-hover); } .history-item-poster-placeholder { width: 48px; height: 60px; border-radius: 6px; flex-shrink: 0; background: var(--adys-bg-hover); display: flex; align-items: center; justify-content: center; font-size: var(--adys-fs-xl); color: var(--adys-text-dim); } .hist-ph-icon { width: 26px; height: 26px; } .history-item-info { flex: 1; cursor: pointer; overflow: hidden; } .history-item-name { font-size: var(--adys-fs-md); color: var(--adys-text); margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .history-item-meta { font-size: var(--adys-fs-xs); color: var(--adys-text-dim); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .history-item-progress { width: 60px; height: 4px; background: var(--adys-bg-hover); border-radius: 2px; overflow: hidden; display: inline-block; } .history-item-progress-bar { height: 100%; background: var(--adys-accent-light); border-radius: 2px; transition: width 0.3s; } .history-item-episode { color: var(--adys-accent-light); font-size: var(--adys-fs-xs); } .history-item-time { font-size: var(--adys-fs-xs); color: var(--adys-text-dim); } .history-item-delete { background: none; border: none; color: var(--adys-text-dim); cursor: pointer; padding: 8px; border-radius: 50%; transition: all var(--adys-fast) var(--adys-ease); opacity: 0.65; display: inline-flex; align-items: center; justify-content: center; } .history-item-delete .hist-del-icon { width: 18px; height: 18px; } .history-item-delete:hover { background: var(--adys-danger); color: #fff; opacity: 1; } .history-modal-footer { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-top: 1px solid var(--adys-border); font-size:12px; color: var(--adys-text-dim); } .history-tip { font-size: var(--adys-fs-xs); } .history-clear-all { background: transparent; border: 1px solid var(--adys-danger); color: var(--adys-danger); border-radius: 16px; padding: 4px 12px; font-size:12px; cursor: pointer; transition: all 0.2s; } .history-clear-all:hover { background: var(--adys-danger); color: #fff; } .history-empty { text-align: center; padding: 40px 20px; color: var(--adys-text-dim); } /* 全局按钮容器 - 按钮水平排列 */ #global-buttons-container { position: fixed; bottom: 20px; right: 20px; display: flex; gap: 12px; z-index: 10000000 !important; font-size: 16px; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #global-buttons-container * { box-sizing: border-box; } .global-history-btn { width: 56px; height: 56px; background-color: var(--adys-accent); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28px; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.3); transition: all 0.2s ease; border: none; } .global-history-btn:hover { background-color: var(--adys-accent-hover); transform: scale(1.05); } .global-history-btn svg { width: 26px; height: 26px; display: block; } /* 当前页面匹配的按钮高亮 */ .global-btn-active { background-color: #ffac2c !important; color: #1a1a1a !important; border: 2px solid #ff8c00 !important; box-shadow: 0 0 8px rgba(255, 172, 44, 0.6); } @media (max-width: 768px) { #global-buttons-container { bottom: calc(30px + env(safe-area-inset-bottom, 0px)); right: 16px; gap: 8px; } .global-history-btn { width: 48px; height: 48px; font-size: 20px; } .global-history-btn svg { width: 22px; height: 22px; } /* 手机版按钮组位置相应调整 */ .mobile-action-buttons { bottom: calc(30px + 48px + 12px + env(safe-area-inset-bottom, 0px)); right: 16px; } /* 移动端扇形菜单... */ #global-buttons-container.mobile-fab-mode { display: block; } #global-buttons-container.mobile-fab-mode .global-fab-item { position: absolute; bottom: 0; right: 0; opacity: 0; pointer-events: none; transform: scale(0.5); transition: opacity 0.25s ease, transform 0.25s ease; z-index: 1; } #global-buttons-container.mobile-fab-mode.expanded .global-fab-item { opacity: 1; pointer-events: auto; transform: scale(1); } #global-buttons-container.mobile-fab-mode .global-fab-main { position: relative; z-index: 2; transition: transform 0.3s ease; } #global-buttons-container.mobile-fab-mode.expanded .global-fab-main { transform: rotate(135deg); } /* 展开时子按钮显示文字标签 */ #global-buttons-container.mobile-fab-mode .global-fab-item::after { content: attr(data-label); position: absolute; right: 58px; top: 50%; transform: translateY(-50%); background: rgba(0, 0, 0, 0.8); color: #fff; font-size: 12px; padding: 3px 10px; border-radius: 4px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s ease 0.1s; } #global-buttons-container.mobile-fab-mode.expanded .global-fab-item::after { opacity: 1; } } /* ========== 红果顶栏「热榜」入口 ========== */ a.hg-rank-entry { flex: 0 0 auto; color: #ff9500 !important; font-size: 17px; font-weight: 600; padding: 4px 10px; margin-left: 6px; white-space: nowrap; text-decoration: none; } /* 搜索浮窗样式 */ .search-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--adys-overlay); z-index: 10000001; display: flex; align-items: center; justify-content: center; } .search-modal-container { background: var(--adys-bg); border-radius: 16px; width: 90%; max-width: 500px; max-height: 92vh; padding:24px; box-shadow: 0 8px 30px rgba(0,0,0,0.5); border: 1px solid var(--adys-border); display: flex; flex-direction: column; gap:12.8px; overflow: hidden; font-size: 16px; line-height: 1.5; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .search-modal-container * { box-sizing: border-box; } .search-modal-container .search-input-row { display: flex; flex-direction: row; align-items: center; gap: 8px; } .search-modal-container .search-btn { flex-shrink: 0; min-width: 80px; } .search-modal-container .search-input { flex: 1; min-width: 0; /* 关键:允许输入框收缩,按钮文字变长为"搜索中…"时不把按钮顶出弹窗右边界 */ background: var(--adys-bg-deep); border: 1px solid var(--adys-border); color: var(--adys-text); font-size:16px; padding:12.8px 16px; border-radius: 8px; outline: none; } .search-modal-container .search-input:focus { border-color: var(--adys-accent); } .search-modal-container .search-btn { background-color: var(--adys-accent); color: white; border: none; padding:12.8px 24px; border-radius: 8px; font-size:16px; cursor: pointer; transition: background 0.2s; white-space: nowrap; } .search-modal-container .search-btn:hover { background-color: var(--adys-accent-hover); } .search-history-section { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } .search-history-label { font-size: var(--adys-fs-sm); color: var(--adys-text-dim); flex-shrink: 0; } .search-history-tag { background: var(--adys-bg-hover); border: 1px solid var(--adys-border); color: var(--adys-text); padding: 4px 12px; border-radius: 16px; font-size:13.12px; cursor: pointer; transition: all 0.2s; white-space: nowrap; } .search-history-tag:hover { background: var(--adys-accent); color: #fff; border-color: var(--adys-accent); } .search-history-clear { background: none; border: none; color: var(--adys-danger); font-size:12px; cursor: pointer; margin-left: auto; opacity: 0.7; } .search-history-clear:hover { opacity: 1; } /* 搜索标签栏 - 高特异性选择器,防止宿主页CSS渗透 */ .search-modal-container .search-tab-bar { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: flex-end !important; gap: 6px !important; border-bottom: 2px solid var(--adys-border) !important; padding: 4px 0 6px !important; overflow-x: auto !important; overflow-y: hidden !important; -webkit-overflow-scrolling: touch; width: 100% !important; min-width: 0 !important; position: relative !important; z-index: 2 !important; scrollbar-width: none !important; background: var(--adys-bg) !important; } .search-modal-container .search-tab-bar::-webkit-scrollbar { display: none !important; } .search-modal-container .search-tab-bar .search-tab { background: rgba(255,255,255,0.08) !important; border: none !important; border-bottom: 2px solid transparent !important; color: #ffffff !important; font-size: 13px !important; line-height: 1.35 !important; padding: 8px 10px !important; cursor: pointer !important; margin-bottom: -2px !important; transition: all 0.2s; font-weight: 600 !important; white-space: nowrap !important; flex-shrink: 0 !important; min-width: max-content !important; min-height: 34px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; opacity: 1 !important; float: none !important; position: relative !important; clear: none !important; text-shadow: 0 1px 2px rgba(0,0,0,0.35) !important; border-radius: 6px 6px 0 0 !important; } .search-modal-container .search-tab-bar .search-tab:hover { background: rgba(255,255,255,0.14) !important; color: #ffffff !important; } .search-modal-container .search-tab-bar .search-tab.active { background: rgba(74,161,80,0.18) !important; color: #6ee36e !important; border-bottom-color: #6ee36e !important; opacity: 1 !important; font-weight: 700 !important; } .search-modal-container .search-tab-bar .search-tab-count { font-size:12px !important; opacity: 0.85 !important; display: inline !important; color: inherit !important; } /* 浅色主题下的标签颜色 */ [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab { background: rgba(0,0,0,0.05) !important; color: #1c1c1e !important; text-shadow: none !important; } [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab:hover { background: rgba(0,0,0,0.09) !important; color: #1c1c1e !important; } [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.active { background: rgba(0,112,17,0.1) !important; color: var(--adys-accent) !important; border-bottom-color: var(--adys-accent) !important; } /* 搜索标签加载中状态(灰色不可点击) */ .search-modal-container .search-tab-bar .search-tab.loading { color: var(--adys-text-dim) !important; opacity: 0.85 !important; cursor: not-allowed !important; pointer-events: none !important; } .search-modal-container .search-tab-bar .search-tab.loading .search-tab-count { color: #6ee36e !important; opacity: 1 !important; font-weight: 700 !important; } [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.loading { color: #8e8e93 !important; } [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.loading .search-tab-count { color: var(--adys-accent) !important; } .search-modal-container .search-tab-bar .search-tab.loading .search-tab-count::after { content: ''; } .search-modal-container .search-tab-bar .search-tab.loaded-failed { opacity: 0.7 !important; } .search-modal-container .search-tab-bar .search-tab.loaded-failed .search-tab-count::after { content: ' (无结果)'; font-size: 11px; } /* 搜索结果区域 */ .search-results-section { flex: 1 1 0; min-height: 45vh; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; } .search-results-loading { text-align: center; padding:32px 16px; color: var(--adys-text-dim); font-size: var(--adys-fs-sm); } .search-results-loading .spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid var(--adys-border); border-top-color: var(--adys-accent); border-radius: 50%; animation: adys-spin 0.8s linear infinite; margin-bottom: 8px; } @keyframes adys-spin { to { transform: rotate(360deg); } } .search-result-card { display: flex; align-items: center; gap: 12px; padding: 10px; border-radius: 10px; background: var(--adys-bg-deep); border: 1px solid var(--adys-border); cursor: pointer; transition: all 0.2s; } .search-result-card:hover { border-color: var(--adys-accent); transform: translateX(4px); box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .search-result-card img { width: 48px; height: 64px; object-fit: cover; border-radius: 6px; flex-shrink: 0; background: var(--adys-bg-hover); } .search-result-card .result-placeholder { width: 48px; height: 64px; border-radius: 6px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: var(--adys-bg-hover); font-size: var(--adys-fs-lg); } .search-result-card .result-info { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: center; gap: 10px; flex-wrap: wrap; } .search-result-card .result-title { font-size: var(--adys-fs-md); color: var(--adys-text); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 0; max-width: 60%; } .search-result-card .result-meta { font-size:12px; color: var(--adys-text-dim); margin-top: 0; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .search-result-card .result-source { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size:10.88px; font-weight: 500; } .result-source.src-dj { background: rgba(255, 172, 44, 0.15); color: #ff8c00; } .result-source.src-db { background: rgba(0, 152, 26, 0.15); color: var(--adys-accent); } .result-source.src-cms { background: rgba(74, 161, 80, 0.15); color: var(--adys-accent-light); margin-left: 4px; } .result-source.src-aikan { background: rgba(255, 107, 107, 0.15); color: #ff6b6b; } .result-source.src-hg { background: rgba(255, 77, 79, 0.15); color: #ff4d4f; } .result-abstract { color: var(--adys-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .result-play-btn { flex-shrink: 0; margin-left: auto; padding: 6px 14px; border-radius: 8px; border: none; cursor: pointer; font-size: 13px; font-weight: 600; background: var(--adys-accent-light); color: #fff !important; transition: all 0.2s; white-space: nowrap; align-self: center; } .result-play-btn:hover { background: #3d8c43; transform: scale(1.05); } .result-play-btn:active { background: #2d6e33; } .search-results-empty { text-align: center; padding:24px 16px; color: var(--adys-text-dim); font-size: var(--adys-fs-sm); } /* ==================== 设置面板样式 ==================== */ .settings-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--adys-overlay); z-index: 10000001; display: flex; align-items: center; justify-content: center; animation: adysFadeIn var(--adys-med) var(--adys-ease); } .settings-modal-container { background: var(--adys-bg); border-radius: var(--adys-r-xl); width: 90%; max-width: 520px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 12px 40px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.15); border: 1px solid var(--adys-border); animation: adysPopIn var(--adys-med) var(--adys-ease); font-size: 16px; line-height: 1.5; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .settings-modal-container * { box-sizing: border-box; } .settings-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--adys-border); font-size: var(--adys-fs-lg); font-weight: 600; color: var(--adys-text); flex-shrink: 0; } .settings-header-title { display: inline-flex; align-items: center; } .settings-gear-icon { width: 20px; height: 20px; margin-right: 8px; color: var(--adys-text-dim); } .settings-modal-body { flex: 1; overflow-y: auto; padding: 12px 20px; } .settings-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 0; border-bottom: 1px solid var(--adys-bg-hover); } .settings-item:last-child { border-bottom: none; } .settings-item-info { flex: 1; overflow: hidden; } .settings-item-title { font-size:16px; color: var(--adys-text); font-weight: 500; } .settings-item-desc { font-size:12px; color: var(--adys-text-dim); margin-top: 3px; line-height: 1.4; } .settings-toggle { position: relative; width: 46px; height: 26px; border-radius: 13px; background: #444; border: none; cursor: pointer; transition: background 0.2s ease; flex-shrink: 0; padding: 0; } .settings-toggle.active { background: var(--adys-accent); } .settings-toggle::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; transition: transform 0.2s ease; } .settings-toggle.active::after { transform: translateX(20px); } .settings-action-btn { background: transparent; border: 1px solid var(--adys-accent-light); color: var(--adys-accent-light); border-radius: 20px; padding: 6px 16px; font-size: var(--adys-fs-sm); cursor: pointer; transition: all 0.2s; white-space: nowrap; flex-shrink: 0; } .settings-action-btn:hover { background: var(--adys-accent-light); color: #fff; } .settings-action-btn.danger { border-color: var(--adys-danger); color: var(--adys-danger); } .settings-action-btn.danger:hover { background: var(--adys-danger); color: #fff; } .settings-modal-footer { padding: 12px 20px; border-top: 1px solid var(--adys-border); text-align: center; flex-shrink: 0; } .settings-close-btn { background: var(--adys-accent); color: white; border: none; padding: 8px 32px; border-radius: 20px; font-size:16px; cursor: pointer; transition: background 0.2s; } .settings-close-btn:hover { background: var(--adys-accent-hover); } /* 播放器内自动连播切换按钮 */ .autoplay-toggle-btn { background: transparent !important; border: none !important; color: var(--adys-text) !important; padding: 4px 8px !important; font-size: var(--adys-fs-sm) !important; line-height: 1.2 !important; border-radius: var(--adys-r-pill) !important; transition: all var(--adys-fast) var(--adys-ease); max-height: 38px; display: inline-flex; align-items: center; gap: 8px; font-weight: 500; cursor: pointer; } .autoplay-toggle-btn .toggle-track { position: relative; width: 40px; height: 22px; border-radius: var(--adys-r-pill); background: var(--adys-bg-hover); border: 1px solid var(--adys-border); transition: background var(--adys-med) var(--adys-ease), border-color var(--adys-med) var(--adys-ease); flex-shrink: 0; box-sizing: border-box; } .autoplay-toggle-btn .toggle-thumb { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.35); transition: transform var(--adys-med) var(--adys-ease); } .autoplay-toggle-btn .toggle-label { color: var(--adys-text-dim); font-size: var(--adys-fs-sm); transition: color var(--adys-fast) var(--adys-ease); } .autoplay-toggle-btn:not(.off) .toggle-track { background: var(--adys-accent); border-color: var(--adys-accent); } .autoplay-toggle-btn:not(.off) .toggle-thumb { transform: translateX(18px); } .autoplay-toggle-btn:not(.off) .toggle-label { color: var(--adys-accent-light); } .autoplay-toggle-btn:hover { transform: scale(1.03); } @media (max-width: 640px) { .search-modal-container { max-height: 95vh !important; width: 95% !important; padding: 16px !important; gap: 10px !important; border-radius: 12px !important; } .search-modal-container .search-results-section { min-height: 55vh !important; } .search-modal-container .search-tab-bar .search-tab { font-size: 12px !important; padding: 7px 4px !important; min-height: 32px !important; flex: 1 1 0 !important; min-width: 0 !important; overflow: hidden !important; text-overflow: ellipsis !important; } .search-modal-container .search-tab-bar { gap: 4px !important; padding: 2px 0 6px !important; } .search-modal-container .search-tab-bar .search-tab-count { font-size: 11px !important; } } @media screen and (min-width: 1025px) { .pc-only { display: block; } .mobile-only { display: none; } .search-container { display: none !important; } .liu-closePlayer{ position: fixed; top: 16px; right: 16px; width: 32px; height: 32px; line-height: 32px; background-color: var(--adys-bg-deep); display: flex; } .liu-closePlayer:hover{ background-color:#1f1f1f; color:white; transform: scale(1.1); } .playSpace { display: grid; grid-template-columns: 2fr 1fr; grid-template-rows: 1fr; grid-column-gap:16px; margin:8px 16px 16px; height: auto; gap:16px; } .player-panel .artplayer-app { width: 100%; height: 600px; border-radius: 6px; overflow: hidden; } .series { height: 600px; display: flex; flex-direction: column; overflow: hidden; } .seletor-title { border-radius: 6px 6px 0 0; margin: 0; flex-shrink: 0; height: 60px; line-height: 60px; font-size:20px; } .history-btn { font-size:20px !important; } .series .series-contianer { flex: 1; height: calc(600px - 60px); margin-top: 0; margin-bottom: 0; padding:16px; padding-bottom: 0; border-radius: 0 0 6px 6px; background-color: var(--adys-bg-deep); grid-template-columns: repeat(4,1fr); overflow-y: auto; overflow-x: hidden; box-sizing: border-box; } .control-panel { margin: 8px 16px 16px; display: flex; flex-direction: row; align-items: center; gap: 12px; } .control-panel .control-btn-group { margin-bottom: 0; flex-shrink: 0; } .control-panel .pc-source-list { flex: 1; min-width: 0; width: auto; } .control-panel .speed-test-btn { max-width: 160px; margin: 0; } } @media screen and (max-width: 1024px) { .liu-closePlayer { display: none !important; } .playSpace{ grid-template-rows: auto 1fr; grid-template-columns:1fr; grid-row-gap:10px; grid-column-gap:0px; margin:0 16px; } .series-contianer{ grid-template-columns: repeat(auto-fill, minmax(66px, 1fr)); height: 160px; padding:0 16px; background-color: var(--adys-bg-deep); border-radius: 0 0 6px 6px; margin-top: 0; } .speed-test-btn { max-width: 100%; margin:16px auto !important; display: block; } .series-tip-active .series-contianer { animation: scrollTip 1.5s ease-in-out 1; } .artplayer-app{ height: calc(42vh); min-height: 240px; border-radius: 6px; overflow: hidden; } .liu-playContainer { padding-top: 15px; padding-bottom: 90px; } .series { margin-top:8px; } .title-right-actions { gap: 8px; } /* 手机版隐藏连播按钮,只保留最近观看 */ .autoplay-toggle-btn { display: none !important; } } @media screen and (max-width: 768px) { .series-contianer{ grid-template-columns: repeat(3,1fr); height: 160px; } .artplayer-app{ height: 32vh; min-height: 200px; } .mobile-only .sourceButtonList { grid-template-columns: repeat(3, 1fr); } .mobile-btn-group { flex-direction: column; gap: 0; } } @media screen and (max-width: 480px) { .series-contianer{ grid-template-columns: repeat(auto-fill, minmax(62px, 1fr)); height: 150px; } .liu-btn{ font-size:16px; padding:10px 12px; } .seletor-title{ font-size:16px; height:45px; line-height:45px; } .search-container { padding: 12px; } .liu-playContainer { padding-bottom: 90px; padding-top: 10px; } } @media screen and (max-width: 375px) { .series-contianer{ grid-template-columns: repeat(2,1fr); height: 140px; grid-column-gap: 8px; grid-row-gap: 8px; } /* 防止搜索切换栏被压缩 */ .search-modal-container .search-input-row, .search-modal-container .search-history-section, .search-modal-container .search-tab-bar { flex-shrink: 0 !important; } .search-modal-container .search-results-section { flex-shrink: 1 !important; flex: 1 1 0 !important; } .series-selector{ font-size:14px; padding: 8px 4px; } .artplayer-app{ height: 28vh; min-height: 180px; } .mobile-only .sourceButtonList { grid-template-columns: repeat(3, 1fr); } } .seletor-title { display: flex !important; flex-direction: row !important; justify-content: space-between !important; } .seletor-title .title-text { order: 0 !important; flex: 1 !important; } .seletor-title .title-right-actions { order: 1 !important; flex-shrink: 0 !important; } /* 片名放大 */ .seletor-title .title-text { font-size: 22px !important; /* 根据需要调整,移动端可改为 18px */ font-weight: 600 !important; letter-spacing: 0.5px; } /* 移动端适配 */ @media screen and (max-width: 768px) { .seletor-title .title-text { color: #ffffff !important; font-weight: 900 !important; font-size: 24px !important; text-shadow: 0 2px 10px rgba(0,0,0,0.9) !important; letter-spacing: 0.5px; } } @media screen and (max-width: 768px) { .seletor-title .title-text { font-size: 20px !important; } } /* 搜索历史区域:固定高度,超出滚动 */ .search-modal-container .search-history-section { max-height: 60px !important; overflow-y: auto !important; flex-wrap: nowrap !important; gap: 4px 8px !important; padding-bottom: 4px !important; align-items: center !important; } .search-modal-container .search-history-tag { white-space: nowrap !important; flex-shrink: 0 !important; } /* 搜索结果区域:提高最小高度(移动端更高) */ .search-modal-container .search-results-section { min-height: 35vh !important; } /* 底部关闭按钮区域缩短 */ .search-modal-container > div:last-child { padding: 8px 20px !important; } .search-modal-container .settings-close-btn { padding: 6px 24px !important; font-size: 14px !important; } /* 移动端专用:结果区更高,关闭区更短 */ @media screen and (max-width: 768px) { .search-modal-container .search-results-section { min-height: 50vh !important; } .search-modal-container > div:last-child { padding: 4px 16px !important; } } `); // ==================== 2. Vue 模板 ==================== const vueAppTemplate = `
{{vod_name}}选集

请不要相信视频中的广告!

最近观看
暂无观看记录
{{ item.movieName }}
第{{ item.playHistory.playingIndex + 1 }}集 {{ formatHistoryTime(item.timestamp) }}
`; // ==================== 3. 全局函数 ==================== // 历史时间格式化 function formatHistoryTime(timestamp) { const diff = Date.now() - timestamp; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return '刚刚'; if (minutes < 60) return `${minutes}分钟前`; if (hours < 24) return `${hours}小时前`; if (days < 7) return `${days}天前`; const date = new Date(timestamp); return `${date.getMonth() + 1}/${date.getDate()}`; } // 全局历史浮窗 function showGlobalHistoryModal() { const existingModal = document.getElementById('global-history-modal'); if (existingModal) { existingModal.remove(); return; } const historyList = getUnifiedHistory(); const modalHtml = `
最近观看
${historyList.length === 0 ? '
暂无观看记录
' : historyList.map(item => { const ph = item.playHistory || {}; const epInfo = ph.playingIndex !== undefined ? `第${ph.playingIndex + 1}集` : ''; const pct = calcProgressPercent(ph); const progressInfo = pct > 0 ? `` : ''; const poster = item.posterUrl ? `` : `
`; return `
${poster}
${escapeHtml(item.movieName)}
${epInfo}${progressInfo}${formatHistoryTime(item.timestamp)}
`}).join('')}
`; const modalElement = htmlToElement(modalHtml); if (!modalElement) return; document.body.appendChild(modalElement); const closeBtn = document.getElementById('global-history-close'); if (closeBtn) { closeBtn.addEventListener('click', () => modalElement.remove()); } modalElement.addEventListener('click', (e) => { if (e.target === modalElement) modalElement.remove(); }); modalElement.querySelectorAll('.history-item-delete').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const movieId = btn.getAttribute('data-delete-id'); if (movieId && deleteUnifiedHistoryItem(movieId)) { tip('已删除该记录及关联播放进度'); modalElement.remove(); showGlobalHistoryModal(); } else { tip('删除失败'); } }); }); const clearAllBtn = document.getElementById('global-history-clear-all'); if (clearAllBtn) { clearAllBtn.addEventListener('click', () => { if (confirm('确定要清空所有最近观看记录吗?这将同时清除所有影片的播放进度。')) { clearAllUnifiedHistory(); modalElement.remove(); tip('已清空所有记录'); } }); } document.querySelectorAll('.history-item').forEach(item => { item.addEventListener('click', (e) => { if (e.target.classList.contains('history-item-delete')) return; const subjectUrl = item.getAttribute('data-subject-url'); const movieName = item.getAttribute('data-movie-name'); const poster = item.getAttribute('data-poster'); // 爱看影视类:站点自带播放器,跳转到其播放页 if (subjectUrl && (subjectUrl.indexOf('ikanbot') !== -1 || subjectUrl.indexOf('imoviebot') !== -1)) { window.location.href = subjectUrl; return; } if (movieName) { modalElement.remove(); // 优先用历史条目存的播放源直接唤起播放器(零搜索) const sourceJson = item.getAttribute('data-source'); if (sourceJson) { try { const sd = JSON.parse(sourceJson); if (sd && sd.sources && sd.sources.length > 0) { // uid 沿用原历史条目,保证直出后播放进度/选源不丢失(避免退回虚拟 uid 重新从第一集开始) cmsPlayData = { title: sd.title || movieName, poster: sd.poster || poster || '', sources: sd.sources, uid: item.getAttribute('data-movie-id') || '' }; initVue(); return; } } catch (err) {} } // 旧条目无源:弹爱看搜索弹窗,自动搜索片名 openSearchModalWithKeyword(movieName); } }); }); } // 搜索浮窗(仅爱看1/爱看2 双站) function showSearchModal() { const existing = document.querySelector('.search-modal-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.className = 'search-modal-overlay'; const container = document.createElement('div'); container.className = 'search-modal-container'; // 搜索输入行 const inputRow = document.createElement('div'); inputRow.className = 'search-input-row'; const input = document.createElement('input'); input.type = 'text'; input.className = 'search-input'; input.placeholder = '搜索爱看1 / 爱看2'; input.autofocus = true; const button = document.createElement('button'); button.className = 'search-btn'; button.textContent = '搜索'; // 固定按钮宽度,防止文字变化抖动 button.style.minWidth = '80px'; // 标签切换栏 const tabBar = document.createElement('div'); tabBar.className = 'search-tab-bar'; tabBar.style.display = 'none'; // 创建两个爱看标签 const tabA1 = document.createElement('button'); tabA1.className = 'search-tab'; tabA1.innerHTML = '爱看1 '; const tabA2 = document.createElement('button'); tabA2.className = 'search-tab'; tabA2.innerHTML = '爱看2 '; // 将所有 tab 存入数组,便于统一控制 const allTabs = [tabA1, tabA2]; const tabMap = { a1: tabA1, a2: tabA2 }; // 标签外观完全由 CSS(含深/浅主题 !important 规则)控制,无需内联样式同步 // 将标签加入 tabBar(顺序:爱看1、爱看2) tabBar.appendChild(tabA1); tabBar.appendChild(tabA2); // 搜索结果区域 const resultsSection = document.createElement('div'); resultsSection.className = 'search-results-section'; // 存储当前搜索结果 let allResults = { a1: [], a2: [] }; let currentTab = 'a1'; // 弹窗是否已关闭/已失效:结果返回后若已失效则不再渲染 let disposed = false; // ---------- 更新标签可见性与激活状态 ---------- function updateTabVisibilityAndActive() { let firstVisible = null; for (const [key, tab] of Object.entries(tabMap)) { const count = allResults[key] ? allResults[key].length : 0; if (count === 0 && !tab.classList.contains('loading')) { tab.style.display = 'none'; } else { tab.style.display = ''; if (count > 0 && !firstVisible) firstVisible = key; } } const activeTab = tabMap[currentTab]; const activeLoading = activeTab && activeTab.classList.contains('loading'); if (!activeLoading && activeTab && activeTab.style.display === 'none' && firstVisible) { allTabs.forEach(t => t.classList.remove('active')); tabMap[firstVisible].classList.add('active'); currentTab = firstVisible; } if (allResults[currentTab] && allResults[currentTab].length > 0) { renderResults(allResults[currentTab]); } else if (activeLoading) { resultsSection.innerHTML = '
正在搜索...
'; } else if (firstVisible) { renderResults(allResults[currentTab]); } else { renderResults([]); } tabBar.style.display = 'flex'; } const renderResults = (list) => { resultsSection.innerHTML = ''; if (!list || list.length === 0) { resultsSection.innerHTML = '
未找到相关结果
'; return; } list.forEach(r => { const card = document.createElement('div'); card.className = 'search-result-card'; const posterHtml = r.poster ? `` : `
🎬
`; const abstractHtml = r.abstract ? `${escapeHtml(r.abstract)}` : ''; const sourceLabel = escapeHtml(r.siteLabel || '爱看'); let extraHtml = ''; if (r.lineCount) { extraHtml = `${escapeHtml(r.lineCount)}`; } card.innerHTML = ` ${posterHtml}
${escapeHtml(r.title)}
${sourceLabel} ${extraHtml} ${abstractHtml}
`; // 爱看结果:点击播放按钮或卡片任意区域 → 关闭弹窗并解析线路弹窗播放 const playAikanDirect = () => { overlay.remove(); playAikanInPlayer(r); }; card.addEventListener('click', (e) => { const playBtn = (e.target && e.target.closest) ? e.target.closest('.result-play-btn') : null; e.stopPropagation(); e.preventDefault(); addSearchHistory(input.value.trim()); renderSearchHistory(); if (playBtn) { e.stopPropagation(); e.preventDefault(); } if (r.url) playAikanDirect(); }); resultsSection.appendChild(card); }); }; const switchTab = (tab) => { if (tabMap[tab].classList.contains('loading')) return; currentTab = tab; allTabs.forEach(t => t.classList.remove('active')); tabMap[tab].classList.add('active'); const list = allResults[tab]; renderResults(list); }; tabA1.addEventListener('click', () => switchTab('a1')); tabA2.addEventListener('click', () => switchTab('a2')); const performSearch = async () => { const keyword = input.value.trim(); if (!keyword) { tip('请输入搜索关键词'); return; } addSearchHistory(keyword); renderSearchHistory(); button.disabled = true; button.textContent = '搜索中...'; allResults = { a1: [], a2: [] }; currentTab = 'a1'; // 重置所有标签状态(显示) allTabs.forEach(t => { t.style.display = ''; t.classList.remove('loading', 'loaded-failed', 'active'); t.querySelector('.search-tab-count').textContent = ''; }); tabA1.classList.add('loading', 'active'); renderResults([]); tabBar.style.display = 'flex'; historyContainer.style.display = 'none'; resultsSection.innerHTML = '
正在搜索...
'; let a1Done = false, a2Done = false; // 标签搜索完成的统一处理 const handleTabDone = (tabKey, tabEl, results, setDone) => { if (disposed) return; allResults[tabKey] = results; setDone(); tabEl.classList.remove('loading'); tabEl.querySelector('.search-tab-count').textContent = results.length > 0 ? `(${results.length})` : ''; if (results.length === 0) tabEl.classList.add('loaded-failed'); updateTabVisibilityAndActive(); }; // 并行搜索两个爱看站:爱看1 = www1.ikanbot.com,爱看2 = ikanbot.eu.org const a1Promise = searchAikan(keyword, 'https://www1.ikanbot.com', '爱看1') .then(r => handleTabDone('a1', tabA1, r, () => { a1Done = true; })) .catch(() => handleTabDone('a1', tabA1, [], () => { a1Done = true; })); const a2Promise = searchAikan(keyword, 'https://ikanbot.eu.org', '爱看2') .then(r => handleTabDone('a2', tabA2, r, () => { a2Done = true; })) .catch(() => handleTabDone('a2', tabA2, [], () => { a2Done = true; })); Promise.allSettled([a1Promise, a2Promise]).then(() => { __diag.info('爱看搜索完成: 关键词="' + keyword + '" 爱看1=' + allResults.a1.length + ' 爱看2=' + allResults.a2.length); historyContainer.style.display = 'none'; button.disabled = false; button.textContent = '搜索'; }); }; button.addEventListener('click', performSearch); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); }); inputRow.appendChild(input); inputRow.appendChild(button); container.appendChild(inputRow); // 搜索历史 const historyContainer = document.createElement('div'); historyContainer.id = 'search-history-container'; container.appendChild(historyContainer); const renderSearchHistory = () => { historyContainer.innerHTML = ''; const historyKeywords = getSearchHistory(); if (historyKeywords.length === 0) return; const limited = historyKeywords.slice(0, 6); const historySection = document.createElement('div'); historySection.className = 'search-history-section'; const label = document.createElement('span'); label.className = 'search-history-label'; label.textContent = '最近搜索'; historySection.appendChild(label); limited.forEach(kw => { const tag = document.createElement('span'); tag.className = 'search-history-tag'; tag.textContent = kw; tag.addEventListener('click', () => { input.value = kw; performSearch(); }); historySection.appendChild(tag); }); const clearBtn = document.createElement('button'); clearBtn.className = 'search-history-clear'; clearBtn.textContent = '清除'; clearBtn.addEventListener('click', () => { const n = clearSearchRelatedData(); historyContainer.innerHTML = ''; tip(`已清除搜索历史及缓存(${n} 项)`); }); historySection.appendChild(clearBtn); historyContainer.appendChild(historySection); }; renderSearchHistory(); container.appendChild(tabBar); container.appendChild(resultsSection); // 底部关闭按钮 const footerClose = document.createElement('div'); footerClose.style.cssText = 'padding: 12px 20px; border-top: 1px solid var(--adys-border); text-align: center; flex-shrink: 0;'; const closeBtnFooter = document.createElement('button'); closeBtnFooter.className = 'settings-close-btn'; closeBtnFooter.textContent = '关闭'; closeBtnFooter.style.cssText = 'background: var(--adys-accent); color: white; border: none; padding: 8px 32px; border-radius: 20px; font-size: 16px; cursor: pointer; transition: background 0.2s;'; closeBtnFooter.addEventListener('click', () => overlay.remove()); closeBtnFooter.addEventListener('mouseenter', () => closeBtnFooter.style.background = 'var(--adys-accent-hover)'); closeBtnFooter.addEventListener('mouseleave', () => closeBtnFooter.style.background = 'var(--adys-accent)'); footerClose.appendChild(closeBtnFooter); container.appendChild(footerClose); overlay.appendChild(container); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); document.body.appendChild(overlay); // 弹窗被移除时标记失效,避免在途搜索结果继续渲染 const overlayObserver = new MutationObserver(() => { if (!overlay.isConnected) { disposed = true; overlayObserver.disconnect(); } }); overlayObserver.observe(document.body, { childList: true }); } // 打开搜索弹窗并以指定片名自动搜索(腾讯点海报/详情页一键搜索入口) function openSearchModalWithKeyword(keyword) { const kw = (keyword || '').trim(); if (!kw) { showSearchModal(); return; } showSearchModal(); setTimeout(() => { const modalInput = document.querySelector('.search-modal-container .search-input'); if (modalInput) { modalInput.value = kw; const searchBtn = document.querySelector('.search-modal-container .search-btn'); if (searchBtn) searchBtn.click(); } }, 100); } // ==================== 统一设置面板 ==================== function showSettingsPanel() { const existing = document.getElementById('settings-panel-overlay'); if (existing) { existing.remove(); return; } const autoPlayOn = GM_getValue(AUTO_NEXT_ENABLED_KEY, true); const hgDetailOn = GM_getValue(HONGUO_DETAIL_KEY, false); const currentTheme = getTheme(); const overlay = document.createElement('div'); overlay.id = 'settings-panel-overlay'; overlay.className = 'settings-modal-overlay'; overlay.innerHTML = `
设置
界面主题
切换深色/浅色主题,即时生效
自动连播
播放完当前集后自动播放下一集
红果详情页
关闭:点海报直接弹窗搜索;开启:点海报进详情页再一键搜索
清除观看历史
删除所有播放进度和最近观看记录
`; document.body.appendChild(overlay); const closeModal = () => overlay.remove(); document.getElementById('settings-close-btn').addEventListener('click', closeModal); const headerClose = document.getElementById('settings-header-close'); if (headerClose) headerClose.addEventListener('click', closeModal); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); // 主题切换 const themeBtn = document.getElementById('settings-toggle-theme'); themeBtn.addEventListener('click', () => { const newTheme = getTheme() === 'dark' ? 'light' : 'dark'; setTheme(newTheme); themeBtn.textContent = newTheme === 'dark' ? '🌙 深色' : '☀️ 浅色'; tip(`已切换到${newTheme === 'dark' ? '深色' : '浅色'}主题`); }); // 自动连播开关 const autoplayToggle = document.getElementById('settings-toggle-autoplay'); autoplayToggle.addEventListener('click', () => { const newVal = !GM_getValue(AUTO_NEXT_ENABLED_KEY, true); GM_setValue(AUTO_NEXT_ENABLED_KEY, newVal); autoplayToggle.classList.toggle('active', newVal); tip(`自动连播已${newVal ? '开启' : '关闭'}`); }); // 红果详情页开关 const hgDetailToggle = document.getElementById('settings-toggle-hgdetail'); hgDetailToggle.addEventListener('click', () => { const newVal = !GM_getValue(HONGUO_DETAIL_KEY, false); GM_setValue(HONGUO_DETAIL_KEY, newVal); hgDetailToggle.classList.toggle('active', newVal); tip(newVal ? '红果详情页已开启:点海报进详情页,由详情页一键搜索触发' : '红果详情页已关闭:点海报直接弹窗搜索片名'); }); // 清除观看历史 document.getElementById('settings-clear-history').addEventListener('click', () => { if (confirm('确定要清除所有观看历史吗?这将同时删除所有影片的播放进度和记录。')) { doClearHistory(); } }); } // 全局浮动按钮注入(站点感知) function injectGlobalButtons() { if (document.getElementById('global-buttons-container')) return; const container = document.createElement('div'); container.id = 'global-buttons-container'; // 创建浮动菜单按钮(图标 + 文案标签,点击先收起菜单再执行动作) const makeFabItem = (icon, label, onClick, active = false) => { const btn = document.createElement('button'); btn.className = 'global-history-btn global-fab-item' + (active ? ' global-btn-active' : ''); btn.innerHTML = icon; btn.title = label; btn.setAttribute('aria-label', label); btn.setAttribute('data-label', label); btn.addEventListener('click', () => { collapseFab(); onClick(); }); return btn; }; const historyBtn = makeFabItem('', '观看历史', showGlobalHistoryModal); const searchBtn = makeFabItem('', '搜索爱看', showSearchModal); const settingsBtn = makeFabItem('', '设置', showSettingsPanel); // 根据站点添加导航按钮 const makeNavBtn = (text, label, urlOrFn, active = false) => { const btn = document.createElement('button'); btn.className = 'global-history-btn global-fab-item' + (active ? ' global-btn-active' : ''); btn.innerHTML = text; btn.title = label; btn.setAttribute('aria-label', label); btn.setAttribute('data-label', label); btn.addEventListener('click', () => { const url = typeof urlOrFn === 'function' ? urlOrFn() : urlOrFn; window.location.href = url; }); return btn; }; const DUANJU_URL = 'https://hongguoduanju.com/category?sort_type=1'; const TENCENT_LIB_URL = 'https://v.qq.com/tv-series-list/v-index.html'; // 红果/爱看/腾讯页面:显示 历史、果、库、搜索、设置 container.appendChild(historyBtn); container.appendChild(makeNavBtn('果', '红果短剧', DUANJU_URL, CURRENT_SITE === 'dj')); container.appendChild(makeNavBtn('库', '腾讯片库', TENCENT_LIB_URL, CURRENT_SITE === 'tx')); container.appendChild(searchBtn); container.appendChild(settingsBtn); // 移动端(或窄屏):折叠为绿色加号,点击展开菜单;电脑宽屏保持横向一排 const useMobileFab = isMobile || window.innerWidth <= 768; if (useMobileFab) { container.classList.add('mobile-fab-mode'); const fabMain = document.createElement('button'); fabMain.className = 'global-history-btn global-fab-main'; fabMain.innerHTML = ''; fabMain.title = '展开菜单'; fabMain.setAttribute('aria-label', '展开菜单'); container.appendChild(fabMain); const updateFabPositions = () => { const items = container.querySelectorAll('.global-fab-item'); const btnSize = 48; const gap = 10; items.forEach((item, i) => { item.style.bottom = (btnSize + gap + i * (btnSize + gap)) + 'px'; }); }; const syncPlayButtons = (hide) => { const mobileActionButtons = document.getElementById('mobile-action-buttons'); if (mobileActionButtons) { mobileActionButtons.style.opacity = hide ? '0' : '1'; mobileActionButtons.style.pointerEvents = hide ? 'none' : 'auto'; mobileActionButtons.style.transition = 'opacity 0.2s ease'; } }; fabMain.addEventListener('click', (e) => { e.stopPropagation(); const isExpanded = container.classList.toggle('expanded'); if (isExpanded) { updateFabPositions(); syncPlayButtons(true); } else { syncPlayButtons(false); } }); document.addEventListener('click', (e) => { if (!container.contains(e.target) && container.classList.contains('expanded')) { container.classList.remove('expanded'); syncPlayButtons(false); } }); } document.body.appendChild(container); } // 红果分类栏「热榜」入口已废弃:顶栏原生已有"榜单",此入口冗余,不再注入并清除历史残留 function injectHongguoRankEntry() { if (CURRENT_SITE !== 'dj') return; const old = document.getElementById('hg-rank-entry'); if (old) old.remove(); } // 红果分类/热榜列表页自动翻页:滚到底自动抓下一页并追加卡片 function initHongguoAutopager() { if (CURRENT_SITE !== 'dj') return; const path = location.pathname; if (!/^\/(category|rank)/.test(path)) return; if (typeof checkIsDetailPage === 'function' && checkIsDetailPage()) return; // 同一路径不重复初始化;SPA 切分类 tab(真人剧/漫剧/AI剧/漫画)后路径变化 → 清理旧状态重初始化 if (window._hgAutopager && window._hgAutopagerPath === path) return; if (typeof window._hgAutopagerCleanup === 'function') { try { window._hgAutopagerCleanup(); } catch (e) {} } window._hgAutopager = true; window._hgAutopagerPath = path; const LINK_SEL = 'a[href*="/detail?series_id="], a[href^="/comic/"]'; const findNextLink = (rootDoc) => rootDoc.querySelector('a[aria-label="下一页"]'); const findListContainer = (rootDoc) => { const links = rootDoc.querySelectorAll(LINK_SEL); if (!links.length) return null; let node = links[0]; let guard = 0; while (node.parentElement && guard++ < 12) { node = node.parentElement; if (node.querySelectorAll(LINK_SEL).length >= Math.min(links.length, 5)) break; } return node; }; let loading = false, stopped = false, nextUrl = (findNextLink(document) || {}).href || null; // 底部状态条 const bar = document.createElement('div'); bar.style.cssText = 'padding:16px;text-align:center;color:#999;font-size:13px;'; bar.textContent = '下拉加载更多'; const loadNext = () => { if (loading || stopped || !nextUrl) return; loading = true; bar.textContent = '加载中…'; GM_xmlhttpRequest({ method: 'GET', url: nextUrl, onload: (res) => { try { const doc = new DOMParser().parseFromString(res.responseText || '', 'text/html'); const curBox = findListContainer(document); const nextBox = findListContainer(doc); if (!curBox || !nextBox) { stopped = true; bar.textContent = '没有更多了'; loading = false; return; } let added = 0; Array.from(nextBox.children).forEach(child => { const isCard = (child.matches && child.matches(LINK_SEL)) || (child.querySelector && child.querySelector(LINK_SEL)); if (isCard) { // 新节点不会被网站自己的懒加载观察器接管,强制封面加载并去掉透明占位 child.querySelectorAll('img').forEach(img => { img.setAttribute('loading', 'eager'); if (img.src) img.src = img.src; }); child.querySelectorAll('[style*="opacity"]').forEach(el => { if ((el.style.opacity || '') === '0') el.style.opacity = '1'; }); curBox.appendChild(child); added++; } }); const nl = findNextLink(doc); if (!added || !nl) { stopped = true; bar.textContent = '没有更多了'; } else { nextUrl = nl.href; bar.textContent = '下拉加载更多'; } } catch (e) { bar.textContent = '加载失败,上滑重试'; } loading = false; }, onerror: () => { loading = false; bar.textContent = '加载失败,上滑重试'; } }); }; const onScroll = () => { // SPA 切 tab 不刷新页面:路径变了(如从真人剧切到漫画)先重建当前列表的加载器,再由它接管滚动 if (location.pathname !== path) { initHongguoAutopager(); return; } if (loading || stopped) return; if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 800) loadNext(); }; const cleanup = () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); if (bar && bar.parentNode) bar.parentNode.removeChild(bar); }; window._hgAutopagerCleanup = cleanup; const boot = () => { if (!document.body) return; document.body.appendChild(bar); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); // 首屏若不足一屏,尝试自动加载一页 if (document.body.offsetHeight - window.innerHeight < 800) loadNext(); }; if (document.body) boot(); else document.addEventListener('DOMContentLoaded', boot); } // 红果列表页海报点击拦截:详情页开关关闭时,点海报不跳详情页,直接弹窗搜索海报片名 function initHongguoCardIntercept() { if (CURRENT_SITE !== 'dj') return; if (window._hgCardIntercept) return; window._hgCardIntercept = true; const DJ_CARD_SEL = 'a[href*="/detail?series_id="], a[href^="/comic/"], a[href*="/series/"]'; const SCRIPT_UI_SEL = '.search-modal-overlay, .settings-modal-overlay, .history-modal, #dgys2-player-root, .adys-btn-group, #mobile-action-buttons, #global-buttons-container'; // 片名提取:卡片名称节点 → img[alt] → title 属性(不用 innerText 兜底,避免误抓"更新至80集"等元数据) const extractTitle = (card) => { let title = ''; const nameEl = card.querySelector('[class*="name"], [class*="title"]'); if (nameEl) title = (nameEl.textContent || '').replace(/\s+/g, ' ').trim(); if (!title) { const img = card.querySelector('img[alt]'); if (img && img.getAttribute('alt')) title = img.getAttribute('alt').trim(); } if (!title && card.getAttribute('title')) title = card.getAttribute('title').trim(); return title; }; const tryIntercept = (e) => { // 详情页开关开启:放行原生跳转,由详情页"一键搜索"按钮触发搜索 if (GM_getValue(HONGUO_DETAIL_KEY, false)) return; if (document.body.classList.contains('adys-playing')) return; // 详情页内不拦截(相关推荐照常进详情页) try { if (checkIsDetailPage()) return; } catch (err) {} const t = e.target; if (!t || !t.closest) return; if (t.closest(SCRIPT_UI_SEL)) return; const card = t.closest(DJ_CARD_SEL); if (!card) return; const title = extractTitle(card); if (!title) return; // 取不到片名放行,避免卡死 e.preventDefault(); e.stopPropagation(); try { e.stopImmediatePropagation(); } catch (err) {} try { openSearchModalWithKeyword(title); } catch (err) { try { __diag.warn('红果卡片搜索异常: ' + err); } catch (_) {} } }; // click 覆盖桌面与 React onClick;touchend 覆盖手机端提前导航 document.addEventListener('click', tryIntercept, true); // 记录 touchstart 位置,touchend 时仅点按(位移很小)才拦截,避免列表滚动抬手误触 let hgTouchX = 0, hgTouchY = 0; document.addEventListener('touchstart', (e) => { const t = e.targetTouches && e.targetTouches[0]; if (t) { hgTouchX = t.clientX; hgTouchY = t.clientY; } }, { capture: true, passive: true }); document.addEventListener('touchend', (e) => { const t = e.changedTouches && e.changedTouches[0]; if (t) { const dx = Math.abs(t.clientX - hgTouchX), dy = Math.abs(t.clientY - hgTouchY); if (dx > 12 || dy > 12) return; // 视为滚动/滑动,不拦截 } tryIntercept(e); }, true); } // 收起扇形菜单的辅助函数 function collapseFab() { const c = document.getElementById('global-buttons-container'); if (c) c.classList.remove('expanded'); // 恢复"一键搜索"按钮组 const mobileActionButtons = document.getElementById('mobile-action-buttons'); if (mobileActionButtons) { mobileActionButtons.style.opacity = '1'; mobileActionButtons.style.pointerEvents = 'auto'; } } // ==================== 4. 标题解析 ==================== function parseTitle(name) { if (!name) return { main: '', suffix: null }; let main = name; let suffix = null; const seasonMatch = name.match(/第(\d+)[季部]/); if (seasonMatch) { suffix = parseInt(seasonMatch[1], 10); main = name.replace(/第\d+[季部]/, '').trim(); } else { const numMatch = name.match(/\d+$/); if (numMatch) { suffix = parseInt(numMatch[0], 10); main = name.replace(/\d+$/, '').trim(); } } return { main, suffix }; } // ==================== 5. 主逻辑 initVue ==================== function initVue() { const globalBtnContainer = document.getElementById('global-buttons-container'); if (globalBtnContainer) globalBtnContainer.style.display = 'none'; // 播放器打开:添加 body 类,CSS !important 隐藏手机浮动按钮(一键搜索/爱看影视) document.body.classList.add('adys-playing'); // 如果有预加载的CMS搜索数据,构造虚拟 videoData const hasCmsData = !!cmsPlayData; if (hasCmsData) { // 历史直出时沿用原历史条目的 uid(site_id),保证播放进度与选源跨会话连续; // 普通 CMS 搜索直出无原始 uid,仍使用虚拟 uid dj_cms_{片名} let uidSite = 'dj'; let uidId = 'cms_' + cmsPlayData.title; if (cmsPlayData.uid) { const sep = cmsPlayData.uid.indexOf('_'); if (sep > 0) { uidSite = cmsPlayData.uid.slice(0, sep); uidId = cmsPlayData.uid.slice(sep + 1); } else uidId = cmsPlayData.uid; } videoDataBase = { videoName: cmsPlayData.title, videoYear: '', videoFirstActor: '', posterUrl: cmsPlayData.poster || '', site: uidSite, id: uidId, subjectUrl: '', }; } const videoData = getVideoDataOnce(); if (!videoData || !videoData.videoName || videoData.videoName === '未知') { __diag.warn('影片信息读取失败: ' + location.href); tip(CURRENT_SITE === 'dj' ? '短剧信息读取失败,请刷新页面重试!' : '影片信息读取失败,请刷新页面重试!'); document.body.classList.remove('adys-playing'); resetPlayBtn(); return; } __diag.info('初始化播放器: 片名=' + videoData.videoName + ' 站点=' + videoData.site + ' uid=' + (videoData.site + '_' + videoData.id)); const oldApp = $('#dgys2-player-root'); if (oldApp) oldApp.remove(); const e = htmlToElement(vueAppTemplate); if (!e) { tip('界面生成失败,请刷新页面重试'); document.body.classList.remove('adys-playing'); resetPlayBtn(); return; } document.body.appendChild(e); document.body.style.setProperty('overflow', 'hidden', 'important'); document.documentElement.style.setProperty('overflow', 'hidden', 'important'); new Vue({ el: '#dgys2-player-root', data: { art: {}, ok: false, vod_name: videoData.videoName, sources: [], selectedSource: 0, playingList: [], playingIndex: 0, isTestingSpeed: false, isTipActive: false, isScrollable: false, searchKeyword: '', retryCount: 0, maxRetry: 1, loadSuccessCount: 0, loadTotalCount: 0, loadFailedCount: 0, wheelHandler: null, speedTestTotal: 0, speedTestCompleted: 0, autoSwitchCount: 0, maxAutoSwitch: 5, failedSourceIndexes: [], playTimer: null, showHistoryModal: false, historyList: [], historyRecorded: false, maxHistoryItems: MAX_HISTORY_ITEMS, autoPlayNext: GM_getValue(AUTO_NEXT_ENABLED_KEY, true), speedTestAbortController: null, autoSpeedTestEnabled: false, manualSpeedTestActive: false, testingSourceSet: new Set(), visibilityHandler: null, }, computed: { successSources() { return this.sources .map((source, idx) => ({ ...source, originalIndex: idx })) .filter(s => s.status === 'success'); }, hasUntestedSources() { return this.successSources.some(s => s.speed === -1); } }, watch: { playingList() { this.$nextTick(() => { this.checkScrollable(); this.scrollToPlayingIndex(); }); }, playingIndex() { this.$nextTick(() => this.scrollToPlayingIndex()); }, ok(newVal) { if (newVal && isMobile) { const mobileGroup = document.getElementById('mobile-action-buttons'); if (mobileGroup) mobileGroup.style.display = 'none'; } } }, methods: { isSourceTesting(index) { return this.testingSourceSet.has(index); }, checkScrollable() { const container = this.$refs.seriesContainer; if (!container) return; container.removeEventListener('scroll', this.checkScrollable); this.isScrollable = container.scrollHeight > container.clientHeight; container.addEventListener('scroll', this.checkScrollable); }, // 手机端/PC 选集列表是固定高度的滚动容器,只显示可视区几集; // 从历史恢复或切换集数后,把当前集所在行滚到容器顶部(置顶), // 手机端空间小,置顶可最大化看到选中集及其后的集数 scrollToPlayingIndex() { try { const container = this.$refs.seriesContainer; if (!container || !container.children || !container.children.length) return; const el = container.querySelector('.series-selector.playing') || container.children[this.playingIndex]; if (!el) return; let padTop = 0; if (window.getComputedStyle) { try { padTop = parseFloat(window.getComputedStyle(container).paddingTop) || 0; } catch (e) {} } const elTop = el.offsetTop - padTop - (container.clientTop || 0); const elBottom = elTop + el.offsetHeight; // 仅在当前集不在可视区内时置顶,避免每帧都跳动 if (elTop < container.scrollTop || elBottom > container.scrollTop + container.clientHeight) { container.scrollTop = Math.max(0, elTop); } } catch (e) {} }, handleSearch() { const keyword = this.searchKeyword.trim(); if (!keyword) return; showSearchModal(); // 将关键词填入弹窗输入框并自动触发搜索 setTimeout(() => { const modalInput = document.querySelector('.search-modal-container .search-input'); if (modalInput) { modalInput.value = keyword; const searchBtn = document.querySelector('.search-modal-container .search-btn'); if (searchBtn) searchBtn.click(); } }, 100); }, toggleAutoPlayNext() { this.autoPlayNext = !this.autoPlayNext; GM_setValue(AUTO_NEXT_ENABLED_KEY, this.autoPlayNext); tip(`自动连播已${this.autoPlayNext ? '开启' : '关闭'}`); }, async startSpeedTest(keepExistingResults = false) { if (this.isTestingSpeed) return; const validSources = this.sources .map((source, index) => ({ ...source, index })) .filter(s => s.status === 'success' && s.playList.length > 0); if (validSources.length === 0) return; if (!keepExistingResults) { validSources.forEach(s => { this.sources[s.index].speed = -1; }); } // 测速候选过滤:命中会话缓存(10 分钟内)直接复用结果,不重新下载分片 const cacheNow = Date.now(); const toTest = validSources.filter(s => { if (this.sources[s.index].speed !== -1) return false; const cached = speedTestSessionCache.get(s.name); if (cached && cacheNow - cached.time < SPEED_TEST_CACHE_TTL) { this.sources[s.index].speed = cached.speed; return false; } return true; }); if (toTest.length === 0) { this.autoSpeedTestEnabled = false; return; } this.isTestingSpeed = true; this.speedTestTotal = toTest.length; this.speedTestCompleted = 0; this.$forceUpdate(); this.speedTestAbortController = new AbortController(); const signal = this.speedTestAbortController.signal; const groupSize = 3; const sourceGroups = []; for (let i = 0; i < toTest.length; i += groupSize) { sourceGroups.push(toTest.slice(i, i + groupSize)); } try { for (const group of sourceGroups) { if (signal.aborted) break; await Promise.all( group.map(async (source) => { if (signal.aborted) return; const index = source.index; this.testingSourceSet.add(index); this.$forceUpdate(); try { const url = source.playList[this.playingIndex].url; const result = await downloadtsList(url, signal); if (signal.aborted) return; if (!result.r || result.content.length === 0) { // m3u8 拉不到或无 ts 分片:保持未测速(-1),不再伪造 0.1 MB/s } else { let tsList = result.content; tsList = tsList.length > 8 ? tsList.slice(0, 8) : tsList; let totalSize = 0; const startTime = Date.now(); await Promise.all( tsList.map(async (tsUrl) => { if (signal.aborted) return; const res = await get({ url: encodeURI(tsUrl), responseType: 'arraybuffer', timeout: 8000 }, 0, 8000, signal); if (signal.aborted) return; if (res.r) { const size = res.content.byteLength ? res.content.byteLength / 1024 / 1024 : 0; totalSize += size; } }) ); if (!signal.aborted) { if (totalSize <= 0) { // 8 秒内一个分片字节都没下成(被防盗链拦截/超时):保持未测速,不显示假 0.1 } else { const duration = (Date.now() - startTime) / 1000; let speed = duration > 0 ? totalSize / duration : 0.01; speed = Math.max(Number(speed.toFixed(2)), 0.01); this.sources[index].speed = speed; } } } } catch (e) { // 异常:保持未测速,不伪造 0.1 } finally { if (!signal.aborted) { this.speedTestCompleted++; this.testingSourceSet.delete(index); this.$forceUpdate(); // 仅缓存真实测到速度的结果;未测速/失败不缓存,下次可重测 if (this.sources[index].speed > 0) { speedTestSessionCache.set(source.name, { speed: this.sources[index].speed, time: Date.now() }); } } else { this.testingSourceSet.delete(index); this.$forceUpdate(); } } }) ); } } catch (e) {} finally { if (!signal.aborted) { this.isTestingSpeed = false; this.speedTestAbortController = null; this.$forceUpdate(); if (this.hasUntestedSources === false) this.autoSpeedTestEnabled = false; } else { this.isTestingSpeed = false; this.speedTestAbortController = null; this.$forceUpdate(); } } }, stopSpeedTest() { if (this.speedTestAbortController) { this.speedTestAbortController.abort(); this.speedTestAbortController = null; } this.isTestingSpeed = false; this.testingSourceSet.clear(); this.$forceUpdate(); }, async handleManualSpeedTest() { if (this.isTestingSpeed) { this.stopSpeedTest(); this.manualSpeedTestActive = false; tip('测速已停止'); return; } if (this.successSources.length === 0) { tip('无可用播放源,无法测速'); return; } if (this.art && !this.art.paused) { this.art.pause(); } this.manualSpeedTestActive = true; try { await this.startSpeedTest(true); } finally { this.manualSpeedTestActive = false; } if (!this.isTestingSpeed && this.hasUntestedSources === false) { if (this.art && this.art.notice) { try { this.art.notice.show = '所有源测速完成'; } catch (e) {} } } }, autoSpeedTest() { if (!this.autoSpeedTestEnabled) return; if (this.isTestingSpeed) return; if (!this.hasUntestedSources) { this.autoSpeedTestEnabled = false; return; } this.startSpeedTest(true); }, sourceSelect(index) { if (this.sources[index].status === 'failed') return; const ct = this.art.currentTime; this.selectedSource = index; this.playingList = this.sources[index].playList; // 新源集数可能少于当前播放集数:越界时回退到该源最后一集,避免播放空 URL if (this.playingIndex >= this.playingList.length) { this.playingIndex = Math.max(0, this.playingList.length - 1); } this.switchUrl(this.playingList[this.playingIndex].url); this.art.once('video:canplay', () => { this.art.seek = ct; }); this.vod_name = this.sources[index].vod_name; savePlayHistory({ selectedSource: index, selectedSourceName: this.sources[index].name }); this.retryCount = 0; this.autoSwitchCount = 0; }, playListSelect(index, autoPlay = false) { this.playingIndex = index; this.switchUrl(this.playingList[this.playingIndex].url); savePlayHistory({ playingIndex: index, currentTime: 0, selectedSource: this.selectedSource, selectedSourceName: this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : '' }); this.retryCount = 0; if (autoPlay) { setTimeout(() => { if (this.art && this.art.play) { this.art.play().catch(e => console.warn('自动播放失败', e)); } }, 200); } }, saveCurrentPlayback() { if (this.art && this.art.currentTime > 0) { savePlayHistory({ currentTime: this.art.currentTime, duration: this.art.duration || 0, playbackRate: this.art.playbackRate, volume: this.art.volume, selectedSource: this.selectedSource, selectedSourceName: this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : '', }); } }, startPlayTimer() { if (this.playTimer) clearInterval(this.playTimer); // 进度写入 5s → 15s 降频(每次写入含 2 次存储读 + 1 次全历史数组写回) this.playTimer = setInterval(() => { this.saveCurrentPlayback(); }, 15000); // 页面隐藏/关闭时立即落盘,补齐降频后的进度丢失窗口 if (!this.pageHideHandler) { this.pageHideHandler = () => this.saveCurrentPlayback(); document.addEventListener('visibilitychange', this.pageHideHandler); window.addEventListener('pagehide', this.pageHideHandler); } }, stopPlayTimerAndSave() { if (this.playTimer) { clearInterval(this.playTimer); this.playTimer = null; } this.saveCurrentPlayback(); }, stopPlayTimer() { if (this.playTimer) { clearInterval(this.playTimer); this.playTimer = null; } }, initArt(url) { if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } try { const autoplay = false; this.art = new Artplayer({ container: '.artplayer-app', url: url, autoplay: autoplay, pip: true, fullscreen: true, fullscreenWeb: true, autoMini: !isMobile, screenshot: true, hotkey: true, airplay: true, playbackRate: true, setting: true, miniProgressBar: !isMobile, theme: 'var(--adys-accent-hover)', moreVideoAttr: { crossOrigin: 'anonymous' }, controls: isMobile ? [{ name: 'resolution', html: '分辨率', position: 'right' }] : [ { name: 'prev-episode', position: 'left', html: '⏮', tooltip: '上一集', click: () => { if (this.playingList.length > 1 && this.playingIndex > 0) { this.playListSelect(this.playingIndex - 1, true); } else { if (this.art && this.art.notice) this.art.notice.show = '已经是第一集了'; } }, }, { name: 'next-episode', position: 'left', html: '⏭', tooltip: '下一集', click: () => { if (this.playingList.length > 1 && this.playingIndex + 1 < this.playingList.length) { this.playListSelect(this.playingIndex + 1, true); } else { if (this.art && this.art.notice) this.art.notice.show = '已经是最后一集了'; } }, }, { name: 'resolution', html: '分辨率', position: 'right' } ], type: 'm3u8', customType: { m3u8: playM3u8 }, }); this.visibilityHandler = () => { if (document.hidden) { if (this.art && !this.art.paused) { this.art.pause(); } } }; document.addEventListener('visibilitychange', this.visibilityHandler); this.art.on('video:play', () => this.startPlayTimer()); this.art.on('video:pause', () => this.stopPlayTimerAndSave()); this.art.on('video:ended', () => this.stopPlayTimer()); // 网页全屏/原生全屏时隐藏底部搜索栏,防止遮挡播放器控制栏 this.art.on('fullscreenWeb', (state) => { if (state) document.body.classList.add('adys-fullscreen-web'); else document.body.classList.remove('adys-fullscreen-web'); }); this.art.on('fullscreen', (state) => { if (state) document.body.classList.add('adys-fullscreen-web'); else document.body.classList.remove('adys-fullscreen-web'); }); this.art.on('error', () => this.handlePlayError()); this.art.on('video:stalled', () => { setTimeout(() => { if (this.art.video.readyState < 3) this.handlePlayError(); }, 5000); }); this.art.on('video:canplay', () => { this.retryCount = 0; }); let seekRetryCount = 0; const trySeek = (targetTime) => { if (this.art.video.readyState >= 1 && this.art.video.duration && !isNaN(this.art.video.duration)) { this.art.seek = targetTime; seekRetryCount = 0; } else if (seekRetryCount < 5) { seekRetryCount++; setTimeout(() => trySeek(targetTime), 500); } }; this.art.on('video:loadedmetadata', () => { this.art.controls.resolution.innerText = this.art.video.videoHeight + 'P'; const history = getPlayHistory(); if (history.currentTime && history.currentTime > 0) trySeek(history.currentTime); if (history.playbackRate) this.art.playbackRate = history.playbackRate; if (history.volume !== undefined) this.art.volume = history.volume; }); this.art.on('video:ended', () => { if (!this.autoPlayNext) return; if (this.playingList.length > 1 && this.playingIndex + 1 < this.playingList.length) { this.playListSelect(this.playingIndex + 1, true); } }); if (!isMobile) { this.art.on('video:play', () => { if (this.isTestingSpeed && !this.manualSpeedTestActive) this.stopSpeedTest(); }); this.art.on('video:pause', () => { if (this.hasUntestedSources && !this.isTestingSpeed) this.autoSpeedTest(); }); this.art.on('video:ended', () => { if (this.hasUntestedSources && !this.isTestingSpeed) this.autoSpeedTest(); }); } else { this.art.on('video:play', () => { if (this.isTestingSpeed && !this.manualSpeedTestActive) this.stopSpeedTest(); }); } this.art.on('destroy', () => { if (!document.hidden && this.art && !this.art.paused && this.art.currentTime > 0) { savePlayHistory({ currentTime: this.art.currentTime }); } this.stopPlayTimer(); this.stopSpeedTest(); if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } }); } catch (e) { tip('播放器初始化失败,请刷新页面重试'); } }, handlePlayError() { __diag.warn('播放出错: 源=' + (this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : this.selectedSource) + ' 第' + (this.playingIndex + 1) + '集 重试计数=' + this.retryCount); if (this.retryCount < this.maxRetry) { this.retryCount++; this.art.notice.show = `播放失败,第${this.retryCount}次重试...`; this.switchUrl(this.playingList[this.playingIndex].url); } else { this.retryCount = 0; if (!this.failedSourceIndexes.includes(this.selectedSource)) this.failedSourceIndexes.push(this.selectedSource); if (this.autoSwitchCount >= this.maxAutoSwitch) { __diag.warn('自动换源次数已达上限'); this.art.notice.show = '自动换源次数已达上限,请手动切换源'; tip('自动换源次数已达上限,请手动切换源'); return; } const validSources = this.sources .map((s, idx) => ({ ...s, index: idx })) .filter(s => s.status === 'success' && !this.failedSourceIndexes.includes(s.index)); if (validSources.length > 0) { const nextSource = validSources[0]; this.autoSwitchCount++; __diag.info('自动换源: ' + (this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : '?') + ' -> ' + nextSource.name); this.art.notice.show = `当前源播放失败,自动切换到${nextSource.name}`; this.sourceSelect(nextSource.index); } else { __diag.error('所有源均播放失败'); this.art.notice.show = '所有源均播放失败,请刷新页面重试'; tip('所有源均播放失败,请刷新页面重试'); } } }, switchUrl(url) { try { if (!this.art) return; // 先销毁旧 Hls 实例:保证「同 URL 失败重试」时真正重建播放器,而不是只换 src 不重载 if (this.art.hls) { try { this.art.hls.destroy(); } catch (e1) {} this.art.hls = null; } // 切换交给 Artplayer(m3u8 走 customType 重建 Hls),不再手工直写 video.src, // 避免原生播放器直接解析 m3u8 导致黑屏或二次初始化。 this.art.switchUrl(url); } catch (e) { __diag.warn('switchUrl 异常: ' + (e && e.message ? e.message : e)); } }, closePlayer() { try { __diag.info('播放器已关闭: ' + (this.vod_name || '')); hideLoadingTip(); this.stopSpeedTest(); this.stopPlayTimer(); if (this.art && this.art.destroy) this.art.destroy(); if (this.$destroy) this.$destroy(); if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } if (this.pageHideHandler) { document.removeEventListener('visibilitychange', this.pageHideHandler); window.removeEventListener('pagehide', this.pageHideHandler); this.pageHideHandler = null; } const container = this.$refs.seriesContainer; if (container) container.removeEventListener('scroll', this.checkScrollable); if (this.wheelHandler) { const sourceList = this.$el && this.$el.querySelector('.pc-source-list'); if (sourceList) sourceList.removeEventListener('wheel', this.wheelHandler); this.wheelHandler = null; } const app = $('#dgys2-player-root'); if (app) app.remove(); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; // 重置视频数据缓存,确保下次播放使用新数据 videoDataBase = null; const globalBtnContainer = document.getElementById('global-buttons-container'); if (globalBtnContainer) globalBtnContainer.style.display = 'flex'; // 播放器关闭:移除 body 类,恢复手机浮动按钮 document.body.classList.remove('adys-playing'); document.body.classList.remove('adys-fullscreen-web'); resetPlayBtn(); } catch (e) {} }, openHistoryModal() { this.loadHistoryList(); this.showHistoryModal = true; }, closeHistoryModal() { this.showHistoryModal = false; }, loadHistoryList() { this.historyList = getUnifiedHistory().slice(0, MAX_HISTORY_ITEMS); }, getHistoryProgressPercent(item) { return calcProgressPercent(item.playHistory); }, formatHistoryTime, jumpToHistory(item) { if (item.subjectUrl && (item.subjectUrl.indexOf('ikanbot') !== -1 || item.subjectUrl.indexOf('imoviebot') !== -1)) { // 爱看影视:跳转到爱看站播放页(爱看站自带播放器,脚本不解析其源) window.location.href = item.subjectUrl; return; } if (item.movieName) { this.closeHistoryModal(); this.closePlayer(); // 优先用历史条目里存的播放源直接唤起播放器(零搜索);旧条目无源时回退缓存直出/资源站搜索 if (item.sourceData && item.sourceData.sources && item.sourceData.sources.length > 0) { // uid 沿用原历史条目,保证直出后播放进度/选源跨会话连续 cmsPlayData = { title: item.sourceData.title || item.movieName, poster: item.sourceData.poster || item.posterUrl || '', sources: item.sourceData.sources, uid: item.movieId || '' }; initVue(); return; } // 旧条目无源:弹爱看搜索弹窗,自动搜索片名 openSearchModalWithKeyword(item.movieName); return; } this.closeHistoryModal(); }, deleteHistoryItem(movieId) { if (deleteUnifiedHistoryItem(movieId)) { this.loadHistoryList(); tip('已删除该记录及关联播放进度'); } else tip('删除失败,请重试'); }, confirmClearAllHistory() { if (confirm('确定要清空所有最近观看记录吗?这将同时清除所有影片的播放进度。')) { clearAllUnifiedHistory(); this.loadHistoryList(); tip('已清空所有记录'); } }, recordCurrentToHistory() { if (this.historyRecorded) return; const videoData = getVideoDataOnce(); if (!videoData) return; const uid = getUid(); // 用当前已加载的播放源构造 sourceData(cmsPlayData 在搜索直出分支用完后已被置空, // 直接引用它会导致历史条目永远存不到源)。历史点击时直接从源唤起播放器(零搜索), // 与搜索弹窗直出播放的体验一致。 const loadedSources = this.sources .filter(s => s.status === 'success' && s.playList && s.playList.length > 0) .map(s => ({ name: s.name, playList: s.playList, vod_name: s.vod_name || '' })); const sourceData = loadedSources.length > 0 ? { title: videoData.videoName, poster: videoData.posterUrl || '', sources: loadedSources } : null; addToUnifiedHistory(uid, videoData.videoName, videoData.subjectUrl, getPlayHistory(), videoData.posterUrl, sourceData); this.historyRecorded = true; } }, async created() { try { let tempSources = []; // 有预加载数据(爱看搜索结果直出)时,跳过搜索直接使用 if (hasCmsData && cmsPlayData.sources && cmsPlayData.sources.length > 0) { this.loadTotalCount = cmsPlayData.sources.length; this.loadSuccessCount = cmsPlayData.sources.length; tempSources = cmsPlayData.sources.map(s => ({ name: s.name, playList: s.playList, vod_name: s.vod_name, speed: -1, status: 'success', errorMsg: '搜索结果直出', })); cmsPlayData = null; // 用完即清,避免下次复用 } else { // 纯净版:无预加载数据(未经过爱看搜索)时不发起任何资源站搜索,直接提示并关闭 hideLoadingTip(); tip('未找到可播放数据,请先搜索爱看'); this.closePlayer(); return; } hideLoadingTip(); // 排序:集数从多到少(爱看直出源均为成功源) tempSources.sort((a, b) => { return b.playList.length - a.playList.length; }); this.sources = tempSources; __diag.info('资源搜索完成: 成功 ' + this.loadSuccessCount + ' / 失败 ' + this.loadFailedCount + ' / 共 ' + this.loadTotalCount + ' (片名=' + videoData.videoName + ')'); const history = getPlayHistory(); let targetSourceIndex = -1; if (history.selectedSourceName) { const nameMatchIndex = this.sources.findIndex(s => s.name === history.selectedSourceName && s.status === 'success'); if (nameMatchIndex !== -1) targetSourceIndex = nameMatchIndex; } if (targetSourceIndex === -1 && history.selectedSource !== undefined && this.sources[history.selectedSource] && this.sources[history.selectedSource].status === 'success') targetSourceIndex = history.selectedSource; // 无历史记录时默认选中第一个(即集数最多的源) if (targetSourceIndex === -1) targetSourceIndex = this.sources.findIndex(s => s.status === 'success'); if (targetSourceIndex !== -1) { this.selectedSource = targetSourceIndex; this.playingList = this.sources[targetSourceIndex].playList; this.vod_name = this.sources[targetSourceIndex].vod_name; // 保存选中的源到播放历史,确保下次打开时恢复同一个源 savePlayHistory({ selectedSource: targetSourceIndex, selectedSourceName: this.sources[targetSourceIndex].name }); let targetPlayingIndex = 0; if (history.playingIndex !== undefined && history.playingIndex >= 0 && history.playingIndex < this.playingList.length) targetPlayingIndex = history.playingIndex; this.playingIndex = targetPlayingIndex; this.ok = true; await this.$nextTick(); this.checkScrollable(); if (isMobile && this.isScrollable) { const uid = getUid(); const key = `tip_shown_${uid}`; const alreadyShown = GM_getValue(key, false); if (!alreadyShown) { this.isTipActive = true; GM_setValue(key, true); setTimeout(() => { this.isTipActive = false; }, 1500); } } this.initArt(this.playingList[this.playingIndex].url); if (!isMobile) { this.autoSpeedTestEnabled = true; setTimeout(() => { if (this.autoSpeedTestEnabled && this.hasUntestedSources) this.startSpeedTest(true); }, 500); } // 海报:已有则不重复获取;没有时优先复用统一历史里已存的同名片海报, // 实在没有才用首个成功源的封面补全,避免每次开播都重新抓海报 try { const vdNow = getVideoDataOnce(); if (vdNow && !vdNow.posterUrl) { const curName = vdNow.videoName || ''; const hist = getUnifiedHistory(); const hit = hist.find(it => it && matchTitle(it.movieName || '', curName) && it.posterUrl); if (hit && hit.posterUrl) vdNow.posterUrl = hit.posterUrl; else { const picSource = this.sources.find(s => s.status === 'success' && s.vod_pic); if (picSource) vdNow.posterUrl = picSource.vod_pic; } } } catch (e) {} this.recordCurrentToHistory(); } else { tip(CURRENT_SITE === 'dj' ? '未搜索到任何可用短剧资源,可使用底部搜索框查找对应短剧' : '未搜索到任何可用资源,可使用底部搜索框查找对应影片'); this.closePlayer(); } } catch (e) { __diag.error('播放器 created 异常: ' + (e && e.stack ? e.stack : (e && e.message))); hideLoadingTip(); tip('界面加载失败,请刷新页面重试'); this.closePlayer(); } finally { resetPlayBtn(); } }, mounted() { if (!isMobile) { this.$nextTick(() => { const sourceList = this.$el.querySelector('.pc-source-list'); if (sourceList && !this.wheelHandler) { this.wheelHandler = (e) => { if (sourceList.scrollWidth > sourceList.clientWidth) { e.preventDefault(); sourceList.scrollLeft += e.deltaY; } }; sourceList.addEventListener('wheel', this.wheelHandler, { passive: false }); } }); } }, beforeDestroy() { if (this.wheelHandler) { const sourceList = this.$el && this.$el.querySelector('.pc-source-list'); if (sourceList) sourceList.removeEventListener('wheel', this.wheelHandler); this.wheelHandler = null; } this.stopPlayTimer(); this.stopSpeedTest(); if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } } }); } // ==================== 6. 播放按钮注入(站点感知)==================== // 创建按钮对的辅助函数(避免重复代码) function createPlayButtonPair() { const playBtn = htmlToElement(``); window.doubanPlayBtn = playBtn; playBtn.onclick = () => { playBtn.innerText = '搜索中...'; playBtn.style.backgroundColor = '#9e9e9e'; playBtn.style.color = '#1a1a1a'; playBtn.disabled = true; playBtn.style.cursor = 'not-allowed'; const vd = getVideoDataOnce(); const name = (vd && vd.videoName && vd.videoName !== '未知') ? vd.videoName : ''; openSearchModalWithKeyword(name); setTimeout(() => { resetBtnEl(playBtn); }, 300); }; return { playBtn }; } // 红果标题挂载点:新版详情页 h1 为 -webkit-box + line-clamp + overflow:hidden, // 按钮注入 h1 内部会被 line-clamp 裁剪或同行挤压 → 注入其父容器(新版父容器为纵向 flex) function getDjTitleEl() { const h1 = document.querySelector('h1, .drama-title, .title, [class*="pc-title-"], [class*="m-title-"]'); if (!h1) return null; try { const cs = getComputedStyle(h1); if ((cs.display === '-webkit-box' || cs.overflow === 'hidden') && h1.parentElement) { return h1.parentElement; } } catch (e) {} return h1; } // 电脑版红果详情:把"一键搜索"塞进原生"播放正片"按钮的位置,隐藏原生按钮 function injectDjPcPlay() { const native = document.querySelector('a[class*="pc-hero-btn-play-"]'); if (!native || !native.parentElement) return false; const holder = native.parentElement; if (holder.querySelector('.play-btn')) return true; const { playBtn } = createPlayButtonPair(); native.style.display = 'none'; holder.insertBefore(playBtn, native); return true; } function hijackRedFruitAutoplay() { if (isMobile || CURRENT_SITE !== 'dj') return; if (window._adysHijackAutoplay) return; window._adysHijackAutoplay = true; let armed = true; const disarm = () => { armed = false; }; document.addEventListener('click', disarm, { once: true, capture: true }); document.addEventListener('keydown', disarm, { once: true, capture: true }); document.addEventListener('touchstart', disarm, { once: true, capture: true }); const isScriptVideo = (video) => !!(video.closest && video.closest('.artplayer-app, .art-video-player, .adys-player')); const hijackOne = (video) => { if (!video || video.dataset.adysHijacked) return; if (isScriptVideo(video)) return; // 不拦截脚本自己的播放器 video.dataset.adysHijacked = '1'; // capture 阶段先于页面 JS 的 play 处理:play 事件可 cancelable,preventDefault 可取消播放 video.addEventListener('play', (e) => { if (armed) { try { e.preventDefault(); } catch (err) {} try { video.pause(); } catch (err) {} } }, true); // 脚本后加载、视频已出现/已播放的情况:立即暂停 try { video.pause(); } catch (err) {} }; const scan = () => { document.querySelectorAll('video').forEach(hijackOne); }; scan(); observeDebounced(document.documentElement, scan, { delay: 100 }); } // MutationObserver:React SPA 重新渲染后自动重新注入按钮 function setupPlayButtonObserver() { if (window.playButtonObserver) { window.playButtonObserver.disconnect(); } let reinjectTimer = null; let reinjectCount = 0; const MAX_REINJECTS = 50; window.playButtonObserver = new MutationObserver(() => { if (reinjectTimer) return; reinjectTimer = setTimeout(() => { reinjectTimer = null; if (reinjectCount >= MAX_REINJECTS) return; // 电脑版红果:优先顶回"播放正片"原位;React 重渲染后原生按钮会恢复,重新隐藏并注入 if (CURRENT_SITE === 'dj' && injectDjPcPlay()) { reinjectCount = 0; return; } const targetEl = CURRENT_SITE === 'dj' ? getDjTitleEl() : $('h1'); if (!targetEl) return; // 按钮稳定存在 → 复位计数。避免 SPA 频繁重渲染导致 reinjectCount 累积到 MAX_REINJECTS 后永久停摆 if (targetEl.querySelector('.play-btn')) { reinjectCount = 0; return; } // 按钮被 React 移除,重新注入(仅一键搜索) const { playBtn } = createPlayButtonPair(); const btnWrapper = document.createElement('div'); btnWrapper.className = 'adys-btn-group'; btnWrapper.appendChild(playBtn); targetEl.appendChild(btnWrapper); reinjectCount++; }, 300); }); window.playButtonObserver.observe(document.body, { childList: true, subtree: true }); } // 移动端 SPA 路由监听:检测 URL 变化后重新注入按钮 let lastCheckedUrl = location.href; let mobileBtnCheckTimer = null; function setupMobileRouteWatcher() { if (window._adouMobileRouteWatcher) return; window._adouMobileRouteWatcher = true; // MutationObserver:DOM 变化时检查按钮是否存在 if (window.playButtonObserver) { window.playButtonObserver.disconnect(); } window.playButtonObserver = new MutationObserver(() => { if (mobileBtnCheckTimer) return; mobileBtnCheckTimer = setTimeout(() => { mobileBtnCheckTimer = null; // URL 变化时重新检测详情页状态 if (location.href !== lastCheckedUrl) { lastCheckedUrl = location.href; videoDataBase = null; // URL 变化后重置视频数据缓存 injectPlayButton(); return; } // 按钮被 SPA 移除时重新注入 if (isMobile) { const group = document.getElementById('mobile-action-buttons'); if (!group && checkIsDetailPage()) { injectPlayButton(); } } // 回到详情页时重新拉起 URL 轮询(轮询可能已因离开详情页而停止) ensureMobileUrlPoll(); }, 300); }); window.playButtonObserver.observe(document.body, { childList: true, subtree: true }); // 定期轮询 URL 变化(SPA pushState/replaceState 不触发 popstate) ensureMobileUrlPoll(); } // 移动端 URL 轮询:自管理生命周期,离开详情页即停止,回到详情页由观察器重新拉起 let mobileUrlPollTimer = null; function ensureMobileUrlPoll() { if (mobileUrlPollTimer) return; if (!checkIsDetailPage()) return; // 仅 SPA 站(红果/爱看)需要轮询:URL 会随站内导航变化 if (CURRENT_SITE === 'db') return; mobileUrlPollTimer = setInterval(() => { if (location.href !== lastCheckedUrl) { lastCheckedUrl = location.href; videoDataBase = null; // URL 变化后重置视频数据缓存,确保读取新页面数据 if (!checkIsDetailPage()) { // 已离开详情页:停止常驻轮询,避免 1s 定时器永久运行 clearInterval(mobileUrlPollTimer); mobileUrlPollTimer = null; return; } injectPlayButton(); injectGlobalButtons(); // SPA 导航后也重新检查全局按钮 } }, 1000); } // 详情页检测(动态读取当前 URL,不依赖脚本启动时的缓存) function checkIsDetailPage() { const currentHref = location.href; const currentPath = location.pathname; if (CURRENT_SITE === 'dj') { // 红果详情页 URL 格式(含新版 /player/xxx): // 1. /detail?series_id=xxx // 2. /series/xxx(slug+数字ID) // 3. /player/<数字ID>(新版详情/播放页) return (/\/detail/.test(currentPath) && /series_id=/.test(currentHref)) || /\/series\/.+/.test(currentPath) || /\/player\/\d+/.test(currentPath); } else if (CURRENT_SITE === 'db') { return /\/subject\/\d+/.test(currentHref) || (location.hostname === 'm.douban.com' && currentPath.startsWith('/movie/subject/')) || (location.hostname === 'www.douban.com' && currentPath === '/doubanapp/dispatch' && currentHref.includes('/subject/')); } return false; } function injectPlayButton() { if (!checkIsDetailPage()) return; // 电脑版红果详情页:默认不播放页面原生视频(避免与脚本播放器双音) hijackRedFruitAutoplay(); try { if (isMobile) { // 手机版:浮动按钮组 let btnGroup = document.getElementById('mobile-action-buttons'); if (!btnGroup) { btnGroup = document.createElement('div'); btnGroup.id = 'mobile-action-buttons'; btnGroup.className = 'mobile-action-buttons'; document.body.appendChild(btnGroup); } if (!btnGroup.querySelector('.play-btn')) { const { playBtn } = createPlayButtonPair(); btnGroup.appendChild(playBtn); } setupMobileRouteWatcher(); } else { // 电脑版:优先顶替原生"播放正片"按钮位置;找不到再退到标题旁 const injectPc = () => { if (CURRENT_SITE === 'dj' && injectDjPcPlay()) { setupPlayButtonObserver(); return true; } const targetEl = CURRENT_SITE === 'dj' ? getDjTitleEl() : $('h1'); if (!targetEl) return false; if (!targetEl.querySelector('.play-btn')) { const { playBtn } = createPlayButtonPair(); const btnWrapper = document.createElement('div'); btnWrapper.className = 'adys-btn-group'; btnWrapper.appendChild(playBtn); targetEl.appendChild(btnWrapper); setupPlayButtonObserver(); } return true; }; if (!injectPc()) setTimeout(injectPc, 1000); } } catch (e) { tip('播放按钮加载失败,请刷新页面重试'); } } // ==================== 8. 菜单命令 ==================== GM_registerMenuCommand('📤 下载诊断日志', () => { const ok = __diag.download(); if (ok) { try { tip('诊断日志已下载,请将文件发送给开发者排查'); } catch (e) {} } }); GM_registerMenuCommand('清除观看历史', () => { if (confirm('确定要清除所有观看历史吗?这将同时删除所有影片的播放进度和记录。')) { doClearHistory(); } }); { let _autoPlayNextEnabled = GM_getValue(AUTO_NEXT_ENABLED_KEY, true); GM_registerMenuCommand(`${_autoPlayNextEnabled ? '✅' : '❌'} 自动连播`, () => { _autoPlayNextEnabled = !_autoPlayNextEnabled; GM_setValue(AUTO_NEXT_ENABLED_KEY, _autoPlayNextEnabled); tip(`自动连播已${_autoPlayNextEnabled ? '开启' : '关闭'}(仅对多集剧集生效)`); }); } // ==================== 8b. 腾讯片库频道整合(移植自 tx-unified-list) ==================== function initTencentLibrary() { if (CURRENT_SITE !== 'tx') return; // ---------- 频道配置 ---------- const TX_CHANNELS = [ { id: '100173', name: '电影', params: 'sort=75' }, { id: '100113', name: '电视剧', params: 'sort=75' }, { id: '100109', name: '综艺', params: 'sort=75' }, { id: '100119', name: '动漫', params: 'sort=75' }, { id: '100150', name: '少儿', params: 'itype=75' }, { id: '120188', name: '短剧', params: 'attraction=1' }, { id: '100105', name: '纪录片', params: 'sort=75' }, { id: '100103', name: '体育', params: 'irace=11' }, ]; const TX_BASE = 'https://v.qq.com/tv-series-list/v-index.html'; function txChannelUrl(c) { return TX_BASE + '?filter_params=' + c.params + '&page_id=channel_list_second_page&channel_id=' + c.id; } function txCurrentChannelId() { try { return new URL(location.href).searchParams.get('channel_id') || ''; } catch (e) { return ''; } } const txState = { activeTab: null }; // ---------- 样式 ---------- const TX_CSS = ` /* ---------- 自然滚动布局(不锁高度) ---------- */ html, body { margin: 0 !important; padding: 0 !important; width: 100% !important; background: #fff !important; } *, *::before, *::after { box-sizing: border-box !important; } /* ---------- 隐藏腾讯外壳 ---------- */ .top-bar-wrap, .left-bar-wrap, .v-left-nav-area, .top-nav-wrap, .search-wrap, #ssi-user-bar, .back-button, .tag-bar, #login_win, .web-login-container, #toast-container, .gatherable-layer, [class*="gatherable"] { display: none !important; } /* ---------- 底部遮挡清除 ---------- */ .screen-list__footer, .screen-list__loading, .lazy-list__footer, .lazy-list__loading, .filter-expand, .quick_pop_footer, .app-download-bar, .app-download, .download-bar, .bottom-guide, .guide-bottom, .v-bottom-bar, .bottom-bar-wrap, .mobile-tab-bar, .screen-app-download, [class*="bottom-banner"], [class*="app-promote"], [class*="download-app"], [class*="footer-fixed"], [class*="fixed-bottom"], [class*="bottom-fixed"], [class*="guide-bar"], [class*="openapp"], [class*="open-app"], [class*="float-bar"], [class*="floatbar"] { display: none !important; visibility: hidden !important; height: 0 !important; pointer-events: none !important; } /* ---------- 强制全宽,去掉左右偏移 ---------- */ #app, .web-page, .v-station-screen-content, .v-station-content, #root, #app-root, .screen-page, .screen-page__main-content, .screen-list, .content-wrap, vstation-screen-content { width: 100% !important; max-width: 100% !important; min-width: 0 !important; margin: 0 !important; padding-left: 0 !important; padding-right: 0 !important; height: auto !important; overflow: visible !important; display: block !important; } .screen-list { padding: 0 !important; background: #fff !important; position: relative !important; height: calc(100vh - 110px) !important; overflow-y: auto !important; overflow-x: hidden !important; } .grid-waterfall.screen-list__grid { display: block !important; padding: 8px 12px 24px !important; margin: 0 !important; } /* ---------- 顶部 #tx-header(吸顶) ---------- */ #tx-header { position: sticky; top: 0; z-index: 20; background: #fff; padding: 10px 12px 0; box-shadow: 0 1px 4px rgba(0,0,0,.06); text-align: left !important; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif !important; color: #1a1a1a !important; } #tx-header .tx-title { font-size: 15px; font-weight: 700; color: #1a1a1a; margin: 0 0 8px; display: flex !important; align-items: center !important; justify-content: flex-start !important; text-align: left !important; gap: 6px; } #tx-header .tx-title * { text-align: left !important; } #tx-channels { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; gap: 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; padding-bottom: 8px; } #tx-channels::-webkit-scrollbar { display: none; } #tx-channels button { flex: 0 0 auto; border: 0; background: #f0f0f3; color: #555; padding: 7px 16px; border-radius: 16px; font-size: 14px; cursor: pointer; white-space: nowrap; font-family: inherit !important; min-width: 0; line-height: 1.2; } #tx-channels button.active { background: #e6f4ff; color: #1a8cff; font-weight: 700; } /* ---------- 筛选标签栏(放在 header 里) ---------- */ .tx-filter-tabs { display: flex !important; flex-wrap: nowrap !important; flex-direction: row !important; gap: 8px !important; overflow-x: auto !important; padding: 8px 0 !important; background: #fff !important; align-items: center !important; scrollbar-width: none; border-top: 1px solid #f0f0f0; } .tx-filter-tabs::-webkit-scrollbar { display: none; } .tx-tab { flex: 0 0 auto; border: 0; background: #f0f0f3; color: #333; padding: 6px 14px; border-radius: 16px; font-size: 13px; cursor: pointer; white-space: nowrap; font-family: inherit !important; line-height: 1.2; } .tx-tab.tx-active { background: #1a8cff; color: #fff; font-weight: 600; } /* 隐藏原站筛选 DOM */ .filter-line { display: none !important; } /* ---------- 居中弹窗 ---------- */ .tx-filter-modal { position: fixed; inset: 0; z-index: 99999; } .tx-filter-mask { position: absolute; inset: 0; background: rgba(0,0,0,.45); } .tx-filter-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); background: #fff; border-radius: 16px; padding: 20px; width: 320px; max-width: 90vw; max-height: 70vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,.2); } .tx-modal-title { font-size: 16px; font-weight: 700; color: #222; margin-bottom: 14px; } .tx-opt { display: inline-block; padding: 7px 14px; margin: 3px; border-radius: 16px; background: #f5f5f7; border: 0; cursor: pointer; font-size: 13px; color: #555; font-family: inherit !important; line-height: 1.2; } .tx-opt.sel { background: #1a8cff; color: #fff; font-weight: 600; } /* ---------- 卡片墙 grid ---------- */ .grid-waterfall__content, .grid-waterfall__content--web { display: grid !important; grid-template-columns: repeat(3, 1fr) !important; gap: 10px !important; padding: 0 !important; width: 100% !important; max-width: 100% !important; margin: 0 !important; justify-items: stretch !important; align-items: stretch !important; justify-content: stretch !important; } .screen-cover, .screen-cover--pc { width: 100% !important; margin: 0 !important; justify-self: stretch !important; border-radius: 8px !important; background: #fff !important; box-shadow: 0 1px 4px rgba(0,0,0,.08); display: flex !important; flex-direction: column !important; overflow: visible !important; height: auto !important; } .screen-cover .poster-view, .screen-cover .item-poster { width: 100% !important; height: auto !important; aspect-ratio: 2 / 3 !important; position: relative !important; border-radius: 8px 8px 0 0 !important; overflow: hidden !important; } .screen-cover .poster-view__img { width: 100% !important; height: 100% !important; object-fit: cover !important; display: block !important; } .screen-cover .item-info { width: 100% !important; padding: 6px 8px 10px !important; overflow: visible !important; } .screen-cover .item-title { font-size: 13px !important; font-weight: 600 !important; color: #222 !important; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; line-height: 1.3; } .screen-cover .item-secondTitle { font-size: 11px !important; color: #999 !important; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; line-height: 1.3; } .screen-cover .item-tags { display: none !important; } /* ---------- 横版频道 ---------- */ body.tx-landscape .grid-waterfall__content, body.tx-landscape .grid-waterfall__content--web { grid-template-columns: repeat(2, 1fr) !important; gap: 12px !important; } body.tx-landscape .screen-cover .poster-view, body.tx-landscape .screen-cover .item-poster { aspect-ratio: 16 / 9 !important; } body.tx-landscape .screen-cover .item-info { padding: 8px 10px 12px !important; height: auto !important; flex: 1; } body.tx-landscape .screen-cover .item-title { font-size: 14px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; display: block !important; line-height: 1.35; /* 单行显示:长片名继续在右侧空白处续写,不折行 */ } body.tx-landscape .screen-cover .item-secondTitle { margin-top: 4px; overflow: hidden !important; text-overflow: ellipsis; display: block; } /* ---------- PC 宽屏 ---------- */ @media (min-width: 769px) { .grid-waterfall__content, .grid-waterfall__content--web { grid-template-columns: repeat(5, 1fr) !important; gap: 16px !important; } body.tx-landscape .grid-waterfall__content, body.tx-landscape .grid-waterfall__content--web { grid-template-columns: repeat(4, 1fr) !important; gap: 18px !important; } .grid-waterfall.screen-list__grid { padding: 16px 24px !important; } #tx-header { padding: 12px 24px 0 !important; } #tx-header .tx-title { font-size: 17px !important; } #tx-channels button { padding: 8px 20px !important; font-size: 14px !important; } .tx-filter-tabs { padding: 10px 0 !important; } } /* ---------- 窄屏手机 ---------- */ @media (max-width: 600px) { #tx-header { padding: 8px 8px 0 !important; } #tx-channels button { padding: 6px 12px; font-size: 13px; } .grid-waterfall.screen-list__grid { padding: 6px !important; } .grid-waterfall__content, .grid-waterfall__content--web { gap: 6px !important; } body.tx-landscape .grid-waterfall__content, body.tx-landscape .grid-waterfall__content--web { gap: 8px !important; } body.tx-landscape .screen-cover .item-title { font-size: 13px !important; } } `; // ---------- 注入 ---------- function txInjectViewport() { let vp = document.querySelector('meta[name="viewport"]'); if (!vp) { vp = document.createElement('meta'); vp.name = 'viewport'; (document.head || document.documentElement).appendChild(vp); } vp.content = 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'; } function txInjectCSS() { if (typeof GM_addStyle === 'function') GM_addStyle(TX_CSS); else { const s = document.createElement('style'); s.textContent = TX_CSS; (document.head || document.documentElement).appendChild(s); } } // ---------- 顶部频道栏 ---------- function txBuildHeader() { if (document.getElementById('tx-header')) return; const activeId = txCurrentChannelId(); const header = document.createElement('div'); header.id = 'tx-header'; let btns = ''; TX_CHANNELS.forEach(c => { btns += ``; }); header.innerHTML = `
腾讯视频 · 全频道榜单
${btns}
`; const main = document.querySelector('.screen-page__main-content') || document.querySelector('.screen-page') || document.getElementById('app-root'); if (!main) return; main.insertBefore(header, main.firstChild); header.querySelectorAll('#tx-channels button').forEach(btn => { btn.addEventListener('click', () => { const c = TX_CHANNELS.find(x => x.id === btn.dataset.id); if (c) location.href = txChannelUrl(c); }); }); const act = header.querySelector('#tx-channels button.active'); if (act && act.scrollIntoView) act.scrollIntoView({ inline: 'center', block: 'nearest' }); } // ---------- 横竖版自动识别 ---------- function txDetectMode() { const imgs = document.querySelectorAll('.poster-view__img'); let chosen = null; imgs.forEach(im => { if (!chosen && im.complete && im.naturalWidth > 50) chosen = im; }); if (!chosen && imgs.length > 0) chosen = imgs[0]; if (!chosen) { setTimeout(txDetectMode, 500); return; } const apply = () => { const w = chosen.naturalWidth, h = chosen.naturalHeight; if (w && h) { const ratio = w / h; if (ratio > 1.25) document.body.classList.add('tx-landscape'); else document.body.classList.remove('tx-landscape'); } }; if (chosen.complete && chosen.naturalWidth) apply(); else chosen.addEventListener('load', apply, { once: true }); setTimeout(apply, 1000); setTimeout(apply, 2500); } // ---------- 自动加载更多 ---------- function txAutoFill() { const sl = document.querySelector('.screen-list'); if (!sl) return; const tryScroll = () => { if (sl.scrollHeight <= sl.clientHeight + 100) { sl.scrollTop = sl.scrollHeight; setTimeout(tryScroll, 1200); } }; setTimeout(tryScroll, 900); } // ---------- 兜底:干掉贴底的 fixed 浮层 ---------- function txKillBottomOverlays() { const vh = window.innerHeight; const vw = window.innerWidth; document.querySelectorAll('body *').forEach(el => { if (el.id === 'tx-header') return; // 豁免脚本自身播放器根节点整棵子树:播放器打开后注入的底部搜索横栏(.search-container) // 同样是 fixed+贴底+宽屏+窄高,几何上与腾讯原生下载条一致,若不豁免会被本函数误杀, // 导致移动版从腾讯卡片唤起播放后底部搜索栏消失(红果无此函数故正常)。 if (el.closest && el.closest('#dgys2-player-root')) return; if (el.closest && (el.closest('#tx-header') || el.closest('.tx-filter-modal'))) return; let cs; try { cs = getComputedStyle(el); } catch (e) { return; } if (cs.position !== 'fixed') return; const r = el.getBoundingClientRect(); if (r.height < 20 || r.height > vh * 0.5) return; if (r.width < vw * 0.6) return; if (r.bottom >= vh - 8 && r.top > vh * 0.5) { el.style.setProperty('display', 'none', 'important'); } }); } // ---------- 弹窗是否打开 ---------- function txIsModalOpen() { const m = document.querySelector('.tx-filter-modal'); return !!(m && m.style.display !== 'none'); } // ---------- 筛选:标签栏 + 居中弹窗 ---------- function txBuildFilterUI(force) { if (!force && txIsModalOpen()) return; const sl = document.querySelector('.screen-list'); if (!sl) return; const lines = document.querySelectorAll('.filter-line'); if (!lines.length) return; const prev = document.querySelector('.tx-tab.tx-active'); if (prev) txState.activeTab = prev.textContent; document.querySelectorAll('.tx-filter-tabs, .tx-filter-modal').forEach(el => el.remove()); const tabs = document.createElement('div'); tabs.className = 'tx-filter-tabs'; const modal = document.createElement('div'); modal.className = 'tx-filter-modal'; modal.style.display = 'none'; modal.innerHTML = '
'; document.body.appendChild(modal); const panel = modal.querySelector('.tx-filter-panel'); modal.querySelector('.tx-filter-mask').addEventListener('click', () => { modal.style.display = 'none'; }); lines.forEach(line => { const col = line.querySelector(':scope > .filter-column'); if (!col) return; const label = col.textContent.trim(); const opts = line.querySelectorAll(':scope > .lazy-list__content .filter'); const tab = document.createElement('button'); tab.className = 'tx-tab'; tab.textContent = label; tab.addEventListener('click', () => { document.querySelectorAll('.tx-tab').forEach(t => t.classList.remove('tx-active')); tab.classList.add('tx-active'); txState.activeTab = label; panel.innerHTML = '
' + label + '
'; opts.forEach(opt => { const b = document.createElement('button'); const isSel = opt.classList.contains('filter--selected') || opt.classList.contains('filter--selected--dark'); b.className = 'tx-opt' + (isSel ? ' sel' : ''); b.textContent = opt.textContent.trim(); b.addEventListener('click', () => { const want = b.textContent.trim(); let clicked = false; document.querySelectorAll('.filter-line').forEach(freshLine => { const freshCol = freshLine.querySelector(':scope > .filter-column'); if (!freshCol || freshCol.textContent.trim() !== label) return; freshLine.querySelectorAll(':scope > .lazy-list__content .filter').forEach(f => { if (f.textContent.trim() === want) { f.click(); clicked = true; } }); }); if (!clicked) { try { opt.click(); } catch (e) {} } modal.style.display = 'none'; setTimeout(txBuildFilterUI, 60); setTimeout(txBuildFilterUI, 400); setTimeout(txBuildFilterUI, 1200); }); panel.appendChild(b); }); modal.style.display = 'block'; }); tabs.appendChild(tab); }); if (txState.activeTab) { const target = [...tabs.querySelectorAll('.tx-tab')] .find(t => t.textContent === txState.activeTab); if (target) target.classList.add('tx-active'); } const header = document.getElementById('tx-header'); if (header) header.appendChild(tabs); else sl.insertBefore(tabs, sl.firstChild); } // ---------- 海报点击:拦截腾讯跳转,改为弹爱看搜索弹窗(自动搜索片名,点结果弹窗播放) ---------- function txBindCardClick() { if (window._txCardBound) return; window._txCardBound = true; const tryIntercept = (e) => { // 播放器已打开时不再拦截卡片(腾讯自身根节点也叫 #app,不能用 getElementById('app') 判断) if (document.body.classList.contains('adys-playing')) return; const t = e.target; if (!t || !t.closest) return; const cover = t.closest('.screen-cover, .screen-cover--pc'); if (!cover) return; // 片名:优先 .item-title;其次卡片内 a[title] let title = ''; const titleEl = cover.querySelector('.item-title'); if (titleEl) title = (titleEl.textContent || '').replace(/\s+/g, ' ').trim(); if (!title) { const a = cover.querySelector('a[title]'); if (a) title = (a.getAttribute('title') || '').trim(); } if (!title) return; // 取不到片名就放行,避免卡死 e.preventDefault(); e.stopPropagation(); try { e.stopImmediatePropagation(); } catch (err) {} // 海报:卡片内首张图 let poster = ''; const img = cover.querySelector('.poster-view__img, .item-poster img, img'); if (img) poster = img.src || img.getAttribute('data-src') || img.getAttribute('data-original') || ''; try { openSearchModalWithKeyword(title); } catch (err) { try { __diag.warn('tx card search err: ' + err); } catch (_) {} } }; // click 覆盖桌面与 React onClick;touchend 覆盖手机端提前导航 document.addEventListener('click', tryIntercept, true); // 记录 touchstart 位置,touchend 时仅点按(位移很小)才拦截,避免列表滚动抬手误触 let txTouchX = 0, txTouchY = 0; document.addEventListener('touchstart', (e) => { const t = e.targetTouches && e.targetTouches[0]; if (t) { txTouchX = t.clientX; txTouchY = t.clientY; } }, { capture: true, passive: true }); document.addEventListener('touchend', (e) => { const t = e.changedTouches && e.changedTouches[0]; if (t) { const dx = Math.abs(t.clientX - txTouchX), dy = Math.abs(t.clientY - txTouchY); if (dx > 12 || dy > 12) return; // 视为滚动/滑动,不拦截 } tryIntercept(e); }, true); } // ---------- 启动 ---------- function bootTx() { txInjectViewport(); txInjectCSS(); txBuildHeader(); txDetectMode(); txAutoFill(); txKillBottomOverlays(); txBindCardClick(); function waitAndExpand() { const expand = document.querySelector('.filter-expand'); if (expand) { expand.click(); setTimeout(() => txBuildFilterUI(true), 1200); } else { setTimeout(waitAndExpand, 300); } } waitAndExpand(); setTimeout(txBuildHeader, 1500); setTimeout(() => { txDetectMode(); txAutoFill(); txKillBottomOverlays(); }, 3000); setTimeout(txKillBottomOverlays, 5000); if (!window._txObserver) { window._txObserver = observeDebounced(document.body, () => { const main = document.querySelector('.screen-page__main-content') || document.querySelector('.screen-page') || document.getElementById('app-root'); if (main && !document.getElementById('tx-header')) txBuildHeader(); if (!txIsModalOpen() && document.querySelector('.filter-line') && !document.querySelector('.tx-filter-tabs')) { setTimeout(() => { try { txBuildFilterUI(); } catch(e){} }, 300); } }, { delay: 300 }); } } bootTx(); } // ==================== 9. 初始化 ==================== function init() { // 初始化主题 initTheme(); // 数据迁移与清理 migrateLegacyData(); __diag.info('初始化完成: site=' + CURRENT_SITE + ' url=' + location.href); // 全局浮动按钮 injectGlobalButtons(); // 腾讯片库:8 频道整合卡片墙(独立模块,内部已按 CURRENT_SITE==='tx' 守卫) initTencentLibrary(); // 红果顶栏热榜入口 injectHongguoRankEntry(); // 红果分类/热榜自动翻页 initHongguoAutopager(); // 红果列表页海报点击拦截(详情页开关关闭时直接弹窗搜索) initHongguoCardIntercept(); // 详情页播放按钮注入 injectPlayButton(); } // 脚本运行于 document-start,UI 注入需等待 DOM 就绪 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();