// ==UserScript== // @name 雨课堂课件定时翻页 // @namespace https://www.yuketang.cn/ // @version 1.0.0 // @description 在雨课堂课件页显示控制面板,按自定义时间自动上一页或下一页。 // @author You // @match https://www.yuketang.cn/v2/web/studentCards/*/ppt* // @match https://www.yuketang.cn/v2/web/studentCards/*/ppt/* // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = 'yuketang-auto-page-settings-v1'; const DEFAULTS = { interval: 3, action: 'next' }; let timer = null; let count = 0; const readSettings = () => { try { return { ...DEFAULTS, ...GM_getValue(STORAGE_KEY, {}) }; } catch (_) { return DEFAULTS; } }; const saveSettings = (settings) => { try { GM_setValue(STORAGE_KEY, settings); } catch (_) { /* 油猴不可用时忽略保存 */ } }; const settings = readSettings(); const host = document.createElement('div'); host.id = 'yt-auto-page-panel-host'; const shadow = host.attachShadow({ mode: 'open' }); document.documentElement.appendChild(host); shadow.innerHTML = `
雨课堂定时翻页
尚未开始
开始后将在每个间隔结束时翻一页。
`; const $ = (selector) => shadow.querySelector(selector); const intervalInput = $('#interval'); const actionSelect = $('#action'); const startButton = $('#start'); const testButton = $('#test'); const status = $('#status'); const dot = $('#dot'); function currentSettings() { const interval = Number(intervalInput.value); return { interval: Number.isFinite(interval) && interval >= 1 ? interval : DEFAULTS.interval, action: actionSelect.value === 'prev' ? 'prev' : 'next', }; } function sendKey(key) { // 雨课堂会监听 document 的方向键;同时发送 keydown/keyup 以兼容不同播放器版本。 for (const type of ['keydown', 'keyup']) { document.dispatchEvent(new KeyboardEvent(type, { key, code: key === 'ArrowRight' ? 'ArrowRight' : 'ArrowLeft', keyCode: key === 'ArrowRight' ? 39 : 37, which: key === 'ArrowRight' ? 39 : 37, bubbles: true, cancelable: true, })); } } function turnPage() { const { action } = currentSettings(); const selector = action === 'next' ? '.pswp__button--arrow--right' : '.pswp__button--arrow--left'; const nativeButton = document.querySelector(selector); // 若图片播放器的原生按钮实际可见,优先点击;否则用方向键切换普通课件。 if (nativeButton && nativeButton.getBoundingClientRect().width > 0) nativeButton.click(); else sendKey(action === 'next' ? 'ArrowRight' : 'ArrowLeft'); count += 1; status.textContent = `已执行 ${count} 次:${action === 'next' ? '下一页' : '上一页'}`; } function stop() { if (timer !== null) clearInterval(timer); timer = null; startButton.textContent = '开始'; dot.classList.remove('running'); } function start() { const config = currentSettings(); intervalInput.value = config.interval; saveSettings(config); stop(); count = 0; timer = setInterval(turnPage, config.interval * 1000); startButton.textContent = '停止'; dot.classList.add('running'); status.textContent = `运行中:每 ${config.interval} 秒${config.action === 'next' ? '下一页' : '上一页'}`; } startButton.addEventListener('click', () => timer === null ? start() : (stop(), status.textContent = '已停止')); testButton.addEventListener('click', () => { saveSettings(currentSettings()); turnPage(); }); intervalInput.addEventListener('change', () => saveSettings(currentSettings())); actionSelect.addEventListener('change', () => saveSettings(currentSettings())); window.addEventListener('beforeunload', stop, { once: true }); })();