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