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