您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
// ==UserScript== // @name Quizlet Spell Auto-Giveup Countdown. // @version 0.0.7 // @grant none // @match https://quizlet.com/*/spell // @match https://quizlet.com/*/spell/stared // @namespace https://github.com/Brandon-Beck // @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. // @require https://gitcdn.xyz/repo/Brandon-Beck/Userscripts/604449d00e316a699e1cd63d54b8b14f824dbb91/common.js // @require https://gitcdn.xyz/repo/Brandon-Beck/Userscripts/e9750f7ddfc0a4717207be2112114a57bb76b00c/uncommon.js // @license MIT // ==/UserScript== 'use strict' // TODO Settings UI /* function newNumberCorrector(min ,max) { return (textVal) => { const val = parseInt(textVal) if ( textVal == null || (typeof textVal === 'string' && (textVal.length === 0 || textVal.match(/[^0-9]/))) || typeof val !== 'number') { return undefined } if (val < min) return min if (val > max) return max // dont triger callback via type change. return textVal } } const settingsUI = new SettingUI() settingsUI.addTextbox({ key: 'max_spell_time' ,title: 'Spell Time' ,settingsTreeConfig: { defaultValue: 40 ,corrector: newNumberCorrector(5 ,5 * 60) } ,min: 5 ,max: 5 * 60 ,titleText: 'Seconds we should wait for the player to type an answer before we snatch the paper from them, check it, and(if incorrect) correct them, and make them do it again.' ,type: 'number' }) settingsUI.addTextbox({ key: 'break_length' ,title: 'Break length' ,settingsTreeConfig: { defaultValue: 5 * 60 ,corrector: newNumberCorrector(0 ,60 * 60) } ,min: 0 ,max: 60 * 60 ,titleText: 'How long of a break(in seconds) the player should have. Reccomend a minimum of 5 minutes. Should be long enough to allow them to use the bathroom.' ,type: 'number' }) settingsUI.addTextbox({ key: 'break_delay' ,title: 'Break delay' ,settingsTreeConfig: { defaultValue: 15 * 60 ,corrector: newNumberCorrector(5 * 60 ,60 * 60) } ,min: 5 * 60 ,max: 60 * 60 ,titleText: 'How long the player should be required to play(in seconds) before their break. Reccomend a maximum of 30 minutes. Should be an appropriate studdy time given their age and willingness to studdy.' ,type: 'number' }) */ function CountdownElm(attr = {}) { const self = this if (!(self instanceof CountdownElm)) { return new CountdownElm(attr) } const elm = attr.elm let endtime = attr.endtime let paused_remainingtime = 0 let is_paused = false let should_reset_time = false self.updateUI = () => { let r = Math.floor(self.remainingtime / 1000 + 0.5) r = Math.max(r ,0) elm.innerHTML = r } self.pause = () => { if (!is_paused) { paused_remainingtime = self.remainingtime is_paused = true should_reset_time = true } } self.unpause = () => { 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 = (milli) => { endtime = Date.now() + milli should_reset_time = false paused_remainingtime = milli } Object.defineProperties(self ,{ endtime: { get() { return endtime } ,set(val) { endtime = val should_reset_time = false self.updateUI() } } ,remainingtime: { get() { if (!is_paused) { return endtime - Date.now() } return paused_remainingtime } } }) return self } function QuizletSpellTimer(initargs = {}) { const self = this if (!(self instanceof QuizletSpellTimer)) { return new QuizletSpellTimer(initArgs) } const qst = self function Print(msg) { console.log(msg) } function toSeconds(hms) { // HH:MM:SS to seconds const 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) { const 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) } 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 } const progress_cont_xp = new XPath('//div[').contains('@class' ,'ModeControls-progress').append(']') const break_countdown_cont = htmlToElement(`<div class='ModeControls-progressSection'> <h4 id="break_tite">Next Break</h4> <br> <h5 id="break_time">0</h5> </div>`) const spell_countdown_cont = htmlToElement(`<div class='ModeControls-progressSection'> <h4 id="spell_tite">Give Up In</h4> <br> <h5 id="spell_time">0</h5> </div>`) const pause_button_cont = htmlToElement(`<div class='ModeControls-progressSection'> <button class="h1" id="pause_play">Play</button> </div>`) const 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': 40 ,'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.audio.loop = true 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() { 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() { 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() { // FIXME for some reason this does not trigger pause qst.elm.spelling_box.dispatchEvent(enter_keyboard_event) //Game.missedTerm() } 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 = 'none' qst.state.spell_countdown.unpause() qst.state.break_countdown.unpause() qst.audio.pause() qst.audio.fastSeek(0) Game.speakCurrent() } 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.disabled = false 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.state.break_countdown.endtime_from_millisec(qst.settings.break.length * 1000) qst.state.break_countdown.unpause() qst.gamestate = GameState.Break } 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=null qst.rev_workaround_cnt = 0 function MainLoop() { clearTimeout(qst.mainloop_timer) qst.ctime = new Date().getTime() const ctimeout = qst.settings.loop_timeout if (qst.gamestate == GameState.Spelling) { paused_overlay.style.display = 'none' if (qst.state.break_countdown.remainingtime <= 0) { onBreak() } else 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 //initNewWord() // Lets give them a full word's worth of time qst.state.break_countdown.pause() qst.state.break_countdown.endtime_from_millisec(qst.settings.break.delay * 1000) qst.state.spell_countdown.endtime_from_millisec(qst.settings.max_spell_time * 1000) } 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() { const spell_cont = XPath("//div[@id='SpellModeTarget']").getElement() paused_overlay.style.backgroundColor = '#c9ceeb' XPath('//div[').contains('@class' ,'ModeLayout-content').append(']').getElement(spell_cont) .appendChild(paused_overlay) qst.elm.spelling_box = new XPath('//div[').contains('@class' ,'UITextarea-content').append(']//textarea').getElement() qst.progress_container = progress_cont_xp.getElement() 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']").getElement(break_countdown_cont) qst.elm.spell_countdown = new XPath("//*[@id='spell_time']").getElement(spell_countdown_cont) qst.elm.pauseplay_button = new XPath("//*[@id='pause_play']").getElement(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) const oldMissed = Game.missedTerm Game.missedTerm = () => { onMissed() oldMissed.apply(Game) } const oldCorrect = Game.beatTerm Game.beatTerm = () => { onCorrect() oldCorrect.apply(Game) } initNewWord() MainLoop() } /* for (let key in initargs) { this.settings[key] = initargs[key]; } */ qst.run = () => { // CheckLoop(progress_cont_xp.toString() ,Init) waitForElementByXpath({ xpath: progress_cont_xp }).then(Init) } return qst } const qst = new QuizletSpellTimer() //setTimeout(qst.run ,3000) qst.run()
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址