UOOC assistant

【使用前先看介绍/有问题可反馈】UOOC 优课联盟助手 (UOOC assistant):可选是否倍速 (若取消勾选则一倍速播放),可选是否静音 (若取消勾选则恢复原音量),可选是否播放 (若取消勾选则暂停播放),可选是否连播 (若取消勾选则循环播放),离开页面保持视频状态,自动回答视频中途弹出问题,可复制已提交测验题目及答案,键盘左右方向键可以控制视频快进/快退,上下方向键可以控制音量增大/减小,空格键可以控制播放/暂停,停止连播支持提醒,如果视频标题下面出现 `倍速/静音/播放/连播` 选项说明脚本正常启动运行。

  1. // ==UserScript==
  2. // @name UOOC assistant
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.7
  5. // @description 【使用前先看介绍/有问题可反馈】UOOC 优课联盟助手 (UOOC assistant):可选是否倍速 (若取消勾选则一倍速播放),可选是否静音 (若取消勾选则恢复原音量),可选是否播放 (若取消勾选则暂停播放),可选是否连播 (若取消勾选则循环播放),离开页面保持视频状态,自动回答视频中途弹出问题,可复制已提交测验题目及答案,键盘左右方向键可以控制视频快进/快退,上下方向键可以控制音量增大/减小,空格键可以控制播放/暂停,停止连播支持提醒,如果视频标题下面出现 `倍速/静音/播放/连播` 选项说明脚本正常启动运行。
  6. // @author cc
  7. // @include http://www.uooc.net.cn/home/learn/index*
  8. // @include http://www.uooconline.com/home/learn/index*
  9. // @include https://www.uooc.net.cn/home/learn/index*
  10. // @include https://www.uooconline.com/home/learn/index*
  11. // @grant none
  12. // @require https://greasyfork.org/scripts/418193-coder-utils.js
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17. const RECURSION_DURATION = 500;
  18. let recursion = () => {
  19. let extraTime = 0;
  20. try {
  21. let done = false;
  22. let video = document.querySelector('#player_html5_api');
  23. if (video) {
  24. if (document.getElementById('rate').checked)
  25. video.playbackRate = 2;
  26. else
  27. video.playbackRate = 1;
  28. if (document.getElementById('volume').checked)
  29. video.muted = true;
  30. else
  31. video.muted = false;
  32. if (document.getElementById('play').checked && !video.ended)
  33. video.play();
  34. else
  35. video.pause();
  36. if (video.ended)
  37. done = true;
  38. let quizLayer = document.querySelector('#quizLayer');
  39. if (quizLayer && quizLayer.style.display != 'none') {
  40. if (done) {
  41. setTimeout(() => {
  42. document.querySelectorAll('.layui-layer-shade').forEach(e => e.style.display = 'none');
  43. }, RECURSION_DURATION << 1);
  44. };
  45. let source = JSON.parse(document.querySelector('div[uooc-video]').getAttribute('source'));
  46. let quizList = source.quiz;
  47. let quizIndex = 0;
  48. let quizQuestion = document.querySelector('.smallTest-view .ti-q-c').innerHTML;
  49. for (let i = 0; i < quizList.length; i++) {
  50. if (quizList[i].question == quizQuestion) {
  51. quizIndex = i;
  52. break;
  53. };
  54. };
  55. let quizAnswer = eval(quizList[quizIndex].answer);
  56. let quizOptions = quizLayer.querySelector('div.ti-alist');
  57. for (let ans of quizAnswer) {
  58. let labelIndex = ans.charCodeAt() - 'A'.charCodeAt();
  59. quizOptions.children[labelIndex].click();
  60. }; // end for
  61. quizLayer.querySelector('button').click();
  62. extraTime = 1000;
  63. }; // end if
  64. if (!done) {
  65. if (video.paused && document.getElementById('play').checked) {
  66. video.play();
  67. } else {
  68. document.querySelectorAll('.layui-layer-shade, #quizLayer').forEach(e => e.style.display = 'none');
  69. };
  70. };
  71. }; // end if (video)
  72. if (!done) {
  73. setTimeout(recursion, RECURSION_DURATION + extraTime);
  74. } else if (video) {
  75. if (!document.getElementById('continue').checked) {
  76. video.currentTime = 0;
  77. // video.ended = false;
  78. setTimeout(recursion, RECURSION_DURATION + extraTime);
  79. } else {
  80. let current_video = document.querySelector('.basic.active');
  81. let next_part = current_video.parentNode;
  82. let next_video = current_video;
  83. // 定义判断是否视频的函数
  84. let isVideo = node => Boolean(node.querySelector('span.icon-video'));
  85. // 定义是否可返回上一级目录的函数
  86. let canBack = () => {
  87. return Boolean(next_part.parentNode.parentNode.tagName === 'LI');
  88. };
  89. // 定义更新至后续视频的函数
  90. let toNextVideo = () => {
  91. next_video = next_video.nextElementSibling;
  92. while (next_video && !isVideo(next_video)) {
  93. next_video = next_video.nextElementSibling;
  94. };
  95. };
  96. // 定义判断是否存在视频的函数
  97. let isExistsVideo = () => {
  98. let _video = next_part.firstElementChild;
  99. while (_video && !isVideo(_video)) {
  100. _video = _video.nextElementSibling;
  101. };
  102. return Boolean(_video && isVideo(_video));
  103. };
  104. // 定义判断是否存在后续视频的函数
  105. let isExistsNextVideo = () => {
  106. let _video = current_video.nextElementSibling;
  107. while (_video && !isVideo(_video)) {
  108. _video = _video.nextElementSibling;
  109. };
  110. return Boolean(_video && isVideo(_video));
  111. };
  112. // 定义检查文件后是否存在后续目录的函数
  113. let isExistsNextListAfterFile = () => {
  114. let part = next_part.nextElementSibling;
  115. return Boolean(part && part.childElementCount > 0);
  116. };
  117. // 定义更新文件后的后续目录的函数
  118. let toNextListAfterFile = () => {
  119. next_part = next_part.nextElementSibling;
  120. };
  121. // 定义返回上一级的函数
  122. let toOuterList = () => {
  123. next_part = next_part.parentNode.parentNode;
  124. };
  125. // 定义返回主条目的函数
  126. let toOuterItem = () => {
  127. next_part = next_part.parentNode;
  128. };
  129. // 定义检查列表后是否存在后续目录的函数
  130. let isExistsNextListAfterList = () => {
  131. return Boolean(next_part.nextElementSibling);
  132. };
  133. // 定义进入列表后的后续目录的函数
  134. let toNextListAfterList = () => {
  135. next_part = next_part.nextElementSibling;
  136. };
  137. // 定义展开目录的函数
  138. let expandList = () => {
  139. next_part.firstElementChild.click();
  140. };
  141. // 定义进入展开目录的第一个块级元素的函数
  142. let toExpandListFirstElement = () => {
  143. next_part = next_part.firstElementChild.nextElementSibling;
  144. if (next_part.classList.contains('unfoldInfo')) {
  145. next_part = next_part.nextElementSibling;
  146. };
  147. };
  148. // 定义判断块级元素是否目录列表的函数
  149. let isList = () => {
  150. return Boolean(next_part.tagName === 'UL');
  151. };
  152. // 定义目录列表的第一个目录的函数
  153. let toInnerList = () => {
  154. next_part = next_part.firstElementChild;
  155. };
  156. // 定义进入文件列表的第一个视频的函数
  157. let toFirstVideo = () => {
  158. next_video = next_part.firstElementChild;
  159. while (next_video && !isVideo(next_video)) {
  160. next_video = next_video.nextElementSibling;
  161. };
  162. };
  163. // 定义模式
  164. let mode = {
  165. FIRST_VIDEO: 'FIRST_VIDEO',
  166. NEXT_VIDEO: 'NEXT_VIDEO',
  167. LAST_LIST: 'LAST_LIST',
  168. NEXT_LIST: 'NEXT_LIST',
  169. INNER_LIST: 'INNER_LIST',
  170. OUTER_LIST: 'OUTER_LIST',
  171. OUTER_ITEM: 'OUTER_ITEM',
  172. }
  173. // 定义搜索函数
  174. let search = (_mode) => {
  175. switch (_mode) {
  176. case mode.FIRST_VIDEO: // mode = 0
  177. if (isExistsVideo()) {
  178. toFirstVideo();
  179. next_video.click();
  180. setTimeout(recursion, RECURSION_DURATION);
  181. } else if (isExistsNextListAfterFile()) {
  182. search(mode.LAST_LIST);
  183. } else {
  184. // perhaps there is an exam, end recursion
  185. Notification.requestPermission().then((permission) => {
  186. if (permission === 'granted') {
  187. let text = '已停止连播,可能遇到测试章节';
  188. new Notification('UOOC Assistant', { body: text });
  189. };
  190. });
  191. };
  192. break;
  193. case mode.NEXT_VIDEO: // mode == 1
  194. if (isExistsNextVideo()) {
  195. toNextVideo();
  196. next_video.click();
  197. setTimeout(recursion, RECURSION_DURATION);
  198. } else if (isExistsNextListAfterFile()) {
  199. search(mode.LAST_LIST);
  200. } else {
  201. search(mode.OUTER_ITEM);
  202. };
  203. break;
  204. case mode.LAST_LIST: // mode == 2
  205. toNextListAfterFile();
  206. toInnerList();
  207. search(mode.INNER_LIST);
  208. break;
  209. case mode.NEXT_LIST: // mode == 3
  210. toNextListAfterList();
  211. search(mode.INNER_LIST);
  212. break;
  213. case mode.INNER_LIST: // mode == 4
  214. expandList();
  215. (function waitForExpand () {
  216. if (next_part.firstElementChild.nextElementSibling) {
  217. toExpandListFirstElement();
  218. if (isList()) {
  219. toInnerList();
  220. search(mode.INNER_LIST);
  221. } else {
  222. search(mode.FIRST_VIDEO);
  223. };
  224. } else {
  225. setTimeout(waitForExpand, RECURSION_DURATION);
  226. };
  227. })();
  228. break;
  229. case mode.OUTER_LIST: // mode == 5
  230. toOuterList();
  231. if (isExistsNextListAfterList()) {
  232. search(mode.NEXT_LIST);
  233. } else if (canBack()) {
  234. search(mode.OUTER_LIST);
  235. } else {
  236. // perhaps there is no next list
  237. };
  238. break;
  239. case mode.OUTER_ITEM: // mode == 6
  240. toOuterItem();
  241. if (isExistsNextListAfterList()) {
  242. toNextListAfterList();
  243. search(mode.INNER_LIST);
  244. } else if (canBack()){
  245. search(mode.OUTER_LIST);
  246. } else {
  247. // perhaps there is no list
  248. };
  249. break;
  250. default:
  251. break;
  252. };
  253. };
  254. try {
  255. search(mode.NEXT_VIDEO);
  256. } catch (err) {
  257. console.error(err);
  258. };
  259. };
  260. };
  261. } catch (err) {
  262. console.error(err);
  263. };
  264. }; // end recursion
  265. let wait = () => {
  266. if (document.readyState == 'complete') {
  267. // get permission
  268. Notification.requestPermission().then((permission) => {
  269. if (permission === 'granted') {
  270. console.log('UOOC Assistant: 已获得通知权限');
  271. } else {
  272. console.log('UOOC Assistant: 无法获得通知权限');
  273. };
  274. });
  275. // define element creating functions
  276. let getCheckbox = (name, text) => {
  277. let p = HTMLElement.$mkel('p', {}, {}, {
  278. 'color': '#cccccc',
  279. 'padding-left': '10px',
  280. });
  281. let checkbox = HTMLElement.$mkel('input', {
  282. id: name,
  283. type: 'checkbox',
  284. name: name,
  285. value: name,
  286. }, {
  287. checked: true,
  288. }, {
  289. 'margin-left': '15px',
  290. 'width': '12px',
  291. 'height': '12px',
  292. });
  293. p.append(checkbox);
  294. let label = HTMLElement.$mkel('label', {
  295. for: name,
  296. }, {
  297. innerText: text,
  298. }, {
  299. 'margin-left': '13px',
  300. 'font-size': '12px',
  301. });
  302. p.append(label);
  303. return p;
  304. };
  305. let getContainer = (_id) => {
  306. return HTMLElement.$mkel('div', {id: _id}, {}, {
  307. 'display': 'flex',
  308. 'flex-direction': 'row',
  309. 'align-items': 'center',
  310. });
  311. };
  312. // set checkbox container
  313. let checkboxContainer = getContainer('checkbox-container');
  314. let rateCheckbox = getCheckbox('rate', '倍速');
  315. let volumeCheckbox = getCheckbox('volume', '静音');
  316. let playCheckbox = getCheckbox('play', '播放');
  317. let continueCheckbox = getCheckbox('continue', '连播');
  318. let copyCheckbox = HTMLElement.$mkel('p', {}, {}, {
  319. 'color': '#cccccc',
  320. 'padding-left': '10px',
  321. });
  322. let btn = HTMLElement.$mkel('button', {}, {innerHTML: '复制题目答案'}, {
  323. 'margin-left': '13px',
  324. 'padding': '0 5px 0',
  325. 'font-size': '12px',
  326. 'cursor': 'pointer',
  327. }, {
  328. click: function(event) {
  329. let testPaperTop = frames[0] ? frames[0].document.querySelector('.testPaper-Top') : document.querySelector('.testPaper-Top');
  330. if (!testPaperTop) {
  331. alert('该页面不是测验页面,无法复制内容');
  332. } else {
  333. if (testPaperTop.querySelector('.fl_right')) {
  334. let queItems = frames[0] ? Array.from(frames[0].document.querySelectorAll('.queItems')) : Array.from(document.querySelectorAll('.queItems'));
  335. let content = queItems.map(queType => {
  336. let res = '';
  337. if (queType.querySelector('.queItems-type').innerText.indexOf('选') >= 0) {
  338. let questions = queType.querySelectorAll('.queContainer');
  339. res += Array.from(questions).map(question => {
  340. let que = question.querySelector('.queBox').innerText.replace(/\n{2,}/g, '\n').replace(/(\w\.)\n/g, '$1 ');
  341. let ans = question.querySelector('.answerBox div:first-child').innerText.replace(/\n/g, '');
  342. let right = question.querySelector('.scores').innerText.match(/\d+\.?\d+/g).map(score => eval(score));
  343. right = right[0] === right[1];
  344. return `${que}\n${ans}\n是否正确:${right}\n`;
  345. }).join('\n');
  346. };
  347. return res;
  348. }).join('\n');
  349. content.$copyToClipboard();
  350. alert('题目及答案已复制到剪切板');
  351. } else {
  352. alert('该测验可能还没提交,无法复制');
  353. };
  354. };
  355. },
  356. });
  357. copyCheckbox.appendChild(btn);
  358. let head = document.querySelector('.learn-head');
  359. if (!head) {
  360. setTimeout(wait, RECURSION_DURATION);
  361. return;
  362. };
  363. checkboxContainer.appendChild(rateCheckbox);
  364. checkboxContainer.appendChild(volumeCheckbox);
  365. checkboxContainer.appendChild(playCheckbox);
  366. checkboxContainer.appendChild(continueCheckbox);
  367. checkboxContainer.appendChild(copyCheckbox);
  368. // set prompt container
  369. let promptContainer = getContainer('prompt-container');
  370. let div = HTMLElement.$mkel('div', {}, {
  371. innerHTML: `提示:<u><a href="https://greasyfork.org/zh-CN/scripts/425837-uooc-assistant-beta" target="_blank" style="color: yellow;">更新内测版本,点此尝鲜试用</a></u>,键盘的 \u2190 \u2192 可以控制快进/快退,\u2191 \u2193 可以控制音量增大/减小,空格键可以控制播放/暂停`,
  372. }, {
  373. 'color': '#cccccc',
  374. 'height': 'min-height',
  375. 'margin': '0 20px 0',
  376. 'padding': '0 5px',
  377. 'border-radius': '5px',
  378. 'font-size': '12px',
  379. });
  380. promptContainer.appendChild(div);
  381. let appreciationCodeContainer = getContainer('appreciation-code-container');
  382. let a = HTMLElement.$mkel('a', {
  383. href: 'https://s1.ax1x.com/2020/11/08/BTeRqe.png',
  384. target: '_blank',
  385. }, {
  386. innerHTML: '<u>本脚本使用完全免费😉,脚本代码编写维护不易,俊男👦靓女👧们有心的话可以点这儿支持一下作者呀❤️~</u>',
  387. }, {
  388. 'color': '#cccccc',
  389. 'font-weight': 'bold',
  390. 'height': 'min-height',
  391. 'margin': '0 20px 0',
  392. 'padding': '0 5px',
  393. 'border-radius': '5px',
  394. 'font-size': '11px',
  395. });
  396. appreciationCodeContainer.appendChild(a);
  397. // set head
  398. head.appendChild(checkboxContainer);
  399. head.appendChild(promptContainer);
  400. head.appendChild(appreciationCodeContainer);
  401. head.style.height = `${head.offsetHeight + 30}px`;
  402. // bind key down events
  403. document.onkeydown = (event) => {
  404. let k = event.key;
  405. let complete = false;
  406. let div = document.querySelector('div.basic.active');
  407. if (div && div.classList.contains('complete'))
  408. complete = true;
  409. let video = document.getElementById('player_html5_api');
  410. if (video) {
  411. switch (k) {
  412. case 'ArrowLeft': {
  413. video.currentTime -= 10;
  414. break;
  415. };
  416. case 'ArrowRight': {
  417. if (complete)
  418. video.currentTime += 10;
  419. break;
  420. };
  421. case 'ArrowUp': {
  422. if (video.volume + 0.1 <= 1.0)
  423. video.volume += 0.1;
  424. else
  425. video.volume = 1.0;
  426. break;
  427. }
  428. case 'ArrowDown': {
  429. if (video.volume - 0.1 >= 0.0)
  430. video.volume -= 0.1;
  431. else
  432. video.volume = 0.0;
  433. break;
  434. };
  435. case ' ': {
  436. let continueCheckbox = document.getElementById('play');
  437. continueCheckbox.checked = !continueCheckbox.checked;
  438. break;
  439. };
  440. };
  441. };
  442. };
  443. // information
  444. console.info('UOOC assistant init ok.');
  445. recursion();
  446. } else {
  447. setTimeout(wait, RECURSION_DURATION);
  448. };
  449. }; // end wait
  450. wait();
  451. })();