// ==UserScript== // @name 我周五要吃德克士 // @namespace scu-ecourse-study-helper // @version 0.1.0 // @description 识别大川学堂视频、倍速、当前章节,并在正常播放结束后提示进入下一节(不伪造学习进度) // @match https://ecourse.scu.edu.cn/learn/course/detail/mooc/courseWare/* // @grant none // @run-at document-idle // ==/UserScript== (() => { 'use strict'; const CFG = { playbackRate: Number(localStorage.getItem('scu_helper_rate') || 1.5), tryAutoplay: true, muteForAutoplay: false, debug: true }; const log = (...args) => CFG.debug && console.log('[SCU Helper]', ...args); let boundVideo = null; let endHandler = null; let routeKey = ''; let panel = null; function getActiveVideo() { // 你提供的页面中,真正课程播放器为 #h5player 下的 video。 const primary = document.querySelector('#h5player video'); if (primary && Number.isFinite(primary.duration) && primary.duration > 0) return primary; // 兜底:排除页面中空的 controls video。 return [...document.querySelectorAll('video')] .find(v => (v.currentSrc || v.src) && Number.isFinite(v.duration) && v.duration > 0) || null; } function getSectionItems() { return [...document.querySelectorAll('a.section_item')] .filter(el => el.querySelector('.course_name')); } function getCurrentSectionItem() { const currentNode = document.querySelector('.el-tree-node.is-current'); return currentNode?.querySelector('a.section_item') || null; } function getSectionName(item) { return item?.querySelector('.course_name')?.textContent?.trim() || ''; } function getCurrentSectionIndex() { const items = getSectionItems(); const current = getCurrentSectionItem(); if (!current) return -1; return items.indexOf(current); } function getNextSectionItem() { const items = getSectionItems(); const index = getCurrentSectionIndex(); if (index >= 0 && index + 1 < items.length) { return items[index + 1]; } // 如果 Element UI 在切换时短暂丢失 is-current,则按顶部课程名匹配。 const title = document.querySelector('.course_title .course_name')?.textContent?.trim(); if (title) { const matched = items.findIndex(x => getSectionName(x) === title); if (matched >= 0 && matched + 1 < items.length) return items[matched + 1]; } return null; } function clickSection(item) { if (!item) return false; const name = getSectionName(item); log('准备进入章节:', name); // 使用站点本身 Vue/Element UI 已绑定的 click handler。 item.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => item.click(), 250); return true; } // 🔥 修改点1:改为 showConfirm 参数,默认为 false(自动跳转) function goNextSection(showConfirm = false) { const next = getNextSectionItem(); if (!next) { alert('当前 DOM 中没有找到下一节。可能已到最后一节,或后续章节尚未展开。'); return; } const name = getSectionName(next); // 只有 showConfirm 为 true 时才弹出确认框 if (showConfirm) { if (!confirm(`当前视频已播放结束。\n\n进入下一节:\n${name}?`)) { return; } } log('自动进入下一节:', name); clickSection(next); } function applyRate(video) { if (!video) return; const rate = Math.max(0.5, Math.min(2, CFG.playbackRate)); video.playbackRate = rate; video.defaultPlaybackRate = rate; log('倍速:', rate); updatePanel(); } async function tryPlay(video) { if (!video || !CFG.tryAutoplay) return; try { if (CFG.muteForAutoplay) video.muted = true; applyRate(video); await video.play(); log('自动播放成功'); } catch (err) { log('浏览器阻止自动播放,需要点击一次页面后再播放:', err?.message || err); updatePanel('浏览器阻止自动播放,点击"播放"即可'); } } function bindVideo(video) { if (!video || video === boundVideo) return; if (boundVideo && endHandler) { boundVideo.removeEventListener('ended', endHandler); } boundVideo = video; applyRate(video); // 🔥 修改点2:视频结束自动跳转,不弹出确认框 endHandler = () => { log('视频 ended:', getSectionName(getCurrentSectionItem())); updatePanel('视频已结束,自动进入下一节...'); setTimeout(() => goNextSection(), 500); // 不传参,默认为 false }; video.addEventListener('ended', endHandler); video.addEventListener('loadedmetadata', () => { applyRate(video); updatePanel(); }); video.addEventListener('ratechange', () => updatePanel()); video.addEventListener('play', () => updatePanel()); video.addEventListener('pause', () => updatePanel()); video.addEventListener('timeupdate', throttle(updatePanel, 1000)); log('绑定课程视频:', { src: video.currentSrc || video.src, duration: video.duration }); tryPlay(video); updatePanel(); } function throttle(fn, wait) { let last = 0; return (...args) => { const now = Date.now(); if (now - last >= wait) { last = now; fn(...args); } }; } function fmt(sec) { if (!Number.isFinite(sec)) return '--:--'; const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; } function createPanel() { if (document.getElementById('scu-helper-panel')) return; panel = document.createElement('div'); panel.id = 'scu-helper-panel'; panel.innerHTML = `