From d81ec73205633b631d1273128e481c9414a30094 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 21:59:34 +0100 Subject: [PATCH] Use inheritance for reviewer --- qt/aqt/data/web/js/reviewer-bottom.ts | 63 ++++++++++++ qt/aqt/reviewer.py | 141 +++++++++++++++++++++----- 2 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 qt/aqt/data/web/js/reviewer-bottom.ts diff --git a/qt/aqt/data/web/js/reviewer-bottom.ts b/qt/aqt/data/web/js/reviewer-bottom.ts new file mode 100644 index 000000000..c11fa2aa2 --- /dev/null +++ b/qt/aqt/data/web/js/reviewer-bottom.ts @@ -0,0 +1,63 @@ +/* Copyright: Ankitects Pty Ltd and contributors + * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ + +/* eslint +@typescript-eslint/no-unused-vars: "off", +*/ + +let time: number; // set in python code +let timerStopped = false; + +let maxTime = 0; + +function updateTime(): void { + const timeNode = document.getElementById("time"); + if (maxTime === 0) { + timeNode.textContent = ""; + return; + } + time = Math.min(maxTime, time); + const m = Math.floor(time / 60); + const s = time % 60; + const sStr = String(s).padStart(2, "0"); + const timeString = `${m}:${sStr}`; + + if (maxTime === time) { + timeNode.innerHTML = `${timeString}`; + } else { + timeNode.textContent = timeString; + } +} + +let intervalId: number | undefined; + +function showQuestion(txt: string, maxTime_: number): void { + showAnswer(txt); + time = 0; + maxTime = maxTime_; + updateTime(); + + if (intervalId !== undefined) { + clearInterval(intervalId); + } + + intervalId = setInterval(function() { + if (!timerStopped) { + time += 1; + updateTime(); + } + }, 1000); +} + +function showAnswer(txt: string, stopTimer = false): void { + document.getElementById("middle").innerHTML = txt; + timerStopped = stopTimer; +} + +function selectedAnswerButton(): string { + const node = document.activeElement as HTMLElement; + if (!node) { + return; + } + return node.dataset.ease; +} \ No newline at end of file diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 535f87b22..6bbc4f986 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -10,7 +10,7 @@ from collections.abc import Callable, Generator, Sequence from dataclasses import dataclass from enum import Enum, auto from functools import partial -from typing import Any, Literal, Match, cast +from typing import Any, Literal, Match, Union, cast import aqt import aqt.browser @@ -341,12 +341,26 @@ class Reviewer: def _initWeb(self) -> None: self._reps = 0 # main window - self.web.load_sveltekit_page("reviewer") + self.web.stdHtml( + self.revHtml(), + css=["css/reviewer.css"], + js=[ + "js/mathjax.js", + "js/vendor/mathjax/tex-chtml-full.js", + "js/reviewer.js", + ], + context=self, + ) # block default drag & drop behavior while allowing drop events to be received by JS handlers self.web.allow_drops = True self.web.eval("_blockDefaultDragDropBehavior();") # show answer / ease buttons - self.bottom.web = self.web + self.bottom.web.stdHtml( + self._bottomHTML(), + css=["css/toolbar-bottom.css", "css/reviewer-bottom.css"], + js=["js/vendor/jquery.min.js", "js/reviewer-bottom.js"], + context=ReviewerBottomBar(self), + ) # Showing the question ########################################################################## @@ -667,8 +681,6 @@ class Reviewer: self.mw.onEditCurrent() elif url == "more": self.showContextMenu() - elif url == "bottomReady": - self._remaining() elif url.startswith("play:"): play_clicked_audio(url, self.card) elif url.startswith("updateToolbar"): @@ -825,13 +837,22 @@ timerStopped = false; ) def _showAnswerButton(self) -> None: + middle = """ +""".format( + tr.actions_shortcut_key(val=tr.studying_space()), + tr.studying_show_answer(), + self._remaining(), + ) # wrap it in a table so it has the same top margin as the ease buttons + middle = ( + "
%s
" + % middle + ) if self.card.should_show_timer(): maxTime = self.card.time_limit() / 1000 else: maxTime = 0 - self._remaining() - self.bottom.web.eval("_showQuestion(%s,%d);" % ("", maxTime)) + self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime)) def _showEaseButtons(self) -> None: if not self._states_mutated: @@ -840,15 +861,23 @@ timerStopped = false; middle = self._answerButtons() conf = self.mw.col.decks.config_dict_for_deck_id(self.card.current_deck_id()) self.bottom.web.eval( - f"_showAnswer({json.dumps(middle)}, {json.dumps(conf['stopTimerOnAnswer'])});" + f"showAnswer({json.dumps(middle)}, {json.dumps(conf['stopTimerOnAnswer'])});" ) - def _remaining(self): + def _remaining(self) -> str: if not self.mw.col.conf["dueCounts"]: return "" - idx, counts = self._v3.counts() - self.bottom.web.eval(f"_updateRemaining({json.dumps(counts)},{idx})") + counts: list[int | str] + idx, counts_ = self._v3.counts() + counts = cast(list[Union[int, str]], counts_) + counts[idx] = f"{counts[idx]}" + + return f""" +{counts[0]} + +{counts[1]} + +{counts[2]} +""" def _defaultEase(self) -> Literal[2, 3]: return 3 @@ -878,32 +907,39 @@ timerStopped = false; ) return buttons_tuple - def _answerButtons(self): + def _answerButtons(self) -> str: default = self._defaultEase() assert isinstance(self.mw.col.sched, V3Scheduler) labels = self.mw.col.sched.describe_next_states(self._v3.states) - def but(i: int, label: str): + def but(i: int, label: str) -> str: if i == default: - id = "defease" + extra = """id="defease" """ else: - id = "" + extra = "" due = self._buttonTime(i, v3_labels=labels) key = ( tr.actions_shortcut_key(val=aqt.mw.pm.get_answer_key(i)) if aqt.mw.pm.get_answer_key(i) else "" ) - return { - "id": id, - "key": key, - "i": i, - "label": label, - "due": due, - } + return """ +""" % ( + extra, + key, + i, + i, + label, + due, + ) - return [but(ease, label) for ease, label in self._answerButtonList()] + buf = "
" + for ease, label in self._answerButtonList(): + buf += but(ease, label) + buf += "
" + return buf def _buttonTime(self, i: int, v3_labels: Sequence[str]) -> str: if self.mw.col.conf["estTimes"]: @@ -1191,6 +1227,65 @@ timerStopped = false; onMark = toggle_mark_on_current_note setFlag = set_flag_on_current_card +class SvelteReviewer(Reviewer): + def _answerButtons(self) -> str: + default = self._defaultEase() + + assert isinstance(self.mw.col.sched, V3Scheduler) + labels = self.mw.col.sched.describe_next_states(self._v3.states) + + def but(i: int, label: str): + if i == default: + id = "defease" + else: + id = "" + due = self._buttonTime(i, v3_labels=labels) + key = ( + tr.actions_shortcut_key(val=aqt.mw.pm.get_answer_key(i)) + if aqt.mw.pm.get_answer_key(i) + else "" + ) + return { + "id": id, + "key": key, + "i": i, + "label": label, + "due": due, + } + + return [but(ease, label) for ease, label in self._answerButtonList()] + + def _remaining(self) -> str: + if not self.mw.col.conf["dueCounts"]: + return "" + + idx, counts = self._v3.counts() + self.bottom.web.eval(f"_updateRemaining({json.dumps(counts)},{idx})") + + def _showAnswerButton(self) -> None: + if self.card.should_show_timer(): + maxTime = self.card.time_limit() / 1000 + else: + maxTime = 0 + self._remaining() + self.bottom.web.eval("_showQuestion(%s,%d);" % ("", maxTime)) + + def _linkHandler(self, url: str) -> None: + if url == "bottomReady": + self._remaining() + return + super()._linkHandler(url) + + def _initWeb(self) -> None: + self._reps = 0 + # main window + self.web.load_sveltekit_page("reviewer") + # block default drag & drop behavior while allowing drop events to be received by JS handlers + self.web.allow_drops = True + self.web.eval("_blockDefaultDragDropBehavior();") + # ensure bottom web functions trigger + self.bottom.web = self.web + # if the last element is a comment, then the RUN_STATE_MUTATION code # breaks due to the comment wrongly commenting out python code.