// ==UserScript== // @name TC视频助手 // @namespace chd-course-auto-next // @version 1.1.0 // @description 自动播放视频、自动下一节,并提供可拖拽悬浮状态面板 // @match https://course-online.chd.edu.cn/* // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // ========================================== // 全局状态 // ========================================== let watchedVideo = null; let changing = false; let completedCount = 0; let autoPlayEnabled = true; let autoNextEnabled = true; let currentTitle = '等待视频...'; let currentStatus = '初始化'; // ========================================== // 创建状态面板 // ========================================== function createPanel() { if (document.getElementById('chd-auto-panel')) { return; } const panel = document.createElement('div'); panel.id = 'chd-auto-panel'; panel.innerHTML = `
🎓 畅课助手
状态 初始化
视频 等待...
播放 等待
进度 00:00 / 00:00
已完成 0 节
等待脚本启动...
`; document.body.appendChild(panel); // ========================================== // CSS // ========================================== const style = document.createElement('style'); style.textContent = ` #chd-auto-panel { position: fixed; top: 80px; right: 20px; width: 280px; z-index: 999999999; background: rgba(25, 25, 28, 0.96); color: #ffffff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.35); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; font-size: 13px; overflow: hidden; user-select: none; /* * 防止拖拽时浏览器把操作解释成页面滚动 */ touch-action: none; } #chd-panel-header { height: 42px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; background: rgba(55,55,60,0.95); font-size: 14px; font-weight: 600; /* * 标题栏作为拖拽区域 */ cursor: grab; touch-action: none; } #chd-panel-header.chd-dragging { cursor: grabbing; } #chd-panel-header span { pointer-events: none; } #chd-collapse { width: 26px; height: 26px; border: none; border-radius: 6px; background: rgba(255,255,255,0.12); color: white; cursor: pointer; font-size: 18px; line-height: 20px; /* * 按钮自己处理点击,不参与拖拽 */ touch-action: manipulation; } #chd-collapse:hover { background: rgba(255,255,255,0.22); } #chd-panel-body { padding: 12px; } .chd-status-row { display: flex; justify-content: space-between; margin-bottom: 9px; line-height: 18px; } .chd-status-row span:first-child { color: #aaaaaa; } .chd-status-row span:last-child { max-width: 175px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #ffffff; } .chd-divider { height: 1px; background: rgba(255,255,255,0.10); margin: 10px 0; } .chd-switch-row { display: flex; align-items: center; justify-content: space-between; margin: 9px 0; color: #dddddd; cursor: pointer; } .chd-switch-row input { width: 16px; height: 16px; cursor: pointer; } #chd-log { min-height: 34px; color: #8fc7ff; font-size: 12px; line-height: 17px; word-break: break-all; } #chd-status { color: #63e6be; } `; document.head.appendChild(style); // ========================================== // 折叠 // ========================================== document .getElementById('chd-collapse') .addEventListener('click', function () { const body = document.getElementById('chd-panel-body'); if (body.style.display === 'none') { body.style.display = 'block'; this.textContent = '−'; } else { body.style.display = 'none'; this.textContent = '+'; } }); // ========================================== // 开关 // ========================================== document .getElementById('chd-auto-play') .addEventListener('change', function () { autoPlayEnabled = this.checked; addLog( autoPlayEnabled ? '自动播放:开启' : '自动播放:关闭' ); }); document .getElementById('chd-auto-next') .addEventListener('change', function () { autoNextEnabled = this.checked; addLog( autoNextEnabled ? '自动下一节:开启' : '自动下一节:关闭' ); }); // ========================================== // 浮窗拖拽 // ========================================== enablePanelDragging(panel); } // ========================================== // 浮窗拖拽功能 // ========================================== function enablePanelDragging(panel) { const header = document.getElementById('chd-panel-header'); if (!header) { return; } let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; // ------------------------------------------ // 恢复上次保存的位置 // ------------------------------------------ try { const savedPosition = localStorage.getItem('chd-panel-position'); if (savedPosition) { const position = JSON.parse(savedPosition); if ( typeof position.left === 'number' && typeof position.top === 'number' ) { panel.style.left = position.left + 'px'; panel.style.top = position.top + 'px'; panel.style.right = 'auto'; // 防止历史位置已经超出屏幕 keepPanelInsideWindow(panel); } } } catch (error) { console.warn( '[畅课助手] 无法读取浮窗位置', error ); } // ------------------------------------------ // 开始拖拽 // ------------------------------------------ header.addEventListener( 'pointerdown', function (event) { // 如果点击的是折叠按钮,不启动拖拽 if ( event.target.closest && event.target.closest('#chd-collapse') ) { return; } // 只响应鼠标左键 if ( event.pointerType === 'mouse' && event.button !== 0 ) { return; } const rect = panel.getBoundingClientRect(); // 第一次拖动时,将 right 定位转换成 left panel.style.left = rect.left + 'px'; panel.style.top = rect.top + 'px'; panel.style.right = 'auto'; startX = event.clientX; startY = event.clientY; startLeft = rect.left; startTop = rect.top; dragging = true; header.classList.add( 'chd-dragging' ); // 捕获指针,避免鼠标移动太快时丢失事件 header.setPointerCapture( event.pointerId ); event.preventDefault(); } ); // ------------------------------------------ // 拖拽移动 // ------------------------------------------ header.addEventListener( 'pointermove', function (event) { if (!dragging) { return; } const deltaX = event.clientX - startX; const deltaY = event.clientY - startY; let newLeft = startLeft + deltaX; let newTop = startTop + deltaY; // 获取当前窗口尺寸 const maxLeft = window.innerWidth - panel.offsetWidth; const maxTop = window.innerHeight - panel.offsetHeight; // 限制在窗口内部 newLeft = Math.max( 0, Math.min(newLeft, maxLeft) ); newTop = Math.max( 0, Math.min(newTop, maxTop) ); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; panel.style.right = 'auto'; event.preventDefault(); } ); // ------------------------------------------ // 结束拖拽 // ------------------------------------------ function stopDragging(event) { if (!dragging) { return; } dragging = false; header.classList.remove( 'chd-dragging' ); try { if ( event && header.hasPointerCapture && header.hasPointerCapture( event.pointerId ) ) { header.releasePointerCapture( event.pointerId ); } } catch (error) { // 忽略指针释放异常 } // 保存当前位置 savePanelPosition(panel); } header.addEventListener( 'pointerup', stopDragging ); header.addEventListener( 'pointercancel', stopDragging ); // ------------------------------------------ // 浏览器窗口大小改变 // ------------------------------------------ window.addEventListener( 'resize', function () { keepPanelInsideWindow(panel); savePanelPosition(panel); } ); } // ========================================== // 限制浮窗不能跑出窗口 // ========================================== function keepPanelInsideWindow(panel) { const rect = panel.getBoundingClientRect(); let left = rect.left; let top = rect.top; const maxLeft = window.innerWidth - panel.offsetWidth; const maxTop = window.innerHeight - panel.offsetHeight; left = Math.max( 0, Math.min(left, Math.max(0, maxLeft)) ); top = Math.max( 0, Math.min(top, Math.max(0, maxTop)) ); panel.style.left = left + 'px'; panel.style.top = top + 'px'; panel.style.right = 'auto'; } // ========================================== // 保存浮窗位置 // ========================================== function savePanelPosition(panel) { try { const rect = panel.getBoundingClientRect(); localStorage.setItem( 'chd-panel-position', JSON.stringify({ left: rect.left, top: rect.top }) ); } catch (error) { console.warn( '[畅课助手] 无法保存浮窗位置', error ); } } // ========================================== // 更新状态 // ========================================== function setStatus(text) { currentStatus = text; const element = document.getElementById('chd-status'); if (element) { element.textContent = text; } } // ========================================== // 日志 // ========================================== function addLog(text) { console.log('[畅课助手]', text); const element = document.getElementById('chd-log'); if (element) { element.textContent = text; } } // ========================================== // 查找视频 // ========================================== function getVideo() { return document.querySelector('video'); } // ========================================== // 查找下一节 // ========================================== function getNextButton() { const list = document.querySelectorAll('button, a'); for (const element of list) { if ( element.textContent.trim() === '下一个' ) { return element; } } return null; } // ========================================== // 获取课程名称 // ========================================== function getTitle() { const title = document.querySelector('.activity-title'); if (title) { return title.textContent.trim(); } return '当前视频'; } // ========================================== // 自动播放 // ========================================== function autoPlay(video) { if (!autoPlayEnabled) { return; } if (!video.paused) { setStatus('播放中'); return; } setStatus('尝试播放'); addLog('正在尝试自动播放...'); const promise = video.play(); if (promise !== undefined) { promise .then(function () { setStatus('播放中'); addLog('✓ 自动播放成功'); }) .catch(function (error) { console.warn(error); setStatus('需要播放'); addLog( '浏览器阻止自动播放' ); clickPlayButton(); }); } } // ========================================== // Video.js 播放按钮 // ========================================== function clickPlayButton() { const selectors = [ '.vjs-big-play-button', '.vjs-play-control', 'button.vjs-big-play-button', 'button.vjs-play-control' ]; for (const selector of selectors) { const button = document.querySelector(selector); if (button) { addLog( '找到播放器按钮,尝试点击' ); button.click(); return; } } addLog( '未找到播放器播放按钮' ); } // ========================================== // 视频结束 // ========================================== function videoEnded() { completedCount++; updateCompleted(); setStatus('视频完成'); addLog( '✓ 视频播放完成' ); if (!autoNextEnabled) { addLog( '自动下一节已关闭' ); return; } if (changing) { return; } const next = getNextButton(); if (!next) { setStatus('等待下一节'); addLog( '未找到“下一个”按钮' ); return; } changing = true; setStatus('切换中'); addLog( '准备进入下一节...' ); setTimeout(function () { next.click(); addLog( '→ 已点击“下一个”' ); setTimeout(function () { changing = false; checkVideo(); }, 2000); }, 1000); } // ========================================== // 更新完成数量 // ========================================== function updateCompleted() { const element = document.getElementById( 'chd-completed' ); if (element) { element.textContent = completedCount + ' 节'; } } // ========================================== // 更新播放进度 // ========================================== function updateProgress(video) { if (!video) { return; } const current = video.currentTime || 0; const duration = video.duration || 0; function format(time) { if (!isFinite(time)) { return '00:00'; } const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0'); } const element = document.getElementById( 'chd-progress' ); if (element) { element.textContent = format(current) + ' / ' + format(duration); } const playStatus = document.getElementById( 'chd-play-status' ); if (playStatus) { if (video.ended) { playStatus.textContent = '已完成'; } else if (video.paused) { playStatus.textContent = '已暂停'; } else { playStatus.textContent = '播放中'; } } } // ========================================== // 检查视频 // ========================================== function checkVideo() { const video = getVideo(); if (!video) { setStatus('等待视频'); return; } if (video === watchedVideo) { updateProgress(video); return; } watchedVideo = video; currentTitle = getTitle(); const titleElement = document.getElementById( 'chd-video-title' ); if (titleElement) { titleElement.textContent = currentTitle; } setStatus('检测到新视频'); addLog( '✓ 检测到新视频' ); video.addEventListener( 'ended', videoEnded ); video.addEventListener( 'play', function () { setStatus('播放中'); addLog( '▶ 视频开始播放' ); } ); video.addEventListener( 'pause', function () { if (!video.ended) { setStatus('已暂停'); addLog( '⏸ 视频暂停' ); } } ); setTimeout(function () { autoPlay(video); }, 500); } // ========================================== // 创建面板 // ========================================== createPanel(); // ========================================== // 定时检查 // ========================================== setInterval(function () { if (!changing) { checkVideo(); } }, 1000); // ========================================== // 初始化 // ========================================== checkVideo(); })();