// ==UserScript==
// @name Quizlet Spell Auto-Giveup Countdown.
// @version 0.0.3
// @grant none
// @match https://quizlet.com/*/spell
// @description A forced, automatic, and short study session for spelling words you obviously do NOT know. Includes a short configurable break and an alarm to get them to come back when the break ends. Intended for use with kids.
// @namespace https://gf.qytechs.cn/users/82098
// ==/UserScript==
function getElementByXpath(path,node=document) {
return document.evaluate(path, node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
function XPath(xpath) {
let self = this;
if (!( self instanceof XPath) ) {
return new XPath(xpath);
}
self.xpath=xpath;
self.contains = function(attr,text) {
self.xpath += "contains(concat(' ', normalize-space(" + attr + "), ' '), ' "
+ text
+ " ')";
return self;
};
self.append = function(text) {
self.xpath += text;
return self;
};
self.as_text = function() {
return self.xpath;
};
self.fetch = function(node=document) {
return getElementByXpath(self.xpath,node);
};
return self;
}
function CountdownElm(attr={}) {
let self = this;
if (!( self instanceof CountdownElm) ) {
return new CountdownElm(attr);
}
let elm=attr.elm;
let endtime=attr.endtime;
let paused_remainingtime=0;
let is_paused = false;
let should_reset_time = false;
self.updateUI=function() {
let r = Math.floor(self.remainingtime/1000 + 0.5);
r = Math.max(r,0);
elm.innerHTML=Math.floor(self.remainingtime/1000 + 0.5);
};
self.pause = function() {
if (!is_paused) {
paused_remainingtime = self.remainingtime;
is_paused = true;
should_reset_time = true;
}
};
self.unpause = function() {
if (is_paused) {
if (should_reset_time) {
self.endtime_from_millisec(paused_remainingtime);
}
paused_remainingtime=0;
is_paused=false;
should_reset_time = false;
}
};
self.endtime_from_millisec = function(milli) {
endtime = new Date().getTime() + milli;
should_reset_time = false;
};
Object.defineProperties(self,{
endtime: {
get: function() {
return endtime;
},
set: function(val) {
endtime = val;
should_reset_time = false;
self.updateUI();
},
},
remainingtime: {
get: function() {
if (!is_paused) {
return endtime - new Date().getTime();
}
else {
return paused_remainingtime;
}
}
}
});
return self;
}
function QuizletSpellTimer(initargs={}) {
let self = this;
if (!( self instanceof QuizletSpellTimer) ) {
return new QuizletSpellTimer(initArgs);
}
let qst = self;
function Print(msg) {
console.log(msg);
}
function toSeconds(hms) { // HH:MM:SS to seconds
let a = hms.split(':'); // split it at the colons
// minutes are worth 60 seconds. Hours are worth 60 minutes.
return (+a[0]) * 60 * 60 + (+a[1]) * 60 + (+a[2]);
}
function htmlToElement(html) {
let template = document.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
}
function isHidden(el) {
return (el.offsetParent === null)
}
function CheckLoop(xpath,fn,cnt=50,delay=100) {
if (getElementByXpath(xpath)) {
fn();
}
else if (cnt != 0) {
setTimeout(function() { CheckLoop(cnt - 1); },delay);
}
else {
Print("Failed to find xpath '" + xpath + "'");
}
}
const keycodes={
backspace:8, tab:9, enter:13,
shift:16, ctrl:17, alt:18,
pause_break:19, capslock:20, escape:27,
space:32, pageup:33, pagedown:34,
end:35, home:36, leftarrow:37,
uparrow:38, rightarrow:39, downarrow:40,
insert:45, delete:46,
0:48, 1:49, 2:50, 3:51,
4:52, 5:53, 6:54, 7:55,
8:56, 9:57, a:65, b:66,
c:67, d:68, e:69, f:70,
g:71, h:72, i:73, j:74,
k:75, l:76, m:77, n:78,
o:79, p:80, q:81, r:82,
s:83, t:84, u:85, v:86,
w:87, x:88, y:89, z:90,
multiply: 106, add: 107, subtract: 109,
decimalpoint: 110, divide: 111,
f1: 112, f2: 113, f3: 114,
f4: 115, f5: 116, f6: 117,
f7: 118, f8: 119, f9: 120,
f10: 121, f11: 122, f12: 123,
numlock: 144, scrolllock: 145,
semicolon: 186, equalsign: 187,
comma: 188, dash: 189, period: 190,
forwardslash: 191, graveaccent: 192,
openbracket: 219, backslash: 220,
closebraket: 221, singlequote: 222
};
const enter_keyboard_event = new KeyboardEvent('keydown',{'keyCode':keycodes.enter,'which':keycodes.enter});
const GameState = {
Spelling: 0,
ReviewingMisspelled: 1,
Break: 2,
BreakOver: 3,
Paused: 4,
};
let progress_cont_xp = new XPath("//div[").contains("@class","ModeControls-progress").append("]");
let break_countdown_cont = htmlToElement(`<div class='ModeControls-progressSection'>
<h4 id="break_tite">Next Break</h4>
<br>
<h5 id="break_time">0</h5>
</div>`);
let spell_countdown_cont = htmlToElement(`<div class='ModeControls-progressSection'>
<h4 id="spell_tite">Give Up In</h4>
<br>
<h5 id="spell_time">0</h5>
</div>`);
let pause_button_cont = htmlToElement(`<div class='ModeControls-progressSection'>
<button class="h1" id="pause_play">Play</button>
</div>`);
let paused_overlay = htmlToElement(`<div style="z-index: 100; background-color: #000; height: 100%; width: 100%; position: absolute; top:0; left:0; display:none; ">
</div>`);
qst.settings = {
per_letter_time: 5,
max_spell_time: 45,
loop_timeout: 300,
break: {
length: toSeconds("00:05:00"),
delay: toSeconds("00:15:00"),
},
sounds: {
break_over: 'https://freesound.org/data/previews/250/250629_4486188-hq.mp3',
},
};
qst.audio = new Audio(qst.settings.sounds.break_over);
qst.elm = {};
qst.state = {};
qst.is_initialized=false;
qst.ctime=new Date().getTime();
qst.pause = {
max_pause_cnt: 2,
pause_history_durration: toSeconds("00:05:00"),
pause_hist: [],
can_pause: function() {
let pause_cnt = 0;
for (const pause of qst.pause.pause_hist ) {
if (pause >= (qst.ctime - (qst.pause.pause_history_durration * 1000)) ) {
pause_cnt += 1;
}
}
return pause_cnt < qst.pause.max_pause_cnt;
},
try_pause: function() {
if (qst.pause.can_pause()) {
qst.pause.pause_hist.push(qst.ctime);
qst.pause.pause_hist.splice(0,qst.pause.pause_hist.length - qst.pause.max_pause_cnt);
return true;
}
return false;
}
}
qst.gamestate= GameState.Spelling;
function isReviewing() {
//Print(Game.$diff.textContent);
if (Game.$diff.textContent) {
return true;
}
return false;
}
function initNewWord() {
qst.state.spell_countdown.unpause();
qst.state.break_countdown.unpause();
qst.state.spell_countdown.endtime_from_millisec(qst.settings.max_spell_time * 1000);
qst.gamestate=GameState.Spelling;
qst.rev_workaround_cnt = 0;
}
function onSpellTimeout() {
qst.elm.spelling_box.dispatchEvent(enter_keyboard_event);
}
function onPause() {
qst.gamestate = GameState.Paused;
paused_overlay.style.display="inline";
qst.state.spell_countdown.pause();
qst.state.break_countdown.pause();
}
function onResume() {
qst.gamestate = GameState.Spelling;
paused_overlay.style.display="inline";
qst.state.spell_countdown.unpause();
qst.state.break_countdown.unpause();
}
function onPausePlay() {
if (qst.gamestate == GameState.Paused || qst.gamestate == GameState.BreakOver ) {
onResume();
}
else if (qst.gamestate == GameState.Spelling || qst.gamestate == GameState.ReviewingMisspelled ) {
if (qst.pause.try_pause()) {
onPause();
}
}
}
function updatePausePlay() {
if (qst.gamestate == GameState.Paused || qst.gamestate == GameState.BreakOver ) {
qst.elm.pauseplay_button.textContent="Play";
}
else if (qst.gamestate == GameState.Spelling || qst.gamestate == GameState.ReviewingMisspelled ) {
qst.elm.pauseplay_button.textContent="Pause";
qst.elm.pauseplay_button.disabled=!qst.pause.can_pause();
}
}
function onBreak() {
onPause();
qst.gamestate=GameState.Break;
qst.state.break_countdown.endtime_from_millisec(qst.settings.break.length * 1000);
}
function onCorrect() {
Print("Correct");
clearTimeout(qst.mainloop_timer);
initNewWord();
qst.rev_workaround_cnt=0;
qst.mainloop_timer=setTimeout(MainLoop,1000);
}
function onMissed() {
Print("Missed");
clearTimeout(qst.mainloop_timer);
qst.gamestate=GameState.ReviewingMisspelled;
qst.state.spell_countdown.pause();
qst.state.break_countdown.pause();
qst.rev_workaround_cnt=0;
qst.mainloop_timer=setTimeout(MainLoop,1000);
}
qst.mainloop_timer;
qst.rev_workaround_cnt=0;
function MainLoop() {
clearTimeout(qst.mainloop_timer);
qst.ctime=new Date().getTime();
let ctimeout = qst.settings.loop_timeout;
if (qst.gamestate == GameState.Spelling) {
paused_overlay.style.display="none";
if (qst.state.break_countdown.remainingtime <= 0) {
onBreak();
}
if (qst.state.spell_countdown.remainingtime <= 0) {
onSpellTimeout();
}
}
else if (qst.gamestate == GameState.ReviewingMisspelled) {
if (! isReviewing() ) {
initNewWord();
qst.gamestate=GameState.Spelling;
}
/*if (! isReviewing() && qst.rev_workaround_cnt < 1) {
qst.rev_workaround_cnt+=1
}
else if (! isReviewing() && qst.rev_workaround_cnt >= 1) {
qst.rev_workaround_cnt=0;
initNewWord();
qst.gamestate=GameState.Spelling;
}*/
}
else if (qst.gamestate == GameState.Break) {
if (qst.state.break_countdown.remainingtime <= 0) {
qst.gamestate == GameState.BreakOver;
qst.audio.play();
}
}
else if (qst.gamestate == GameState.BreakOver) {
qst.gamestate=GameState.Spelling;
qst.state.break_countdown.endtime_from_millisec(qst.settings.break.delay * 1000);
initNewWord(); // Lets give them a full word's worth of time
}
qst.state.spell_countdown.updateUI();
qst.state.break_countdown.updateUI();
updatePausePlay();
qst.mainloop_timer=setTimeout(MainLoop,ctimeout);
}
// NOTE: Game.curTerm.getRawWord() -> Current term string.
function Init() {
let spell_cont = XPath("//div[@id='SpellModeTarget']").fetch();
paused_overlay.style.backgroundColor="#c9ceeb";
XPath("//div[").contains("@class","ModeLayout-content").append("]").fetch(spell_cont).appendChild(paused_overlay);
qst.elm.spelling_box= new XPath("//div[").contains("@class","UITextarea-content").append("]//textarea").fetch();
qst.progress_container = progress_cont_xp.fetch();
qst.progress_container.appendChild(break_countdown_cont);
qst.progress_container.appendChild(spell_countdown_cont);
qst.progress_container.appendChild(pause_button_cont);
qst.elm.break_countdown = new XPath("//*[@id='break_time']").fetch(break_countdown_cont);
qst.elm.spell_countdown = new XPath("//*[@id='spell_time']").fetch(spell_countdown_cont);
qst.elm.pauseplay_button = new XPath("//*[@id='pause_play']").fetch(pause_button_cont);
qst.state={
spell_countdown: CountdownElm({
elm: qst.elm.spell_countdown,
endtime: 0,
}),
break_countdown: CountdownElm({
elm: qst.elm.break_countdown,
endtime: 0,
}),
};
qst.elm.pauseplay_button.onclick=onPausePlay;
qst.state.break_countdown.endtime_from_millisec(qst.settings.break.delay * 1000);
let oldMissed = Game.missedTerm;
Game.missedTerm = function () {
onMissed();
oldMissed.apply(this);
};
let oldCorrect = Game.beatTerm;
Game.beatTerm = function () {
onCorrect();
oldCorrect.apply(this);
};
initNewWord();
MainLoop();
}
qst.run=function() {
CheckLoop(progress_cont_xp.as_text(), Init);
};
return qst;
}
let qst = new QuizletSpellTimer();
setTimeout(qst.run, 3000);