// ==UserScript== // @name 企学宝 · 防切屏暂停 + 自动下一节 // @namespace local.qixuebao // @version 1.0.0 // @description 课程播放页保持播放(绕过 hasFocus 切屏检测),播完自动点「下一节」 // @match https://*.lmschina.net/static/admin/* // @run-at document-start // @grant none // ==/UserScript== (function () { 'use strict'; /* ================== 配置 ================== */ var CFG = { keepPlaying: true, // 防切屏暂停 autoNext: true, // 播完自动下一节 nextDelay: 2500, // 结束后等多久点「下一节」(毫秒) debug: true // 是否在 Console 打印调试信息 }; var log = CFG.debug ? function () { console.log.apply(console, ['[qxb]'].concat([].slice.call(arguments))); } : function () {}; // 只在课程播放页生效;考试、编辑器等页面保持原生行为 function onPlayPage() { return location.hash.indexOf('#/course/play/') === 0; } /* ========== 1. 防切屏暂停 ========== 站点检测点: - document.hasFocus() (每次 timeupdate 都会查,无焦点即 pause) - window.onblur = fn (挂上后一失焦立即 pause) 所以这里伪装 hasFocus,并在事件捕获阶段拦截 blur / visibilitychange。 */ if (CFG.keepPlaying) { var hiddenDesc = Object.getOwnPropertyDescriptor(Document.prototype, 'hidden'); var visDesc = Object.getOwnPropertyDescriptor(Document.prototype, 'visibilityState'); var nativeHasFocus = Document.prototype.hasFocus; var fakeHasFocus = function () { return onPlayPage() ? true : nativeHasFocus.call(document); }; try { Document.prototype.hasFocus = fakeHasFocus; document.hasFocus = fakeHasFocus; } catch (e) { log('hasFocus 伪装失败', e); } try { Object.defineProperty(document, 'hidden', { configurable: true, get: function () { return onPlayPage() ? false : hiddenDesc.get.call(document); } }); Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { return onPlayPage() ? 'visible' : visDesc.get.call(document); } }); } catch (e) { log('visibility 伪装失败', e); } // 本脚本先于页面注册监听器,捕获阶段 stopImmediatePropagation 能挡住页面自己的处理 var swallow = function (e) { if (onPlayPage()) e.stopImmediatePropagation(); }; document.addEventListener('visibilitychange', swallow, true); document.addEventListener('webkitvisibilitychange', swallow, true); window.addEventListener('blur', swallow, true); log('keepPlaying 已启用'); } /* ========== 2. 播完自动下一节 ========== 播完时站点渲染
: - 非最后一节:

下一节名称

+ primary 按钮「下一节」 - 最后一节: primary 按钮包在 里(返回课程),无

所以只在「有

且按钮不在 内」时点击。 */ if (CFG.autoNext) { var scheduled = false; function overlayOpen() { var boxes = document.querySelectorAll('.el-message-box__wrapper, .el-dialog__wrapper'); for (var i = 0; i < boxes.length; i++) { if (boxes[i].offsetParent !== null) return true; // 有答题/提示框,交给人工 } return false; } function nextButton() { var end = document.querySelector('.end-page'); if (!end) return null; if (!end.querySelector('p')) return null; var btns = end.querySelectorAll('button.el-button--primary'); for (var i = 0; i < btns.length; i++) { if (!btns[i].closest('a') && !btns[i].disabled) return btns[i]; } return null; } function tick() { if (!onPlayPage()) { scheduled = false; return; } if (!document.querySelector('.end-page')) { scheduled = false; return; } if (scheduled) return; scheduled = true; setTimeout(function () { scheduled = false; if (overlayOpen()) { log('有弹窗,暂不切下一节'); return; } var b = nextButton(); if (b) { log('点击「下一节」'); b.click(); } }, CFG.nextDelay); } setInterval(tick, 800); log('autoNext 已启用'); } })();