// ==UserScript== // @name 湖南大学继续教育小助手 // @namespace http://jxjyjd.hnu.edu.cn/ // @version 1.1.0 // @description 基于 ScriptCat 6925 号脚本二次开发修复。自动播放、静音、防暂停、切后台计时、播放完自动下一节。极简悬浮胶囊 UI。本版补回精简时丢掉的功能:时长/进度读取(SDK 方法兜底)、播放失败重试、保活卡住检测、ended 事件绑定、SDK 静音与重播、Visibility 补丁补全(moz/ms + onvisibilitychange 拦截)、胶囊补回播放/暂停/静音/日志、SPA 换课重置播放器。 // @author xqqbt & AI & dameng9 // @match https://jxjyjd.hnu.edu.cn/course/*/task/*/show // @match https://jxjyjd.hnu.edu.cn/my/course/* // @grant none // @run-at document-idle // @noframes // @license GPL-3.0 // ==/UserScript== /* * 说明: * 本脚本基于 ScriptCat (https://scriptcat.org/zh-CN/script-show-page/6925) 二次开发修复。 * 1. 保留极简悬浮胶囊 (Glassmorphism Capsule UI),但补回控制按钮与日志。 * 2. 核心逻辑对齐完整版 v1.3.3:时长读取兜底、播放重试、保活、事件绑定、SDK 静音/重播。 */ (function () { 'use strict'; if (window.__HNU_HELPER_V110__) return; if (window !== window.top) return; window.__HNU_HELPER_V110__ = true; const CFG = { autoPlay: true, autoNext: true, mute: true, replayToFinish: true, // 视频放完但学习时长未满:不跳节,重播继续计时 switchDelay: 3000, loopInterval: 3000, // 主循环:检测播放器 + 保活 + 结束检测 activityInterval: 15000, // 模拟用户活动,防无操作检测 }; const state = { win: null, playerObj: null, innerPlayer: null, videoEl: null, currentTaskId: null, playing: false, endedFired: false, completedFired: false, lastCheckTime: -1, retryCount: 0, remainLocal: null, remainStamp: 0, logLines: [], }; const $ = (s) => document.querySelector(s); const $$ = (s) => Array.from(document.querySelectorAll(s)); function log(msg) { const line = `[小助手v1.1.0][${new Date().toLocaleTimeString()}] ${msg}`; console.log(line); state.logLines.push(line); if (state.logLines.length > 300) state.logLines.shift(); updateUI(); } /* ===================== Page Visibility 补丁(顶层 + iframe) ===================== */ /* 平台计时靠 iframe 内部真实可见性,补丁必须同时打到顶层和 task-content-iframe。 除读属性外,还要拦 onvisibilitychange 赋值——反刷代码常靠属性赋值挂监听。 */ function patchVisibility(win, doc) { if (!win || !doc || win.__AL_VIS_PATCHED__) return; win.__AL_VIS_PATCHED__ = true; ['hidden', 'webkitHidden', 'mozHidden', 'msHidden'].forEach(p => { if (p in doc) try { Object.defineProperty(doc, p, { configurable: true, get: () => false }); } catch (e) {} }); ['visibilityState', 'webkitVisibilityState', 'mozVisibilityState', 'msVisibilityState'].forEach(p => { if (p in doc) try { Object.defineProperty(doc, p, { configurable: true, get: () => 'visible' }); } catch (e) {} }); try { if (typeof win.hasFocus === 'function') win.hasFocus = () => true; } catch (e) {} ['onvisibilitychange', 'onblur', 'onfocus'].forEach(prop => { const target = (prop === 'onblur' || prop === 'onfocus') ? win : doc; if (prop in target) { try { Object.defineProperty(target, prop, { configurable: true, get: () => undefined, set: () => {} }); } catch (e) {} } }); ['visibilitychange', 'webkitvisibilitychange', 'mozvisibilitychange', 'blur'].forEach(evt => { const target = (evt === 'blur') ? win : doc; try { target.addEventListener(evt, e => e.stopImmediatePropagation(), true); } catch (e) {} }); if (!win.__AL_ACTIVITY_TIMER__) { win.__AL_ACTIVITY_TIMER__ = setInterval(() => { try { doc.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: 0, clientY: 0, movementX: 1, movementY: 0 })); } catch (e) {} }, CFG.activityInterval); } } patchVisibility(window, document); /* ===================== 播放器查找 ===================== */ function getIframeWin() { const ifr = document.getElementById('task-content-iframe') || document.querySelector('.task-content-iframe'); try { return ifr ? ifr.contentWindow : null; } catch (e) { return null; } } function findVideo(win) { try { let v = win.document.querySelector('video'); if (v) return v; for (const fr of win.document.querySelectorAll('iframe')) { try { const w2 = fr.contentWindow; const v2 = w2 && w2.document && w2.document.querySelector('video'); if (v2) return v2; } catch (e) {} } } catch (e) {} return null; } /* ===================== 读取播放进度 / 时长 ===================== */ function tryNum(obj, names) { if (!obj) return null; for (const n of names) { try { const v = obj[n]; if (typeof v === 'function') { const r = v.call(obj); if (typeof r === 'number' && isFinite(r)) return r; } else if (typeof v === 'number' && isFinite(v)) { return v; } } catch (e) {} } return null; } function getCurTime() { if (state.videoEl && typeof state.videoEl.currentTime === 'number' && state.videoEl.currentTime > 0) { return state.videoEl.currentTime; } const a = tryNum(state.innerPlayer, ['currentTime', 'getCurrentTime', 'getTime']); if (a !== null && a > 0) return a; const b = tryNum(state.playerObj, ['currentTime', 'getCurrentTime', 'getTime']); return b !== null && b > 0 ? b : 0; } function getDuration() { // 真实时长优先从 iframe 里的