// ==UserScript==
// @name 网课全自动助手 (elearnmooc)
// @name:en elearnmooc_helper
// @icon https://www.elearnmooc.com/favicon.ico
// @namespace https://github.com/Relianttt
// @version 1.0
// @description elearnmooc 网课全自动助手:图形化控制面板,支持自动倍速播放、静音、自动连播下一节、智能处理结束弹窗、列表页自动检索未完成任务
// @description:en Auto course helper for elearnmooc: GUI control panel with auto playback speed, mute, auto-next, smart popup handling, and auto-scan for incomplete tasks
// @author reliant
// @license MIT
// @icon https://www.elearnmooc.com/favicon.ico
// @match *://www.elearnmooc.com/*
// @match *://elearnmooc.com/*
// @grant none
// @run-at document-end
// @noframes
// ==/UserScript==
(function () {
'use strict';
// --- 配置存储键名 ---
const STORAGE_KEY = 'mooc_auto_helper_config';
// --- 默认配置 ---
const defaultConfig = {
speed: 2.0,
isMuted: true,
autoNext: true,
autoScan: true
};
// --- 从 localStorage 加载配置 ---
function loadConfig() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
return { ...defaultConfig, ...JSON.parse(saved) };
}
} catch (e) {
console.log('加载配置失败,使用默认值');
}
return { ...defaultConfig };
}
// --- 保存配置到 localStorage ---
function saveConfig() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
} catch (e) {
console.log('保存配置失败');
}
}
// --- 全局配置(从 localStorage 加载)---
let config = loadConfig();
// --- 模拟鼠标点击 ---
function simulateClick(element) {
// 获取元素位置用于事件坐标
const rect = element.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
// 事件通用配置
const eventOptions = {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y,
screenX: x,
screenY: y,
button: 0,
buttons: 1
};
// 1. 先聚焦元素
if (element.focus) element.focus();
// 2. 派发 pointerdown 事件(现代浏览器)
try {
element.dispatchEvent(new PointerEvent('pointerdown', eventOptions));
} catch (e) { }
// 3. 派发 mousedown 事件
element.dispatchEvent(new MouseEvent('mousedown', eventOptions));
// 4. 派发 pointerup 事件
try {
element.dispatchEvent(new PointerEvent('pointerup', eventOptions));
} catch (e) { }
// 5. 派发 mouseup 事件
element.dispatchEvent(new MouseEvent('mouseup', eventOptions));
// 6. 派发 click 事件
element.dispatchEvent(new MouseEvent('click', eventOptions));
// 7. 最后调用原生 click 方法(双重保险)
if (element.click) element.click();
}
// --- UI 界面渲染 ---
const panel = document.createElement('div');
panel.style = "position:fixed;top:120px;right:20px;z-index:99999;width:210px;padding:15px;background:#fff;border:2px solid #007bff;border-radius:10px;box-shadow:0 4px 15px rgba(0,0,0,0.2);font-family:sans-serif;";
panel.innerHTML = `
网课自动助手
识别中...
`;
document.body.appendChild(panel);
// --- 获取 UI 元素 ---
const speedRange = panel.querySelector('#speedRange');
const speedVal = panel.querySelector('#speedVal');
const statusInfo = panel.querySelector('#statusInfo');
const muteCheck = panel.querySelector('#muteCheck');
const nextCheck = panel.querySelector('#nextCheck');
const scanCheck = panel.querySelector('#scanCheck');
// --- 从配置恢复 UI 状态 ---
speedRange.value = config.speed;
speedVal.innerText = config.speed;
muteCheck.checked = config.isMuted;
nextCheck.checked = config.autoNext;
scanCheck.checked = config.autoScan;
// --- 绑定事件:更改时保存配置 ---
speedRange.oninput = () => {
config.speed = parseFloat(speedRange.value);
speedVal.innerText = config.speed;
saveConfig();
};
muteCheck.onchange = () => { config.isMuted = muteCheck.checked; saveConfig(); };
nextCheck.onchange = () => { config.autoNext = nextCheck.checked; saveConfig(); };
scanCheck.onchange = () => { config.autoScan = scanCheck.checked; saveConfig(); };
// --- 核心逻辑 ---
function mainLoop() {
const video = document.querySelector('video.videoplayer');
const nextBtn = document.querySelector('.next_chapter');
// 优先检测场景 B:处理结束弹窗
// 优先检查 alertbox_group 内的按钮
let confirmBackBtn = document.querySelector('div.alertbox_group button.theme_2');
// 如果没找到,尝试查找其他弹窗结构中的确定按钮
if (!confirmBackBtn) {
// 查找所有 theme_2 按钮,检查是否在弹窗中
const allTheme2Btns = document.querySelectorAll('button.theme_2');
for (let btn of allTheme2Btns) {
// 检查按钮文本是否为"确定",且附近有弹窗提示文字
if (btn.innerText.includes("确定")) {
// 检查是否在弹窗容器中(不是笔记区等其他区域)
const parent = btn.closest('.alertbox, .layer, .modal, .popup, .dialog, [class*="alert"], [class*="layer"]');
if (parent) {
confirmBackBtn = btn;
break;
}
// 或者检查页面上是否有"返回课程内容"相关文字
if (document.body.innerText.includes("是否返回课程内容") ||
document.body.innerText.includes("返回列表")) {
confirmBackBtn = btn;
break;
}
}
}
}
if (confirmBackBtn && confirmBackBtn.innerText.includes("确定")) {
statusInfo.innerText = "状态: 正在自动返回列表";
// 从当前 URL 获取 courseId 和 termId
const urlParams = new URLSearchParams(window.location.search);
const courseId = urlParams.get('courseId');
const termId = urlParams.get('termId');
if (courseId && termId) {
const targetUrl = `/pages/learning/videoCourseware.jsp?courseId=${courseId}&termId=${termId}`;
window.location.href = targetUrl;
} else {
// 如果获取不到参数,降级为模拟点击
simulateClick(confirmBackBtn);
}
return;
}
// 场景 A:视频播放中
if (video) {
statusInfo.innerText = "状态: 正在监控播放器";
video.muted = muteCheck.checked;
if (video.playbackRate !== config.speed) video.playbackRate = config.speed;
if (video.paused && !video.ended) video.play().catch(() => { });
video.onended = () => {
if (nextCheck.checked && nextBtn && !nextBtn.disabled) {
nextBtn.click();
}
};
return;
}
// 场景 C:列表页扫描与自动展开
if (scanCheck.checked) {
const statusLabels = document.querySelectorAll('.loadStatus');
for (let label of statusLabels) {
if (label.innerText.includes("进行中")) {
const chapterHeader = label.closest('.chapter_title_box');
if (chapterHeader) {
const parent = chapterHeader.parentElement;
const contentArea = parent.querySelector('.chapter_content');
if (contentArea && (contentArea.style.display === 'none' || getComputedStyle(contentArea).display === 'none')) {
statusInfo.innerText = "状态: 正在展开进行中章节...";
chapterHeader.click();
return;
}
if (contentArea) {
const allPlayIcons = contentArea.querySelectorAll('i.fa-play-circle.video_play_icon');
for (let icon of allPlayIcons) {
if (!icon.classList.contains('setColor')) {
// 使用模拟点击触发播放
statusInfo.innerText = "状态: 发现未播放任务,正在进入...";
simulateClick(icon);
return;
}
}
}
}
}
}
statusInfo.innerText = "状态: 扫描中/暂无未完任务";
}
}
// 每 2.5 秒执行一次
setInterval(mainLoop, 2500);
})();