// ==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
// @require https://greasyfork.org/scripts/422854-bubble-message.js
// @downloadURL 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 = `提示:该版本为内测版,使用时请先关闭正式版,若出现 BUG 点此反馈,键盘的 \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 = '本脚本使用完全免费,您的打赏是作者维护下去的最大动力!点此打赏作者😊'
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)
}
})()
}