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 = (
+ "
"
+ % 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.