UOOC assistant beta

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

目前為 2021-05-02 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         UOOC assistant beta
// @name         UOOC 优课联盟助手 (内测版)
// @namespace    http://tampermonkey.net/
// @version      1.0.5
// @description  【使用前先看介绍/有问题可反馈】UOOC 优课联盟助手 (内测版) (UOOC assistant beta):可选是否倍速 (若取消勾选则一倍速播放),可选是否静音 (若取消勾选则恢复原音量),可选是否播放 (若取消勾选则暂停播放),可选是否连播 (若取消勾选则循环播放),离开页面保持视频状态,自动回答视频中途弹出问题,可复制已提交测验题目及答案;键盘左右方向键可以控制视频快进/快退,上下方向键可以控制音量增大/减小,空格键可以控制播放/暂停;如果视频标题下面出现 `倍速/静音/播放/连播` 选项说明脚本正常启动运行。
// @author       cc
// @include      http://www.uooc.net.cn/home/learn/index*
// @grant        none
// ==/UserScript==

function bindChapterChange () {
  function bindSubChapterChange () {
    var sourceView = document.querySelector('[source-view]')
    var sObserver = new MutationObserver(function (mutations) {
      if (document.querySelector('[source-view] [uooc-video] video')) setTimeout(start, 250)
    })
    sObserver.observe(sourceView, { childList: true })
  }
  var mainLeft = document.querySelector('.learn-main-left')
  var mObserver = new MutationObserver(function (mutations) {
    bindSubChapterChange()
  })
  mObserver.observe(mainLeft, { childList: true })
  bindSubChapterChange()
}
function autoQuiz () {
  function autoQuizAnswer () {
    let quizLayer = document.getElementById('quizLayer')
    let source = JSON.parse(document.querySelector('div[uooc-video]').getAttribute('source'))
    let quizQuestion = document.querySelector('.smallTest-view .ti-q-c').innerHTML
    let quizAnswer = source.quiz.find(q => q.question === quizQuestion).answer
    let quizOptions = quizLayer.querySelector('div.ti-alist')
    for (let ans of eval(quizAnswer)) quizOptions.children[ans.charCodeAt() - 'A'.charCodeAt()].click()
    quizLayer.querySelector('button').click()
  }
  var learnView = document.querySelector('.lean_view')
  var observer = new MutationObserver(function (mutations) {
    for (let mutation of mutations) {
      let node = mutation.addedNodes[0]
      if (node && node.id && node.id.includes('layui-layer')) {
        autoQuizAnswer()
        break
      }
    }
  })
  observer.observe(learnView, { childList: true })
}
function bindVideoEvents () {
  var video = document.querySelector('#player_html5_api')
  video.onpause = function () { if (document.querySelector('#play').checked) this.play() }
  video.onended = findNextVideo
}
function start () {
  bindKeyboardEvents()
  bindVideoEvents()
  autoQuiz()
  var video = document.getElementById('player_html5_api')
  var volume = document.getElementById('volume')
  var play = document.getElementById('play')
  var rate = document.getElementById('rate')
  if (volume.checked) video.muted = true
  if (play.checked && video.paused) video.play()
  if (rate.checked) video.playbackRate = 2.0
}
function placeComponents () {
  function copyToClipboard (content) {
    var t = document.createElement('textarea')
    t.value = content
    document.body.appendChild(t)
    t.select()
    document.execCommand('copy')
    document.body.removeChild(t)
  }
  function getCheckbox (name, text) {
    var p = document.createElement('p')
    p.style = 'color: #ccc; padding-left: 10px;'
    var checkbox = document.createElement('input')
    checkbox.id = checkbox.name = checkbox.value = name
    checkbox.type = 'checkbox'
    checkbox.checked = true
    checkbox.style = 'margin-left: 15px; width: 12px; height: 12px;'
    p.append(checkbox)
    var label = document.createElement('label')
    label.for = name
    label.innerText = text
    label.style = 'margin-left: 13px; font-size: 12px;'
    p.append(label)
    return p
  }
  function getContainer (_id) {
    var container = document.createElement('div')
    container.id = _id
    container.style = 'display: flex; flex-direction: row; align-items: center;'
    return container
  }
  function getCopyButton () {
    var copyButton = document.createElement('p')
    copyButton.style = 'color: #ccc; padding-left: 10px;'
    var btn = document.createElement('button')
    btn.innerText = '复制题目答案'
    btn.style = 'margin-left: 13px; padding: 0 5px 0; font-size: 12px; cursor: pointer;'
    btn.onclick = function () {
      var testPaperTop = frames[0] ? frames[0].document.querySelector('.testPaper-Top') : document.querySelector('.testPaper-Top');
      if (!testPaperTop) {
        alert('该页面不是测验页面,无法复制内容')
      } else {
        if (testPaperTop.querySelector('.fl_right')) {
          var queItems = frames[0] ? Array.from(frames[0].document.querySelectorAll('.queItems')) : Array.from(document.querySelectorAll('.queItems'));
          var content = queItems.map(queType => {
            var res = ''
            if (queType.querySelector('.queItems-type').innerText.indexOf('选') >= 0) {
              var questions = queType.querySelectorAll('.queContainer')
              res += Array.from(questions).map((question) => {
                var que = question.querySelector('.queBox').innerText.replace(/\n{2,}/g, '\n').replace(/(\w\.)\n/g, '$1 ')
                var ans = question.querySelector('.answerBox div:first-child').innerText.replace(/\n/g, '')
                var right = question.querySelector('.scores').innerText.match(/\d+\.?\d+/g).map(score => eval(score))
                right = right[0] === right[1]
                return `${que}\n${ans}\n是否正确:${right}\n`
              }).join('\n')
            }
            return res
          }).join('\n')
          copyToClipboard(content)
          alert('题目及答案已复制到剪切板')
        } else {
          alert('该测验可能还没提交,无法复制')
        }
      }
    }
    copyButton.appendChild(btn)
    return copyButton
  }
  function setCheckboxes (container) {
    var rateCheckbox = getCheckbox('rate', '倍速')
    var volumeCheckbox = getCheckbox('volume', '静音')
    var playCheckbox = getCheckbox('play', '播放')
    var continueCheckbox = getCheckbox('continue', '连播')
    var copyButton = getCopyButton()
    var video = document.getElementById('player_html5_api')
    rateCheckbox.firstElementChild.checked
    rateCheckbox.firstElementChild.onchange = function (event) {
      if (event.target.checked) video.playbackRate = 2
    }
    volumeCheckbox.firstElementChild.onchange = function (event) {
      if (event.target.checked) video.muted = true
      else video.muted = false
    }
    playCheckbox.firstElementChild.onchange = function (event) {
      if (event.target.checked) video.play()
      else video.pause()
    }
    container.appendChild(rateCheckbox)
    container.appendChild(volumeCheckbox)
    container.appendChild(playCheckbox)
    container.appendChild(continueCheckbox)
    container.appendChild(copyButton)
  }
  function setPrompt (container) {
    var div = document.createElement('div')
    div.innerHTML = `提示:<u>该版本为内测版,使用时请先关闭正式版</u>,<u><a href="https://greasyfork.org/zh-CN/scripts/425837-uooc-assistant-beta/feedback" target="_blank" style="color: yellow;">若出现 BUG 点此反馈</a></u>,键盘的 \u2190 和 \u2192 可以控制快进/快退,\u2191 和 \u2193 可以控制音量增大/减小,空格键可以控制播放/暂停`
    div.style = 'color: #cccccc; height: min-height; margin: 0 20px 0; padding: 0 5px; border-radius: 5px; font-size: 12px;'
    container.appendChild(div)
  }
  function setAppreciationCode (container) {
    var a = document.createElement('a')
    a.href = 'https://s1.ax1x.com/2020/11/08/BTeRqe.png'
    a.target = '_blank'
    a.innerHTML = '<u>本脚本使用完全免费,您的打赏是作者维护下去的最大动力!点此打赏作者😊</u>'
    a.style = 'color: #cccccc; font-weight: bold; height: min-height; margin: 0 20px 0; padding: 0 5px; border-radius: 5px; font-size: 11px;'
    container.appendChild(a)
  }
  var head = document.querySelector('.learn-head')
  var checkboxContainer = getContainer('checkbox-container')
  setCheckboxes(checkboxContainer)
  var promptContainer = getContainer('prompt-container')
  setPrompt(promptContainer)
  var appreciationCodeContainer = getContainer('appreciation-code-container')
  setAppreciationCode(appreciationCodeContainer)
  head.appendChild(checkboxContainer)
  head.appendChild(promptContainer)
  head.appendChild(appreciationCodeContainer)
  head.style.height = `${head.offsetHeight + 30}px`
}
function bindKeyboardEvents () {
  document.onkeydown = function (event) {
    var complete = false
    var basicActiveDiv = document.querySelector('div.basic.active')
    var video = document.getElementById('player_html5_api')
    if (basicActiveDiv && basicActiveDiv.classList.contains('complete')) complete = true
    switch (event.key) {
      case 'ArrowLeft': {
        video.currentTime -= 10
        break
      }
      case 'ArrowRight': {
        if (complete) video.currentTime += 10
        break
      }
      case 'ArrowUp': {
        if (video.volume + 0.1 <= 1.0) video.volume += 0.1
        else video.volume = 1.0
        break
      }
      case 'ArrowDown': {
        if (video.volume - 0.1 >= 0.0) video.volume -= 0.1
        else video.volume = 0.0
        break
      }
      case ' ': {
        let continueCheckbox = document.getElementById('play')
        continueCheckbox.click()
        break
      }
    }
  }
}
function findNextVideo () {
  var video = document.getElementById('player_html5_api')
  if (video) {
    if (!document.getElementById('continue').checked) {
      video.currentTime = 0
    } else {
      let current_video = document.querySelector('.basic.active')
      let next_part = current_video.parentNode
      let next_video = current_video
      // 定义判断是否视频的函数
      let isVideo = (node) => { return Boolean(node.querySelector('span.icon-video')) }
      // 定义是否可返回上一级目录的函数
      let canBack = () => { return Boolean(next_part.parentNode.parentNode.tagName === 'LI') }
      // 定义更新至后续视频的函数
      let toNextVideo = () => {
        next_video = next_video.nextElementSibling
        while (next_video && !isVideo(next_video)) next_video = next_video.nextElementSibling
      }
      // 定义判断是否存在视频的函数
      let isExistsVideo = () => {
        let _video = next_part.firstElementChild
        while (_video && !isVideo(_video)) _video = _video.nextElementSibling
        return Boolean(_video && isVideo(_video))
      }
      // 定义判断是否存在后续视频的函数
      let isExistsNextVideo = () => {
        let _video = current_video.nextElementSibling
        while (_video && !isVideo(_video)) _video = _video.nextElementSibling
        return Boolean(_video && isVideo(_video))
      }
      // 定义检查文件后是否存在后续目录的函数
      let isExistsNextListAfterFile = () => {
        let part = next_part.nextElementSibling
        return Boolean(part && part.childElementCount > 0)
      }
      // 定义更新文件后的后续目录的函数
      let toNextListAfterFile = () => { next_part = next_part.nextElementSibling }
      // 定义返回上一级的函数
      let toOuterList = () => { next_part = next_part.parentNode.parentNode }
      // 定义返回主条目的函数
      let toOuterItem = () => { next_part = next_part.parentNode }
      // 定义检查列表后是否存在后续目录的函数
      let isExistsNextListAfterList = () => { return Boolean(next_part.nextElementSibling) }
      // 定义进入列表后的后续目录的函数
      let toNextListAfterList = () => { next_part = next_part.nextElementSibling }
      // 定义展开目录的函数
      let expandList = () => { next_part.firstElementChild.click() }
      // 定义进入展开目录的第一个块级元素的函数
      let toExpandListFirstElement = () => {
        next_part = next_part.firstElementChild.nextElementSibling
        if (next_part.classList.contains('unfoldInfo')) next_part = next_part.nextElementSibling
      }
      // 定义判断块级元素是否目录列表的函数
      let isList = () => { return Boolean(next_part.tagName === 'UL') }
      // 定义目录列表的第一个目录的函数
      let toInnerList = () => { next_part = next_part.firstElementChild }
      // 定义进入文件列表的第一个视频的函数
      let toFirstVideo = () => {
        next_video = next_part.firstElementChild
        while (next_video && !isVideo(next_video)) next_video = next_video.nextElementSibling
      }
      // 定义模式
      let mode = {
        FIRST_VIDEO: 'FIRST_VIDEO',
        NEXT_VIDEO: 'NEXT_VIDEO',
        LAST_LIST: 'LAST_LIST',
        NEXT_LIST: 'NEXT_LIST',
        INNER_LIST: 'INNER_LIST',
        OUTER_LIST: 'OUTER_LIST',
        OUTER_ITEM: 'OUTER_ITEM',
      }
      // 定义搜索函数
      let search = (_mode) => {
        switch (_mode) {
          case mode.FIRST_VIDEO: // mode == 0
            if (isExistsVideo()) {
              toFirstVideo()
              next_video.click()
              start()
            } else if (isExistsNextListAfterFile()) {
              search(mode.LAST_LIST)
            } else {
              // perhaps there is an exam, end recursion
            }
            break
          case mode.NEXT_VIDEO: // mode == 1
            if (isExistsNextVideo()) {
              toNextVideo()
              next_video.click()
              start()
            } else if (isExistsNextListAfterFile()) {
              search(mode.LAST_LIST)
            } else {
              search(mode.OUTER_ITEM)
            }
            break
          case mode.LAST_LIST: // mode == 2
            toNextListAfterFile()
            toInnerList()
            search(mode.INNER_LIST)
            break
          case mode.NEXT_LIST: // mode == 3
            toNextListAfterList()
            search(mode.INNER_LIST)
            break
          case mode.INNER_LIST: // mode == 4
            expandList()
            function waitForExpand () {
              if (next_part.firstElementChild.nextElementSibling) {
                if (next_part.firstElementChild.nextElementSibling.childElementCount === 0) {
                  search(mode.OUTER_LIST)
                } else {
                  toExpandListFirstElement()
                  if (isList()) {
                    toInnerList()
                    search(mode.INNER_LIST)
                  } else {
                    search(mode.FIRST_VIDEO)
                  }
                }
              } else {
                setTimeout(waitForExpand, 250)
              }
            }
            waitForExpand()
            break
          case mode.OUTER_LIST: // mode == 5
            toOuterList()
            if (isExistsNextListAfterList()) {
              search(mode.NEXT_LIST)
            } else if (canBack()) {
              search(mode.OUTER_LIST)
            } else {
              // perhaps there is no next list
            }
            break
          case mode.OUTER_ITEM: // mode == 6
            toOuterItem()
            if (isExistsNextListAfterList()) {
              toNextListAfterList()
              search(mode.INNER_LIST)
            } else if (canBack()){
              search(mode.OUTER_LIST)
            } else {
              // perhaps there is no list
            }
            break
          default:
            break
        }
      }
      try {
        search(mode.NEXT_VIDEO)
      } catch (err) {
        console.error(err)
      }
    }
  }
}
window.onload = function () {
  (function waitHead () {
    if (document.querySelector('.learn-head')) {
      placeComponents()
      bindChapterChange()
      function ready () {
        start()
        console.log('UOOC assistant beta has initialized.')
      }
      if (document.getElementById('player_html5_api')) {
        ready()
      } else {
        var iconVideo = document.querySelector('.icon-video')
        if (iconVideo) {
          iconVideo.click()
          setTimeout(ready, 250)
        }
      }
    } else {
      setTimeout(waitHead, 250)
    }
  })()
}