// ==UserScript== // @name 云南开放大学-暴力自动阅读小小站 // @namespace http://scriptcat.org/ // @version 2.7.0 // @description 支持视频自动播放 + PDF简单自动停留阅读等,验证平台是否“只需打开停留即可完成”,可支持倍速播放。过程中可能会有作业+考试+章节+期末考试+下线考试等资料都可以反馈小小站1077472126 // @match https://teach.ynou.edu.cn/* // @grant none // @run-at document-idle // @grant GM_getResourceText // @grant GM_getValue // @grant GM_info // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // @antifeature ads 课程解答 // @license MIT // ==/UserScript== (function () { 'use strict'; const PANEL_ID = '__course_list_panel_v270__'; const HIGHLIGHT_CLASS = '__course_list_highlight_v270__'; const STORAGE_KEY = '__clp_v270_progress__'; const STORAGE_CONFIG_KEY = '__clp_v270_config__'; let courseItems = []; let filteredItems = []; let statusText = '就绪'; let monitorTimer = null; let selectionObserver = null; let panelCollapsed = false; let lastSelectedId = ''; let lastVideoSrc = ''; let lastPdfSrc = ''; let currentVideoBound = null; let currentBoundDoc = null; let autoNextEnabled = true; let autoLocateCurrent = true; let keywordCache = ''; let isAutoAdvancing = false; let lastEndedToken = ''; let lastNearEndToken = ''; let playbackRate = 1.0; let progressStore = {}; let rangeStart = ''; let rangeEnd = ''; let resumeEnabled = true; let pdfSimpleEnabled = true; let pdfReadSeconds = 30; let pdfTimerToken = ''; let pdfTimerStartedAt = 0; let pdfTimerLeft = 0; let pdfTimerHandle = null; let saveProgressThrottle = 0; let activeResumeToken = ''; let lastScanAt = 0; let dragState = { dragging: false, startX: 0, startY: 0, startLeft: 0, startTop: 0 }; function getText(el) { if (!el) return ''; const direct = (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim(); if (direct) return direct; const attrs = [ el.getAttribute?.('title'), el.getAttribute?.('aria-label'), el.getAttribute?.('alt'), el.getAttribute?.('data-title'), el.getAttribute?.('data-name') ].filter(Boolean); return attrs.join(' ').replace(/\s+/g, ' ').trim(); } function getAnchorDisplayText(a, index = 0) { if (!a) return `第${index + 1}项`; const candidates = [ a.getAttribute('title'), a.getAttribute('aria-label'), a.getAttribute('data-title'), a.getAttribute('data-name'), getText(a) ]; const li = a.closest('li'); const parent = a.parentElement; if (li) { candidates.push(li.getAttribute?.('title')); candidates.push(getText(li)); } if (parent) { candidates.push(parent.getAttribute?.('title')); candidates.push(getText(parent)); } let text = candidates .filter(Boolean) .map(x => String(x).replace(/\s+/g, ' ').trim()) .find(x => x && x.length > 0); if (!text) text = `第${index + 1}项`; if (text.length > 120) text = text.slice(0, 120).trim(); return text; } function escapeHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); } function toInt(v) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : 0; } function hasNumberPrefix(text) { return /^\d+[/.\-、()()]?\s*/.test(String(text || '').trim()); } function loadJson(key, fallback) { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } } function saveJson(key, data) { try { localStorage.setItem(key, JSON.stringify(data)); } catch (e) { console.warn('localStorage 保存失败:', e); } } function loadConfig() { const cfg = loadJson(STORAGE_CONFIG_KEY, {}); playbackRate = Number(cfg.playbackRate || 1.0); rangeStart = cfg.rangeStart ?? ''; rangeEnd = cfg.rangeEnd ?? ''; autoNextEnabled = cfg.autoNextEnabled ?? true; autoLocateCurrent = cfg.autoLocateCurrent ?? true; keywordCache = cfg.keywordCache ?? ''; resumeEnabled = cfg.resumeEnabled ?? true; panelCollapsed = cfg.panelCollapsed ?? false; pdfSimpleEnabled = cfg.pdfSimpleEnabled ?? true; pdfReadSeconds = Number(cfg.pdfReadSeconds || 30); } function saveConfig() { saveJson(STORAGE_CONFIG_KEY, { playbackRate, rangeStart, rangeEnd, autoNextEnabled, autoLocateCurrent, keywordCache, resumeEnabled, panelCollapsed, pdfSimpleEnabled, pdfReadSeconds }); } function loadProgressStore() { progressStore = loadJson(STORAGE_KEY, {}); } function saveProgressStore() { saveJson(STORAGE_KEY, progressStore); } function setStatus(text) { statusText = text; const el = document.getElementById('__clp_status__'); if (el) el.textContent = text; } function isVisible(el, doc = document) { if (!el) return false; try { const rect = el.getBoundingClientRect(); const style = (doc.defaultView || window).getComputedStyle(el); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 8 && rect.height > 8; } catch { return false; } } function removeOldHighlight() { document.querySelectorAll('.' + HIGHLIGHT_CLASS).forEach(el => { el.classList.remove(HIGHLIGHT_CLASS); }); } function highlightElement(el) { if (!el) return; removeOldHighlight(); el.classList.add(HIGHLIGHT_CLASS); try { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch {} } function isCatalogPage() { return !!document.querySelector('.r-tree.dTree2') || !!document.querySelector('a[onclick*="playFile"]') || !!document.querySelector('a[href*="playVideo"]') || !!document.querySelector('iframe[src*="pdfjs/web/viewer.html"]'); } function isPlayVideoPage() { return /\/play\/playVideo\.action/i.test(location.href); } function getPageMode() { if (isCatalogPage()) return 'catalog'; if (isPlayVideoPage()) return 'player'; return 'unknown'; } function getCurrentSelectedAnchor() { const selectors = [ '.r-tree.dTree2 a.selected', '.r-tree.dTree2 a.curSelectedNode', '.r-tree.dTree2 a.nodeSel', 'a.selected', 'li.current a', 'li.active a', 'a[aria-current="page"]' ]; for (const sel of selectors) { const el = document.querySelector(sel); if (el) return el; } return null; } function getVideoRootLi() { const spans = [...document.querySelectorAll('.r-tree.dTree2 li.parent_li > span.parent_tit, span.parent_tit')]; for (const span of spans) { const text = getText(span); if (text.includes('视频课堂') || text.includes('视频') || text.includes('章节辅导') || text.includes('课程资源')) { return span.closest('li.parent_li, li'); } } return null; } function uniqueBy(arr, keyFn) { const map = new Map(); arr.forEach(item => { const key = keyFn(item); if (!map.has(key)) map.set(key, item); }); return [...map.values()]; } function getSameOriginDocsLevel1() { const list = [{ doc: document, path: 'top', frame: null }]; const iframes = [...document.querySelectorAll('iframe, frame')]; for (let i = 0; i < iframes.length; i++) { const f = iframes[i]; try { const doc = f.contentDocument || f.contentWindow?.document; if (doc) { list.push({ doc, path: `frame${i}`, frame: f }); } } catch {} } return list; } function getVisibleVideoDetail() { const docs = getSameOriginDocsLevel1(); let fallback = null; for (const ctx of docs) { let videos = []; try { videos = [...ctx.doc.querySelectorAll('video')]; } catch {} const visible = videos.filter(v => isVisible(v, ctx.doc)); if (visible.length) { return { video: visible[0], doc: ctx.doc, frame: ctx.frame, source: ctx.path, visible: true }; } if (!fallback && videos.length) { fallback = { video: videos[0], doc: ctx.doc, frame: ctx.frame, source: ctx.path, visible: false }; } } return fallback || { video: null, doc: document, frame: null, source: 'none', visible: false }; } function getVisiblePdfDetail() { const candidates = [...document.querySelectorAll('iframe, embed, object')]; const visibleCandidates = candidates.filter(el => { const src = el.getAttribute('src') || el.getAttribute('data') || ''; return /pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(src) && isVisible(el); }); if (visibleCandidates.length) { const el = visibleCandidates[0]; return { element: el, src: el.getAttribute('src') || el.getAttribute('data') || '', tag: el.tagName.toLowerCase(), visible: true }; } const any = candidates.find(el => { const src = el.getAttribute('src') || el.getAttribute('data') || ''; return /pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(src); }); if (any) { return { element: any, src: any.getAttribute('src') || any.getAttribute('data') || '', tag: any.tagName.toLowerCase(), visible: false }; } return { element: null, src: '', tag: '', visible: false }; } function getVisibleVideo() { return getVisibleVideoDetail().video || null; } function getCurrentContentInfo() { const videoDetail = getVisibleVideoDetail(); if (videoDetail.video) { const video = videoDetail.video; const currentTime = Number(video.currentTime || 0); const duration = Number(video.duration || 0); const progress = duration > 0 ? (currentTime / duration) * 100 : 0; return { type: 'video', exists: true, src: video.currentSrc || video.src || '', paused: !!video.paused, muted: !!video.muted, currentTime, duration, progress, ended: !!video.ended, readyState: Number(video.readyState || 0), source: videoDetail.source || 'top' }; } const pdfDetail = getVisiblePdfDetail(); if (pdfDetail.element) { return { type: 'pdf', exists: true, src: pdfDetail.src || '', paused: false, muted: false, currentTime: 0, duration: 0, progress: 0, ended: false, readyState: 1, source: pdfDetail.tag || 'iframe' }; } return { type: 'none', exists: false, src: '', paused: true, muted: false, currentTime: 0, duration: 0, progress: 0, ended: false, readyState: 0, source: 'none' }; } async function waitForAnyVideo(timeout = 12000) { const start = Date.now(); while (Date.now() - start < timeout) { const detail = getVisibleVideoDetail(); if (detail.video) return detail; await wait(500); } return { video: null, doc: document, frame: null, source: 'timeout', visible: false }; } function formatTime(sec) { if (!Number.isFinite(sec)) return '--:--'; const s = Math.floor(sec % 60); const m = Math.floor((sec / 60) % 60); const h = Math.floor(sec / 3600); if (h > 0) return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } function getVideoInfo(video = null) { const detail = getVisibleVideoDetail(); const target = video || detail.video; if (!target) { return { exists: false, src: '', paused: true, muted: false, currentTime: 0, duration: 0, ended: false, readyState: 0, progress: 0, playbackRate, source: detail.source, inIframe: !!detail.frame }; } const currentTime = Number(target.currentTime || 0); const duration = Number(target.duration || 0); const progress = duration > 0 ? (currentTime / duration) * 100 : 0; return { exists: true, src: target.currentSrc || target.src || '', paused: !!target.paused, muted: !!target.muted, currentTime, duration, ended: !!target.ended, readyState: Number(target.readyState || 0), progress, playbackRate: Number(target.playbackRate || 1), source: detail.source || 'top', inIframe: !!detail.frame }; } function getCourseKey(item) { if (!item) return ''; return String(item.id || item.dataId || item.text || item.index || location.href); } function getCurrentSelectedItem() { const current = getCurrentSelectedAnchor(); if (!current) return null; return courseItems.find(item => item.anchor === current || item.id === current.id) || null; } function getCurrentPseudoItem() { const current = getCurrentSelectedItem(); if (current) return current; const title = document.title || '当前内容'; const params = new URLSearchParams(location.search); return { index: 1, text: title, shortText: title, id: location.pathname + location.search, dataId: params.get('courseId') || params.get('ksId') || '', anchor: null, element: null, resourceType: getCurrentContentInfo().type }; } function getCurrentCourseProgress() { const current = getCurrentSelectedItem() || getCurrentPseudoItem(); if (!current) return null; return progressStore[getCourseKey(current)] || null; } function detectAnchorResourceType(a, text = '') { const href = a.getAttribute('href') || ''; const onclick = a.getAttribute('onclick') || ''; const mix = `${href} ${onclick} ${text}`.toLowerCase(); if (/playfile|playvideo/.test(mix)) return 'video'; if (/pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(href)) return 'pdf'; if (/pdf|讲义|教材|文档|附件|参考答案/.test(mix) && href) return 'pdf'; return 'unknown'; } function buildCandidateAnchors() { const selectors = [ 'a[onclick*="playFile"]', 'a[href*="playVideo"]', 'a[href*="playFile"]', 'a[href*="pdfjs/web/viewer.html"]', 'a[href$=".pdf"]', 'a[href*=".pdf?"]', '.r-tree.dTree2 a', 'li a' ]; let all = []; selectors.forEach(sel => { try { all = all.concat([...document.querySelectorAll(sel)]); } catch {} }); return uniqueBy(all, a => `${a.id}|${a.getAttribute('href') || ''}|${a.getAttribute('onclick') || ''}|${getText(a)}|${a.getAttribute('title') || ''}`); } function findCourseItems() { const root = getVideoRootLi(); let anchors = []; let globalAnchors = buildCandidateAnchors(); if (root) { try { anchors = [...root.querySelectorAll('a[onclick*="playFile"], a[href*="playVideo"], a[href*="playFile"], a[href*="pdfjs/web/viewer.html"], a[href$=".pdf"], a[href*=".pdf?"], a')]; } catch (e) { console.warn('root query 失败:', e); } } if (!anchors.length) anchors = globalAnchors; anchors = uniqueBy(anchors, a => `${a.id}|${a.getAttribute('href') || ''}|${a.getAttribute('onclick') || ''}|${a.getAttribute('title') || ''}|${getText(a)}`); const mapped = anchors.map((a, idx) => { const text = getAnchorDisplayText(a, idx); const onclick = a.getAttribute('onclick') || ''; const href = a.getAttribute('href') || ''; const resourceType = detectAnchorResourceType(a, text); const score = (onclick.includes('playFile') ? 5 : 0) + (/playVideo|playFile/i.test(href) ? 4 : 0) + (/pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(href) ? 4 : 0) + (hasNumberPrefix(text) ? 3 : 0) + ((text.includes('视频') || text.includes('第') || text.includes('参考答案') || text.includes('讲义')) ? 1 : 0); return { index: idx + 1, text: text || `第${idx + 1}项`, shortText: text || `第${idx + 1}项`, id: a.id || `auto_${idx}`, dataId: a.dataset.id || '', onclick, href, visible: isVisible(a), active: a.classList.contains('selected') || a.classList.contains('active') || a.classList.contains('curSelectedNode') || a.classList.contains('nodeSel'), anchor: a, element: a.closest('li') || a, score, resourceType }; }).filter(item => { if (item.resourceType === 'video') return true; if (item.resourceType === 'pdf') return true; if (item.score >= 4) return true; if (item.text && hasNumberPrefix(item.text)) return true; return false; }); const finalItems = uniqueBy(mapped, item => `${item.text}|${item.onclick}|${item.href}`).map((item, idx) => ({ ...item, index: idx + 1, progressInfo: progressStore[getCourseKey(item)] || null })); console.log('[findCourseItems] rootFound=', !!root); console.log('[findCourseItems] rootAnchors=', anchors.length); console.log('[findCourseItems] globalAnchors=', globalAnchors.length); console.log('[findCourseItems] finalItems=', finalItems.length, finalItems.slice(0, 10)); return finalItems; } function findItemByIndex(index) { return courseItems.find(x => x.index === index); } function getRangeNormalized() { const start = toInt(rangeStart); const end = toInt(rangeEnd); return { start: start > 0 ? start : 0, end: end > 0 ? end : 0 }; } function inPlayRange(item) { if (!item) return false; const { start, end } = getRangeNormalized(); if (start > 0 && item.index < start) return false; if (end > 0 && item.index > end) return false; return true; } function getNextItemFromCurrent() { const current = getCurrentSelectedItem(); if (!current) return null; const { start, end } = getRangeNormalized(); if (start > 0 && current.index < start) { return findItemByIndex(start) || null; } const next = courseItems.find(x => x.index === current.index + 1) || null; if (!next) return null; if (end > 0 && next.index > end) return null; return next; } function updateOptionUI() { const autoNext = document.getElementById('__clp_auto_next__'); const autoLocate = document.getElementById('__clp_auto_locate__'); const resumeEl = document.getElementById('__clp_resume_enabled__'); const rateEl = document.getElementById('__clp_rate__'); const rangeStartEl = document.getElementById('__clp_range_start__'); const rangeEndEl = document.getElementById('__clp_range_end__'); const pdfSimpleEl = document.getElementById('__clp_pdf_simple__'); const pdfSecondsEl = document.getElementById('__clp_pdf_seconds__'); if (autoNext) autoNext.checked = autoNextEnabled; if (autoLocate) autoLocate.checked = autoLocateCurrent; if (resumeEl) resumeEl.checked = resumeEnabled; if (rateEl) rateEl.value = String(playbackRate); if (rangeStartEl) rangeStartEl.value = rangeStart; if (rangeEndEl) rangeEndEl.value = rangeEnd; if (pdfSimpleEl) pdfSimpleEl.checked = pdfSimpleEnabled; if (pdfSecondsEl) pdfSecondsEl.value = String(pdfReadSeconds); const catalogOnlyEls = document.querySelectorAll('[data-catalog-only="1"]'); catalogOnlyEls.forEach(el => { el.style.display = courseItems.length > 0 ? '' : 'none'; }); } function applyPlaybackRate(video = null) { const target = video || getVisibleVideo(); if (!target) return; try { target.playbackRate = playbackRate; } catch (e) { console.warn('设置倍速失败:', e); } } function saveCurrentProgress(force = false) { const now = Date.now(); if (!force && now - saveProgressThrottle < 1500) return; saveProgressThrottle = now; const current = getCurrentSelectedItem() || getCurrentPseudoItem(); if (!current) return; const content = getCurrentContentInfo(); if (content.type === 'video') { if (!Number.isFinite(content.currentTime) || content.currentTime <= 0) return; progressStore[getCourseKey(current)] = { index: current.index || 1, text: current.text || document.title || '当前视频', currentTime: content.currentTime, duration: content.duration || 0, progress: content.progress || 0, src: content.src || '', updatedAt: Date.now(), completed: content.ended || (content.duration > 0 && content.progress >= 99.5), pageMode: getPageMode(), pageUrl: location.href, resourceType: 'video' }; saveProgressStore(); return; } if (content.type === 'pdf') { progressStore[getCourseKey(current)] = { index: current.index || 1, text: current.text || document.title || '当前PDF', currentTime: pdfTimerStartedAt ? Math.max(0, (Date.now() - pdfTimerStartedAt) / 1000) : 0, duration: Number(pdfReadSeconds || 0), progress: pdfReadSeconds > 0 && pdfTimerStartedAt ? Math.min(100, ((Date.now() - pdfTimerStartedAt) / 1000 / pdfReadSeconds) * 100) : 0, src: content.src || '', updatedAt: Date.now(), completed: false, pageMode: getPageMode(), pageUrl: location.href, resourceType: 'pdf' }; saveProgressStore(); } } function markCurrentCompleted(extra = {}) { const item = getCurrentSelectedItem() || getCurrentPseudoItem(); if (!item) return; const content = getCurrentContentInfo(); const isPdf = content.type === 'pdf' || item.resourceType === 'pdf'; progressStore[getCourseKey(item)] = { index: item.index || 1, text: item.text || document.title || '当前内容', currentTime: isPdf ? Number(pdfReadSeconds || 0) : (content.duration || content.currentTime || 0), duration: isPdf ? Number(pdfReadSeconds || 0) : (content.duration || 0), progress: 100, src: content.src || '', updatedAt: Date.now(), completed: true, pageMode: getPageMode(), pageUrl: location.href, resourceType: isPdf ? 'pdf' : 'video', ...extra }; saveProgressStore(); } async function restoreProgressForCurrent(video = null) { if (!resumeEnabled) return; const targetVideo = video || getVisibleVideo(); if (!targetVideo) return; const item = getCurrentSelectedItem() || getCurrentPseudoItem(); if (!item) return; const rec = progressStore[getCourseKey(item)]; if (!rec || rec.resourceType !== 'video' || !Number.isFinite(rec.currentTime) || rec.currentTime <= 0 || rec.completed) return; const token = `${getCourseKey(item)}|${targetVideo.currentSrc || targetVideo.src || ''}`; if (activeResumeToken === token) return; const duration = Number(targetVideo.duration || 0); if (duration > 0 && rec.currentTime >= duration - 2) return; try { activeResumeToken = token; await wait(300); const targetTime = Math.max(0, rec.currentTime - 1); if (targetTime > 0) { targetVideo.currentTime = targetTime; setStatus(`已恢复断点:${item.text} -> ${formatTime(targetTime)}`); updateVideoInfoUI(); } } catch (e) { console.warn('恢复断点失败:', e); } } async function tryPlayVideo(video = null) { const targetVideo = video || getVisibleVideo(); if (!targetVideo) return { ok: false, reason: '未找到 video 元素' }; applyPlaybackRate(targetVideo); try { if (!targetVideo.paused) return { ok: true, reason: '视频已在播放' }; await targetVideo.play(); applyPlaybackRate(targetVideo); return { ok: true, reason: 'play() 成功' }; } catch { try { targetVideo.muted = true; await targetVideo.play(); applyPlaybackRate(targetVideo); return { ok: true, reason: 'muted + play() 成功' }; } catch { return { ok: false, reason: '浏览器拦截自动播放或站点脚本干预' }; } } } function stopPdfCountdown() { if (pdfTimerHandle) { clearInterval(pdfTimerHandle); pdfTimerHandle = null; } pdfTimerToken = ''; pdfTimerStartedAt = 0; pdfTimerLeft = 0; } function startPdfCountdown(item, src = '') { if (!pdfSimpleEnabled || !autoNextEnabled || !item) return; const token = `${item.index}|${item.id}|${src || item.href || ''}|${pdfReadSeconds}`; if (pdfTimerToken === token && pdfTimerHandle) return; stopPdfCountdown(); pdfTimerToken = token; pdfTimerStartedAt = Date.now(); pdfTimerLeft = pdfReadSeconds; setStatus(`PDF 简单阅读计时开始:${item.text}(${pdfReadSeconds}秒)`); updateVideoInfoUI(); pdfTimerHandle = setInterval(async () => { const elapsed = Math.floor((Date.now() - pdfTimerStartedAt) / 1000); pdfTimerLeft = Math.max(0, pdfReadSeconds - elapsed); updateVideoInfoUI(); saveCurrentProgress(false); if (pdfTimerLeft <= 0) { clearInterval(pdfTimerHandle); pdfTimerHandle = null; const current = getCurrentSelectedItem() || item; markCurrentCompleted({ completeReason: 'pdf_simple_wait' }); setStatus(`PDF 停留计时完成:${current.text}`); if (!isAutoAdvancing) { await autoPlayNext('pdfTimeout'); } } }, 1000); } async function playCurrentVideoOnly() { const content = getCurrentContentInfo(); if (content.type === 'video') { setStatus('尝试播放当前视频...'); const result = await tryPlayVideo(); updateVideoInfoUI(); setStatus(result.ok ? `成功:${result.reason}` : `失败:${result.reason}`); return; } if (content.type === 'pdf') { const item = getCurrentSelectedItem() || getCurrentPseudoItem(); startPdfCountdown(item, content.src || ''); setStatus(`当前为 PDF,已开始简单阅读计时 ${pdfReadSeconds} 秒`); return; } setStatus('当前未检测到视频/PDF内容'); } function triggerAnchorOpen(a) { if (!a) return false; try { highlightElement(a.closest('li') || a); a.click(); ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach(type => { a.dispatchEvent(new MouseEvent(type, { view: window, bubbles: true, cancelable: true })); }); if (typeof a.onclick === 'function') a.onclick(); return true; } catch (err) { console.error('触发课程点击失败:', err); return false; } } async function waitForSelectionChange(oldId, timeout = 6000) { const start = Date.now(); while (Date.now() - start < timeout) { const current = getCurrentSelectedAnchor(); const newId = current?.id || ''; if (newId && newId !== oldId) { return { changed: true, anchor: current, id: newId }; } await wait(200); } const current = getCurrentSelectedAnchor(); return { changed: false, anchor: current, id: current?.id || '' }; } async function waitForContentChange(oldVideoSrc, oldPdfSrc, timeout = 10000) { const start = Date.now(); while (Date.now() - start < timeout) { const video = getVisibleVideo(); const videoSrc = video?.currentSrc || video?.src || ''; const pdf = getVisiblePdfDetail(); const pdfSrc = pdf.src || ''; if (videoSrc && videoSrc !== oldVideoSrc) { return { changed: true, type: 'video', src: videoSrc }; } if (pdfSrc && pdfSrc !== oldPdfSrc) { return { changed: true, type: 'pdf', src: pdfSrc }; } await wait(250); } const content = getCurrentContentInfo(); return { changed: false, type: content.type, src: content.src || '' }; } async function waitForVideoReady(timeout = 10000) { const start = Date.now(); while (Date.now() - start < timeout) { const detail = getVisibleVideoDetail(); const video = detail.video; if (video) { const src = video.currentSrc || video.src || ''; if (src || video.readyState >= 1) { return { ok: true, video, doc: detail.doc }; } } await wait(300); } const detail = getVisibleVideoDetail(); return { ok: false, video: detail.video, doc: detail.doc }; } async function openCourseAndPlay(item, options = {}) { if (!item?.anchor) { setStatus('当前项没有可点击链接'); return false; } const { autoTriggered = false, locate = true, retry = 2 } = options; for (let attempt = 1; attempt <= retry; attempt++) { const oldVideo = getVisibleVideo(); const oldVideoSrc = oldVideo?.currentSrc || oldVideo?.src || ''; const oldPdfSrc = getVisiblePdfDetail().src || ''; const oldSelectedId = getCurrentSelectedAnchor()?.id || ''; if (locate) highlightElement(item.element); setStatus(`${autoTriggered ? '自动' : '手动'}打开:${item.text}(第 ${attempt}/${retry} 次)`); const clicked = triggerAnchorOpen(item.anchor); if (!clicked) continue; await waitForSelectionChange(oldSelectedId, 5000); const changed = await waitForContentChange(oldVideoSrc, oldPdfSrc, 10000); const currentType = changed.type || item.resourceType || getCurrentContentInfo().type; if (currentType === 'video') { const readyResult = await waitForVideoReady(10000); const targetVideo = readyResult.video || getVisibleVideo(); applyPlaybackRate(targetVideo); await restoreProgressForCurrent(targetVideo); const playResult = await tryPlayVideo(targetVideo); updateVideoInfoUI(); if (playResult.ok) { stopPdfCountdown(); lightweightRescan(false, true); setStatus(`成功:${item.text} -> ${playResult.reason}`); return true; } await wait(700); continue; } if (currentType === 'pdf' || item.resourceType === 'pdf') { stopPdfCountdown(); lightweightRescan(false, true); startPdfCountdown(item, changed.src || item.href || ''); updateVideoInfoUI(); setStatus(`成功:已打开 PDF -> ${item.text},开始停留 ${pdfReadSeconds} 秒`); return true; } await wait(700); } setStatus(`最终失败:${item.text}`); return false; } function shouldTriggerNearEnd(info) { if (!info.exists) return false; if (!Number.isFinite(info.duration) || info.duration <= 0) return false; if (info.paused || info.ended) return false; const remain = info.duration - info.currentTime; return remain <= 0.8 && info.progress >= 98.5; } async function autoPlayNext(reason = 'ended') { if (!autoNextEnabled || isAutoAdvancing) return; const currentItem = getCurrentSelectedItem(); if (!currentItem) return; const content = getCurrentContentInfo(); const currentSrc = content.src || ''; const token = `${reason}|${currentSrc}|${currentItem.index}|${currentItem.id || ''}`; if (reason === 'ended' && lastEndedToken === token) return; if (reason === 'nearEnd' && lastNearEndToken === token) return; if (reason === 'ended') lastEndedToken = token; if (reason === 'nearEnd') lastNearEndToken = token; const nextItem = getNextItemFromCurrent(); if (!nextItem) { setStatus(`已播放完范围末尾或最后一项:${currentItem.text}`); return; } isAutoAdvancing = true; try { setStatus(`当前内容已完成,自动切换下一项:${nextItem.text}`); await openCourseAndPlay(nextItem, { autoTriggered: true, locate: autoLocateCurrent, retry: 3 }); } finally { setTimeout(() => { isAutoAdvancing = false; }, 1500); } } function locateCurrentSelected() { const a = getCurrentSelectedAnchor(); if (!a) { setStatus('未找到当前选中课程'); return; } highlightElement(a.closest('li') || a); setStatus(`已定位当前课程:${a.getAttribute('title') || getText(a)}`); } function copyCurrentContentUrl() { const content = getCurrentContentInfo(); const src = content.src || ''; if (!src) { setStatus('复制失败:当前没有内容地址'); return; } navigator.clipboard.writeText(src).then(() => { setStatus('已复制当前内容地址'); }).catch(() => { const ta = document.createElement('textarea'); ta.value = src; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); setStatus('已复制当前内容地址'); }); } function clearAllProgress() { if (!confirm('确定清空所有断点记录吗?')) return; progressStore = {}; saveProgressStore(); stopPdfCountdown(); lightweightRescan(false, true); updateVideoInfoUI(); setStatus('已清空所有断点记录'); } function updateVideoInfoUI() { const infoEl = document.getElementById('__clp_video_info__'); if (!infoEl) return; const content = getCurrentContentInfo(); const currentAnchor = getCurrentSelectedAnchor(); const currentTitle = currentAnchor?.getAttribute('title') || getText(currentAnchor) || document.title || '无'; const currentProgress = getCurrentCourseProgress(); const mode = getPageMode(); if (!content.exists) { infoEl.innerHTML = `
页面模式${mode}
当前课程${escapeHtml(currentTitle)}
内容类型未检测到
列表项数量${courseItems.length}
倍速${playbackRate}x
PDF简单阅读${pdfSimpleEnabled ? '开启' : '关闭'}
`; return; } if (content.type === 'pdf') { const elapsed = pdfTimerStartedAt ? Math.floor((Date.now() - pdfTimerStartedAt) / 1000) : 0; const remain = pdfTimerHandle ? Math.max(0, pdfReadSeconds - elapsed) : 0; const progress = pdfReadSeconds > 0 ? Math.min(100, (elapsed / pdfReadSeconds) * 100) : 0; const fileName = content.src ? content.src.split('/').pop() : '无'; infoEl.innerHTML = `
页面模式${mode}
当前课程${escapeHtml(currentTitle)}
内容类型PDF
阅读模式简单停留计时
计时状态${pdfTimerHandle ? '计时中' : '未开始'}
已停留${formatTime(elapsed)}
目标时长${formatTime(pdfReadSeconds)}
剩余${formatTime(remain)}
估算进度${progress.toFixed(1)}%
断点记录${currentProgress ? formatTime(currentProgress.currentTime || 0) : '无'}
自动下一项${autoNextEnabled ? '开启' : '关闭'}
PDF简单阅读${pdfSimpleEnabled ? '开启' : '关闭'}
Src: ${escapeHtml(fileName)}
`; return; } const stateText = content.ended ? 'ended' : (content.paused ? 'paused' : 'playing'); const remain = Number.isFinite(content.duration) ? Math.max(0, content.duration - content.currentTime) : 0; const fileName = content.src ? content.src.split('/').pop() : '无'; infoEl.innerHTML = `
页面模式${mode}
当前课程${escapeHtml(currentTitle)}
内容类型视频
状态${stateText}
静音${content.muted ? '是' : '否'}
倍速${playbackRate}x
进度${content.progress.toFixed(1)}%
时间${formatTime(content.currentTime)} / ${formatTime(content.duration)}
剩余${formatTime(remain)}
ReadyState${content.readyState}
断点记录${currentProgress ? formatTime(currentProgress.currentTime || 0) : '无'}
来源${escapeHtml(content.source || 'top')}
Src: ${escapeHtml(fileName)}
`; } function renderEmptyPlaceholder() { const tbody = document.getElementById('__clp_table_body__'); const summary = document.getElementById('__clp_summary__'); if (!tbody || !summary) return; summary.innerHTML = ` 页面模式 ${getPageMode()} 列表项 ${courseItems.length} 倍速 ${playbackRate}x ${escapeHtml(statusText)} `; tbody.innerHTML = ` 当前未识别到可用课程列表。
仍可使用:播放当前、倍速、PDF简单停留、复制内容地址。
如仍异常,请把控制台里 [findCourseItems] 的几行日志发给我。 `; } function renderTable(items) { const tbody = document.getElementById('__clp_table_body__'); const summary = document.getElementById('__clp_summary__'); if (!tbody || !summary) return; if (!items.length) { renderEmptyPlaceholder(); return; } const currentItem = getCurrentSelectedItem(); const rangeText = (() => { const { start, end } = getRangeNormalized(); if (!start && !end) return '全范围'; return `${start || 1} - ${end || courseItems.length || '-'}`; })(); summary.innerHTML = ` 总数 ${courseItems.length} 显示 ${items.length} 当前 ${currentItem ? currentItem.index : '-'}/${courseItems.length || '-'} 范围 ${rangeText} 倍速 ${playbackRate}x ${escapeHtml(statusText)} `; tbody.innerHTML = items.map(item => { const p = item.progressInfo; const completed = !!p?.completed; const inRangeFlag = inPlayRange(item); const typeText = item.resourceType === 'pdf' ? 'PDF' : (item.resourceType === 'video' ? '视频' : '未知'); return ` ${item.index}
${escapeHtml(item.text)}
${item.active ? '当前播放项' : ''} ${completed ? '已完成' : ''} ${p && !completed ? `断点 ${escapeHtml(formatTime(p.currentTime || 0))}` : ''} ${inRangeFlag ? '范围内' : ''} ${item.resourceType === 'pdf' ? 'PDF' : '视频'}
${escapeHtml(item.dataId || '')} ${escapeHtml(typeText)} ${item.active ? '已选中' : '普通'}
`; }).join(''); } function applyFilterAndRender() { const input = document.getElementById('__clp_search__'); const keyword = input?.value?.trim().toLowerCase() || keywordCache || ''; keywordCache = keyword; saveConfig(); let items = [...courseItems]; if (keyword) { items = items.filter(item => item.text.toLowerCase().includes(keyword) || item.shortText.toLowerCase().includes(keyword) || String(item.dataId).toLowerCase().includes(keyword) || String(item.resourceType).toLowerCase().includes(keyword) ); } filteredItems = items; renderTable(items); } function lightweightRescan(render = true, force = false) { const now = Date.now(); if (!force && now - lastScanAt < 1200) { if (!render) applyFilterAndRender(); return; } lastScanAt = now; courseItems = findCourseItems(); if (render) createPanel(); else applyFilterAndRender(); console.log('课程列表扫描完成:', courseItems); } function bindVideoEvents(video, docRef = document) { if (!video) return; if (currentVideoBound === video && currentBoundDoc === docRef) return; currentVideoBound = video; currentBoundDoc = docRef; ['play', 'pause', 'loadedmetadata', 'timeupdate', 'ratechange', 'ended'].forEach(evt => { video.addEventListener(evt, async () => { if (evt === 'loadedmetadata') { applyPlaybackRate(video); await restoreProgressForCurrent(video); } if (evt === 'timeupdate') saveCurrentProgress(false); if (evt === 'pause') saveCurrentProgress(true); if (evt === 'ended') { markCurrentCompleted(); updateVideoInfoUI(); setStatus('检测到当前视频播放完成'); await autoPlayNext('ended'); return; } updateVideoInfoUI(); }); }); } function setupVideoMonitor() { if (monitorTimer) clearInterval(monitorTimer); monitorTimer = setInterval(async () => { const detail = getVisibleVideoDetail(); const video = detail.video; if (video) { bindVideoEvents(video, detail.doc); applyPlaybackRate(video); } const videoInfo = getVideoInfo(video); const currentVideoSrc = videoInfo.src || ''; const currentPdf = getVisiblePdfDetail(); const currentPdfSrc = currentPdf.src || ''; if (currentVideoSrc !== lastVideoSrc) { lastVideoSrc = currentVideoSrc; activeResumeToken = ''; if (currentVideoSrc) { stopPdfCountdown(); lastEndedToken = ''; lastNearEndToken = ''; setStatus(`视频已切换:${currentVideoSrc.split('/').pop()}`); await restoreProgressForCurrent(video); } } if (currentPdfSrc !== lastPdfSrc) { lastPdfSrc = currentPdfSrc; if (currentPdfSrc) { const item = getCurrentSelectedItem() || getCurrentPseudoItem(); setStatus(`PDF 已切换:${(item?.text || '当前PDF')}`); if (item && (item.resourceType === 'pdf' || /pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(currentPdfSrc))) { startPdfCountdown(item, currentPdfSrc); } } else { stopPdfCountdown(); } } const selected = getCurrentSelectedAnchor(); const selectedId = selected?.id || ''; if (selectedId !== lastSelectedId) { lastSelectedId = selectedId; lightweightRescan(false, true); if (selected) { const title = selected.getAttribute('title') || getText(selected); setStatus(`当前课程已切换:${title}`); if (autoLocateCurrent) { highlightElement(selected.closest('li') || selected); } } const currentItem = getCurrentSelectedItem(); if (currentItem?.resourceType !== 'pdf' && !currentPdfSrc) { stopPdfCountdown(); } } if (shouldTriggerNearEnd(videoInfo)) { markCurrentCompleted(); await autoPlayNext('nearEnd'); } updateVideoInfoUI(); }, 1500); } function setupSelectionObserver() { if (selectionObserver) selectionObserver.disconnect(); const root = getVideoRootLi() || document.body; if (!root) return; let timer = null; selectionObserver = new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => { const currentId = getCurrentSelectedAnchor()?.id || ''; if (currentId && currentId !== lastSelectedId) { lastSelectedId = currentId; lightweightRescan(false, true); } }, 600); }); selectionObserver.observe(root, { subtree: true, attributes: true, attributeFilter: ['class'] }); } function exportJson() { const data = { pageMode: getPageMode(), pageUrl: location.href, title: document.title, courseItems: courseItems.map(item => ({ index: item.index, text: item.text, id: item.id, dataId: item.dataId, onclick: item.onclick, href: item.href, visible: item.visible, active: item.active, score: item.score, resourceType: item.resourceType, progressInfo: item.progressInfo || null })), currentContent: getCurrentContentInfo(), progressStore }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'video-course-list-v270.json'; a.click(); URL.revokeObjectURL(url); } function bindDragEvents() { const panel = document.getElementById(PANEL_ID); const header = document.getElementById('__clp_drag_handle__'); if (!panel || !header) return; header.addEventListener('mousedown', (e) => { const avoid = e.target.closest('button, input, select, label'); if (avoid) return; const rect = panel.getBoundingClientRect(); dragState.dragging = true; dragState.startX = e.clientX; dragState.startY = e.clientY; dragState.startLeft = rect.left; dragState.startTop = rect.top; panel.style.left = rect.left + 'px'; panel.style.top = rect.top + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', (e) => { if (!dragState.dragging) return; const rect = panel.getBoundingClientRect(); const width = rect.width; const height = rect.height; let left = dragState.startLeft + (e.clientX - dragState.startX); let top = dragState.startTop + (e.clientY - dragState.startY); left = clamp(left, 0, window.innerWidth - Math.min(width, window.innerWidth)); top = clamp(top, 0, window.innerHeight - Math.min(height, window.innerHeight)); panel.style.left = left + 'px'; panel.style.top = top + 'px'; }); document.addEventListener('mouseup', () => { dragState.dragging = false; document.body.style.userSelect = ''; }); } function applyPanelCollapsedUI() { const panel = document.getElementById(PANEL_ID); const content = document.getElementById('__clp_content__'); const toggleBtn = document.getElementById('__clp_toggle__'); if (!panel || !content || !toggleBtn) return; if (panelCollapsed) { panel.classList.add('panel-collapsed'); content.style.display = 'none'; toggleBtn.textContent = '展开'; } else { panel.classList.remove('panel-collapsed'); content.style.display = 'flex'; toggleBtn.textContent = '收起'; } saveConfig(); } function injectStyle() { const style = document.createElement('style'); style.textContent = ` #${PANEL_ID} { position: fixed; top: 20px; right: 20px; width: 980px; min-width: 760px; max-width: calc(100vw - 20px); height: 84vh; min-height: 480px; max-height: 90vh; background: #fff; border: 1px solid #dcdfe6; border-radius: 16px; box-shadow: 0 16px 42px rgba(0,0,0,0.18); z-index: 999999; font-family: Arial, sans-serif; overflow: hidden; resize: both; display: flex; flex-direction: column; } #${PANEL_ID}.panel-collapsed { width: 260px !important; min-width: 260px !important; max-width: 260px !important; height: 56px !important; min-height: 56px !important; max-height: 56px !important; resize: none !important; overflow: hidden !important; } #${PANEL_ID} .panel-header { background: linear-gradient(135deg, #2f80ed, #56ccf2); color: #fff; padding: 12px 14px; font-size: 16px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; gap: 12px; cursor: move; flex-shrink: 0; height: 56px; box-sizing: border-box; } #${PANEL_ID} .panel-title { display: flex; align-items: center; gap: 10px; min-width: 0; } #${PANEL_ID} .panel-subtitle { font-size: 12px; opacity: 0.9; font-weight: normal; } #${PANEL_ID}.panel-collapsed .panel-subtitle, #${PANEL_ID}.panel-collapsed #__clp_refresh__, #${PANEL_ID}.panel-collapsed #__clp_export__ { display: none; } #${PANEL_ID} .panel-actions { display: flex; gap: 8px; } #${PANEL_ID} .panel-btn { background: rgba(255,255,255,0.18); color: #fff; border: none; border-radius: 8px; padding: 6px 10px; cursor: pointer; font-size: 12px; } #${PANEL_ID} .panel-content { display: flex; flex-direction: column; min-height: 0; flex: 1; } #${PANEL_ID} .panel-toolbar { padding: 10px 12px; background: #f7f9fc; border-bottom: 1px solid #ebeef5; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } #${PANEL_ID} .toolbar-left, #${PANEL_ID} .toolbar-right { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } #${PANEL_ID} .toolbar-left { flex: 1 1 320px; min-width: 240px; } #${PANEL_ID} .toolbar-right { flex: 999 1 auto; } #${PANEL_ID} input, #${PANEL_ID} select { box-sizing: border-box; padding: 9px 12px; border: 1px solid #dcdfe6; border-radius: 10px; outline: none; font-size: 13px; background: #fff; } #${PANEL_ID} input[type="text"] { flex: 1; min-width: 180px; } #${PANEL_ID} .tool-btn { border: none; border-radius: 10px; padding: 8px 12px; cursor: pointer; font-size: 12px; background: #ecf5ff; color: #409eff; white-space: nowrap; min-width: 88px; } #${PANEL_ID} .btn-warn { background: #fdf6ec; color: #e6a23c; } #${PANEL_ID} .btn-green { background: #e8f5e9; color: #2e7d32; } #${PANEL_ID} .btn-danger { background: #fdecea; color: #d93025; } #${PANEL_ID} .panel-options { padding: 8px 12px; background: #fafbfd; border-bottom: 1px solid #ebeef5; display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; } #${PANEL_ID} .panel-options label { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } #${PANEL_ID} .range-wrap { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; } #${PANEL_ID} .range-wrap input { width: 88px; } #${PANEL_ID} .panel-summary { padding: 8px 12px; font-size: 13px; color: #666; background: #fcfcfc; border-bottom: 1px solid #ebeef5; display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; } #${PANEL_ID} .summary-left { display: flex; gap: 8px; flex-wrap: wrap; } #${PANEL_ID} .summary-pill { padding: 4px 8px; background: #f4f7fb; border: 1px solid #e7edf5; border-radius: 999px; color: #5a6472; font-size: 12px; } #${PANEL_ID} .panel-monitor { padding: 10px 12px; background: #fcfcfc; border-bottom: 1px solid #ebeef5; font-size: 12px; color: #444; } #${PANEL_ID} .monitor-grid { display: grid; grid-template-columns: repeat(6, minmax(100px, 1fr)); gap: 8px 12px; } #${PANEL_ID} .monitor-grid > div { display: flex; flex-direction: column; min-width: 0; } #${PANEL_ID} .monitor-grid b { color: #657286; font-size: 11px; margin-bottom: 2px; } #${PANEL_ID} .monitor-grid span, #${PANEL_ID} .monitor-src { word-break: break-all; } #${PANEL_ID} .panel-body { flex: 1; min-height: 0; overflow: auto; background: #fff; } #${PANEL_ID} table { width: 100%; border-collapse: collapse; table-layout: fixed; } #${PANEL_ID} th, #${PANEL_ID} td { border-bottom: 1px solid #f0f0f0; padding: 9px 8px; font-size: 12px; text-align: left; vertical-align: top; word-break: break-all; } #${PANEL_ID} th { background: #f8f9fb; position: sticky; top: 0; z-index: 2; } #${PANEL_ID} .course-title { font-size: 13px; color: #333; line-height: 1.45; } #${PANEL_ID} .row-badge-wrap { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px; } #${PANEL_ID} .tag-mini { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #ecf5ff; color: #409eff; font-size: 11px; line-height: 1.6; } #${PANEL_ID} .tag-active { background: #fff3e0; color: #e67e22; } #${PANEL_ID} .tag-done { background: #e8f5e9; color: #2e7d32; } #${PANEL_ID} .tag-progress { background: #ede7f6; color: #6a1b9a; } #${PANEL_ID} .tag-range { background: #e3f2fd; color: #1565c0; } #${PANEL_ID} .tag-pdf { background: #fdecea; color: #d93025; } #${PANEL_ID} .tag-video { background: #e8f5e9; color: #2e7d32; } #${PANEL_ID} .row-actions { display: flex; gap: 6px; flex-wrap: wrap; } #${PANEL_ID} .op-btn { border: none; border-radius: 8px; padding: 6px 10px; cursor: pointer; font-size: 12px; min-width: 74px; } #${PANEL_ID} .btn-locate { background: #e8f5e9; color: #2e7d32; } #${PANEL_ID} .btn-play { background: #ecf5ff; color: #409eff; } #${PANEL_ID} .btn-next { background: #fff3e0; color: #ef6c00; } #${PANEL_ID} .row-hover:hover { background: #f9fbff; } #${PANEL_ID} .row-active { background: #fffaf2; } #${PANEL_ID} .course-name-cell { cursor: pointer; } #__clp_status__ { color: #e6a23c; font-weight: 600; word-break: break-all; } .${HIGHLIGHT_CLASS} { outline: 3px solid #ff4d4f !important; background-color: rgba(255, 235, 59, 0.20) !important; } @media (max-width: 1200px) { #${PANEL_ID} { width: calc(100vw - 24px); min-width: 320px; left: 12px !important; right: 12px !important; top: 12px !important; height: 88vh; } #${PANEL_ID} .monitor-grid { grid-template-columns: repeat(2, minmax(120px, 1fr)); } } `; document.head.appendChild(style); } function createPanel() { const old = document.getElementById(PANEL_ID); const oldRect = old?.getBoundingClientRect(); if (old) old.remove(); const panel = document.createElement('div'); panel.id = PANEL_ID; panel.innerHTML = `
助学课程列表 反馈意见表1077472126 仅供学习,不得用于其它途径
范围: -
加载中... ${escapeHtml(statusText)}
内容信息: 加载中...
序号 课程标题 data-id 类型 状态 操作
`; document.body.appendChild(panel); if (oldRect) { panel.style.left = oldRect.left + 'px'; panel.style.top = oldRect.top + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; } bindPanelEvents(); bindDragEvents(); updateOptionUI(); applyFilterAndRender(); updateVideoInfoUI(); applyPanelCollapsedUI(); } function bindPanelEvents() { document.getElementById('__clp_refresh__')?.addEventListener('click', async () => { lightweightRescan(true, true); const detail = await waitForAnyVideo(4000); if (detail.video) { bindVideoEvents(detail.video, detail.doc); applyPlaybackRate(detail.video); } updateVideoInfoUI(); setStatus('已刷新'); }); document.getElementById('__clp_export__')?.addEventListener('click', exportJson); document.getElementById('__clp_toggle__')?.addEventListener('click', () => { panelCollapsed = !panelCollapsed; applyPanelCollapsedUI(); setStatus(panelCollapsed ? '面板已整体收起' : '面板已展开'); }); document.getElementById('__clp_search__')?.addEventListener('input', (e) => { keywordCache = e.target.value.trim().toLowerCase(); saveConfig(); applyFilterAndRender(); }); document.getElementById('__clp_rate__')?.addEventListener('change', (e) => { playbackRate = Number(e.target.value || 1); applyPlaybackRate(); saveConfig(); updateVideoInfoUI(); setStatus(`已设置倍速:${playbackRate}x`); }); document.getElementById('__clp_pdf_seconds__')?.addEventListener('change', (e) => { pdfReadSeconds = Number(e.target.value || 30); saveConfig(); stopPdfCountdown(); const content = getCurrentContentInfo(); const item = getCurrentSelectedItem(); if (content.type === 'pdf' && item && pdfSimpleEnabled) { startPdfCountdown(item, content.src || ''); } updateVideoInfoUI(); setStatus(`已设置 PDF 停留时长:${pdfReadSeconds} 秒`); }); document.getElementById('__clp_locate_current__')?.addEventListener('click', locateCurrentSelected); document.getElementById('__clp_play_current__')?.addEventListener('click', playCurrentVideoOnly); document.getElementById('__clp_next__')?.addEventListener('click', async () => { const nextItem = getNextItemFromCurrent(); if (!nextItem) { setStatus('没有下一项可处理(可能超出范围或未识别当前项)'); return; } await openCourseAndPlay(nextItem, { autoTriggered: false, locate: true, retry: 3 }); }); document.getElementById('__clp_copy_video__')?.addEventListener('click', copyCurrentContentUrl); document.getElementById('__clp_clear_progress__')?.addEventListener('click', clearAllProgress); document.getElementById('__clp_apply_range__')?.addEventListener('click', () => { rangeStart = document.getElementById('__clp_range_start__')?.value?.trim() || ''; rangeEnd = document.getElementById('__clp_range_end__')?.value?.trim() || ''; saveConfig(); lightweightRescan(false, true); setStatus(`已应用范围:${rangeStart || '起始'} - ${rangeEnd || '末尾'}`); }); document.getElementById('__clp_auto_next__')?.addEventListener('change', (e) => { autoNextEnabled = !!e.target.checked; saveConfig(); setStatus(autoNextEnabled ? '已开启自动下一项' : '已关闭自动下一项'); }); document.getElementById('__clp_auto_locate__')?.addEventListener('change', (e) => { autoLocateCurrent = !!e.target.checked; saveConfig(); setStatus(autoLocateCurrent ? '已开启当前项跟踪定位' : '已关闭当前项跟踪定位'); }); document.getElementById('__clp_resume_enabled__')?.addEventListener('change', (e) => { resumeEnabled = !!e.target.checked; saveConfig(); setStatus(resumeEnabled ? '已开启视频断点恢复' : '已关闭视频断点恢复'); }); document.getElementById('__clp_pdf_simple__')?.addEventListener('change', (e) => { pdfSimpleEnabled = !!e.target.checked; saveConfig(); const content = getCurrentContentInfo(); const item = getCurrentSelectedItem(); if (!pdfSimpleEnabled) { stopPdfCountdown(); } else if (content.type === 'pdf' && item) { startPdfCountdown(item, content.src || ''); } updateVideoInfoUI(); setStatus(pdfSimpleEnabled ? '已开启 PDF 简单自动阅读' : '已关闭 PDF 简单自动阅读'); }); document.getElementById(PANEL_ID)?.addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const action = btn.dataset.action; const index = Number(btn.dataset.index); const item = findItemByIndex(index); if (!item) return; if (action === 'locate') { highlightElement(item.element); setStatus(`已定位:${item.text}`); } if (action === 'play') { await openCourseAndPlay(item, { autoTriggered: false, locate: true, retry: 3 }); } if (action === 'playnext') { rangeStart = String(item.index); autoNextEnabled = true; saveConfig(); updateOptionUI(); const ok = await openCourseAndPlay(item, { autoTriggered: false, locate: true, retry: 3 }); if (ok) { lightweightRescan(false, true); setStatus(`已从该项开始处理,后续按范围自动连播:${item.text}`); } } }); document.getElementById(PANEL_ID)?.addEventListener('dblclick', async (e) => { const cell = e.target.closest('.course-name-cell'); if (!cell) return; const index = Number(cell.dataset.index); const item = findItemByIndex(index); if (!item) return; await openCourseAndPlay(item, { autoTriggered: false, locate: true, retry: 3 }); }); } async function init() { loadConfig(); loadProgressStore(); injectStyle(); lightweightRescan(true, true); setTimeout(() => lightweightRescan(true, true), 1500); setTimeout(() => lightweightRescan(true, true), 3500); setTimeout(() => lightweightRescan(true, true), 6000); updateVideoInfoUI(); setupVideoMonitor(); setupSelectionObserver(); const selected = getCurrentSelectedAnchor(); lastSelectedId = selected?.id || ''; lastVideoSrc = getVideoInfo().src || ''; lastPdfSrc = getVisiblePdfDetail().src || ''; setStatus(`页面模式:${getPageMode()}`); const detail = await waitForAnyVideo(12000); if (detail.video) { bindVideoEvents(detail.video, detail.doc); applyPlaybackRate(detail.video); updateVideoInfoUI(); setStatus(`已检测到视频:${detail.frame ? 'iframe' : 'top'} / ${detail.source}`); } else { const pdf = getVisiblePdfDetail(); if (pdf.element) { const item = getCurrentSelectedItem() || getCurrentPseudoItem(); if (item && pdfSimpleEnabled) startPdfCountdown(item, pdf.src || ''); setStatus(`已检测到 PDF,简单阅读模式 ${pdfSimpleEnabled ? '开启' : '关闭'}`); } else { setStatus(`未检测到内容,列表项:${courseItems.length}`); } } } window.addEventListener('beforeunload', () => { saveCurrentProgress(true); saveConfig(); }); window.addEventListener('load', () => { setTimeout(init, 2000); }); })();