// ==UserScript== // @name 课享(courshare/learnspace)自动连播助手·防检测单面板版 // @namespace https://learn.courshare.cn/ // @version 1.5.1 // @license MIT // @description learn.courshare.cn 专用:单面板、跨嵌套 frame 统一找视频;固定 1x 原速;实体开关(自动播放/自动连播/静音/后台保活)+ 上/重播/下;三级导航(小节→讲→章);功能区/日志双 Tab(日志持久化保存);浅色文字深色底保证可读;可拖拽/折叠/悬浮球;设置自动记忆。 // @author QYK // @match *://learn.courshare.cn/* // @match *://*.courshare.cn/* // @run-at document-idle // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // ==/UserScript== /* * ============================================================================ * v1.4.0 修复"讲内多小节时自动切换总回到第一个小节": * 原因:旧版把「左栏讲课」与「上方小节行(0101010000…)」当成同一层目录, * 且当前项判定失败时 idx=-1 仍从第 0 个开始点 → 永远切到第一个。 * 方案:三级导航,逐级推进 * ① 小节行:按纯数字编码顺序,从"当前编码"往后切下一小节; * ② 讲:小节行到边界后,才切左栏下一讲; * ③ 章:本章讲完,才切下一章 Tab。 * 任一当前项判定失败即"不乱点",交给下一级,绝不盲点第一个。 * 辅助:当前小节编码取页面中字号最大的纯数字元素(视频下方大标题)。 * 防检测原则:固定 1x 不调速;不篡改 visibilityState(只读); * 不点大播放按钮;仅自然 ended 后切节;静音为用户可控特性(默认开)。 * ============================================================================ */ (function () { 'use strict'; if (window.top !== window) return; // 只在最顶层建唯一面板 /* ------------------------- 存储 ------------------------- */ const store = { get(key, def) { try { if (typeof GM_getValue === 'function') return GM_getValue(key, def); const v = localStorage.getItem('csa2_' + key); return v === null ? def : JSON.parse(v); } catch (e) { return def; } }, set(key, val) { try { if (typeof GM_setValue === 'function') return GM_setValue(key, val); localStorage.setItem('csa2_' + key, JSON.stringify(val)); } catch (e) {} } }; const cfg = { autoNext: store.get('autoNext', true), // 自动连播:播完切下一节 autoPlay: store.get('autoPlay', true), // 自动播放:被暂停自动恢复 mute: store.get('mute', true), // 静音:默认开 keepAlive: store.get('keepAlive', true) // 后台保活 }; let inited = false; let panel = null; let lastEndedAt = 0; let statusBadge = null, progressText = null, logListEl = null; let statusTimer = null; const $ = (sel, root) => (root || document).querySelector(sel); const $$ = (sel, root) => Array.from((root || document).querySelectorAll(sel)); const clsOf = el => (el && typeof el.className === 'string') ? el.className : ''; const isVisible = el => { if (!el || el.nodeType !== 1) return false; const r = el.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return false; const cs = getComputedStyle(el); return cs.display !== 'none' && cs.visibility !== 'hidden' && cs.opacity !== '0'; }; const fmt = s => { if (!isFinite(s) || s < 0) return '--:--'; s = Math.floor(s); const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; const mm = String(m).padStart(2, '0'), ss = String(sec).padStart(2, '0'); return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`; }; /* ------------------------- 日志(持久化保存) ------------------------- */ const MAX_LOG = 300; let logHistory = []; try { const h = store.get('log', []); if (Array.isArray(h)) logHistory = h.slice(-MAX_LOG); } catch (e) {} function pushLog(line) { logHistory.push(line); if (logHistory.length > MAX_LOG) logHistory = logHistory.slice(-MAX_LOG); try { store.set('log', logHistory); } catch (e) {} } function renderLog() { if (!logListEl) return; const frag = document.createDocumentFragment(); logHistory.slice(-200).forEach(line => { const d = document.createElement('div'); d.className = 'csa2-logline'; d.textContent = line; frag.appendChild(d); }); logListEl.innerHTML = ''; logListEl.appendChild(frag); logListEl.scrollTop = logListEl.scrollHeight; } function log(msg) { const line = `[${new Date().toTimeString().slice(0, 8)}] ${msg}`; pushLog(line); if (logListEl) { const d = document.createElement('div'); d.className = 'csa2-logline'; d.textContent = line; logListEl.appendChild(d); while (logListEl.children.length > 200) logListEl.removeChild(logListEl.firstChild); logListEl.scrollTop = logListEl.scrollHeight; } console.log('%c[自动连播]', 'color:#16a34a', msg); } /* ============================================================================ * 一、跨 frame 基础 * ========================================================================== */ function allDocs() { const docs = [document]; const walk = win => { let frames; try { frames = win.frames; } catch (e) { return; } for (let i = 0; i < frames.length; i++) { try { const d = frames[i].document; if (d) { docs.push(d); walk(frames[i]); } } catch (e) { /* 跨域跳过 */ } } }; walk(window); return docs; } function findVideos() { const out = []; allDocs().forEach(d => $$('video', d).forEach(v => { if (isVisible(v)) out.push(v); })); return out; } function pickMainVideo() { const vids = findVideos(); let best = null; vids.forEach(v => { if ((v.duration || 0) > 5 && (!best || v.currentTime > 0)) best = v; }); return best || vids[0] || null; } /* ============================================================================ * 二、通用判定:高亮 / 当前 / 完成 / 锁定 * ========================================================================== */ const ACTIVE_RE = /(^|[\s_-])(active|current|selected|cur|on|is-on|playing|checked|choose)([\s_-]|$)/i; const DONE_RE = /done|complete|finished|learned|已完成|已学完|已学习|√|✔|✓/; const LOCK_RE = /lock|locked|锁|未开放|未解锁/; /* 高亮判定:class 命中 或 蓝色系底色(小节行/Tab 高亮多为蓝底白字) */ function isHighlighted(el) { let n = el; for (let i = 0; i < 4 && n; i++, n = n.parentElement) { const cls = clsOf(n); if (cls && ACTIVE_RE.test(cls)) return true; let bg = ''; try { bg = getComputedStyle(n).backgroundColor; } catch (e) {} const m = bg && bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/); if (m) { const r = +m[1], g = +m[2], b = +m[3], a = m[4] !== undefined ? +m[4] : 1; if (a > 0.25 && b > 140 && b >= r + 30 && b >= g + 10) return true; } } return false; } /* 当前项:看自身及最多 3 层祖先的 class / aria(平台常把 active 挂外层) */ function isActive(el) { let n = el; for (let i = 0; i < 3 && n; i++, n = n.parentElement) { const cls = clsOf(n); if (cls && ACTIVE_RE.test(cls)) return true; if (n.getAttribute && (n.getAttribute('aria-current') === 'true' || n.getAttribute('aria-selected') === 'true')) return true; } return false; } const isDone = el => DONE_RE.test(clsOf(el) + ' ' + (el.textContent || '')); const isLocked = el => LOCK_RE.test(clsOf(el) + ' ' + (el.outerHTML || '')); /* ============================================================================ * 三、① 小节行导航(纯数字编码,如 0101010000) * ========================================================================== */ function digitItems() { const out = []; allDocs().forEach(d => { $$('a,button,li,div,span', d).forEach(el => { const t = (el.textContent || '').trim(); if (!/^\d{6,}$/.test(t)) return; // 纯数字编码 if (t.length > 24) return; // 排除整行拼接(多个编码相连) if (!isVisible(el)) return; // 可点击性:标签 / onclick / 祖先 cursor:pointer 或语义类名 let clickable = el.tagName === 'A' || el.tagName === 'BUTTON' || el.tagName === 'LI' || !!el.onclick; let n = el; for (let i = 0; i < 3 && n; i++, n = n.parentElement) { try { if (getComputedStyle(n).cursor === 'pointer') { clickable = true; break; } } catch (e) {} if (n.onclick) { clickable = true; break; } if (/(^|[\s_-])(item|tab|cell|section|node|chapter|lesson|sub|point|li)([\s_-]|$)/i.test(clsOf(n))) { clickable = true; break; } } if (!clickable) return; const r = el.getBoundingClientRect(); out.push({ el, code: t, r }); }); }); // 去重(父子元素可能都被捕获) const seen = new Set(), res = []; out.forEach(o => { const k = o.code + '@' + Math.round(o.r.left) + ',' + Math.round(o.r.top); if (!seen.has(k)) { seen.add(k); res.push(o); } }); // 先按行、再按 x 排序(小节行横向排列) res.sort((a, b) => (Math.round(a.r.top / 10) - Math.round(b.r.top / 10)) || (a.r.left - b.r.left)); return res; } /* 当前小节编码:取页面中字号最大的纯数字元素(视频下方大标题) */ function getCurrentCode() { let code = null, size = 0; allDocs().forEach(d => $$('div,span,h1,h2,h3,h4,strong,b,p,a,td', d).forEach(el => { const t = (el.textContent || '').trim(); if (!/^\d{6,}$/.test(t) || t.length > 24) return; if (!isVisible(el)) return; let fs = 0; try { fs = parseFloat(getComputedStyle(el).fontSize) || 0; } catch (e) {} if (fs > size) { size = fs; code = t; } })); return code; } /* 在当前小节行内前后移动;到行边界返回 false(交给上层切讲/章) */ function gotoSubSection(dir) { const items = digitItems(); if (items.length < 2) return false; // 按纵向位置分行(小节行是横向排列,同一行 top 接近) const rows = []; items.forEach(o => { const row = rows.find(r => Math.abs(r[0].r.top - o.r.top) < 14); if (row) row.push(o); else rows.push([o]); }); rows.forEach(r => r.sort((a, b) => a.r.left - b.r.left)); const curCode = getCurrentCode(); let row = null, idx = -1; // 优先:包含当前编码(或高亮项)且至少 2 项的 "行" for (const r of rows) { if (r.length < 2) continue; // 跳过孤立的大标题 let i = curCode ? r.findIndex(o => o.code === curCode) : -1; if (i < 0) i = r.findIndex(o => isHighlighted(o.el)); if (i >= 0) { row = r; idx = i; break; } } // 兜底:取项数最多的行 + 高亮项 if (!row) { row = rows.filter(r => r.length >= 2).sort((a, b) => b.length - a.length)[0]; if (!row) return false; idx = row.findIndex(o => isHighlighted(o.el)); if (idx < 0) return false; // 判定失败:绝不盲点第一个 } const target = row[idx + (dir === 'next' ? 1 : -1)]; if (!target) return false; // 行首/行尾 → 交给上层 target.el.click(); log((dir === 'next' ? '下一小节' : '上一小节') + ':' + target.code); return true; } /* ============================================================================ * 四、② 左栏"讲"目录 * ========================================================================== */ const ITEM_SELECTORS = [ 'a[href*="itemId"]', 'a[href*="item_id"]', '[onclick*="itemId"]', '[onclick*="goItem"]', '[onclick*="previewItem"]', 'li[onclick]', '.tree li', '[class*="chapter"] li', '[class*="catalog"] li', '[class*="directory"] li', '[class*="outline"] li', '[class*="section"] li', '[class*="course-item"]', '[class*="node-item"]', '[class*="menu-item"]', '[class*="item"] a', '[class*="node"] a' ]; function getCatalogItems() { const set = new Map(); allDocs().forEach(d => { ITEM_SELECTORS.forEach(sel => $$(sel, d).forEach(el => { const txt = (el.textContent || '').trim(); if (!txt || txt.length >= 80) return; if (/^\d{6,}$/.test(txt)) return; // 排除小节编码 if (/^第\s*\d+\s*章/.test(txt)) return; // 排除章节 Tab set.set(el, txt); })); }); return Array.from(set.keys()).filter(isVisible); } function gotoLecture(dir) { const items = getCatalogItems(); if (!items.length) return false; let idx = items.findIndex(isActive); if (idx < 0) idx = items.findIndex(el => isHighlighted(el)); if (idx < 0) return false; // 不乱点第一个 const step = dir === 'next' ? 1 : -1; for (let k = idx + step; k >= 0 && k < items.length; k += step) { const el = items[k]; if (isLocked(el)) continue; if (dir === 'next' && isDone(el)) continue; el.scrollIntoView({ block: 'nearest' }); el.click(); log((dir === 'next' ? '已切换到' : '已回退到') + ':' + (el.textContent || '').trim().slice(0, 30)); return true; } return false; } /* ============================================================================ * 五、③ 章节 Tab * ========================================================================== */ function gotoChapter(dir) { const raw = []; allDocs().forEach(d => $$('a,button,li,div,span', d).forEach(el => { const t = (el.textContent || '').trim(); if (!/^第\s*\d+\s*章/.test(t) || t.length > 40) return; if (!isVisible(el)) return; raw.push({ el, t, left: el.getBoundingClientRect().left }); })); const seen = new Set(), list = []; raw.forEach(o => { const k = o.t + '@' + Math.round(o.left); if (!seen.has(k)) { seen.add(k); list.push(o); } }); if (list.length < 2) return false; let idx = list.findIndex(o => isHighlighted(o.el)); if (idx < 0) idx = list.findIndex(o => isActive(o.el)); if (idx < 0) return false; const target = list[idx + (dir === 'next' ? 1 : -1)]; if (!target) return false; target.el.click(); log((dir === 'next' ? '下一章' : '上一章') + ':' + target.t.slice(0, 20)); return true; } /* 播放器自带"上/下一节"按钮兜底(按文字/class 识别) */ function clickNavButton(dir) { const re = dir === 'next' ? /下一节|下一讲|下一课|下一单元|next/iu : /上一节|上一讲|上一课|上一单元|prev|previous/iu; for (const d of allDocs()) { const hit = $$('a,button,div,span', d).filter(isVisible) .find(el => { const t = (el.textContent || '').trim(); return t.length >= 1 && t.length < 12 && re.test(t); }); if (hit) { hit.click(); return true; } } for (const d of allDocs()) { const sel = dir === 'next' ? '[class*="next"]' : '[class*="prev"],[class*="previous"]'; const btn = $(sel, d); if (btn && isVisible(btn)) { btn.click(); return true; } } return false; } /* 统一入口:小节 → 讲 → 原生按钮 → 章,逐级推进 */ function advanceNav(dir) { if (gotoSubSection(dir)) return true; // ① 小节行优先(修复本次问题) if (gotoLecture(dir)) return true; // ② 小节走完 → 下一讲 if (clickNavButton(dir)) return true; // ③ 播放器原生按钮兜底 if (gotoChapter(dir)) return true; // ④ 本章讲完 → 下一章 log('已到边界,未找到可切换的内容'); return false; } /* ============================================================================ * 六、视频绑定:只在自然 ended 后切节;被动续播按开关 * ========================================================================== */ function applyAudio() { findVideos().forEach(v => { try { v.muted = cfg.mute; } catch (e) {} }); } function bindVideo(v) { if (!v || v.__csa2Bound) return; v.__csa2Bound = true; try { v.playbackRate = 1; } catch (e) {} // 仅固化 1x,绝不写回其它倍速 try { v.muted = cfg.mute; } catch (e) {} v.addEventListener('ended', () => { const now = Date.now(); if (now - lastEndedAt < 2000) return; lastEndedAt = now; if (cfg.autoNext) { log('本节自然播完,切换下一节…'); setTimeout(() => advanceNav('next'), 800); } }); v.addEventListener('pause', () => { if (v.ended) return; setTimeout(() => { if (v.ended || !v.paused) return; const hidden = document.hidden; const shouldResume = cfg.autoPlay || (cfg.keepAlive && hidden); if (!shouldResume) return; if (cfg.keepAlive && hidden && !v.muted) { try { v.muted = true; } catch (e) {} } v.play().catch(() => {}); }, 500); }); } function refreshStatus() { findVideos().forEach(bindVideo); applyAudio(); const v = pickMainVideo(); if (!v) { if (statusBadge) { statusBadge.textContent = '等待视频'; statusBadge.className = 'csa2-badge idle'; } if (progressText) progressText.textContent = '未检测到视频'; return; } const dur = v.duration || 0; const pct = dur > 0 ? Math.min(100, (v.currentTime / dur) * 100) : 0; if (statusBadge) { statusBadge.textContent = v.paused ? '已暂停' : '播放中'; statusBadge.className = 'csa2-badge ' + (v.paused ? 'pause' : 'run'); } if (progressText) progressText.textContent = `${fmt(v.currentTime)} / ${fmt(dur)} (${pct.toFixed(1)}%)`; } /* ============================================================================ * 七、面板 * ========================================================================== */ function buildPanel() { if (inited) return; inited = true; const css = ` #csa2-panel{position:fixed;right:18px;bottom:96px;width:300px;z-index:2147483647;font:13px/1.55 "Microsoft YaHei",system-ui,sans-serif;color:#f1f5f9; background:linear-gradient(165deg,#243049 0%,#1a2233 100%);border:1px solid #3a4861;border-radius:12px;box-shadow:0 12px 38px rgba(0,0,0,.5);} #csa2-panel *{box-sizing:border-box;} #csa2-head{display:flex;align-items:center;gap:8px;padding:9px 12px;cursor:move;border-bottom:1px solid #3a4861;border-radius:12px 12px 0 0;background:linear-gradient(90deg,#0f5132,#16a34a);} #csa2-head .csa2-title{flex:1;font-weight:700;color:#ffffff;} #csa2-head .csa2-ic{width:22px;height:22px;line-height:20px;text-align:center;border-radius:6px;background:rgba(255,255,255,.18);color:#ffffff;cursor:pointer;font-size:14px;user-select:none;} #csa2-head .csa2-ic:hover{background:rgba(255,255,255,.34);} #csa2-body{padding:12px;display:flex;flex-direction:column;gap:11px;} .csa2-tabs{display:flex;gap:6px;} .csa2-tab{flex:1;padding:7px 0;border:1px solid #3a4861;background:#22304a;color:#aeb9cc;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;transition:all .15s;} .csa2-tab.on{background:#16a34a;border-color:#16a34a;color:#ffffff;} .csa2-tab:hover{filter:brightness(1.12);} .csa2-pane{display:flex;flex-direction:column;gap:11px;} .csa2-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;} .csa2-badge{padding:2px 9px;border-radius:99px;font-size:12px;font-weight:700;} .csa2-badge.run{background:#14532d;color:#86efac;} .csa2-badge.pause{background:#78350f;color:#fcd34d;} .csa2-badge.idle{background:#3a4861;color:#cbd5e1;} #csa2-progress{flex:1;color:#cbd5e1;font-variant-numeric:tabular-nums;font-size:12px;} .csa2-sec-label{color:#aeb9cc;font-size:12px;width:48px;flex:0 0 auto;} .csa2-fixed{padding:4px 10px;background:#22304a;color:#86efac;border:1px solid #16a34a;border-radius:7px;font-size:12px;font-weight:600;} .csa2-sw{display:flex;align-items:center;gap:9px;cursor:pointer;user-select:none;font-size:12.5px;color:#e2e8f0 !important;} .csa2-sw input{position:absolute;width:0;height:0;opacity:0;} .csa2-track{position:relative;width:38px;height:20px;border-radius:99px;background:#3a4861;transition:background .2s;flex:0 0 auto;} .csa2-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#f1f5f9;transition:transform .2s;box-shadow:0 1px 2px rgba(0,0,0,.4);} .csa2-sw input:checked + .csa2-track{background:#16a34a;} .csa2-sw input:checked + .csa2-track .csa2-knob{transform:translateX(18px);} .csa2-sw input:focus-visible + .csa2-track{box-shadow:0 0 0 2px #4ade80;} .csa2-sw.on{color:#86efac !important;} .csa2-sw-label{flex:1;line-height:1.35;color:#e2e8f0 !important;-webkit-text-fill-color:#e2e8f0 !important;} .csa2-sw.on .csa2-sw-label{color:#86efac !important;-webkit-text-fill-color:#86efac !important;} .csa2-ops{display:flex;gap:8px;} .csa2-ops button{flex:1;padding:8px 0;border:0;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;background:#33415a;color:#f1f5f9;transition:filter .12s, transform .05s;} .csa2-ops button.primary{background:#16a34a;color:#ffffff;} .csa2-ops button:hover{filter:brightness(1.15);} .csa2-ops button:active{transform:translateY(1px);} .csa2-loglist{max-height:210px;overflow:auto;background:#0e1626;border:1px solid #2a3754;border-radius:8px;padding:8px;font-size:11.5px;line-height:1.7;color:#aeb9cc;white-space:pre-wrap;word-break:break-all;} .csa2-logline{padding:1px 0;border-bottom:1px dashed #22304a;} .csa2-logline:last-child{border-bottom:0;} .csa2-logclear{align-self:flex-end;padding:5px 12px;background:#33415a;color:#f1f5f9;border:0;border-radius:7px;cursor:pointer;font-size:12px;} .csa2-logclear:hover{filter:brightness(1.15);} #csa2-ball{position:fixed;right:18px;bottom:96px;width:46px;height:46px;border-radius:50%;background:#16a34a;color:#ffffff;display:none;align-items:center;justify-content:center;z-index:2147483647;cursor:pointer;font-size:20px;box-shadow:0 8px 22px rgba(0,0,0,.45);} `; if (typeof GM_addStyle === 'function') GM_addStyle(css); else { const st = document.createElement('style'); st.textContent = css; document.head.appendChild(st); } panel = document.createElement('div'); panel.id = 'csa2-panel'; panel.innerHTML = `
🎓自动连播(防检测) − ×
检测中 --:-- / --:--
倍速 1x 原速 · 固定
`; document.body.appendChild(panel); statusBadge = $('#csa2-status', panel); progressText = $('#csa2-progress', panel); logListEl = $('#csa2-loglist', panel); const bindToggle = (id, key, after) => { const el = $('#' + id, panel); el.checked = cfg[key]; el.parentElement.classList.toggle('on', cfg[key]); el.onchange = () => { cfg[key] = el.checked; store.set(key, cfg[key]); el.parentElement.classList.toggle('on', el.checked); if (after) after(); }; }; bindToggle('t-play', 'autoPlay'); bindToggle('t-next', 'autoNext'); bindToggle('t-mute', 'mute', applyAudio); bindToggle('t-keep', 'keepAlive'); /* ---- 双 Tab 切换:功能区 / 日志 ---- */ const tabFunc = $('#csa2-tab-func', panel); const tabLog = $('#csa2-tab-log', panel); const paneFunc = $('#csa2-pane-func', panel); const paneLog = $('#csa2-pane-log', panel); function showTab(isFunc) { tabFunc.classList.toggle('on', isFunc); tabLog.classList.toggle('on', !isFunc); paneFunc.style.display = isFunc ? 'flex' : 'none'; paneLog.style.display = isFunc ? 'none' : 'flex'; if (!isFunc) renderLog(); // 切到日志时刷新(显示已保存记录) } tabFunc.onclick = () => showTab(true); tabLog.onclick = () => showTab(false); /* ---- 清空日志(同时清掉持久化记录) ---- */ $('#csa2-logclear', panel).onclick = () => { logHistory = []; try { store.set('log', []); } catch (e) {} renderLog(); log('日志已清空'); }; $('#b-prev', panel).onclick = () => advanceNav('prev'); $('#b-next', panel).onclick = () => advanceNav('next'); $('#b-replay', panel).onclick = () => { const v = pickMainVideo(); if (v) { try { v.currentTime = 0; v.play().catch(() => {}); } catch (e) {} log('已重播本节'); } else log('未检测到视频,无法重播'); }; const ball = document.createElement('div'); ball.id = 'csa2-ball'; ball.textContent = '▶'; document.body.appendChild(ball); $('#csa2-fold', panel).onclick = () => { const body = $('#csa2-body', panel); const hidden = body.style.display === 'none'; body.style.display = hidden ? 'flex' : 'none'; $('#csa2-fold', panel).textContent = hidden ? '−' : '+'; }; $('#csa2-hide', panel).onclick = () => { panel.style.display = 'none'; ball.style.display = 'flex'; }; ball.onclick = () => { panel.style.display = 'block'; ball.style.display = 'none'; }; /* 拖拽(关键:拖动时清掉 right/bottom,改用 left/top,避免面板被拉高、背景不跟手) */ let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false; $('#csa2-head', panel).addEventListener('mousedown', e => { if (e.target.classList.contains('csa2-ic')) return; dragging = true; sx = e.clientX; sy = e.clientY; const rect = panel.getBoundingClientRect(); panel.style.right = 'auto'; panel.style.bottom = 'auto'; panel.style.left = rect.left + 'px'; panel.style.top = rect.top + 'px'; ox = rect.left; oy = rect.top; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (!dragging) return; panel.style.left = Math.max(0, ox + e.clientX - sx) + 'px'; panel.style.top = Math.max(0, oy + e.clientY - sy) + 'px'; }); document.addEventListener('mouseup', () => dragging = false); /* 后台保活:隐藏时被暂停 → 续播(静音方可稳过浏览器节流) */ document.addEventListener('visibilitychange', () => { if (document.hidden && cfg.keepAlive) { const v = pickMainVideo(); if (v && v.paused && !v.ended) { if (!v.muted) { try { v.muted = true; } catch (e) {} } v.play().catch(() => {}); } } }); applyAudio(); renderLog(); log('就绪:1x原速 · 静音 · 自动连播'); } /* ------------------------- 启动 ------------------------- */ let bootTries = 0; function boot() { const hasMedia = findVideos().length > 0 || getCatalogItems().length > 0 || digitItems().length > 0; if (hasMedia) { buildPanel(); refreshStatus(); clearInterval(statusTimer); statusTimer = setInterval(refreshStatus, 2000); } else if (++bootTries < 90) { setTimeout(boot, 2000); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); })();