Last active
August 22, 2024 06:07
-
-
Save tan9/41686eab8e704bb18885a24339010d65 to your computer and use it in GitHub Desktop.
CHT e-Learning Assistant
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name CHT e-Learning Assistant | |
// @source https://gist.github.com/tan9/41686eab8e704bb18885a24339010d65 | |
// @version 0.8.12 | |
// @description Learn without pain (and ideally, with plenty of gain...) | |
// @author tan9, danny, Ray941216 | |
// @downloadURL https://gist.github.com/tan9/41686eab8e704bb18885a24339010d65/raw/ezlearning.user.js | |
// @updateURL https://gist.github.com/tan9/41686eab8e704bb18885a24339010d65/raw/ezlearning.user.js | |
// @require https://www.gstatic.com/firebasejs/4.9.0/firebase.js | |
// @require https://code.jquery.com/jquery-1.12.4.min.js | |
// @match https://plearn.elearning.cht.com.tw/mod/quiz/* | |
// @match https://ilearn.elearning.cht.com.tw/mod/quiz/* | |
// @icon https://web-eshop.cdn.hinet.net/eshop/img/favicon.ico | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
// Initialize Firebase | |
var config = { | |
apiKey: "AIzaSyCYyoclF3BBHMNackHbhD2YWt3e5wkqZSM", | |
authDomain: "cht-ezlearning.firebaseapp.com", | |
databaseURL: "https://cht-ezlearning.firebaseio.com", | |
projectId: "cht-ezlearning", | |
storageBucket: "", | |
messagingSenderId: "86945052698" | |
}; | |
firebase.initializeApp(config); | |
var title = $("h1").text(); | |
if (!title) { | |
title = $('a[title]').filter((_,el)=>/^[a-zA-Z]\d{3}-.*/.test($(el).attr('title'))).first().attr('title'); | |
} | |
/** | |
* Firebase Realtime Database 的 key 有些限制,避免資料塞不進去要先做些字元替換處理。 | |
* | |
* @param key 待轉換的 key 字串。 | |
*/ | |
const normalize = (str) => { | |
return str.split('').map(char => { | |
const code = char.charCodeAt(0); | |
// Handle non-printable characters and other special cases | |
if (code < 32 || code === 127) { | |
return `\\u${code.toString(16).padStart(4, '0')}`; | |
} | |
// Handle other special characters | |
switch (char) { | |
case '&': return '&'; | |
case '.': return '.'; | |
case '#': return '#'; | |
case '$': return '$'; | |
case '/': return '/'; | |
case '[': return '['; | |
case ']': return ']'; | |
case '\n': return '
'; | |
default: return char; | |
} | |
}).join(''); | |
}; | |
var removeLeadingSubstrings = (str, substrings) => { | |
var pattern = new RegExp(`^(${substrings.map(s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')).join('|')})+`); | |
while (pattern.test(str)) { | |
str = str.replace(pattern, ''); | |
} | |
return str; | |
}; | |
var quizKey = normalize(title); | |
if (title && location.pathname === "/mod/quiz/attempt.php") { | |
// 在測驗頁,先加入預設 style | |
const style = document.createElement('style'); | |
style.textContent = `.answer .option-hint { | |
opacity: 0; | |
} | |
.answer div:hover .option-hint { | |
opacity: 0.6; | |
} | |
.que .content .hint-text { | |
display: none; | |
} | |
.que .content .hint-text::before { | |
display: block; | |
content: '💡'; | |
padding-right: .25rem; | |
} | |
.que:hover .content .hint-text { | |
display: flex; | |
color: #8e662e; | |
background-color: #fcefdc; | |
border-color: #fbe8cd; | |
border: 1 solid transparent; | |
border-radius: .25rem; | |
padding: .75rem 1.25rem; | |
margin-top: 1.25rem; | |
flex-grow: 1; | |
}`; | |
document.head.append(style); | |
// 查資料庫啦 | |
firebase.database().ref(quizKey).once("value", (snapshot) => { | |
var quiz = snapshot.val(); | |
if (quiz) { | |
// 資料庫裡有資料了,將題庫取出 | |
console.log(`Quiz data loaded, ${Object.keys(quiz).length} questions found.`); | |
// 將這次測驗的內容逐題與資料庫比對 | |
$(".qtext").parent().parent().each((idx, questionElement) => { | |
var question = $(".qtext", questionElement).text(); | |
var answers = $(".answernumber + div", questionElement).map((idx, answerElement) => $(answerElement).text()).get(); | |
// 如果有提示就顯示提示 | |
if (quiz[normalize(question)]) { | |
if (quiz[normalize(question)].tip) { | |
$(".qtext", questionElement).parent().append(`<div class="hint-text">${quiz[normalize(question)].tip}</div>`); | |
} | |
} | |
// 現在檢討頁答錯的題目並不會給正確的解答,因此有可能題庫資料裡的答案是錯誤的 | |
let hasAnswer = answers.some((answer) => | |
quiz[normalize(question)] && quiz[normalize(question)].answers[normalize(answer)] && quiz[normalize(question)].answers[normalize(answer)].correct === true | |
); | |
if (hasAnswer) { | |
// 有正確解答逐一選項標示 | |
answers.forEach((answer, idx) => { | |
var emoji; | |
if (quiz[normalize(question)] && quiz[normalize(question)].answers[normalize(answer)] && quiz[normalize(question)].answers[normalize(answer)].correct === true) { | |
emoji = '✔️'; | |
} else if (quiz[normalize(question)] && quiz[normalize(question)].answers[normalize(answer)] && quiz[normalize(question)].answers[normalize(answer)].correct === false) { | |
emoji = '❌'; | |
} else { | |
emoji = '🤔'; | |
} | |
$("div.flex-fill", questionElement)[idx].innerHTML += ` <sup class="option-hint">${emoji}</sup>`; | |
}); | |
} else { | |
// 沒正確解答,全部都標為不知道 | |
answers.forEach((answer, idx) => | |
$("div.flex-fill", questionElement)[idx].innerHTML += ` <sup class="option-hint">🤔</sup>` | |
); | |
} | |
}); | |
// 避免大家太早按送出而被抓包,加個倒數 | |
var submit = $("input[type=submit]") && $("input[type=submit]")[0]; | |
if (submit && $(".qtext").length > 5) { | |
var originalText = submit.value; | |
var countDownSeconds = 60 + Math.round(Math.random() * 60); | |
submit.disabled = true; | |
submit.value = `${originalText} (${countDownSeconds})`; | |
var countDown = setInterval(() => { | |
if (countDownSeconds > 0) { | |
countDownSeconds--; | |
submit.value = `${originalText} (${countDownSeconds})`; | |
} else { | |
submit.value = originalText; | |
submit.disabled = false; | |
clearInterval(countDown); | |
} | |
}, 1000) | |
} | |
} else { | |
console.log("No quiz data in firebase."); | |
$(".answer label").each((idx, label) => label.innerHTML += ` <sup class="option-hint">🤔</sup>`); | |
} | |
}); | |
} | |
if (title && location.pathname === "/mod/quiz/review.php") { | |
// 在檢討頁裡,用力來 parse 資料補完資料庫! | |
var quiz = {}; | |
$(".qtext").parent().parent().each((idx, questionElement) => { | |
var answerTextExtractor = (idx, answerElement) => $(answerElement).text(); | |
// 逐條解析題目轉成資料物件 | |
var question = $(".qtext", questionElement).text(); | |
var answers = $(".answer .answernumber + div", questionElement).map(answerTextExtractor).get(); | |
var correctAnswers = $("img.questioncorrectnessicon[src$='answer'] + + > .answernumber + div", questionElement).map(answerTextExtractor).get(); | |
var incorrectAnswers = $(".answer > div.incorrect > input:checked + > .answernumber + div", questionElement).map(answerTextExtractor).get(); | |
quiz[normalize(question)] = { answers: {} }; | |
answers.forEach(answer => { | |
quiz[normalize(question)].answers[normalize(answer)] = { correct: correctAnswers.includes(answer) ? true : false }; | |
}); | |
// 如果這題錯了,盡量解析看看有沒有有用的提示 | |
if ($(questionElement).parent().hasClass('incorrect')) { | |
var tip = $(".feedback .specificfeedback", questionElement).text(); | |
if (tip.includes("很可惜") && tip.includes("您答錯了")) { | |
tip = removeLeadingSubstrings(tip, ["很可惜", "您答錯了", ",", "!", "。", "要不要再從頭複習看看?", "這是送分題", " "]); | |
if (tip) { | |
quiz[normalize(question)].tip = tip; | |
} | |
} | |
} | |
}); | |
console.log("quiz:", quiz); | |
firebase.database().ref(quizKey).once("value", (snapshot) => { | |
var persistent = snapshot.val(); | |
if (persistent) { | |
// 已經有舊資料了,合併題庫內容 | |
var mergedQuiz = mergeQuiz(persistent, quiz); | |
// 如果題庫有擴充,就回寫回去 | |
if (JSON.stringify(mergedQuiz) !== JSON.stringify(persistent)) { | |
firebase.database().ref(quizKey).update( | |
mergedQuiz, | |
() => console.log("Quiz updated to firebase.") | |
); | |
} else { | |
console.log("Quiz database up-to-date, no new questions has been found."); | |
} | |
} else { | |
firebase.database().ref(quizKey).set( | |
quiz, | |
() => console.log("Quiz set to firebase.") | |
); | |
} | |
}); | |
function mergeQuiz(existingQuiz, newQuiz) { | |
var mergedQuiz = Object.entries(newQuiz) | |
.filter( | |
([question, questionMeta]) => | |
// 新的問題,或是有更詳細解答的才要合併 | |
!existingQuiz[question] || Object.entries(questionMeta.answers).some(([answerText, answerMeta]) => answerMeta.correct !== null) | |
).reduce((accumulator, [question, questionMeta]) => { | |
var mergedMeta = {}; | |
if (existingQuiz[question]) { | |
// 舊題目,好好合併 | |
mergedMeta = {...existingQuiz[question], ...questionMeta}; | |
mergedMeta.answers = {...existingQuiz[question].answers, | |
...Object.fromEntries( | |
Object.entries(questionMeta.answers) | |
.filter(([answerText, answerMeta]) => | |
// 有更明確解答,或是舊題庫沒有的答案才合併 | |
answerMeta.correct !== null || !existingQuiz[question].answers[answerText] | |
) | |
) | |
} | |
} else { | |
// 新題目,直接用新的 | |
mergedMeta = questionMeta; | |
} | |
accumulator[question] = mergedMeta; | |
return accumulator; | |
}, {}); | |
console.log("merged quiz:", mergedQuiz); | |
return $.extend(true, {}, existingQuiz, mergedQuiz); | |
} | |
} | |
})(); |
關於自動更新的那個網址,應該改成『https://gist.github.com/tan9/41686eab8e704bb18885a24339010d65/raw/ezlearning.user.js』不指定 commit 才會是最新版(由於 github 有 cdn 快取,所以可能頻繁測試會無法發動成功)
網址更新囉,謝謝
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
關於自動更新的那個網址,應該改成『https://gist.github.com/tan9/41686eab8e704bb18885a24339010d65/raw/ezlearning.user.js』不指定 commit 才會是最新版(由於 github 有 cdn 快取,所以可能頻繁測試會無法發動成功)