// ==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();
})();