diff --git a/qt/aqt/main.py b/qt/aqt/main.py
index c707d1b2a..3affb3a2e 100644
--- a/qt/aqt/main.py
+++ b/qt/aqt/main.py
@@ -1075,9 +1075,9 @@ title="{}" {}>{}""".format(
self.overview = Overview(self)
def setupReviewer(self) -> None:
- from aqt.reviewer import Reviewer
+ from aqt.reviewer import Reviewer, SvelteReviewer
- self.reviewer = Reviewer(self)
+ self.reviewer = SvelteReviewer(self) if True else Reviewer(self)
# Syncing
##########################################################################
diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py
index 3a05eec2c..899829128 100644
--- a/qt/aqt/mediasrv.py
+++ b/qt/aqt/mediasrv.py
@@ -334,6 +334,7 @@ def is_sveltekit_page(path: str) -> bool:
"import-csv",
"import-page",
"image-occlusion",
+ "reviewer",
]
diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py
index 6d68f9e3a..058cb17ff 100644
--- a/qt/aqt/reviewer.py
+++ b/qt/aqt/reviewer.py
@@ -1233,6 +1233,69 @@ timerStopped = false;
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()] # type: ignore
+
+ 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})")
+ return ""
+
+ 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._showQuestion()
+ 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
+ self.mw.bottomWeb.hide()
+
+
# if the last element is a comment, then the RUN_STATE_MUTATION code
# breaks due to the comment wrongly commenting out python code.
# To prevent this we put the js code on a separate line
diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte
new file mode 100644
index 000000000..2baa1b1f6
--- /dev/null
+++ b/ts/routes/reviewer/+page.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
diff --git a/ts/routes/reviewer/index.ts b/ts/routes/reviewer/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte b/ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte
new file mode 100644
index 000000000..96918be95
--- /dev/null
+++ b/ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte b/ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte
new file mode 100644
index 000000000..4761dfdcd
--- /dev/null
+++ b/ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+ {#if underlined}
+
+ {:else}
+
+ {/if}
+
diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte
new file mode 100644
index 000000000..9dd6160b7
--- /dev/null
+++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte
new file mode 100644
index 000000000..59747cc5d
--- /dev/null
+++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte
@@ -0,0 +1,20 @@
+
+
+
+{#if reviewerInfo}
+
+{/if}
diff --git a/qt/aqt/data/web/css/reviewer-bottom.scss b/ts/routes/reviewer/reviewer-bottom/index.scss
similarity index 87%
rename from qt/aqt/data/web/css/reviewer-bottom.scss
rename to ts/routes/reviewer/reviewer-bottom/index.scss
index 59098a5fb..0f189d6b9 100644
--- a/qt/aqt/data/web/css/reviewer-bottom.scss
+++ b/ts/routes/reviewer/reviewer-bottom/index.scss
@@ -1,9 +1,10 @@
/* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
-@use "../../../../../ts/lib/sass/root-vars";
-@use "../../../../../ts/lib/sass/vars" as *;
-@use "../../../../../ts/lib/sass/card-counts";
+@use "../../../lib/sass/root-vars";
+@use "../../../lib/sass/vars" as *;
+@use "../../../lib/sass/card-counts";
+@use "../../../lib/sass/buttons";
:root {
--focus-color: #{palette-of(border-focus)};
@@ -16,6 +17,8 @@
body {
margin: 0;
padding: 0;
+ font-size: 12px;
+ height: 72px;
}
#middle td[align="center"] {
diff --git a/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts
new file mode 100644
index 000000000..deedbd4bd
--- /dev/null
+++ b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts
@@ -0,0 +1,90 @@
+import { bridgeCommand } from "@tslib/bridgecommand";
+import { writable } from "svelte/store";
+import type { AnswerButtonInfo } from "./types";
+
+export function setupBottomBar() {
+ /*
+ let timerStopped = false;
+
+ let maxTime = 0;
+
+ function updateTime(): void {
+ const timeNode = document.getElementById("time");
+ if (maxTime === 0) {
+ timeNode.textContent = "";
+ return;
+ }
+ globalThis.time = Math.min(maxTime, globalThis.time);
+ const m = Math.floor(globalThis.time / 60);
+ const s = globalThis.time % 60;
+ const sStr = String(s).padStart(2, "0");
+ const timeString = `${m}:${sStr}`;
+
+ if (maxTime === time) {
+ timeNode.innerHTML = `${timeString}`;
+ } else {
+ timeNode.textContent = timeString;
+ }
+ }*/
+
+ const answerButtons = writable([]);
+ const remaining = writable<[number, number, number]>([0, 0, 0]);
+ const remainingIndex = writable(-1);
+
+ let intervalId: number | undefined;
+
+ function _showQuestion(_txt: string, _maxTime_: number): void {
+ _showAnswer([]);
+ globalThis.time = 0;
+ // maxTime = maxTime_;
+ // updateTime();
+
+ if (intervalId !== undefined) {
+ clearInterval(intervalId);
+ }
+
+ /*
+ intervalId = setInterval(function() {
+ if (!timerStopped) {
+ globalThis.time += 1;
+ //updateTime();
+ }
+ }, 1000);*/
+ }
+
+ function _showAnswer(info: AnswerButtonInfo[], _stopTimer = false): void {
+ console.log(info);
+ answerButtons.set(info);
+ // timerStopped = stopTimer;
+ }
+
+ function _updateRemaining(counts: [number, number, number], idx: number) {
+ remaining.set(counts);
+ remainingIndex.set(idx);
+ }
+
+ globalThis.showQuestion = _showQuestion;
+ globalThis.showAnswer = _showAnswer;
+ globalThis._updateRemaining = _updateRemaining;
+
+ /*
+ function selectedAnswerButton(): string | undefined {
+ const node = document.activeElement as HTMLElement;
+ if (!node) {
+ return;
+ }
+ return node.dataset.ease;
+ }
+ */
+
+ // TODO This should probably be a "ready" command now that it is part of the actual reviewer,
+ // Currently this depends on this component mounting after the reviewer which it should but seems hacky.
+ // Maybe use a counter with a counter.subscribe($counter == 2 then call("ready"))
+ bridgeCommand("bottomReady");
+
+ return {
+ answerButtons,
+ remaining,
+ remainingIndex,
+ };
+}
diff --git a/ts/routes/reviewer/reviewer-bottom/types.ts b/ts/routes/reviewer/reviewer-bottom/types.ts
new file mode 100644
index 000000000..4068ca1f9
--- /dev/null
+++ b/ts/routes/reviewer/reviewer-bottom/types.ts
@@ -0,0 +1,9 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+export interface AnswerButtonInfo {
+ "extra": string;
+ "key": string;
+ "i": number;
+ "label": string;
+ "due": string;
+}
diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/reviewer.svelte
new file mode 100644
index 000000000..aee74e922
--- /dev/null
+++ b/ts/routes/reviewer/reviewer.svelte
@@ -0,0 +1,16 @@
+
+
+
+ {@html $html}
+
+
+
diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts
new file mode 100644
index 000000000..c2f4ea781
--- /dev/null
+++ b/ts/routes/reviewer/reviewer.ts
@@ -0,0 +1,18 @@
+import { writable } from "svelte/store";
+import { preloadAnswerImages } from "../../reviewer/images";
+
+export function setupReviewer() {
+ const html = writable("");
+ const cardClass = writable("");
+
+ function showQuestion(q, a, cc) {
+ html.set(q);
+ cardClass.set(cc);
+ preloadAnswerImages(a);
+ }
+
+ globalThis._showAnswer = html.set;
+ globalThis._showQuestion = showQuestion;
+
+ return { html, cardClass };
+}
diff --git a/ts/routes/reviewer/reviewerOuter.svelte b/ts/routes/reviewer/reviewerOuter.svelte
new file mode 100644
index 000000000..c261675f7
--- /dev/null
+++ b/ts/routes/reviewer/reviewerOuter.svelte
@@ -0,0 +1,21 @@
+
+
+
+{#if reviewerInfo}
+
+{/if}