// ==UserScript==
// @name 云班课一键秒资源
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 自由选择秒速/快速/标准/慢速,批量完成视频/文档/图片/音频,不触发下载
// @author 睡衣宝宝
// @match https://www.mosoteach.cn/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ─── 速度档位定义 ─────────────────
const SPEED_PROFILES = {
instant: {
label: '⚠️❗️⚡秒速⚡❗️⚠️',
previewWait: 500, // 非视频预览停留 0.5 秒
videoAfterWait: 500, // 视频快进后等待 0.5 秒
loopInterval: 200 // 处理间隔 0.2 秒
},
fast: {
label: '⚡ 快速',
previewWait: 2000,
videoAfterWait: 1500,
loopInterval: 500
},
normal: {
label: '⏺ 标准',
previewWait: 4000,
videoAfterWait: 3000,
loopInterval: 1000
},
slow: {
label: '🐢 慢速',
previewWait: 6000,
videoAfterWait: 5000,
loopInterval: 2000
}
};
// ─── 资源获取与过滤 ─────────────────
function getAllRows() {
return Array.from(document.querySelectorAll('.res-row'));
}
function filterRows(rows, type) {
if (type === 'all') return rows;
const mimeMap = {
video: 'video',
doc: 'application',
image: 'image',
audio: 'audio'
};
const targetMime = mimeMap[type];
if (!targetMime) return rows;
return rows.filter(row => row.getAttribute('data-mime') === targetMime);
}
// ─── 模拟点击 ─────────────────────
function simulateClick(el) {
const rect = el.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
['pointerdown', 'mousedown', 'click', 'mouseup', 'pointerup'].forEach(type => {
el.dispatchEvent(new MouseEvent(type, {
bubbles: true, cancelable: true,
clientX: x, clientY: y, view: window
}));
});
}
// ─── 视频快进 ─────────────────────
async function completeVideo() {
const video = await new Promise((resolve, reject) => {
const start = Date.now();
const timer = setInterval(() => {
const v = document.querySelector('video');
if (v && v.duration && !isNaN(v.duration) && v.duration > 0) {
clearInterval(timer);
resolve(v);
}
if (Date.now() - start > 15000) {
clearInterval(timer);
reject(new Error('视频加载超时'));
}
}, 300);
});
video.currentTime = video.duration;
if (window.videojs) {
try {
const vjsEl = document.querySelector('.video-js');
if (vjsEl) {
const player = videojs(vjsEl.id);
player.currentTime(player.duration());
player.trigger('ended');
}
} catch (e) {}
}
}
// ─── 弹窗与关闭 ─────────────────
function isDialogOpen() {
const closeBtn = document.querySelector('div.close-window[title="关闭"]');
return closeBtn && closeBtn.offsetParent !== null;
}
async function waitForDialogAppear(timeout = 8000) {
const start = Date.now();
while (!isDialogOpen() && Date.now() - start < timeout) {
await sleep(300);
}
return isDialogOpen();
}
async function waitForDialogClose(timeout = 5000) {
const start = Date.now();
while (isDialogOpen() && Date.now() - start < timeout) {
await sleep(300);
}
}
async function closeDialog() {
const closeBtn = document.querySelector('div.close-window[title="关闭"]');
if (closeBtn && closeBtn.offsetParent !== null) {
simulateClick(closeBtn);
console.log('[关闭] 点击 close-window');
await waitForDialogClose();
} else {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true }));
await sleep(500);
await waitForDialogClose();
}
}
// ─── 点击目标 ────────────────────
function getClickTarget(row, type) {
if (type === 'video') {
return row;
} else {
const img = row.querySelector('a.res-url img.res-icon');
if (img) return img;
const infoBtn = row.querySelector('button[title="信息"]');
if (infoBtn) return infoBtn;
return row;
}
}
// ─── 处理单个资源 ─────────────────
async function processRow(row, type, speed) {
const target = getClickTarget(row, type);
const name = row.querySelector('.res-name')?.textContent?.trim() || '未知';
console.log(`[处理] ${name} (速度:${speed.label})`);
simulateClick(target);
const appeared = await waitForDialogAppear(8000);
if (!appeared) {
console.warn(`[警告] ${name} 弹窗未出现`);
return;
}
if (type === 'video') {
try {
await completeVideo();
await sleep(speed.videoAfterWait);
} catch (e) {
console.warn('[视频] 快进失败:', e.message);
await sleep(speed.videoAfterWait);
}
} else {
await sleep(speed.previewWait);
}
if (isDialogOpen()) {
await closeDialog();
console.log(`[完成] ${name}`);
}
}
// ─── UI 面板 ────────────────────
const PANEL_ID = 'batch-res-panel-v3';
let isDragging = false, dragStartX, dragStartY, panelStartX, panelStartY;
function createPanel() {
const old = document.getElementById(PANEL_ID);
if (old) old.remove();
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.style.cssText = `
position:fixed;z-index:999999;top:80px;right:20px;width:400px;max-height:80vh;
background:#fff;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,0.15);
display:flex;flex-direction:column;font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
color:#1e293b;user-select:none;overflow:hidden;
`;
// 进度条
const progressBar = document.createElement('div');
progressBar.id = 'progress-bar';
progressBar.style.cssText = 'height:4px;background:#e2e8f0;width:100%;flex-shrink:0;';
const fill = document.createElement('div');
fill.id = 'progress-fill';
fill.style.cssText = 'height:100%;width:0%;background:#3b82f6;transition:width 0.3s;';
progressBar.appendChild(fill);
panel.appendChild(progressBar);
// 标题栏
const header = document.createElement('div');
header.style.cssText = 'padding:12px 16px 0;cursor:move;display:flex;justify-content:space-between;align-items:center;';
header.innerHTML = `📚 资源批量完成`;
panel.appendChild(header);
// 工具栏
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:6px;padding:8px 16px;border-bottom:1px solid #f1f5f9;align-items:center;flex-wrap:wrap;';
controls.innerHTML = `
`;
panel.appendChild(controls);
// 列表
const listContainer = document.createElement('div');
listContainer.style.cssText = 'flex:1;overflow-y:auto;padding:8px 16px;max-height:300px;';
const ul = document.createElement('ul');
ul.id = 'res-checklist';
ul.style.cssText = 'list-style:none;padding:0;margin:0;';
listContainer.appendChild(ul);
panel.appendChild(listContainer);
// 底部按钮
const footer = document.createElement('div');
footer.style.cssText = 'display:flex;gap:6px;padding:8px 16px 12px;border-top:1px solid #f1f5f9;';
footer.innerHTML = `
`;
panel.appendChild(footer);
document.body.appendChild(panel);
// 拖动
header.addEventListener('mousedown', e => {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') return;
isDragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
panelStartX = panel.offsetLeft;
panelStartY = panel.offsetTop;
panel.style.transition = 'none';
});
document.addEventListener('mousemove', e => {
if (!isDragging) return;
panel.style.left = (panelStartX + e.clientX - dragStartX) + 'px';
panel.style.top = (panelStartY + e.clientY - dragStartY) + 'px';
panel.style.right = 'auto';
});
document.addEventListener('mouseup', () => { isDragging = false; panel.style.transition = ''; });
return panel;
}
// ─── 状态管理 ──────────────────────
const STATE = {
running: false,
paused: false,
stopRequested: false,
currentIndex: 0,
selectedRows: [],
currentType: 'video',
speedKey: 'normal'
};
function getSpeedProfile() {
return SPEED_PROFILES[STATE.speedKey] || SPEED_PROFILES.normal;
}
function updateProgress(current, total) {
const fill = document.getElementById('progress-fill');
if (fill) fill.style.width = (total > 0 ? (current/total)*100 : 0) + '%';
const text = document.getElementById('panel-progress');
if (text) text.textContent = total ? `${current}/${total}` : '';
}
function refreshChecklist() {
const ul = document.getElementById('res-checklist');
if (!ul) return;
ul.innerHTML = '';
const allRows = getAllRows();
const filtered = filterRows(allRows, STATE.currentType);
const selectedSet = new Set(STATE.selectedRows);
filtered.forEach((row, idx) => {
const mime = row.getAttribute('data-mime');
const iconMap = { video: '🎬', application: '📄', image: '🖼', audio: '🎵' };
const typeIcon = iconMap[mime] || '';
const name = typeIcon + (row.querySelector('.res-name')?.textContent?.trim() || `资源${idx+1}`);
const li = document.createElement('li');
li.style.cssText = 'display:flex;align-items:center;gap:8px;padding:4px 0;cursor:pointer;';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = selectedSet.has(row);
cb.addEventListener('change', () => {
if (cb.checked) STATE.selectedRows.push(row);
else STATE.selectedRows = STATE.selectedRows.filter(r => r !== row);
updateProgress(0, STATE.selectedRows.length);
});
const label = document.createElement('span');
label.textContent = `${idx+1}. ${name}`;
label.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;';
li.appendChild(cb);
li.appendChild(label);
li.addEventListener('click', e => {
if (e.target !== cb) {
cb.checked = !cb.checked;
cb.dispatchEvent(new Event('change'));
}
});
ul.appendChild(li);
});
if (STATE.selectedRows.length === 0 && filtered.length > 0) {
STATE.selectedRows = [...filtered];
ul.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = true);
}
updateProgress(0, STATE.selectedRows.length);
}
function updateButtons() {
const start = document.getElementById('btn-start');
const pause = document.getElementById('btn-pause');
const stop = document.getElementById('btn-stop');
if (!start || !pause || !stop) return;
if (STATE.running && !STATE.paused) {
start.style.display = 'none';
pause.style.display = 'block';
stop.style.display = 'block';
} else if (STATE.running && STATE.paused) {
start.style.display = 'block';
start.textContent = '▶ 继续';
start.style.background = '#3b82f6';
pause.style.display = 'none';
stop.style.display = 'block';
} else {
start.style.display = 'block';
start.textContent = '▶ 开始';
start.style.background = '#3b82f6';
pause.style.display = 'none';
stop.style.display = 'none';
}
}
async function processLoop(rows, type) {
const speed = getSpeedProfile();
for (let i = 0; i < rows.length; i++) {
if (STATE.stopRequested) break;
while (STATE.paused && !STATE.stopRequested) await sleep(200);
if (STATE.stopRequested) break;
STATE.currentIndex = i + 1;
updateProgress(i, rows.length);
try {
await processRow(rows[i], type, speed);
} catch (e) {
console.error(`处理错误:`, e);
if (isDialogOpen()) await closeDialog();
}
await sleep(speed.loopInterval);
}
updateProgress(rows.length, rows.length);
STATE.running = false;
STATE.paused = false;
updateButtons();
console.log('✅ 所选资源处理完毕');
}
function bindEvents() {
document.getElementById('type-select')?.addEventListener('change', e => {
STATE.currentType = e.target.value;
STATE.selectedRows = [];
refreshChecklist();
});
document.getElementById('speed-select')?.addEventListener('change', e => {
STATE.speedKey = e.target.value;
console.log(`[速度] 切换为 ${getSpeedProfile().label}`);
});
document.getElementById('btn-select-all')?.addEventListener('click', () => {
const filtered = filterRows(getAllRows(), STATE.currentType);
STATE.selectedRows = [...filtered];
document.querySelectorAll('#res-checklist input[type=checkbox]').forEach(cb => cb.checked = true);
updateProgress(0, STATE.selectedRows.length);
});
document.getElementById('btn-invert')?.addEventListener('click', () => {
const filtered = filterRows(getAllRows(), STATE.currentType);
const selSet = new Set(STATE.selectedRows);
STATE.selectedRows = filtered.filter(r => !selSet.has(r));
document.querySelectorAll('#res-checklist input[type=checkbox]').forEach((cb, idx) => {
cb.checked = !selSet.has(filtered[idx]);
});
updateProgress(0, STATE.selectedRows.length);
});
document.getElementById('btn-start')?.addEventListener('click', () => {
if (STATE.running && STATE.paused) {
STATE.paused = false;
updateButtons();
return;
}
if (STATE.running) return;
if (STATE.selectedRows.length === 0) {
alert('请至少勾选一个资源!');
return;
}
STATE.running = true;
STATE.paused = false;
STATE.stopRequested = false;
STATE.currentIndex = 0;
updateButtons();
processLoop([...STATE.selectedRows], STATE.currentType).then(() => {
updateProgress(STATE.selectedRows.length, STATE.selectedRows.length);
});
});
document.getElementById('btn-pause')?.addEventListener('click', () => {
if (!STATE.running || STATE.paused) return;
STATE.paused = true;
updateButtons();
});
document.getElementById('btn-stop')?.addEventListener('click', () => {
if (!STATE.running) return;
STATE.stopRequested = true;
STATE.running = false;
STATE.paused = false;
STATE.currentIndex = 0;
updateButtons();
updateProgress(0, STATE.selectedRows.length);
});
document.getElementById('btn-close-panel')?.addEventListener('click', () => {
document.getElementById(PANEL_ID)?.remove();
});
}
function waitForRows() {
return new Promise(resolve => {
if (document.querySelector('.res-row')) return resolve();
const obs = new MutationObserver(() => {
if (document.querySelector('.res-row')) {
obs.disconnect();
resolve();
}
});
obs.observe(document.body, { childList: true, subtree: true });
setTimeout(() => { obs.disconnect(); resolve(); }, 10000);
});
}
(async () => {
await waitForRows();
if (getAllRows().length === 0) return;
createPanel();
refreshChecklist();
bindEvents();
})();
})();