From 6869e9fd3605a48dcc6fb4e4b47836d2b85f18cf Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 18:42:20 +0100 Subject: [PATCH 001/237] reviewer-bottom entrypoint --- build/configure/src/aqt.rs | 2 +- build/configure/src/web.rs | 12 ++++++++++++ qt/aqt/reviewer.py | 7 +------ ts/routes/reviewer-bottom/AnswerButton.svelte | 0 .../routes/reviewer-bottom/index.scss | 6 +++--- ts/routes/reviewer-bottom/index.svelte | 0 .../routes/reviewer-bottom/index.ts | 5 +++++ 7 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 ts/routes/reviewer-bottom/AnswerButton.svelte rename qt/aqt/data/web/css/reviewer-bottom.scss => ts/routes/reviewer-bottom/index.scss (89%) create mode 100644 ts/routes/reviewer-bottom/index.svelte rename qt/aqt/data/web/js/reviewer-bottom.ts => ts/routes/reviewer-bottom/index.ts (96%) diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index 83be77e91..183b7f0b3 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -170,7 +170,7 @@ fn build_imgs(build: &mut Build) -> Result<()> { } fn build_js(build: &mut Build) -> Result<()> { - for ts_file in &["deckbrowser", "webview", "toolbar", "reviewer-bottom"] { + for ts_file in &["deckbrowser", "webview", "toolbar"] { build.add_action( "qt:aqt:data:web:js", EsbuildScript { diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index ef2d268bb..9ee5d74b5 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use anyhow::Ok; use anyhow::Result; use ninja_gen::action::BuildAction; use ninja_gen::copy::CopyFiles; @@ -228,6 +229,17 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { ":sveltekit" ], )?; + build_page( + "reviewer-bottom", + true, + inputs![ + // + ":ts:lib", + ":ts:components", + ":sass", + ":sveltekit" + ], + )?; Ok(()) } diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index a8839c598..4d7f175ab 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -355,12 +355,7 @@ class Reviewer: self.web.allow_drops = True self.web.eval("_blockDefaultDragDropBehavior();") # show answer / ease buttons - 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), - ) + self.bottom.web.load_ts_page("reviewer-bottom") # Showing the question ########################################################################## diff --git a/ts/routes/reviewer-bottom/AnswerButton.svelte b/ts/routes/reviewer-bottom/AnswerButton.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/qt/aqt/data/web/css/reviewer-bottom.scss b/ts/routes/reviewer-bottom/index.scss similarity index 89% rename from qt/aqt/data/web/css/reviewer-bottom.scss rename to ts/routes/reviewer-bottom/index.scss index 59098a5fb..fbe069779 100644 --- a/qt/aqt/data/web/css/reviewer-bottom.scss +++ b/ts/routes/reviewer-bottom/index.scss @@ -1,9 +1,9 @@ /* 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"; :root { --focus-color: #{palette-of(border-focus)}; diff --git a/ts/routes/reviewer-bottom/index.svelte b/ts/routes/reviewer-bottom/index.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/qt/aqt/data/web/js/reviewer-bottom.ts b/ts/routes/reviewer-bottom/index.ts similarity index 96% rename from qt/aqt/data/web/js/reviewer-bottom.ts rename to ts/routes/reviewer-bottom/index.ts index 4ebf34510..ecb93d309 100644 --- a/qt/aqt/data/web/js/reviewer-bottom.ts +++ b/ts/routes/reviewer-bottom/index.ts @@ -5,6 +5,10 @@ @typescript-eslint/no-unused-vars: "off", */ +import "./index.scss" + +console.log("Hello world") + let time: number; // set in python code let timerStopped = false; @@ -61,3 +65,4 @@ function selectedAnswerButton(): string { } return node.dataset.ease; } + From 244aade83630dfdf2d6678c02789a13b20681256 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 18:46:35 +0100 Subject: [PATCH 002/237] Fix: Showquestion is not defined --- qt/aqt/reviewer.py | 2 +- ts/routes/reviewer-bottom/index.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 4d7f175ab..2ae054184 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -847,7 +847,7 @@ timerStopped = false; maxTime = self.card.time_limit() / 1000 else: maxTime = 0 - self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime)) + self.bottom.web.eval("anki.showQuestion(%s,%d);" % (json.dumps(middle), maxTime)) def _showEaseButtons(self) -> None: if not self._states_mutated: diff --git a/ts/routes/reviewer-bottom/index.ts b/ts/routes/reviewer-bottom/index.ts index ecb93d309..0f495bcdd 100644 --- a/ts/routes/reviewer-bottom/index.ts +++ b/ts/routes/reviewer-bottom/index.ts @@ -7,8 +7,6 @@ import "./index.scss" -console.log("Hello world") - let time: number; // set in python code let timerStopped = false; @@ -35,7 +33,7 @@ function updateTime(): void { let intervalId: number | undefined; -function showQuestion(txt: string, maxTime_: number): void { +export function showQuestion(txt: string, maxTime_: number): void { showAnswer(txt); time = 0; maxTime = maxTime_; From 34c1dfd8497a1ff9a8e16d57020ef8a5a6ff1ab3 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 20:34:26 +0100 Subject: [PATCH 003/237] Added: Svelte component --- ts/routes/reviewer-bottom/index.svelte | 3 +++ ts/routes/reviewer-bottom/index.ts | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/ts/routes/reviewer-bottom/index.svelte b/ts/routes/reviewer-bottom/index.svelte index e69de29bb..049eff39e 100644 --- a/ts/routes/reviewer-bottom/index.svelte +++ b/ts/routes/reviewer-bottom/index.svelte @@ -0,0 +1,3 @@ +
+ Hello from svelte +
\ No newline at end of file diff --git a/ts/routes/reviewer-bottom/index.ts b/ts/routes/reviewer-bottom/index.ts index 0f495bcdd..e4bec417a 100644 --- a/ts/routes/reviewer-bottom/index.ts +++ b/ts/routes/reviewer-bottom/index.ts @@ -5,7 +5,9 @@ @typescript-eslint/no-unused-vars: "off", */ +import { mount } from "svelte"; import "./index.scss" +import ReviewerBottom from "./index.svelte" let time: number; // set in python code let timerStopped = false; @@ -64,3 +66,7 @@ function selectedAnswerButton(): string { return node.dataset.ease; } +mount( + ReviewerBottom, + {target: document.body}, +) From 758cfa269360d8d75716c46265894af26d674d94 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 21:03:40 +0100 Subject: [PATCH 004/237] Buttons template --- qt/aqt/reviewer.py | 4 +++- ts/routes/reviewer-bottom/index.scss | 1 + ts/routes/reviewer-bottom/index.svelte | 25 ++++++++++++++++++++++--- ts/routes/reviewer-bottom/index.ts | 8 ++++---- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 2ae054184..b75f64fb3 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -847,7 +847,9 @@ timerStopped = false; maxTime = self.card.time_limit() / 1000 else: maxTime = 0 - self.bottom.web.eval("anki.showQuestion(%s,%d);" % (json.dumps(middle), maxTime)) + self.bottom.web.eval( + "anki.showQuestion(%s,%d);" % (json.dumps(middle), maxTime) + ) def _showEaseButtons(self) -> None: if not self._states_mutated: diff --git a/ts/routes/reviewer-bottom/index.scss b/ts/routes/reviewer-bottom/index.scss index fbe069779..7c9985ba6 100644 --- a/ts/routes/reviewer-bottom/index.scss +++ b/ts/routes/reviewer-bottom/index.scss @@ -4,6 +4,7 @@ @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)}; diff --git a/ts/routes/reviewer-bottom/index.svelte b/ts/routes/reviewer-bottom/index.svelte index 049eff39e..b4a9ba53b 100644 --- a/ts/routes/reviewer-bottom/index.svelte +++ b/ts/routes/reviewer-bottom/index.svelte @@ -1,3 +1,22 @@ -
- Hello from svelte -
\ No newline at end of file +
+
+
+ +
+
+ +
+
+ +
+
+
+ + diff --git a/ts/routes/reviewer-bottom/index.ts b/ts/routes/reviewer-bottom/index.ts index e4bec417a..d8b7a4c55 100644 --- a/ts/routes/reviewer-bottom/index.ts +++ b/ts/routes/reviewer-bottom/index.ts @@ -6,8 +6,8 @@ */ import { mount } from "svelte"; -import "./index.scss" -import ReviewerBottom from "./index.svelte" +import "./index.scss"; +import ReviewerBottom from "./index.svelte"; let time: number; // set in python code let timerStopped = false; @@ -68,5 +68,5 @@ function selectedAnswerButton(): string { mount( ReviewerBottom, - {target: document.body}, -) + { target: document.body }, +); From 7788aa7785524c7bd114451b7094bf9b0f51c0ba Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 21:55:21 +0100 Subject: [PATCH 005/237] Answer buttons --- qt/aqt/reviewer.py | 30 ++++++++----------- ts/routes/reviewer-bottom/AnswerButton.svelte | 9 ++++++ ts/routes/reviewer-bottom/index.svelte | 17 ++++++++++- ts/routes/reviewer-bottom/index.ts | 10 ++++--- ts/routes/reviewer-bottom/types.ts | 7 +++++ 5 files changed, 50 insertions(+), 23 deletions(-) create mode 100644 ts/routes/reviewer-bottom/types.ts diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index b75f64fb3..ab51ff805 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -858,7 +858,7 @@ 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"anki.showAnswer({json.dumps(middle)}, {json.dumps(conf['stopTimerOnAnswer'])});" ) def _remaining(self) -> str: @@ -912,31 +912,25 @@ timerStopped = false; def but(i: int, label: str) -> str: if i == default: - extra = """id="defease" """ + id = "defease" else: - extra = "" + 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 """ -""" % ( - extra, - key, - i, - i, - label, - due, - ) + return { + "id": id, + "key": key, + "i": i, + "label": label, + "due": due, + } - buf = "
" - for ease, label in self._answerButtonList(): - buf += but(ease, label) - buf += "
" - return buf + return [but(ease, label) for ease, label in self._answerButtonList()] + def _buttonTime(self, i: int, v3_labels: Sequence[str]) -> str: if self.mw.col.conf["estTimes"]: diff --git a/ts/routes/reviewer-bottom/AnswerButton.svelte b/ts/routes/reviewer-bottom/AnswerButton.svelte index e69de29bb..2a04b7a20 100644 --- a/ts/routes/reviewer-bottom/AnswerButton.svelte +++ b/ts/routes/reviewer-bottom/AnswerButton.svelte @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/ts/routes/reviewer-bottom/index.svelte b/ts/routes/reviewer-bottom/index.svelte index b4a9ba53b..f2c8ff02d 100644 --- a/ts/routes/reviewer-bottom/index.svelte +++ b/ts/routes/reviewer-bottom/index.svelte @@ -1,10 +1,25 @@ + +
- + {#if $answerButtons.length} + {#each $answerButtons as answerButton} + + {/each} + {:else} + + {/if}
diff --git a/ts/routes/reviewer-bottom/index.ts b/ts/routes/reviewer-bottom/index.ts index d8b7a4c55..95ab43f00 100644 --- a/ts/routes/reviewer-bottom/index.ts +++ b/ts/routes/reviewer-bottom/index.ts @@ -8,6 +8,7 @@ import { mount } from "svelte"; import "./index.scss"; import ReviewerBottom from "./index.svelte"; +import { writable } from "svelte/store"; let time: number; // set in python code let timerStopped = false; @@ -34,9 +35,10 @@ function updateTime(): void { } let intervalId: number | undefined; +let answerButtons = writable([]) export function showQuestion(txt: string, maxTime_: number): void { - showAnswer(txt); + showAnswer([]); time = 0; maxTime = maxTime_; updateTime(); @@ -53,8 +55,8 @@ export function showQuestion(txt: string, maxTime_: number): void { }, 1000); } -function showAnswer(txt: string, stopTimer = false): void { - document.getElementById("middle").innerHTML = txt; +export function showAnswer(info: AnswerButtonInfo[], stopTimer = false): void { + answerButtons.set(info); timerStopped = stopTimer; } @@ -68,5 +70,5 @@ function selectedAnswerButton(): string { mount( ReviewerBottom, - { target: document.body }, + { target: document.body, props: {answerButtons} }, ); diff --git a/ts/routes/reviewer-bottom/types.ts b/ts/routes/reviewer-bottom/types.ts new file mode 100644 index 000000000..5a4ec8116 --- /dev/null +++ b/ts/routes/reviewer-bottom/types.ts @@ -0,0 +1,7 @@ +interface AnswerButtonInfo { + "extra": string, + "key": string, + "i": number, + "label": string, + "due": string, +} \ No newline at end of file From 5d536f2f8eb890924c95eb3a0e9e0a4c14e92f20 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 22:01:48 +0100 Subject: [PATCH 006/237] Added: edit button --- ts/routes/reviewer-bottom/index.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/routes/reviewer-bottom/index.svelte b/ts/routes/reviewer-bottom/index.svelte index f2c8ff02d..468de32c4 100644 --- a/ts/routes/reviewer-bottom/index.svelte +++ b/ts/routes/reviewer-bottom/index.svelte @@ -2,6 +2,7 @@ import type { Writable } from "svelte/store"; import AnswerButton from "./AnswerButton.svelte"; import { bridgeCommand } from "@tslib/bridgecommand"; + import * as tr from "@generated/ftl" export let answerButtons: Writable $: console.log($answerButtons) @@ -10,7 +11,7 @@
- +
{#if $answerButtons.length} From 8c0d1d1720e30f7757535b5df8fb3968c44069ac Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 22:30:28 +0100 Subject: [PATCH 007/237] More i18n --- ts/routes/reviewer-bottom/index.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ts/routes/reviewer-bottom/index.svelte b/ts/routes/reviewer-bottom/index.svelte index 468de32c4..280f1c500 100644 --- a/ts/routes/reviewer-bottom/index.svelte +++ b/ts/routes/reviewer-bottom/index.svelte @@ -2,7 +2,7 @@ import type { Writable } from "svelte/store"; import AnswerButton from "./AnswerButton.svelte"; import { bridgeCommand } from "@tslib/bridgecommand"; - import * as tr from "@generated/ftl" + import * as tr from "@generated/ftl"; export let answerButtons: Writable $: console.log($answerButtons) @@ -19,11 +19,11 @@ {/each} {:else} - + {/if}
- +
From f4eb7e0ff9fd3d93f2cfb4a41aa91275ba88a71c Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 23:32:33 +0100 Subject: [PATCH 008/237] Use sveltekit --- build/configure/src/web.rs | 12 --- qt/aqt/mediasrv.py | 1 + qt/aqt/reviewer.py | 6 +- ts/routes/reviewer-bottom/+page.svelte | 69 +++++++++++++++++ .../{index.svelte => ReviewerBottom.svelte} | 0 ts/routes/reviewer-bottom/index.ts | 74 ------------------- 6 files changed, 73 insertions(+), 89 deletions(-) create mode 100644 ts/routes/reviewer-bottom/+page.svelte rename ts/routes/reviewer-bottom/{index.svelte => ReviewerBottom.svelte} (100%) delete mode 100644 ts/routes/reviewer-bottom/index.ts diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index 9ee5d74b5..56ef63bad 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -229,18 +229,6 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { ":sveltekit" ], )?; - build_page( - "reviewer-bottom", - true, - inputs![ - // - ":ts:lib", - ":ts:components", - ":sass", - ":sveltekit" - ], - )?; - Ok(()) } diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 820e762d9..0a4fd0ea0 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-bottom" ] diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index ab51ff805..c6779063f 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -355,7 +355,7 @@ class Reviewer: self.web.allow_drops = True self.web.eval("_blockDefaultDragDropBehavior();") # show answer / ease buttons - self.bottom.web.load_ts_page("reviewer-bottom") + self.bottom.web.load_sveltekit_page("reviewer-bottom") # Showing the question ########################################################################## @@ -848,7 +848,7 @@ timerStopped = false; else: maxTime = 0 self.bottom.web.eval( - "anki.showQuestion(%s,%d);" % (json.dumps(middle), maxTime) + "_showQuestion(%s,%d);" % (json.dumps(middle), maxTime) ) def _showEaseButtons(self) -> None: @@ -858,7 +858,7 @@ 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"anki.showAnswer({json.dumps(middle)}, {json.dumps(conf['stopTimerOnAnswer'])});" + f"_showAnswer({json.dumps(middle)}, {json.dumps(conf['stopTimerOnAnswer'])});" ) def _remaining(self) -> str: diff --git a/ts/routes/reviewer-bottom/+page.svelte b/ts/routes/reviewer-bottom/+page.svelte new file mode 100644 index 000000000..fa264b129 --- /dev/null +++ b/ts/routes/reviewer-bottom/+page.svelte @@ -0,0 +1,69 @@ + + + \ No newline at end of file diff --git a/ts/routes/reviewer-bottom/index.svelte b/ts/routes/reviewer-bottom/ReviewerBottom.svelte similarity index 100% rename from ts/routes/reviewer-bottom/index.svelte rename to ts/routes/reviewer-bottom/ReviewerBottom.svelte diff --git a/ts/routes/reviewer-bottom/index.ts b/ts/routes/reviewer-bottom/index.ts deleted file mode 100644 index 95ab43f00..000000000 --- a/ts/routes/reviewer-bottom/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* 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", -*/ - -import { mount } from "svelte"; -import "./index.scss"; -import ReviewerBottom from "./index.svelte"; -import { writable } from "svelte/store"; - -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; -let answerButtons = writable([]) - -export function showQuestion(txt: string, maxTime_: number): void { - showAnswer([]); - time = 0; - maxTime = maxTime_; - updateTime(); - - if (intervalId !== undefined) { - clearInterval(intervalId); - } - - intervalId = setInterval(function() { - if (!timerStopped) { - time += 1; - updateTime(); - } - }, 1000); -} - -export function showAnswer(info: AnswerButtonInfo[], stopTimer = false): void { - answerButtons.set(info); - timerStopped = stopTimer; -} - -function selectedAnswerButton(): string { - const node = document.activeElement as HTMLElement; - if (!node) { - return; - } - return node.dataset.ease; -} - -mount( - ReviewerBottom, - { target: document.body, props: {answerButtons} }, -); From 7e92c4016938be6964d3d85e89bdf8f4c19b873b Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 25 Aug 2025 23:35:22 +0100 Subject: [PATCH 009/237] Added: More bridge command --- ts/routes/reviewer-bottom/ReviewerBottom.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer-bottom/ReviewerBottom.svelte index 280f1c500..0345097ae 100644 --- a/ts/routes/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer-bottom/ReviewerBottom.svelte @@ -23,7 +23,7 @@ {/if}
- +
From 992c8ad73101b4e18534759329203242fda3b941 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 00:45:58 +0100 Subject: [PATCH 010/237] Added: Remaining --- qt/aqt/reviewer.py | 16 +-- ts/routes/reviewer-bottom/+page.svelte | 126 ++++++++++-------- .../reviewer-bottom/RemainingNumber.svelte | 12 ++ .../reviewer-bottom/ReviewerBottom.svelte | 35 +++-- 4 files changed, 114 insertions(+), 75 deletions(-) create mode 100644 ts/routes/reviewer-bottom/RemainingNumber.svelte diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index c6779063f..5ba90995f 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -259,8 +259,6 @@ class Reviewer: if self._reps is None: self._initWeb() - self._showQuestion() - def _get_next_v3_card(self) -> None: assert isinstance(self.mw.col.sched, V3Scheduler) output = self.mw.col.sched.get_queued_cards() @@ -676,6 +674,9 @@ class Reviewer: self.mw.onEditCurrent() elif url == "more": self.showContextMenu() + elif url == "bottomReady": + self._showQuestion() + self._remaining() elif url.startswith("play:"): play_clicked_audio(url, self.card) elif url.startswith("updateToolbar"): @@ -866,15 +867,8 @@ timerStopped = false; return "" 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]} -""" + idx, counts = self._v3.counts() + self.bottom.web.eval(f"_updateRemaining({json.dumps(counts)},{idx})") def _defaultEase(self) -> Literal[2, 3]: return 3 diff --git a/ts/routes/reviewer-bottom/+page.svelte b/ts/routes/reviewer-bottom/+page.svelte index fa264b129..a5ea9fe09 100644 --- a/ts/routes/reviewer-bottom/+page.svelte +++ b/ts/routes/reviewer-bottom/+page.svelte @@ -1,69 +1,85 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/ts/routes/reviewer-bottom/RemainingNumber.svelte b/ts/routes/reviewer-bottom/RemainingNumber.svelte new file mode 100644 index 000000000..6192df946 --- /dev/null +++ b/ts/routes/reviewer-bottom/RemainingNumber.svelte @@ -0,0 +1,12 @@ + + + + {#if underlined} + + {:else} + + {/if} + diff --git a/ts/routes/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer-bottom/ReviewerBottom.svelte index 0345097ae..dedf7f011 100644 --- a/ts/routes/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer-bottom/ReviewerBottom.svelte @@ -3,9 +3,13 @@ import AnswerButton from "./AnswerButton.svelte"; import { bridgeCommand } from "@tslib/bridgecommand"; import * as tr from "@generated/ftl"; + import RemainingNumber from "./RemainingNumber.svelte"; export let answerButtons: Writable - $: console.log($answerButtons) + export let remaining: Writable + export let remainingIndex: Writable + + $: console.log($remaining)
@@ -13,14 +17,21 @@
-
- {#if $answerButtons.length} - {#each $answerButtons as answerButton} - - {/each} - {:else} - - {/if} +
+ + {$remaining[0]} + + {$remaining[1]} + + {$remaining[2]} + +
+ {#if $answerButtons.length} + {#each $answerButtons as answerButton} + + {/each} + {:else} + + {/if} +
@@ -35,4 +46,10 @@ grid-template-columns: auto 1fr auto; justify-items: center; } + + .review-buttons { + display: flex; + flex-direction: column; + align-items: center + } From 8a57d1c5e10ca9208a13652dbe44762f2633da48 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 01:36:17 +0100 Subject: [PATCH 011/237] Fix: showQuestion issues --- qt/aqt/reviewer.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 5ba90995f..fd13584f4 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -259,6 +259,8 @@ class Reviewer: if self._reps is None: self._initWeb() + self._showQuestion() + def _get_next_v3_card(self) -> None: assert isinstance(self.mw.col.sched, V3Scheduler) output = self.mw.col.sched.get_queued_cards() @@ -675,7 +677,6 @@ class Reviewer: elif url == "more": self.showContextMenu() elif url == "bottomReady": - self._showQuestion() self._remaining() elif url.startswith("play:"): play_clicked_audio(url, self.card) @@ -833,23 +834,13 @@ 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.bottom.web.eval( - "_showQuestion(%s,%d);" % (json.dumps(middle), maxTime) + "_showQuestion(%s,%d);" % ("", maxTime) ) def _showEaseButtons(self) -> None: From 9dbb7abdbbebef24d21191089acbb3940724d2d6 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 01:46:27 +0100 Subject: [PATCH 012/237] Remove unneeded globals --- ts/routes/reviewer-bottom/+page.svelte | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ts/routes/reviewer-bottom/+page.svelte b/ts/routes/reviewer-bottom/+page.svelte index a5ea9fe09..2b801cf3a 100644 --- a/ts/routes/reviewer-bottom/+page.svelte +++ b/ts/routes/reviewer-bottom/+page.svelte @@ -5,9 +5,9 @@ import ReviewerBottom from "./ReviewerBottom.svelte"; import "./index.scss" - globalThis.answerButtons = writable([]) - globalThis.remaining = writable<[number, number, number]>([0, 0, 0]) - globalThis.remainingIndex = writable(-1) + let answerButtons = writable([]) + let remaining = writable<[number, number, number]>([0, 0, 0]) + let remainingIndex = writable(-1) onMount(() => { let timerStopped = false; @@ -56,13 +56,13 @@ function _showAnswer(info: AnswerButtonInfo[], stopTimer = false): void { console.log(info) - globalThis.answerButtons.set(info); + answerButtons.set(info); timerStopped = stopTimer; } function _updateRemaining(counts: [number, number, number], idx: number) { - globalThis.remaining.set(counts) - globalThis.remainingIndex.set(idx) + remaining.set(counts) + remainingIndex.set(idx) } globalThis._showQuestion = _showQuestion; @@ -82,4 +82,4 @@ - \ No newline at end of file + \ No newline at end of file From b256e88b1d62f9fd28467ba93974caf1ae15c11c Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 01:56:30 +0100 Subject: [PATCH 013/237] Fix: Large font size --- ts/routes/reviewer-bottom/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/routes/reviewer-bottom/index.scss b/ts/routes/reviewer-bottom/index.scss index 7c9985ba6..331f2de00 100644 --- a/ts/routes/reviewer-bottom/index.scss +++ b/ts/routes/reviewer-bottom/index.scss @@ -17,6 +17,7 @@ body { margin: 0; padding: 0; + font-size: 12px; } #middle td[align="center"] { From 28402c548db22b970e2085e5484c6225ab903b16 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 01:58:00 +0100 Subject: [PATCH 014/237] Fix: Id not class --- ts/routes/reviewer-bottom/ReviewerBottom.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer-bottom/ReviewerBottom.svelte index dedf7f011..bc1c7b88f 100644 --- a/ts/routes/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer-bottom/ReviewerBottom.svelte @@ -12,7 +12,7 @@ $: console.log($remaining) -
+
From a3653695626e41cd4e2160c9da859a04c66325ae Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 01:58:52 +0100 Subject: [PATCH 015/237] align items: center --- ts/routes/reviewer-bottom/ReviewerBottom.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/routes/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer-bottom/ReviewerBottom.svelte index bc1c7b88f..31e3f82fd 100644 --- a/ts/routes/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer-bottom/ReviewerBottom.svelte @@ -45,6 +45,7 @@ display: grid; grid-template-columns: auto 1fr auto; justify-items: center; + align-items: center; } .review-buttons { From 6c540c89f10593122cc9a62e6169dd35b638c921 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 02:03:13 +0100 Subject: [PATCH 016/237] ./check --- qt/aqt/reviewer.py | 2 +- ts/routes/reviewer-bottom/+page.svelte | 24 +++++++++++-------- ts/routes/reviewer-bottom/AnswerButton.svelte | 1 + .../reviewer-bottom/ReviewerBottom.svelte | 1 + ts/routes/reviewer-bottom/types.ts | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index fd13584f4..b3232a9cf 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, Union, cast +from typing import Any, Literal, Match, cast import aqt import aqt.browser diff --git a/ts/routes/reviewer-bottom/+page.svelte b/ts/routes/reviewer-bottom/+page.svelte index 2b801cf3a..566b287e6 100644 --- a/ts/routes/reviewer-bottom/+page.svelte +++ b/ts/routes/reviewer-bottom/+page.svelte @@ -3,13 +3,15 @@ import { writable } from "svelte/store"; import { bridgeCommand } from "@tslib/bridgecommand"; import ReviewerBottom from "./ReviewerBottom.svelte"; + import type {AnswerButtonInfo} from "./types" import "./index.scss" - let answerButtons = writable([]) - let remaining = writable<[number, number, number]>([0, 0, 0]) - let remainingIndex = writable(-1) + const answerButtons = writable([]) + const remaining = writable<[number, number, number]>([0, 0, 0]) + const remainingIndex = writable(-1) onMount(() => { + /* let timerStopped = false; let maxTime = 0; @@ -31,33 +33,34 @@ } else { timeNode.textContent = timeString; } - } + }*/ let intervalId: number | undefined; - function _showQuestion(txt: string, maxTime_: number): void { + function _showQuestion(_txt: string, _maxTime_: number): void { _showAnswer([]); globalThis.time = 0; - maxTime = maxTime_; + // maxTime = maxTime_; // updateTime(); if (intervalId !== undefined) { clearInterval(intervalId); } + /* intervalId = setInterval(function() { if (!timerStopped) { globalThis.time += 1; //updateTime(); } - }, 1000); + }, 1000);*/ } - function _showAnswer(info: AnswerButtonInfo[], stopTimer = false): void { + function _showAnswer(info: AnswerButtonInfo[], _stopTimer = false): void { console.log(info) answerButtons.set(info); - timerStopped = stopTimer; + // timerStopped = stopTimer; } function _updateRemaining(counts: [number, number, number], idx: number) { @@ -69,6 +72,7 @@ globalThis._showAnswer = _showAnswer; globalThis._updateRemaining = _updateRemaining; + /* function selectedAnswerButton(): string | undefined { const node = document.activeElement as HTMLElement; if (!node) { @@ -76,7 +80,7 @@ } return node.dataset.ease; } - + */ bridgeCommand("bottomReady"); }); diff --git a/ts/routes/reviewer-bottom/AnswerButton.svelte b/ts/routes/reviewer-bottom/AnswerButton.svelte index 2a04b7a20..645a4f4fe 100644 --- a/ts/routes/reviewer-bottom/AnswerButton.svelte +++ b/ts/routes/reviewer-bottom/AnswerButton.svelte @@ -1,5 +1,6 @@ diff --git a/ts/routes/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer-bottom/ReviewerBottom.svelte index 31e3f82fd..0e2bded32 100644 --- a/ts/routes/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer-bottom/ReviewerBottom.svelte @@ -4,6 +4,7 @@ import { bridgeCommand } from "@tslib/bridgecommand"; import * as tr from "@generated/ftl"; import RemainingNumber from "./RemainingNumber.svelte"; + import type { AnswerButtonInfo } from "./types"; export let answerButtons: Writable export let remaining: Writable diff --git a/ts/routes/reviewer-bottom/types.ts b/ts/routes/reviewer-bottom/types.ts index 5a4ec8116..48fe4567d 100644 --- a/ts/routes/reviewer-bottom/types.ts +++ b/ts/routes/reviewer-bottom/types.ts @@ -1,4 +1,4 @@ -interface AnswerButtonInfo { +export interface AnswerButtonInfo { "extra": string, "key": string, "i": number, From 860a8b429539b81199e456ae1e0d712831840ee5 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 02:08:24 +0100 Subject: [PATCH 017/237] ./check --- qt/aqt/mediasrv.py | 2 +- qt/aqt/reviewer.py | 12 ++--- ts/routes/reviewer-bottom/+page.svelte | 24 +++++----- ts/routes/reviewer-bottom/AnswerButton.svelte | 10 +++-- .../reviewer-bottom/RemainingNumber.svelte | 12 +++-- .../reviewer-bottom/ReviewerBottom.svelte | 44 ++++++++++++++----- ts/routes/reviewer-bottom/types.ts | 14 +++--- 7 files changed, 74 insertions(+), 44 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 0a4fd0ea0..530b5d022 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -334,7 +334,7 @@ def is_sveltekit_page(path: str) -> bool: "import-csv", "import-page", "image-occlusion", - "reviewer-bottom" + "reviewer-bottom", ] diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index b3232a9cf..9acfd1254 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -839,9 +839,7 @@ timerStopped = false; maxTime = self.card.time_limit() / 1000 else: maxTime = 0 - self.bottom.web.eval( - "_showQuestion(%s,%d);" % ("", maxTime) - ) + self.bottom.web.eval("_showQuestion(%s,%d);" % ("", maxTime)) def _showEaseButtons(self) -> None: if not self._states_mutated: @@ -853,11 +851,10 @@ timerStopped = false; f"_showAnswer({json.dumps(middle)}, {json.dumps(conf['stopTimerOnAnswer'])});" ) - def _remaining(self) -> str: + def _remaining(self): if not self.mw.col.conf["dueCounts"]: return "" - counts: list[int | str] idx, counts = self._v3.counts() self.bottom.web.eval(f"_updateRemaining({json.dumps(counts)},{idx})") @@ -889,13 +886,13 @@ timerStopped = false; ) return buttons_tuple - def _answerButtons(self) -> str: + def _answerButtons(self): 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) -> str: + def but(i: int, label: str): if i == default: id = "defease" else: @@ -915,7 +912,6 @@ timerStopped = false; } return [but(ease, label) for ease, label in self._answerButtonList()] - def _buttonTime(self, i: int, v3_labels: Sequence[str]) -> str: if self.mw.col.conf["estTimes"]: diff --git a/ts/routes/reviewer-bottom/+page.svelte b/ts/routes/reviewer-bottom/+page.svelte index 566b287e6..c92e56462 100644 --- a/ts/routes/reviewer-bottom/+page.svelte +++ b/ts/routes/reviewer-bottom/+page.svelte @@ -1,14 +1,18 @@ + - - \ No newline at end of file + diff --git a/ts/routes/reviewer-bottom/AnswerButton.svelte b/ts/routes/reviewer-bottom/AnswerButton.svelte index 645a4f4fe..96918be95 100644 --- a/ts/routes/reviewer-bottom/AnswerButton.svelte +++ b/ts/routes/reviewer-bottom/AnswerButton.svelte @@ -1,10 +1,14 @@ + - \ No newline at end of file + diff --git a/ts/routes/reviewer-bottom/RemainingNumber.svelte b/ts/routes/reviewer-bottom/RemainingNumber.svelte index 6192df946..4761dfdcd 100644 --- a/ts/routes/reviewer-bottom/RemainingNumber.svelte +++ b/ts/routes/reviewer-bottom/RemainingNumber.svelte @@ -1,12 +1,16 @@ + {#if underlined} - + {:else} - + {/if} diff --git a/ts/routes/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer-bottom/ReviewerBottom.svelte index 0e2bded32..9dd6160b7 100644 --- a/ts/routes/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer-bottom/ReviewerBottom.svelte @@ -1,3 +1,7 @@ +
- +
- {$remaining[0]} + - {$remaining[1]} + - {$remaining[2]} + + {$remaining[0]} + + + + {$remaining[1]} + + + + {$remaining[2]} +
{#if $answerButtons.length} @@ -30,12 +45,19 @@ {/each} {:else} - + {/if}
- +
@@ -52,6 +74,6 @@ .review-buttons { display: flex; flex-direction: column; - align-items: center + align-items: center; } diff --git a/ts/routes/reviewer-bottom/types.ts b/ts/routes/reviewer-bottom/types.ts index 48fe4567d..4068ca1f9 100644 --- a/ts/routes/reviewer-bottom/types.ts +++ b/ts/routes/reviewer-bottom/types.ts @@ -1,7 +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, -} \ No newline at end of file + "extra": string; + "key": string; + "i": number; + "label": string; + "due": string; +} From d49f1eb430a4c5d860a07721b6a5f71468c54a2b Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 02:23:23 +0100 Subject: [PATCH 018/237] Fix: Update remaining on answer shown --- qt/aqt/reviewer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 9acfd1254..6edd73efc 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -839,6 +839,7 @@ timerStopped = false; maxTime = self.card.time_limit() / 1000 else: maxTime = 0 + self._remaining() self.bottom.web.eval("_showQuestion(%s,%d);" % ("", maxTime)) def _showEaseButtons(self) -> None: From fac5d64558c80b889486c8450d066c735a70d279 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 26 Aug 2025 16:11:45 +0100 Subject: [PATCH 019/237] Manually specify height --- ts/routes/reviewer-bottom/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/routes/reviewer-bottom/index.scss b/ts/routes/reviewer-bottom/index.scss index 331f2de00..9675a96a1 100644 --- a/ts/routes/reviewer-bottom/index.scss +++ b/ts/routes/reviewer-bottom/index.scss @@ -18,6 +18,7 @@ body { margin: 0; padding: 0; font-size: 12px; + height: 72px; } #middle td[align="center"] { From 4bf38ec2af5be2c8a435d144913e15e42e01bd75 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 18:40:37 +0100 Subject: [PATCH 020/237] Added: Reviewer entrypoint --- qt/aqt/mediasrv.py | 2 +- qt/aqt/reviewer.py | 13 ++----------- ts/routes/reviewer/+page.svelte | 5 +++++ ts/routes/reviewer/index.ts | 0 .../reviewer-bottom/AnswerButton.svelte | 0 .../reviewer-bottom/RemainingNumber.svelte | 0 .../reviewer-bottom/ReviewerBottom.svelte | 0 .../reviewer-bottom/ReviewerBottomOuter.svelte} | 0 ts/routes/{ => reviewer}/reviewer-bottom/index.scss | 8 ++++---- ts/routes/{ => reviewer}/reviewer-bottom/types.ts | 0 10 files changed, 12 insertions(+), 16 deletions(-) create mode 100644 ts/routes/reviewer/+page.svelte create mode 100644 ts/routes/reviewer/index.ts rename ts/routes/{ => reviewer}/reviewer-bottom/AnswerButton.svelte (100%) rename ts/routes/{ => reviewer}/reviewer-bottom/RemainingNumber.svelte (100%) rename ts/routes/{ => reviewer}/reviewer-bottom/ReviewerBottom.svelte (100%) rename ts/routes/{reviewer-bottom/+page.svelte => reviewer/reviewer-bottom/ReviewerBottomOuter.svelte} (100%) rename ts/routes/{ => reviewer}/reviewer-bottom/index.scss (89%) rename ts/routes/{ => reviewer}/reviewer-bottom/types.ts (100%) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 530b5d022..83f45c933 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -334,7 +334,7 @@ def is_sveltekit_page(path: str) -> bool: "import-csv", "import-page", "image-occlusion", - "reviewer-bottom", + "reviewer", ] diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 6edd73efc..535f87b22 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -341,21 +341,12 @@ class Reviewer: def _initWeb(self) -> None: self._reps = 0 # main window - 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, - ) + 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();") # show answer / ease buttons - self.bottom.web.load_sveltekit_page("reviewer-bottom") + self.bottom.web = self.web # Showing the question ########################################################################## diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte new file mode 100644 index 000000000..2299e944d --- /dev/null +++ b/ts/routes/reviewer/+page.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file 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-bottom/AnswerButton.svelte b/ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte similarity index 100% rename from ts/routes/reviewer-bottom/AnswerButton.svelte rename to ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte diff --git a/ts/routes/reviewer-bottom/RemainingNumber.svelte b/ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte similarity index 100% rename from ts/routes/reviewer-bottom/RemainingNumber.svelte rename to ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte diff --git a/ts/routes/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte similarity index 100% rename from ts/routes/reviewer-bottom/ReviewerBottom.svelte rename to ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte diff --git a/ts/routes/reviewer-bottom/+page.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte similarity index 100% rename from ts/routes/reviewer-bottom/+page.svelte rename to ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte diff --git a/ts/routes/reviewer-bottom/index.scss b/ts/routes/reviewer/reviewer-bottom/index.scss similarity index 89% rename from ts/routes/reviewer-bottom/index.scss rename to ts/routes/reviewer/reviewer-bottom/index.scss index 9675a96a1..0f189d6b9 100644 --- a/ts/routes/reviewer-bottom/index.scss +++ b/ts/routes/reviewer/reviewer-bottom/index.scss @@ -1,10 +1,10 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -@use "../../lib/sass/root-vars"; -@use "../../lib/sass/vars" as *; -@use "../../lib/sass/card-counts"; -@use "../../lib/sass/buttons"; +@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)}; diff --git a/ts/routes/reviewer-bottom/types.ts b/ts/routes/reviewer/reviewer-bottom/types.ts similarity index 100% rename from ts/routes/reviewer-bottom/types.ts rename to ts/routes/reviewer/reviewer-bottom/types.ts From d81ec73205633b631d1273128e481c9414a30094 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 21:59:34 +0100 Subject: [PATCH 021/237] 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. From 7cb8e622547900f3c9a42303bdfc2c31e3d6fb4c Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 21:59:46 +0100 Subject: [PATCH 022/237] revert random rust changes --- build/configure/src/web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index 56ef63bad..3e695e01e 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -1,7 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use anyhow::Ok; use anyhow::Result; use ninja_gen::action::BuildAction; use ninja_gen::copy::CopyFiles; @@ -229,6 +228,7 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { ":sveltekit" ], )?; + Ok(()) } From f01e0f8d0b0133af8a22a95024c9ec05c802801a Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 22:01:41 +0100 Subject: [PATCH 023/237] toggle svelte reviewer on --- qt/aqt/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index c707d1b2a..0c009a16c 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 ########################################################################## From e46d98e2e044ec45f286d3b30e9f968ab6fe5a88 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 22:05:35 +0100 Subject: [PATCH 024/237] ./check --- build/configure/src/web.rs | 2 +- qt/aqt/data/web/js/reviewer-bottom.ts | 2 +- qt/aqt/main.py | 2 +- qt/aqt/reviewer.py | 1 + ts/routes/reviewer/+page.svelte | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index 3e695e01e..ef2d268bb 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -228,7 +228,7 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { ":sveltekit" ], )?; - + Ok(()) } diff --git a/qt/aqt/data/web/js/reviewer-bottom.ts b/qt/aqt/data/web/js/reviewer-bottom.ts index c11fa2aa2..4ebf34510 100644 --- a/qt/aqt/data/web/js/reviewer-bottom.ts +++ b/qt/aqt/data/web/js/reviewer-bottom.ts @@ -60,4 +60,4 @@ function selectedAnswerButton(): string { return; } return node.dataset.ease; -} \ No newline at end of file +} diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 0c009a16c..3affb3a2e 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1077,7 +1077,7 @@ title="{}" {}>{}""".format( def setupReviewer(self) -> None: from aqt.reviewer import Reviewer, SvelteReviewer - self.reviewer = SvelteReviewer(self) if True else Reviewer(self) + self.reviewer = SvelteReviewer(self) if True else Reviewer(self) # Syncing ########################################################################## diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 6bbc4f986..8cd02813d 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1227,6 +1227,7 @@ timerStopped = false; onMark = toggle_mark_on_current_note setFlag = set_flag_on_current_card + class SvelteReviewer(Reviewer): def _answerButtons(self) -> str: default = self._defaultEase() diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte index 2299e944d..8892f24c8 100644 --- a/ts/routes/reviewer/+page.svelte +++ b/ts/routes/reviewer/+page.svelte @@ -2,4 +2,4 @@ import ReviewerBottomOuter from "./reviewer-bottom/ReviewerBottomOuter.svelte"; - \ No newline at end of file + From 9d3451f97b92b127b392adb35c8734c47531662f Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 22:07:44 +0100 Subject: [PATCH 025/237] fix py issues --- qt/aqt/reviewer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 8cd02813d..1108b9e8f 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1254,7 +1254,7 @@ class SvelteReviewer(Reviewer): "due": due, } - return [but(ease, label) for ease, label in self._answerButtonList()] + return [but(ease, label) for ease, label in self._answerButtonList()] # type: ignore def _remaining(self) -> str: if not self.mw.col.conf["dueCounts"]: @@ -1262,6 +1262,7 @@ class SvelteReviewer(Reviewer): 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(): From c64dd6c9593ae2ae2ca20c38868b73eda13a5975 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 22:29:23 +0100 Subject: [PATCH 026/237] Neaten bottom code --- .../ReviewerBottomOuter.svelte | 83 ++---------------- .../reviewer/reviewer-bottom/reviewer.ts | 86 +++++++++++++++++++ 2 files changed, 92 insertions(+), 77 deletions(-) create mode 100644 ts/routes/reviewer/reviewer-bottom/reviewer.ts diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte index c92e56462..2e95d1965 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte @@ -4,88 +4,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> - +{#if reviewerInfo} + +{/if} diff --git a/ts/routes/reviewer/reviewer-bottom/reviewer.ts b/ts/routes/reviewer/reviewer-bottom/reviewer.ts new file mode 100644 index 000000000..ce25f719c --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/reviewer.ts @@ -0,0 +1,86 @@ +import { writable } from "svelte/store"; +import type { AnswerButtonInfo } from "./types"; +import { bridgeCommand } from "@tslib/bridgecommand"; + +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; + } + */ + bridgeCommand("bottomReady"); + + return { + answerButtons, + remaining, + remainingIndex, + }; +} \ No newline at end of file From 7805b1b426bd53a28bb7f89dbd21fbde859f436f Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 22:34:26 +0100 Subject: [PATCH 027/237] Fix: Wrong function name --- ts/routes/reviewer/reviewer-bottom/reviewer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer-bottom/reviewer.ts b/ts/routes/reviewer/reviewer-bottom/reviewer.ts index ce25f719c..1729f8c2c 100644 --- a/ts/routes/reviewer/reviewer-bottom/reviewer.ts +++ b/ts/routes/reviewer/reviewer-bottom/reviewer.ts @@ -64,7 +64,7 @@ export function setupBottomBar() { } globalThis._showQuestion = _showQuestion; - globalThis._showAnswer = _showAnswer; + globalThis.showAnswer = _showAnswer; globalThis._updateRemaining = _updateRemaining; /* From eac356139c4bdf0e019d32acb0e4e68b213df614 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 22:48:51 +0100 Subject: [PATCH 028/237] Added: Reviewer framework --- ts/routes/reviewer/+page.svelte | 14 ++++++++++++- .../ReviewerBottomOuter.svelte | 2 +- .../{reviewer.ts => reviewer-bottom.ts} | 0 ts/routes/reviewer/reviewer.svelte | 15 +++++++++++++ ts/routes/reviewer/reviewer.ts | 7 +++++++ ts/routes/reviewer/reviewerOuter.svelte | 21 +++++++++++++++++++ 6 files changed, 57 insertions(+), 2 deletions(-) rename ts/routes/reviewer/reviewer-bottom/{reviewer.ts => reviewer-bottom.ts} (100%) create mode 100644 ts/routes/reviewer/reviewer.svelte create mode 100644 ts/routes/reviewer/reviewer.ts create mode 100644 ts/routes/reviewer/reviewerOuter.svelte diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte index 8892f24c8..2baa1b1f6 100644 --- a/ts/routes/reviewer/+page.svelte +++ b/ts/routes/reviewer/+page.svelte @@ -1,5 +1,17 @@ - +
+ + +
+ + diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte index 2e95d1965..5c66485e0 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte @@ -6,7 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { onMount } from "svelte"; import ReviewerBottom from "./ReviewerBottom.svelte"; import "./index.scss"; - import { setupBottomBar } from "./reviewer"; + import { setupBottomBar } from "./reviewer-bottom"; let reviewerInfo: null | ReturnType = null; diff --git a/ts/routes/reviewer/reviewer-bottom/reviewer.ts b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts similarity index 100% rename from ts/routes/reviewer/reviewer-bottom/reviewer.ts rename to ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/reviewer.svelte new file mode 100644 index 000000000..85124fb0a --- /dev/null +++ b/ts/routes/reviewer/reviewer.svelte @@ -0,0 +1,15 @@ + + +
+ {@html $html} +
+ + diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts new file mode 100644 index 000000000..866e74d57 --- /dev/null +++ b/ts/routes/reviewer/reviewer.ts @@ -0,0 +1,7 @@ +import { writable } from "svelte/store" + +export function setupReviewer() { + const html = writable("") + + return {html} +} \ No newline at end of file diff --git a/ts/routes/reviewer/reviewerOuter.svelte b/ts/routes/reviewer/reviewerOuter.svelte new file mode 100644 index 000000000..71a818942 --- /dev/null +++ b/ts/routes/reviewer/reviewerOuter.svelte @@ -0,0 +1,21 @@ + + + +{#if reviewerInfo} + +{/if} From d0d1c519e6d08db52a447166734515b8ad963774 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 22:57:36 +0100 Subject: [PATCH 029/237] Naive reviewer --- ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts | 2 +- ts/routes/reviewer/reviewer.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts index 1729f8c2c..267531d2d 100644 --- a/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts +++ b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts @@ -63,7 +63,7 @@ export function setupBottomBar() { remainingIndex.set(idx); } - globalThis._showQuestion = _showQuestion; + globalThis.showQuestion = _showQuestion; globalThis.showAnswer = _showAnswer; globalThis._updateRemaining = _updateRemaining; diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 866e74d57..d1f2f77f0 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,7 +1,16 @@ import { writable } from "svelte/store" +import { preloadAnswerImages } from "../../reviewer/images" export function setupReviewer() { const html = writable("") + function showQuestion(q, a, bodyclass) { + html.set(q) + preloadAnswerImages(a) + } + + globalThis._showAnswer = html.set + globalThis._showQuestion = showQuestion + return {html} } \ No newline at end of file From f35b2cf5d219ecc1d3413ea6ac874542f4f27442 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 23:07:04 +0100 Subject: [PATCH 030/237] bodyclass --- ts/routes/reviewer/reviewer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index d1f2f77f0..48d35f7ba 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -6,6 +6,7 @@ export function setupReviewer() { function showQuestion(q, a, bodyclass) { html.set(q) + document.body.className = bodyclass preloadAnswerImages(a) } From 0acc8d14e0227a8daf1fd016875385d910654177 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 23:14:19 +0100 Subject: [PATCH 031/237] Fix: Card style --- ts/routes/reviewer/reviewer.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/reviewer.svelte index 85124fb0a..d01486806 100644 --- a/ts/routes/reviewer/reviewer.svelte +++ b/ts/routes/reviewer/reviewer.svelte @@ -4,12 +4,12 @@ export let html: Writable -
+
{@html $html}
From a08bca267353cc78dbb3ed25be274b8fe2517fdc Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 23:24:45 +0100 Subject: [PATCH 032/237] dumb initial _showQuestion call --- qt/aqt/reviewer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 1108b9e8f..d51c7c32c 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1274,6 +1274,7 @@ class SvelteReviewer(Reviewer): def _linkHandler(self, url: str) -> None: if url == "bottomReady": + self._showQuestion() self._remaining() return super()._linkHandler(url) From 45793b3b64763b44522aca59562c9517818805a9 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 23:36:29 +0100 Subject: [PATCH 033/237] Added: Todo --- ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts index 267531d2d..bb44dad2b 100644 --- a/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts +++ b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts @@ -76,6 +76,10 @@ export function setupBottomBar() { 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 { From 2e0a75ed83658ab464b0c2f7ba33d6c4422d9368 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 23:38:28 +0100 Subject: [PATCH 034/237] fix: "card" class included bottombar --- ts/routes/reviewer/reviewer.svelte | 3 ++- ts/routes/reviewer/reviewer.ts | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/reviewer.svelte index d01486806..f4fdd37af 100644 --- a/ts/routes/reviewer/reviewer.svelte +++ b/ts/routes/reviewer/reviewer.svelte @@ -2,9 +2,10 @@ import type { Writable } from "svelte/store"; export let html: Writable + export let cardClass: Writable -
+
{@html $html}
diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 48d35f7ba..d7d1fa35f 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -3,15 +3,16 @@ import { preloadAnswerImages } from "../../reviewer/images" export function setupReviewer() { const html = writable("") + const cardClass = writable("") - function showQuestion(q, a, bodyclass) { + function showQuestion(q, a, cc) { html.set(q) - document.body.className = bodyclass + cardClass.set(cc) preloadAnswerImages(a) } globalThis._showAnswer = html.set globalThis._showQuestion = showQuestion - return {html} + return {html, cardClass} } \ No newline at end of file From d775efcb0627db7e3c941ed87359231b059bacaa Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 3 Sep 2025 23:40:37 +0100 Subject: [PATCH 035/237] hide bottomweb --- qt/aqt/reviewer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index d51c7c32c..ccbb43773 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1288,6 +1288,7 @@ class SvelteReviewer(Reviewer): 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 From c7fd7a096579f0d8e0996940cb158b6e2c7107d5 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 4 Sep 2025 00:25:22 +0100 Subject: [PATCH 036/237] re-add reviewer-bottom to build script --- build/configure/src/aqt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index 183b7f0b3..83be77e91 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -170,7 +170,7 @@ fn build_imgs(build: &mut Build) -> Result<()> { } fn build_js(build: &mut Build) -> Result<()> { - for ts_file in &["deckbrowser", "webview", "toolbar"] { + for ts_file in &["deckbrowser", "webview", "toolbar", "reviewer-bottom"] { build.add_action( "qt:aqt:data:web:js", EsbuildScript { From 1e67a773c6e21dc9938d9e148f02452b94cbf8be Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 4 Sep 2025 00:26:10 +0100 Subject: [PATCH 037/237] ./check --- .../ReviewerBottomOuter.svelte | 2 +- .../reviewer-bottom/reviewer-bottom.ts | 6 ++--- ts/routes/reviewer/reviewer.svelte | 6 ++--- ts/routes/reviewer/reviewer.ts | 22 +++++++++---------- ts/routes/reviewer/reviewerOuter.svelte | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte index 5c66485e0..59747cc5d 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte @@ -11,7 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let reviewerInfo: null | ReturnType = null; onMount(() => { - reviewerInfo = setupBottomBar() + reviewerInfo = setupBottomBar(); }); diff --git a/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts index bb44dad2b..deedbd4bd 100644 --- a/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts +++ b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts @@ -1,6 +1,6 @@ +import { bridgeCommand } from "@tslib/bridgecommand"; import { writable } from "svelte/store"; import type { AnswerButtonInfo } from "./types"; -import { bridgeCommand } from "@tslib/bridgecommand"; export function setupBottomBar() { /* @@ -77,7 +77,7 @@ export function setupBottomBar() { } */ - // TODO This should probably be a "ready" command now that it is part of the actual reviewer, + // 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"); @@ -87,4 +87,4 @@ export function setupBottomBar() { remaining, remainingIndex, }; -} \ No newline at end of file +} diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/reviewer.svelte index f4fdd37af..aee74e922 100644 --- a/ts/routes/reviewer/reviewer.svelte +++ b/ts/routes/reviewer/reviewer.svelte @@ -1,8 +1,8 @@
@@ -11,6 +11,6 @@ diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index d7d1fa35f..c2f4ea781 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,18 +1,18 @@ -import { writable } from "svelte/store" -import { preloadAnswerImages } from "../../reviewer/images" +import { writable } from "svelte/store"; +import { preloadAnswerImages } from "../../reviewer/images"; export function setupReviewer() { - const html = writable("") - const cardClass = writable("") + const html = writable(""); + const cardClass = writable(""); function showQuestion(q, a, cc) { - html.set(q) - cardClass.set(cc) - preloadAnswerImages(a) + html.set(q); + cardClass.set(cc); + preloadAnswerImages(a); } - globalThis._showAnswer = html.set - globalThis._showQuestion = showQuestion + globalThis._showAnswer = html.set; + globalThis._showQuestion = showQuestion; - return {html, cardClass} -} \ No newline at end of file + return { html, cardClass }; +} diff --git a/ts/routes/reviewer/reviewerOuter.svelte b/ts/routes/reviewer/reviewerOuter.svelte index 71a818942..c261675f7 100644 --- a/ts/routes/reviewer/reviewerOuter.svelte +++ b/ts/routes/reviewer/reviewerOuter.svelte @@ -7,12 +7,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { setupReviewer } from "./reviewer"; import Reviewer from "./reviewer.svelte"; - import "../../reviewer/reviewer.scss" + import "../../reviewer/reviewer.scss"; let reviewerInfo: null | ReturnType = null; onMount(() => { - reviewerInfo = setupReviewer() + reviewerInfo = setupReviewer(); }); From da90e3c718cfda966e9aab7b3a1814b09348e993 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 22 Sep 2025 01:23:44 +0100 Subject: [PATCH 038/237] iframe poc --- ts/routes/reviewer/reviewer.svelte | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/reviewer.svelte index aee74e922..0cd281607 100644 --- a/ts/routes/reviewer/reviewer.svelte +++ b/ts/routes/reviewer/reviewer.svelte @@ -1,16 +1,40 @@
- {@html $html} +
From f68f5284a564b7787262f66d6b9ec23878e83623 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 22 Sep 2025 02:26:03 +0100 Subject: [PATCH 039/237] Fix: showQuestion _showAnswerButton --- qt/aqt/reviewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index ccbb43773..65ebbbbde 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1270,7 +1270,7 @@ class SvelteReviewer(Reviewer): else: maxTime = 0 self._remaining() - self.bottom.web.eval("_showQuestion(%s,%d);" % ("", maxTime)) + self.bottom.web.eval("showQuestion(\"\",%d);" % (maxTime)) def _linkHandler(self, url: str) -> None: if url == "bottomReady": From ccdc391097c57f873bef80e618e8f175e4bb8132 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Sun, 28 Sep 2025 00:43:02 +0100 Subject: [PATCH 040/237] Remove html writable --- qt/aqt/reviewer.py | 2 +- ts/routes/reviewer/+page.svelte | 4 ++-- ts/routes/reviewer/reviewer.svelte | 22 ++----------------- ts/routes/reviewer/reviewer.ts | 29 ++++++++++++++++++++----- ts/routes/reviewer/reviewerOuter.svelte | 21 ------------------ 5 files changed, 29 insertions(+), 49 deletions(-) delete mode 100644 ts/routes/reviewer/reviewerOuter.svelte diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 65ebbbbde..dfe00eff8 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1270,7 +1270,7 @@ class SvelteReviewer(Reviewer): else: maxTime = 0 self._remaining() - self.bottom.web.eval("showQuestion(\"\",%d);" % (maxTime)) + self.bottom.web.eval('showQuestion("",%d);' % (maxTime)) def _linkHandler(self, url: str) -> None: if url == "bottomReady": diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte index 2baa1b1f6..d89a8dc6c 100644 --- a/ts/routes/reviewer/+page.svelte +++ b/ts/routes/reviewer/+page.svelte @@ -1,10 +1,10 @@
- +
diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/reviewer.svelte index 0cd281607..921fd2248 100644 --- a/ts/routes/reviewer/reviewer.svelte +++ b/ts/routes/reviewer/reviewer.svelte @@ -1,27 +1,9 @@
diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index c2f4ea781..34288b1e3 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,18 +1,37 @@ import { writable } from "svelte/store"; +import { isNightMode } from "../../html-filter/helpers"; import { preloadAnswerImages } from "../../reviewer/images"; -export function setupReviewer() { - const html = writable(""); +export function setupReviewer(iframe: HTMLIFrameElement) { const cardClass = writable(""); + function updateHtml(htmlString) { + if (iframe?.contentDocument) { + const nightMode = isNightMode(); + iframe.contentDocument.body.innerHTML = htmlString; + iframe.contentDocument.head.innerHTML = document.head.innerHTML; + iframe.contentDocument.body.className = nightMode + ? "nightMode card" + : "card"; + const root = iframe.contentDocument.querySelector("html")!; + root.className = nightMode + ? "night-mode" + : ""; + root.setAttribute("data-bs-theme", nightMode ? "dark" : "light"); + // @ts-ignore + iframe.contentDocument.pycmd = bridgeCommand; + } + } + function showQuestion(q, a, cc) { - html.set(q); + updateHtml(q); + // html.set(q); cardClass.set(cc); preloadAnswerImages(a); } - globalThis._showAnswer = html.set; + globalThis._showAnswer = updateHtml; globalThis._showQuestion = showQuestion; - return { html, cardClass }; + return { cardClass }; } diff --git a/ts/routes/reviewer/reviewerOuter.svelte b/ts/routes/reviewer/reviewerOuter.svelte deleted file mode 100644 index c261675f7..000000000 --- a/ts/routes/reviewer/reviewerOuter.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - -{#if reviewerInfo} - -{/if} From 5998aa913d1fa42cec7ea40f61bdc67f937b22d8 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Sun, 28 Sep 2025 00:44:37 +0100 Subject: [PATCH 041/237] reviewer.svelte -> Reviewer.svelte --- ts/routes/reviewer/+page.svelte | 2 +- ts/routes/reviewer/{reviewer.svelte => Reviewer.svelte} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename ts/routes/reviewer/{reviewer.svelte => Reviewer.svelte} (100%) diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte index d89a8dc6c..4849370c7 100644 --- a/ts/routes/reviewer/+page.svelte +++ b/ts/routes/reviewer/+page.svelte @@ -1,6 +1,6 @@
diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte similarity index 100% rename from ts/routes/reviewer/reviewer.svelte rename to ts/routes/reviewer/Reviewer.svelte From 654701da4d860301923f788ad5c525e783aac080 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Sun, 28 Sep 2025 01:47:30 +0100 Subject: [PATCH 042/237] reviewer-inner poc --- qt/aqt/mediasrv.py | 1 + ts/routes/reviewer-inner/+page.svelte | 30 +++++++++++++++++ ts/routes/reviewer/Reviewer.svelte | 7 +++- .../reviewer-bottom/reviewer-bottom.ts | 5 --- ts/routes/reviewer/reviewer.ts | 33 ++++++++++--------- 5 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 ts/routes/reviewer-inner/+page.svelte diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 83f45c933..eda971121 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -335,6 +335,7 @@ def is_sveltekit_page(path: str) -> bool: "import-page", "image-occlusion", "reviewer", + "reviewer-inner", ] diff --git a/ts/routes/reviewer-inner/+page.svelte b/ts/routes/reviewer-inner/+page.svelte new file mode 100644 index 000000000..060424096 --- /dev/null +++ b/ts/routes/reviewer-inner/+page.svelte @@ -0,0 +1,30 @@ + diff --git a/ts/routes/reviewer/Reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte index 921fd2248..4383a2227 100644 --- a/ts/routes/reviewer/Reviewer.svelte +++ b/ts/routes/reviewer/Reviewer.svelte @@ -7,7 +7,12 @@
- +
\ No newline at end of file diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte index 9dd6160b7..f6360bdbe 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -28,28 +28,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
- - - {$remaining[0]} - + - - {$remaining[1]} - + - - {$remaining[2]} - - -
- {#if $answerButtons.length} - {#each $answerButtons as answerButton} - - {/each} - {:else} - - {/if} -
+ {#if $answerButtons.length} + {#each $answerButtons as answerButton} + + {/each} + {:else} + + + {$remaining[0]} + + + + {$remaining[1]} + + + + {$remaining[2]} + + + + {/if}
{/if}
-
+
-
- {#if $answerButtons.length} - {#each $answerButtons as answerButton} - - {/each} - {:else} - - - {$remaining[0]} - + - - {$remaining[1]} - + - - {$remaining[2]} - - - - {/if} -
-
+ {#if answerShown} + {#each $answerButtons as answerButton} + + {/each} + {:else} + + + {$remaining[0]} + + + + {$remaining[1]} + + + + {$remaining[2]} + + + + {/if} + +
diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte index a2224d2a0..8f978a5cd 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -8,17 +8,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import AnswerButton from "./AnswerButton.svelte"; import { bridgeCommand } from "@tslib/bridgecommand"; import * as tr from "@generated/ftl"; - import RemainingNumber from "./RemainingNumber.svelte"; import type { ReviewerState } from "../reviewer"; - import { writable } from "svelte/store"; + import Remaining from "./Remaining.svelte"; export let state: ReviewerState; const answerButtons = state.answerButtons; const answerShown = state.answerShown; - // Placeholders - const remaining = writable([0, 0, 0]); - const remainingIndex = writable(0); $: button_count = $answerShown ? $answerButtons.length : 1; @@ -39,17 +35,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/each} {:else} - - - {$remaining[0]} - + - - {$remaining[1]} - + - - {$remaining[2]} - - + @@ -78,10 +64,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html grid-auto-flow: column; } - .remaining-count { - text-align: center; - } - .more, .edit { width: 100%; diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index f71f30347..8bee7a23a 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,20 +1,17 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import { - CardAnswer, - type NextCardDataResponse_AnswerButton, - type NextCardDataResponse_NextCardData, -} from "@generated/anki/scheduler_pb"; +import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; import { nextCardData } from "@generated/backend"; -import { writable } from "svelte/store"; +import { derived, writable } from "svelte/store"; export class ReviewerState { answerHtml = ""; - cardData: NextCardDataResponse_NextCardData | undefined = undefined; + _cardData: NextCardDataResponse_NextCardData | undefined = undefined; beginAnsweringMs = Date.now(); readonly cardClass = writable(""); - readonly answerButtons = writable([]); readonly answerShown = writable(false); + readonly cardData = writable(undefined); + readonly answerButtons = derived(this.cardData, ($cardData) => $cardData?.answerButtons ?? []); iframe: HTMLIFrameElement | undefined = undefined; @@ -36,22 +33,25 @@ export class ReviewerState { const resp = await nextCardData({ answer: answer || undefined, }); + // TODO: "Congratulation screen" logic - this.cardData = resp.nextCard; - this.answerButtons.set(this.cardData?.answerButtons ?? []); - const question = resp.nextCard?.front || ""; + this._cardData = resp.nextCard; + this.cardData.set(this._cardData); this.answerShown.set(false); + + const question = resp.nextCard?.front || ""; this.updateHtml(question); + this.beginAnsweringMs = Date.now(); } get currentCard() { - return this.cardData?.queue?.cards[0]; + return this._cardData?.queue?.cards[0]; } public showAnswer() { this.answerShown.set(true); - this.updateHtml(this.cardData?.back || ""); + this.updateHtml(this._cardData?.back || ""); } public easeButtonPressed(rating: number) { From 9550b127d9e290ba191d4c1f8430d574f4a898c1 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 7 Oct 2025 01:10:25 +0100 Subject: [PATCH 067/237] Basic fix for shortcuts --- qt/aqt/reviewer.py | 3 +++ ts/routes/reviewer/Reviewer.svelte | 1 + ts/routes/reviewer/reviewer.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index a7366a258..8a82a9af6 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1298,6 +1298,9 @@ class SvelteReviewer(Reviewer): self.bottom.web = self.web self.mw.bottomWeb.hide() + def _shortcutKeys(self) -> Sequence[tuple[str, Callable] | tuple[Qt.Key, Callable]]: + return [] + # if the last element is a comment, then the RUN_STATE_MUTATION code # breaks due to the comment wrongly commenting out python code. diff --git a/ts/routes/reviewer/Reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte index 4f7bc0536..f136a761f 100644 --- a/ts/routes/reviewer/Reviewer.svelte +++ b/ts/routes/reviewer/Reviewer.svelte @@ -10,6 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $: if (iframe) { state.registerIFrame(iframe); + state.registerShortcuts(); } diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 8bee7a23a..45cdab145 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -25,6 +25,35 @@ export class ReviewerState { iframe.addEventListener("load", this.onReady.bind(this)); } + onKeyDown(e: KeyboardEvent) { + switch (e.key) { + case "1": { + this.easeButtonPressed(0); + break; + } + case "2": { + this.easeButtonPressed(1); + break; + } + case "3": { + this.easeButtonPressed(2); + break; + } + case "4": { + this.easeButtonPressed(3); + break; + } + case " ": { + this.showAnswer(); + break; + } + } + } + + public registerShortcuts() { + document.addEventListener("keydown", this.onKeyDown.bind(this)); + } + updateHtml(htmlString: string) { this.iframe?.contentWindow?.postMessage({ type: "html", value: htmlString }, "*"); } From b80dc25999a2b44bef170572a0183386d898920b Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 7 Oct 2025 01:22:31 +0100 Subject: [PATCH 068/237] Added: shown check for ease function --- ts/routes/reviewer/reviewer.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 45cdab145..04de5a9af 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -2,7 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; import { nextCardData } from "@generated/backend"; -import { derived, writable } from "svelte/store"; +import { derived, get, writable } from "svelte/store"; export class ReviewerState { answerHtml = ""; @@ -44,7 +44,11 @@ export class ReviewerState { break; } case " ": { - this.showAnswer(); + if (!get(this.answerShown)) { + this.showAnswer(); + } else { + this.easeButtonPressed(2); + } break; } } @@ -84,6 +88,10 @@ export class ReviewerState { } public easeButtonPressed(rating: number) { + if (!get(this.answerShown)) { + return; + } + const states = this.currentCard!.states!; const newState = [ From a48dcb0bb5444fe55531cdf2c9a57a7140ed3dde Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 7 Oct 2025 13:11:12 +0100 Subject: [PATCH 069/237] encode_iri_paths --- rslib/src/scheduler/service/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 160ff8ba4..1bcc35b68 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -34,6 +34,7 @@ use crate::scheduler::states::CardState; use crate::scheduler::states::SchedulingStates; use crate::search::SortMode; use crate::stats::studied_today; +use crate::text::encode_iri_paths; impl crate::services::SchedulerService for Collection { /// This behaves like _updateCutoff() in older code - it also unburies at @@ -409,11 +410,17 @@ impl crate::services::SchedulerService for Collection { }) .collect(); + let prepare_card_text_for_display = |html: &str| { + let html = [style.clone(), html.to_string()].concat(); + let html = encode_iri_paths(&html).to_string(); + html + }; + Ok(NextCardDataResponse { next_card: Some(NextCardData { queue: Some(queue.into()), - front: [style.clone(), render.question().to_string()].concat(), - back: [style, render.answer().to_string()].concat(), + front: prepare_card_text_for_display(&render.question()), + back: prepare_card_text_for_display(&render.answer()), answer_buttons, }), From b77ee225e5e1ecfe96972aa343798c363b815bce Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 10 Oct 2025 20:33:22 +0100 Subject: [PATCH 070/237] Fix: Night mode --- ts/routes/reviewer-inner/index.ts | 23 ++++++++--------------- ts/routes/reviewer/+page.svelte | 6 +++++- ts/routes/reviewer/Reviewer.svelte | 4 ++-- ts/routes/reviewer/reviewer.ts | 20 +++++++++++++++++++- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 7bd319339..7480589de 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -2,6 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import "../base.scss"; import "../../reviewer/reviewer.scss"; +import { enableNightMode } from "../reviewer/reviewer"; addEventListener("message", (e) => { switch (e.data.type) { @@ -9,21 +10,6 @@ addEventListener("message", (e) => { document.body.innerHTML = e.data.value; break; } - case "nightMode": { - // This method currently "Flashbangs" the user if they have nightmode on and is a placeholder - // I will probably use #night-mode in the url. - const root = document.querySelector("html")!; - const nightMode = e.data.value; - if (e.data.value) { - root.classList.add("night-mode"); - } else { - root.classList.remove("night-mode"); - } - document.body.className = nightMode ? "nightMode card" : "card"; - root.className = nightMode ? "night-mode" : ""; - root.setAttribute("data-bs-theme", nightMode ? "dark" : "light"); - break; - } default: { console.warn(`Unknown message type: ${e.data.type}`); break; @@ -39,3 +25,10 @@ function pycmd(cmd: string) { window.parent.postMessage({ type: "pycmd", value: cmd }, "*"); } globalThis.pycmd = pycmd; + +const params = new URLSearchParams(location.search); +const theme = params.get("nightMode"); +if (theme !== null) { + enableNightMode(); +} +document.documentElement.classList.add("card"); diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte index 8e037789e..2e9053f07 100644 --- a/ts/routes/reviewer/+page.svelte +++ b/ts/routes/reviewer/+page.svelte @@ -3,11 +3,15 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
diff --git a/ts/routes/reviewer/Reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte index f136a761f..6a71d5d4b 100644 --- a/ts/routes/reviewer/Reviewer.svelte +++ b/ts/routes/reviewer/Reviewer.svelte @@ -3,7 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> From 9085154e3d12976ea4678610e0d24116ec839ad0 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 22 Oct 2025 18:15:16 +0100 Subject: [PATCH 078/237] Added: Placeholder more menu Until I can access ContextMenu.svelte from the other pr. --- .../reviewer/reviewer-bottom/More.svelte | 59 +++++++++++++++++++ .../reviewer/reviewer-bottom/MoreItem.svelte | 14 +++++ .../reviewer-bottom/MoreSubmenu.svelte | 22 +++++++ .../reviewer-bottom/ReviewerBottom.svelte | 8 +-- 4 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 ts/routes/reviewer/reviewer-bottom/More.svelte create mode 100644 ts/routes/reviewer/reviewer-bottom/MoreItem.svelte create mode 100644 ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte new file mode 100644 index 000000000..a1a72611f --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -0,0 +1,59 @@ + + + + + + +
+ + { + showFlags = !showFlags; + }} + > + {tr.studyingFlagCard()} + +
+ {#each flags as flag} + {flag.colour} + {/each} +
+
+
+
+ + diff --git a/ts/routes/reviewer/reviewer-bottom/MoreItem.svelte b/ts/routes/reviewer/reviewer-bottom/MoreItem.svelte new file mode 100644 index 000000000..88a0f4b09 --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/MoreItem.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte b/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte new file mode 100644 index 000000000..f017bfa15 --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte @@ -0,0 +1,22 @@ + + + +
+ (showFloating = false)}> + + + + + + +
diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte index 8f978a5cd..6537df95f 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -10,6 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import * as tr from "@generated/ftl"; import type { ReviewerState } from "../reviewer"; import Remaining from "./Remaining.svelte"; + import More from "./More.svelte"; export let state: ReviewerState; @@ -42,12 +43,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if}
- +
From b2fe07462ac4d7713119eef6fbdaf34e5db91479 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 22 Oct 2025 18:36:42 +0100 Subject: [PATCH 079/237] Added: Context menu flag setting --- qt/aqt/mediasrv.py | 5 ++++- ts/routes/reviewer/reviewer-bottom/More.svelte | 18 +++++++++++++++--- .../reviewer-bottom/MoreSubmenu.svelte | 2 -- .../reviewer-bottom/ReviewerBottom.svelte | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index d69e04610..9a57741de 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -745,6 +745,8 @@ exposed_backend_list = [ # DeckConfigService "get_ignored_before_count", "get_retention_workload", + # CardsService + "set_flag", ] @@ -812,8 +814,9 @@ def _check_dynamic_request_permissions(): "/_anki/setSchedulingStates", "/_anki/i18nResources", "/_anki/congratsInfo", - # TODO: Unsure about this + # TODO: Correctly set the auth token "/_anki/nextCardData", + "/_anki/setFlag", ): pass else: diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index a1a72611f..3ba88f0ad 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -7,11 +7,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import * as tr from "@generated/ftl"; import MoreSubmenu from "./MoreSubmenu.svelte"; import MoreItem from "./MoreItem.svelte"; + import { setFlag } from "@generated/backend"; + import type { ReviewerState } from "../reviewer"; let showFloating = false; let showFlags = false; + export let state: ReviewerState; - let flags = [ + const flags = [ { colour: tr.actionsFlagRed(), shortcut: "Ctrl+1" }, { colour: tr.actionsFlagOrange(), shortcut: "Ctrl+2" }, { colour: tr.actionsFlagGreen(), shortcut: "Ctrl+3" }, @@ -20,6 +23,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html { colour: tr.actionsFlagTurquoise(), shortcut: "Ctrl+6" }, { colour: tr.actionsFlagPurple(), shortcut: "Ctrl+7" }, ]; + + function changeFlag(index: number) { + setFlag({ cardIds: [state.currentCard!.card!.id], flag: index }); + } @@ -44,8 +51,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {tr.studyingFlagCard()}
- {#each flags as flag} - {flag.colour} + {#each flags as flag, i} + changeFlag(i + 1)} + > + {flag.colour} + {/each}
diff --git a/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte b/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte index f017bfa15..463a66b38 100644 --- a/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte +++ b/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte @@ -3,10 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte index 6537df95f..7941ec83b 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -43,7 +43,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if}
- +
From fc880259a81c536108b4fd4332eca04c86a54152 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 24 Oct 2025 09:58:43 +0100 Subject: [PATCH 080/237] Give main webview api access --- qt/aqt/mediasrv.py | 3 --- qt/aqt/webview.py | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 9a57741de..ca8bea681 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -814,9 +814,6 @@ def _check_dynamic_request_permissions(): "/_anki/setSchedulingStates", "/_anki/i18nResources", "/_anki/congratsInfo", - # TODO: Correctly set the auth token - "/_anki/nextCardData", - "/_anki/setFlag", ): pass else: diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 95d84c00e..fefbae17c 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -142,6 +142,7 @@ class AnkiWebPage(QWebEnginePage): AnkiWebViewKind.IMPORT_ANKI_PACKAGE, AnkiWebViewKind.IMPORT_CSV, AnkiWebViewKind.IMPORT_LOG, + AnkiWebViewKind.MAIN, ) global _profile_with_api_access, _profile_without_api_access From 3cf58419d7269c5bd43f0c240f8b21c5e8d0e4e3 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 24 Oct 2025 10:44:31 +0100 Subject: [PATCH 081/237] Fix: More button shrunk slightly --- ts/routes/reviewer/reviewer-bottom/More.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 3ba88f0ad..d6783e2c6 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -68,4 +68,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html div :global(button) { width: fit-content; } + + button { + line-height: 18px; + } From cd5f25469ef9705e5fdf459d063be526258eae57 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 27 Oct 2025 11:21:03 +0000 Subject: [PATCH 082/237] MathJax Problems with font cors (I'm shocked that using the import with the .js worked without a hitch O_O) --- ts/reviewer/index.ts | 2 +- ts/routes/reviewer-inner/index.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/ts/reviewer/index.ts b/ts/reviewer/index.ts index d0370cbc9..63771f0b0 100644 --- a/ts/reviewer/index.ts +++ b/ts/reviewer/index.ts @@ -104,7 +104,7 @@ async function setInnerHTML(element: Element, html: string): Promise { } } -const renderError = (type: string) => (error: unknown): string => { +export const renderError = (type: string) => (error: unknown): string => { const errorMessage = String(error).substring(0, 2000); let errorStack: string; if (error instanceof Error) { diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 5304cb344..091dcf696 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -2,15 +2,18 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import "../base.scss"; import "../../reviewer/reviewer.scss"; +import "mathjax/es5/tex-chtml-full.js"; +import { renderError } from "../../reviewer"; import { enableNightMode } from "../reviewer/reviewer"; import type { InnerReviewerRequest } from "./reviewerRequest"; +declare const MathJax: any; const urlParams = new URLSearchParams(location.search); const style = document.createElement("style"); document.head.appendChild(style); -addEventListener("message", (e: MessageEvent) => { +addEventListener("message", async (e: MessageEvent) => { switch (e.data.type) { case "html": { document.body.innerHTML = e.data.value; @@ -26,6 +29,17 @@ addEventListener("message", (e: MessageEvent) => { document.body.classList.add("nightMode"); } } + + // wait for mathjax to ready + await MathJax.startup.promise + .then(() => { + // clear MathJax buffers from previous typesets + MathJax.typesetClear(); + + return MathJax.typesetPromise([document.body]); + }) + .catch(renderError("MathJax")); + break; } default: { From 385d487220dc1bf8d53e423d52542bb59dce4bac Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 29 Oct 2025 14:16:15 +0000 Subject: [PATCH 083/237] Fix/Missing python import in write_header --- rslib/proto/python.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rslib/proto/python.rs b/rslib/proto/python.rs index a5adb4179..a2aa09707 100644 --- a/rslib/proto/python.rs +++ b/rslib/proto/python.rs @@ -249,6 +249,7 @@ import anki.stats_pb2 import anki.sync_pb2 import anki.tags_pb2 import anki.ankihub_pb2 +import anki.frontend_pb2 class RustBackendGenerated: def _run_command(self, service: int, method: int, input: Any) -> bytes: From 386133743e705c5f258b9b54b71c5b5861b48bd4 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 29 Oct 2025 15:29:38 +0000 Subject: [PATCH 084/237] Added: Playaudio endpoint --- proto/anki/frontend.proto | 8 ++++++++ qt/aqt/mediasrv.py | 11 ++++++++++- qt/aqt/sound.py | 12 ++++++++---- ts/routes/reviewer-inner/index.ts | 6 +++++- ts/routes/reviewer/reviewer.ts | 12 +++++++++++- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 1d733a369..1f6b23101 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -30,6 +30,8 @@ service FrontendService { // Save colour picker's custom colour palette rpc SaveCustomColours(generic.Empty) returns (generic.Empty); + + rpc PlayAudio(PlayAudioRequest) returns (generic.Empty); } service BackendFrontendService {} @@ -43,3 +45,9 @@ message SetSchedulingStatesRequest { string key = 1; scheduler.SchedulingStates states = 2; } + +message PlayAudioRequest { + bool answer_side = 1; + uint32 index = 2; + uint64 cid = 3; +} \ No newline at end of file diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index ca8bea681..c8e145f1d 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -28,9 +28,10 @@ import aqt import aqt.main import aqt.operations from anki import hooks -from anki.cards import Card +from anki.cards import Card, CardId from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode from anki.decks import UpdateDeckConfigs +from anki.frontend_pb2 import PlayAudioRequest from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest from anki.scheduler_pb2 import NextCardDataResponse from anki.template import ( @@ -45,6 +46,7 @@ from aqt.operations import on_op_finished from aqt.operations.deck import update_deck_configs as update_deck_configs_op from aqt.progress import ProgressUpdate from aqt.qt import * +from aqt.sound import play_clicked_audio_with_index from aqt.theme import ThemeManager from aqt.utils import aqt_data_path, show_warning, tr @@ -683,6 +685,12 @@ def next_card_data() -> bytes: return data.SerializeToString() +def play_audio(): + req = PlayAudioRequest.FromString(request.data) + card = aqt.mw.col.get_card(CardId(req.cid)) + play_clicked_audio_with_index(req.index, req.answer_side, card) + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -700,6 +708,7 @@ post_handler_list = [ deck_options_ready, save_custom_colours, next_card_data, + play_audio, ] diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index f54ebd3e8..c7896b34b 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -925,11 +925,15 @@ def play_clicked_audio(pycmd: str, card: Card) -> None: """eg. if pycmd is 'play:q:0', play the first audio on the question side.""" play, context, str_idx = pycmd.split(":") idx = int(str_idx) - if context == "q": - tags = card.question_av_tags() - else: + play_clicked_audio_with_index(idx, context == "q", card) + + +def play_clicked_audio_with_index(index: int, answer_side: bool, card: Card): + if answer_side: tags = card.answer_av_tags() - av_player.play_tags([tags[idx]]) + else: + tags = card.question_av_tags() + av_player.play_tags([tags[index]]) # Init defaults diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 091dcf696..b80ccabbc 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -54,6 +54,10 @@ base.href = "/"; document.head.appendChild(base); function pycmd(cmd: string) { - window.parent.postMessage({ type: "pycmd", value: cmd }, "*"); + const match = cmd.match(/play:(q|a):(\d+)/); + if (match) { + const [_, context, index] = match; + window.parent.postMessage({ type: "audio", answerSide: context == "a", index: parseInt(index) }, "*"); + } } globalThis.pycmd = pycmd; diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 6b466663b..a9e0c2b9d 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; -import { nextCardData } from "@generated/backend"; +import { nextCardData, playAudio } from "@generated/backend"; import { derived, get, writable } from "svelte/store"; import type { InnerReviewerRequest } from "../reviewer-inner/reviewerRequest"; @@ -37,6 +37,16 @@ export class ReviewerState { onReady() { this.showQuestion(null); + addEventListener("message", this.onMessage.bind(this)); + } + + onMessage(e: MessageEvent) { + switch (e.data.type) { + case "audio": { + playAudio({ answerSide: e.data.answerSide, index: e.data.index, cid: this.currentCard!.card!.id }); + break; + } + } } public registerIFrame(iframe: HTMLIFrameElement) { From 246e6f6df630102f630c4c8586d64a721930eb2e Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 29 Oct 2025 15:48:21 +0000 Subject: [PATCH 085/237] Added: "ReviewerRequest" type --- ts/routes/reviewer-inner/index.ts | 8 ++++++-- .../{reviewerRequest.ts => innerReviewerRequest.ts} | 0 ts/routes/reviewer/reviewer.ts | 5 +++-- ts/routes/reviewer/reviewerRequest.ts | 9 +++++++++ 4 files changed, 18 insertions(+), 4 deletions(-) rename ts/routes/reviewer-inner/{reviewerRequest.ts => innerReviewerRequest.ts} (100%) create mode 100644 ts/routes/reviewer/reviewerRequest.ts diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index b80ccabbc..dc32fac87 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -5,7 +5,8 @@ import "../../reviewer/reviewer.scss"; import "mathjax/es5/tex-chtml-full.js"; import { renderError } from "../../reviewer"; import { enableNightMode } from "../reviewer/reviewer"; -import type { InnerReviewerRequest } from "./reviewerRequest"; +import type { ReviewerRequest } from "../reviewer/reviewerRequest"; +import type { InnerReviewerRequest } from "./innerReviewerRequest"; declare const MathJax: any; const urlParams = new URLSearchParams(location.search); @@ -57,7 +58,10 @@ function pycmd(cmd: string) { const match = cmd.match(/play:(q|a):(\d+)/); if (match) { const [_, context, index] = match; - window.parent.postMessage({ type: "audio", answerSide: context == "a", index: parseInt(index) }, "*"); + window.parent.postMessage( + { type: "audio", answerSide: context == "a", index: parseInt(index) } satisfies ReviewerRequest, + "*", + ); } } globalThis.pycmd = pycmd; diff --git a/ts/routes/reviewer-inner/reviewerRequest.ts b/ts/routes/reviewer-inner/innerReviewerRequest.ts similarity index 100% rename from ts/routes/reviewer-inner/reviewerRequest.ts rename to ts/routes/reviewer-inner/innerReviewerRequest.ts diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index a9e0c2b9d..d22f9482d 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -3,7 +3,8 @@ import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; import { nextCardData, playAudio } from "@generated/backend"; import { derived, get, writable } from "svelte/store"; -import type { InnerReviewerRequest } from "../reviewer-inner/reviewerRequest"; +import type { InnerReviewerRequest } from "../reviewer-inner/innerReviewerRequest"; +import type { ReviewerRequest } from "./reviewerRequest"; export function isNightMode() { // https://stackoverflow.com/a/57795518 @@ -40,7 +41,7 @@ export class ReviewerState { addEventListener("message", this.onMessage.bind(this)); } - onMessage(e: MessageEvent) { + onMessage(e: MessageEvent) { switch (e.data.type) { case "audio": { playAudio({ answerSide: e.data.answerSide, index: e.data.index, cid: this.currentCard!.card!.id }); diff --git a/ts/routes/reviewer/reviewerRequest.ts b/ts/routes/reviewer/reviewerRequest.ts new file mode 100644 index 000000000..aa4d3c786 --- /dev/null +++ b/ts/routes/reviewer/reviewerRequest.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 +interface AudioMessage { + type: "audio"; + answerSide: boolean; + index: number; +} + +export type ReviewerRequest = AudioMessage; From 28d41c8a9cd344bfeb09e095f503a72aa8b931a3 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 30 Oct 2025 15:49:01 +0000 Subject: [PATCH 086/237] Revert "Fix/Missing python import in write_header" This reverts commit 385d487220dc1bf8d53e423d52542bb59dce4bac. --- rslib/proto/python.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rslib/proto/python.rs b/rslib/proto/python.rs index a2aa09707..a5adb4179 100644 --- a/rslib/proto/python.rs +++ b/rslib/proto/python.rs @@ -249,7 +249,6 @@ import anki.stats_pb2 import anki.sync_pb2 import anki.tags_pb2 import anki.ankihub_pb2 -import anki.frontend_pb2 class RustBackendGenerated: def _run_command(self, service: int, method: int, input: Any) -> bytes: From 7561bf8d6ab0d759cc0af175c70c3b94097f14dd Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 30 Oct 2025 17:56:02 +0000 Subject: [PATCH 087/237] Added: Placeholder audio autoplay --- proto/anki/frontend.proto | 4 +++- qt/aqt/mediasrv.py | 9 +++++++-- qt/aqt/sound.py | 11 +++-------- ts/routes/reviewer/reviewer.ts | 8 +++++++- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 1f6b23101..03a9ffec6 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -31,6 +31,8 @@ service FrontendService { // Save colour picker's custom colour palette rpc SaveCustomColours(generic.Empty) returns (generic.Empty); + // Plays an audio tag at an index in a specific card + // If the index is blank, plays all audio on that side rpc PlayAudio(PlayAudioRequest) returns (generic.Empty); } @@ -48,6 +50,6 @@ message SetSchedulingStatesRequest { message PlayAudioRequest { bool answer_side = 1; - uint32 index = 2; + optional uint32 index = 2; uint64 cid = 3; } \ No newline at end of file diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index c8e145f1d..3657379d1 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -46,7 +46,7 @@ from aqt.operations import on_op_finished from aqt.operations.deck import update_deck_configs as update_deck_configs_op from aqt.progress import ProgressUpdate from aqt.qt import * -from aqt.sound import play_clicked_audio_with_index +from aqt.sound import play_tags from aqt.theme import ThemeManager from aqt.utils import aqt_data_path, show_warning, tr @@ -688,7 +688,12 @@ def next_card_data() -> bytes: def play_audio(): req = PlayAudioRequest.FromString(request.data) card = aqt.mw.col.get_card(CardId(req.cid)) - play_clicked_audio_with_index(req.index, req.answer_side, card) + # TODO: Pass tags with next_card_data rather than rendering the card here. + tags = card.answer_av_tags() if req.answer_side else card.question_av_tags() + if req.index is None: + play_tags(tags) + else: + play_tags([tags[req.index]]) post_handler_list = [ diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index c7896b34b..2a2169079 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -925,16 +925,11 @@ def play_clicked_audio(pycmd: str, card: Card) -> None: """eg. if pycmd is 'play:q:0', play the first audio on the question side.""" play, context, str_idx = pycmd.split(":") idx = int(str_idx) - play_clicked_audio_with_index(idx, context == "q", card) + tags = card.question_av_tags() if context == "q" else card.answer_av_tags() + play_tags([tags[idx]]) -def play_clicked_audio_with_index(index: int, answer_side: bool, card: Card): - if answer_side: - tags = card.answer_av_tags() - else: - tags = card.question_av_tags() - av_player.play_tags([tags[index]]) - +play_tags = av_player.play_tags # Init defaults ########################################################################## diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index d22f9482d..2c76ba9b9 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -41,10 +41,14 @@ export class ReviewerState { addEventListener("message", this.onMessage.bind(this)); } + playAudio(answerSide: boolean, index?: number) { + playAudio({ answerSide, index, cid: this.currentCard!.card!.id }); + } + onMessage(e: MessageEvent) { switch (e.data.type) { case "audio": { - playAudio({ answerSide: e.data.answerSide, index: e.data.index, cid: this.currentCard!.card!.id }); + this.playAudio(e.data.answerSide, e.data.index); break; } } @@ -108,6 +112,7 @@ export class ReviewerState { const question = resp.nextCard?.front || ""; this.updateHtml(question, resp?.nextCard?.css, resp?.nextCard?.bodyClass); + this.playAudio(false) this.beginAnsweringMs = Date.now(); } @@ -118,6 +123,7 @@ export class ReviewerState { public showAnswer() { this.answerShown.set(true); + this.playAudio(true) this.updateHtml(this._cardData?.back || ""); } From fc8a2f97397ad9b9e0181b3a83e6aee454c51195 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 30 Oct 2025 18:00:09 +0000 Subject: [PATCH 088/237] Fix: Hide iframe until loaded to prevent flash --- ts/routes/reviewer/Reviewer.svelte | 1 + ts/routes/reviewer/reviewer.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/ts/routes/reviewer/Reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte index 6a71d5d4b..7c958ecea 100644 --- a/ts/routes/reviewer/Reviewer.svelte +++ b/ts/routes/reviewer/Reviewer.svelte @@ -32,5 +32,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html iframe { width: 100%; height: 100%; + visibility: hidden; } diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 2c76ba9b9..e69a786a1 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -37,6 +37,7 @@ export class ReviewerState { iframe: HTMLIFrameElement | undefined = undefined; onReady() { + this.iframe!.style.visibility = "visible"; this.showQuestion(null); addEventListener("message", this.onMessage.bind(this)); } From 112f951a13356002c316dbb8d6613bf99b96db9a Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 30 Oct 2025 18:11:41 +0000 Subject: [PATCH 089/237] Fix: use HasField --- qt/aqt/mediasrv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 3657379d1..2ed59ffd5 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -690,10 +690,10 @@ def play_audio(): card = aqt.mw.col.get_card(CardId(req.cid)) # TODO: Pass tags with next_card_data rather than rendering the card here. tags = card.answer_av_tags() if req.answer_side else card.question_av_tags() - if req.index is None: - play_tags(tags) - else: + if req.HasField("index"): play_tags([tags[req.index]]) + else: + play_tags(tags) post_handler_list = [ From 8da0491ae5accb5bd7e1fbbf5aa9fc5495d106ce Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 08:28:39 +0000 Subject: [PATCH 090/237] Refactor PlayAudio --- proto/anki/frontend.proto | 8 +++----- proto/anki/scheduler.proto | 7 +++++-- qt/aqt/mediasrv.py | 22 ++++++++++++---------- rslib/src/scheduler/service/mod.rs | 4 +++- ts/routes/reviewer/reviewer.ts | 11 ++++------- 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 03a9ffec6..bd4c3d34b 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -10,6 +10,7 @@ package anki.frontend; import "anki/scheduler.proto"; import "anki/generic.proto"; import "anki/search.proto"; +import "anki/card_rendering.proto"; service FrontendService { // Returns values from the reviewer @@ -31,8 +32,7 @@ service FrontendService { // Save colour picker's custom colour palette rpc SaveCustomColours(generic.Empty) returns (generic.Empty); - // Plays an audio tag at an index in a specific card - // If the index is blank, plays all audio on that side + // Plays the listed audio tags rpc PlayAudio(PlayAudioRequest) returns (generic.Empty); } @@ -49,7 +49,5 @@ message SetSchedulingStatesRequest { } message PlayAudioRequest { - bool answer_side = 1; - optional uint32 index = 2; - uint64 cid = 3; + repeated card_rendering.AVTag tags = 1; } \ No newline at end of file diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index e9ee3b89f..14f5b184d 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -306,13 +306,16 @@ message NextCardDataResponse { string css = 5; string body_class = 6; + repeated card_rendering.AVTag question_av_tags = 7; + repeated card_rendering.AVTag answer_av_tags = 8; + // TODO: We can probably make this a little faster by using oneof and // preventing the partial_front and back being sent to svelte where it isn't // used. Alternatively we can use a completely different message for both // Rust -> Python and the Python -> Svelte though this would be more // complicated to implement. - repeated card_rendering.RenderedTemplateNode partial_front = 7; - repeated card_rendering.RenderedTemplateNode partial_back = 8; + repeated card_rendering.RenderedTemplateNode partial_front = 9; + repeated card_rendering.RenderedTemplateNode partial_back = 10; } optional NextCardData next_card = 1; diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 2ed59ffd5..f3f5d716f 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -28,7 +28,7 @@ import aqt import aqt.main import aqt.operations from anki import hooks -from anki.cards import Card, CardId +from anki.cards import Card from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode from anki.decks import UpdateDeckConfigs from anki.frontend_pb2 import PlayAudioRequest @@ -38,6 +38,7 @@ from anki.template import ( PartiallyRenderedCard, TemplateRenderContext, apply_custom_filters, + av_tags_to_native, ) from anki.utils import dev_mode from aqt.changenotetype import ChangeNotetypeDialog @@ -670,8 +671,15 @@ def next_card_data() -> bytes: qside, ) - qside = ctx.col()._backend.extract_av_tags(text=qside, question_side=True).text - aside = ctx.col()._backend.extract_av_tags(text=aside, question_side=False).text + q_avtags = ctx.col()._backend.extract_av_tags(text=qside, question_side=True) + a_avtags = ctx.col()._backend.extract_av_tags(text=aside, question_side=False) + + # Assumes the av tags are empty in the original response + data.next_card.question_av_tags.extend(q_avtags.av_tags) + data.next_card.answer_av_tags.extend(a_avtags.av_tags) + + qside = q_avtags.text + aside = a_avtags.text qside = aqt.mw.prepare_card_text_for_display(qside) aside = aqt.mw.prepare_card_text_for_display(aside) @@ -687,13 +695,7 @@ def next_card_data() -> bytes: def play_audio(): req = PlayAudioRequest.FromString(request.data) - card = aqt.mw.col.get_card(CardId(req.cid)) - # TODO: Pass tags with next_card_data rather than rendering the card here. - tags = card.answer_av_tags() if req.answer_side else card.question_av_tags() - if req.HasField("index"): - play_tags([tags[req.index]]) - else: - play_tags(tags) + play_tags(av_tags_to_native(req.tags)) post_handler_list = [ diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index d9cd5a009..a49f31d4f 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -413,11 +413,13 @@ impl crate::services::SchedulerService for Collection { next_card: Some(NextCardData { queue: Some(queue.into()), - css: render.css, + css: render.css.clone(), // Filled by python front: "".to_string(), back: "".to_string(), body_class: "".to_string(), + question_av_tags: vec![], + answer_av_tags: vec![], partial_front: rendered_nodes_to_proto(render.qnodes), partial_back: rendered_nodes_to_proto(render.anodes), diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index e69a786a1..0e1c8ec51 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -42,14 +42,11 @@ export class ReviewerState { addEventListener("message", this.onMessage.bind(this)); } - playAudio(answerSide: boolean, index?: number) { - playAudio({ answerSide, index, cid: this.currentCard!.card!.id }); - } - onMessage(e: MessageEvent) { switch (e.data.type) { case "audio": { - this.playAudio(e.data.answerSide, e.data.index); + const tags = get(this.answerShown) ? this._cardData!.answerAvTags : this._cardData!.questionAvTags; + playAudio({ tags: [tags[e.data.index]] }); break; } } @@ -113,7 +110,7 @@ export class ReviewerState { const question = resp.nextCard?.front || ""; this.updateHtml(question, resp?.nextCard?.css, resp?.nextCard?.bodyClass); - this.playAudio(false) + playAudio({ tags: this._cardData!.questionAvTags }); this.beginAnsweringMs = Date.now(); } @@ -124,7 +121,7 @@ export class ReviewerState { public showAnswer() { this.answerShown.set(true); - this.playAudio(true) + playAudio({ tags: this._cardData!.answerAvTags }); this.updateHtml(this._cardData?.back || ""); } From d47cb1bd55bbbae8608daf38bce7b2f4f733c6a4 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 08:36:20 +0000 Subject: [PATCH 091/237] PlayAudio -> PlayAVTags --- proto/anki/frontend.proto | 6 +++--- qt/aqt/mediasrv.py | 8 ++++---- ts/routes/reviewer/reviewer.ts | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index bd4c3d34b..358edfda6 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -32,8 +32,8 @@ service FrontendService { // Save colour picker's custom colour palette rpc SaveCustomColours(generic.Empty) returns (generic.Empty); - // Plays the listed audio tags - rpc PlayAudio(PlayAudioRequest) returns (generic.Empty); + // Plays the listed AV tags + rpc PlayAVTags(PlayAVTagsRequest) returns (generic.Empty); } service BackendFrontendService {} @@ -48,6 +48,6 @@ message SetSchedulingStatesRequest { scheduler.SchedulingStates states = 2; } -message PlayAudioRequest { +message PlayAVTagsRequest { repeated card_rendering.AVTag tags = 1; } \ No newline at end of file diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index f3f5d716f..898a05b25 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -31,7 +31,7 @@ from anki import hooks from anki.cards import Card from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode from anki.decks import UpdateDeckConfigs -from anki.frontend_pb2 import PlayAudioRequest +from anki.frontend_pb2 import PlayAVTagsRequest from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest from anki.scheduler_pb2 import NextCardDataResponse from anki.template import ( @@ -693,8 +693,8 @@ def next_card_data() -> bytes: return data.SerializeToString() -def play_audio(): - req = PlayAudioRequest.FromString(request.data) +def play_avtags(): + req = PlayAVTagsRequest.FromString(request.data) play_tags(av_tags_to_native(req.tags)) @@ -715,7 +715,7 @@ post_handler_list = [ deck_options_ready, save_custom_colours, next_card_data, - play_audio, + play_avtags, ] diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 0e1c8ec51..cee3b2038 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; -import { nextCardData, playAudio } from "@generated/backend"; +import { nextCardData, playAvtags } from "@generated/backend"; import { derived, get, writable } from "svelte/store"; import type { InnerReviewerRequest } from "../reviewer-inner/innerReviewerRequest"; import type { ReviewerRequest } from "./reviewerRequest"; @@ -46,7 +46,7 @@ export class ReviewerState { switch (e.data.type) { case "audio": { const tags = get(this.answerShown) ? this._cardData!.answerAvTags : this._cardData!.questionAvTags; - playAudio({ tags: [tags[e.data.index]] }); + playAvtags({ tags: [tags[e.data.index]] }); break; } } @@ -110,7 +110,7 @@ export class ReviewerState { const question = resp.nextCard?.front || ""; this.updateHtml(question, resp?.nextCard?.css, resp?.nextCard?.bodyClass); - playAudio({ tags: this._cardData!.questionAvTags }); + playAvtags({ tags: this._cardData!.questionAvTags }); this.beginAnsweringMs = Date.now(); } @@ -121,7 +121,7 @@ export class ReviewerState { public showAnswer() { this.answerShown.set(true); - playAudio({ tags: this._cardData!.answerAvTags }); + playAvtags({ tags: this._cardData!.answerAvTags }); this.updateHtml(this._cardData?.back || ""); } From cf9c265570a4ac8345e3617a63e4219512b90467 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 09:01:29 +0000 Subject: [PATCH 092/237] Fix: Respect autoplay setting --- proto/anki/scheduler.proto | 9 +++++---- rslib/src/scheduler/service/mod.rs | 13 ++++++++----- ts/routes/reviewer/reviewer.ts | 8 ++++++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 14f5b184d..3588c4117 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -305,17 +305,18 @@ message NextCardDataResponse { string back = 4; string css = 5; string body_class = 6; + bool autoplay = 7; - repeated card_rendering.AVTag question_av_tags = 7; - repeated card_rendering.AVTag answer_av_tags = 8; + repeated card_rendering.AVTag question_av_tags = 8; + repeated card_rendering.AVTag answer_av_tags = 9; // TODO: We can probably make this a little faster by using oneof and // preventing the partial_front and back being sent to svelte where it isn't // used. Alternatively we can use a completely different message for both // Rust -> Python and the Python -> Svelte though this would be more // complicated to implement. - repeated card_rendering.RenderedTemplateNode partial_front = 9; - repeated card_rendering.RenderedTemplateNode partial_back = 10; + repeated card_rendering.RenderedTemplateNode partial_front = 10; + repeated card_rendering.RenderedTemplateNode partial_back = 11; } optional NextCardData next_card = 1; diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index a49f31d4f..addbc4c87 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -409,22 +409,25 @@ impl crate::services::SchedulerService for Collection { }) .collect(); + let config = self.deck_config_for_card(&next_card.card)?; + Ok(NextCardDataResponse { next_card: Some(NextCardData { queue: Some(queue.into()), css: render.css.clone(), + partial_front: rendered_nodes_to_proto(render.qnodes), + partial_back: rendered_nodes_to_proto(render.anodes), + + answer_buttons, + autoplay: !config.inner.disable_autoplay, + // Filled by python front: "".to_string(), back: "".to_string(), body_class: "".to_string(), question_av_tags: vec![], answer_av_tags: vec![], - - partial_front: rendered_nodes_to_proto(render.qnodes), - partial_back: rendered_nodes_to_proto(render.anodes), - - answer_buttons, }), }) } else { diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index cee3b2038..cac8429fd 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -110,7 +110,9 @@ export class ReviewerState { const question = resp.nextCard?.front || ""; this.updateHtml(question, resp?.nextCard?.css, resp?.nextCard?.bodyClass); - playAvtags({ tags: this._cardData!.questionAvTags }); + if (this._cardData?.autoplay) { + playAvtags({ tags: this._cardData!.questionAvTags }); + } this.beginAnsweringMs = Date.now(); } @@ -121,7 +123,9 @@ export class ReviewerState { public showAnswer() { this.answerShown.set(true); - playAvtags({ tags: this._cardData!.answerAvTags }); + if (this._cardData?.autoplay) { + playAvtags({ tags: this._cardData!.answerAvTags }); + } this.updateHtml(this._cardData?.back || ""); } From bbf575e491d3e620b0b1a85304f858269bdbd0fe Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 12:06:58 +0000 Subject: [PATCH 093/237] Added: TypeAnswer replacement --- proto/anki/scheduler.proto | 1 + rslib/src/scheduler/service/mod.rs | 39 ++++++++++++++++++++++++++- ts/routes/reviewer/reviewer.ts | 6 ++++- ts/routes/reviewer/reviewerRequest.ts | 7 ++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 3588c4117..2cf0108f8 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -306,6 +306,7 @@ message NextCardDataResponse { string css = 5; string body_class = 6; bool autoplay = 7; + optional string typed_answer = 12; repeated card_rendering.AVTag question_av_tags = 8; repeated card_rendering.AVTag answer_av_tags = 9; diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index addbc4c87..5ba32acb9 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -4,6 +4,8 @@ mod answering; mod states; +use std::sync::LazyLock; + use anki_proto::cards; use anki_proto::generic; use anki_proto::scheduler; @@ -25,6 +27,7 @@ use fsrs::ComputeParametersInput; use fsrs::FSRSItem; use fsrs::FSRSReview; use fsrs::FSRS; +use regex::Regex; use crate::backend::Backend; use crate::card_rendering::service::rendered_nodes_to_proto; @@ -34,7 +37,9 @@ use crate::scheduler::new::NewCardDueOrder; use crate::scheduler::states::CardState; use crate::scheduler::states::SchedulingStates; use crate::search::SortMode; +use crate::services::NotesService; use crate::stats::studied_today; +use crate::template::RenderedNode; impl crate::services::SchedulerService for Collection { /// This behaves like _updateCutoff() in older code - it also unburies at @@ -411,16 +416,48 @@ impl crate::services::SchedulerService for Collection { let config = self.deck_config_for_card(&next_card.card)?; + // Typed answer replacements + static ANSWER_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"\[\[type:(.+?:)?(.+?)\]\]").unwrap()); + + const ANSWER_HTML: &str = "
+ +
"; + + let mut q_nodes = render.qnodes; + let typed_answer_parent_node = q_nodes.iter_mut().find_map(|node| { + if let RenderedNode::Text { text } = node { + let mut out = None; + *text = ANSWER_REGEX + .replace(text, |cap: ®ex::Captures<'_>| { + out = Some(cap[2].to_string()); + ANSWER_HTML + }) + .to_string(); + out + } else { + None + } + }); + + let typed_answer = typed_answer_parent_node.map(|field| { + let note = self.get_note(next_card.card.note_id.into()).unwrap(); + let notetype = self.get_notetype(note.notetype_id.into()).unwrap().unwrap(); + note.fields[notetype.get_field_ord(&field).unwrap()].clone() + }); + Ok(NextCardDataResponse { next_card: Some(NextCardData { queue: Some(queue.into()), css: render.css.clone(), - partial_front: rendered_nodes_to_proto(render.qnodes), + partial_front: rendered_nodes_to_proto(q_nodes), partial_back: rendered_nodes_to_proto(render.anodes), answer_buttons, autoplay: !config.inner.disable_autoplay, + typed_answer, // Filled by python front: "".to_string(), diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index cac8429fd..40149c457 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -27,6 +27,7 @@ export function updateNightMode() { export class ReviewerState { answerHtml = ""; + currentTypedAnswer = ""; _cardData: NextCardDataResponse_NextCardData | undefined = undefined; beginAnsweringMs = Date.now(); readonly cardClass = writable(""); @@ -42,13 +43,16 @@ export class ReviewerState { addEventListener("message", this.onMessage.bind(this)); } - onMessage(e: MessageEvent) { + async onMessage(e: MessageEvent) { switch (e.data.type) { case "audio": { const tags = get(this.answerShown) ? this._cardData!.answerAvTags : this._cardData!.questionAvTags; playAvtags({ tags: [tags[e.data.index]] }); break; } + case "typed": { + this.currentTypedAnswer = e.data.value; + } } } diff --git a/ts/routes/reviewer/reviewerRequest.ts b/ts/routes/reviewer/reviewerRequest.ts index aa4d3c786..6cefa1919 100644 --- a/ts/routes/reviewer/reviewerRequest.ts +++ b/ts/routes/reviewer/reviewerRequest.ts @@ -6,4 +6,9 @@ interface AudioMessage { index: number; } -export type ReviewerRequest = AudioMessage; +interface CompareTypedAnswerMessage { + type: "typed"; + value: string; +} + +export type ReviewerRequest = AudioMessage | CompareTypedAnswerMessage; From b66a10ea26451eef3d561090e06c0d130fbfcc75 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 12:38:11 +0000 Subject: [PATCH 094/237] Added: ShowQusestion TypedAnswer replacement --- qt/aqt/mediasrv.py | 1 + ts/routes/reviewer-inner/index.ts | 11 +++++++++++ ts/routes/reviewer/reviewer.ts | 23 ++++++++++++++++++++--- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 898a05b25..aab4e58c0 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -763,6 +763,7 @@ exposed_backend_list = [ "get_retention_workload", # CardsService "set_flag", + "compare_answer", ] diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index dc32fac87..ba70ffde3 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -65,3 +65,14 @@ function pycmd(cmd: string) { } } globalThis.pycmd = pycmd; + +function _typeAnsPress() { + const elem = document.getElementById("typeans")! as HTMLInputElement; + let key = (window.event as KeyboardEvent).key; + key = key.length == 1 ? key : ""; + window.parent.postMessage( + { type: "typed", value: elem.value + key } satisfies ReviewerRequest, + "*", + ); +} +globalThis._typeAnsPress = _typeAnsPress; diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 40149c457..1fa8e526a 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; -import { nextCardData, playAvtags } from "@generated/backend"; +import { compareAnswer, nextCardData, playAvtags } from "@generated/backend"; import { derived, get, writable } from "svelte/store"; import type { InnerReviewerRequest } from "../reviewer-inner/innerReviewerRequest"; import type { ReviewerRequest } from "./reviewerRequest"; @@ -25,6 +25,8 @@ export function updateNightMode() { } } +const typedAnswerRegex = /\[\[type:(.+?:)?(.+?)\]\]/m; + export class ReviewerState { answerHtml = ""; currentTypedAnswer = ""; @@ -125,12 +127,27 @@ export class ReviewerState { return this._cardData?.queue?.cards[0]; } - public showAnswer() { + async showTypedAnswer(html: string) { + if (!this._cardData?.typedAnswer) { + return html; + } + const compareAnswerResp = await compareAnswer({ + expected: this._cardData?.typedAnswer, + provided: this.currentTypedAnswer, + combining: false, + }); + const display = compareAnswerResp.val; + + console.log({ typedAnswerRegex, html, display }); + return html.replace(typedAnswerRegex, display); + } + + public async showAnswer() { this.answerShown.set(true); if (this._cardData?.autoplay) { playAvtags({ tags: this._cardData!.answerAvTags }); } - this.updateHtml(this._cardData?.back || ""); + this.updateHtml(await this.showTypedAnswer(this._cardData?.back || "")); } public easeButtonPressed(rating: number) { From 23babca417ef9ad85f074db1d6c42621acca2639 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 12:41:12 +0000 Subject: [PATCH 095/237] postParentMessage function --- ts/routes/reviewer-inner/index.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index ba70ffde3..bcf537b4b 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -54,14 +54,22 @@ const base = document.createElement("base"); base.href = "/"; document.head.appendChild(base); +function postParentMessage(message: ReviewerRequest) { + window.parent.postMessage( + message, + "*", + ); +} + function pycmd(cmd: string) { const match = cmd.match(/play:(q|a):(\d+)/); if (match) { const [_, context, index] = match; - window.parent.postMessage( - { type: "audio", answerSide: context == "a", index: parseInt(index) } satisfies ReviewerRequest, - "*", - ); + postParentMessage({ + type: "audio", + answerSide: context === "a", + index: parseInt(index), + }); } } globalThis.pycmd = pycmd; @@ -70,9 +78,8 @@ function _typeAnsPress() { const elem = document.getElementById("typeans")! as HTMLInputElement; let key = (window.event as KeyboardEvent).key; key = key.length == 1 ? key : ""; - window.parent.postMessage( - { type: "typed", value: elem.value + key } satisfies ReviewerRequest, - "*", + postParentMessage( + { type: "typed", value: elem.value + key }, ); } globalThis._typeAnsPress = _typeAnsPress; From 4144fefcd78b605fa71c432ada69e29da9011da2 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 12:42:24 +0000 Subject: [PATCH 096/237] CompareTypedAnswerMessage -> UpdateTypedAnswerMessage --- ts/routes/reviewer/reviewerRequest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/routes/reviewer/reviewerRequest.ts b/ts/routes/reviewer/reviewerRequest.ts index 6cefa1919..8ed9590cf 100644 --- a/ts/routes/reviewer/reviewerRequest.ts +++ b/ts/routes/reviewer/reviewerRequest.ts @@ -6,9 +6,9 @@ interface AudioMessage { index: number; } -interface CompareTypedAnswerMessage { +interface UpdateTypedAnswerMessage { type: "typed"; value: string; } -export type ReviewerRequest = AudioMessage | CompareTypedAnswerMessage; +export type ReviewerRequest = AudioMessage | UpdateTypedAnswerMessage; From dc016c509a519a9d8851a9b51f0bd2ae37e6b035 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 13:01:29 +0000 Subject: [PATCH 097/237] Fix: Handle Inner iframe keypresses --- ts/routes/reviewer-inner/index.ts | 20 +++++++++++++------- ts/routes/reviewer/reviewer.ts | 13 +++++++++++-- ts/routes/reviewer/reviewerRequest.ts | 7 ++++++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index bcf537b4b..9b581b198 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -8,6 +8,13 @@ import { enableNightMode } from "../reviewer/reviewer"; import type { ReviewerRequest } from "../reviewer/reviewerRequest"; import type { InnerReviewerRequest } from "./innerReviewerRequest"; +function postParentMessage(message: ReviewerRequest) { + window.parent.postMessage( + message, + "*", + ); +} + declare const MathJax: any; const urlParams = new URLSearchParams(location.search); @@ -50,17 +57,16 @@ addEventListener("message", async (e: MessageEvent) => { } }); +addEventListener("keydown", (e) => { + if (e.key.length == 1 && "1234 ".includes(e.key) && !document.activeElement?.matches("#typeans")) { + postParentMessage({ type: "keyPress", key: e.key }); + } +}); + const base = document.createElement("base"); base.href = "/"; document.head.appendChild(base); -function postParentMessage(message: ReviewerRequest) { - window.parent.postMessage( - message, - "*", - ); -} - function pycmd(cmd: string) { const match = cmd.match(/play:(q|a):(\d+)/); if (match) { diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 1fa8e526a..cd0514ec3 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -54,6 +54,11 @@ export class ReviewerState { } case "typed": { this.currentTypedAnswer = e.data.value; + break; + } + case "keyPress": { + this.handleKeyPress(e.data.key); + break; } } } @@ -63,8 +68,8 @@ export class ReviewerState { iframe.addEventListener("load", this.onReady.bind(this)); } - onKeyDown(e: KeyboardEvent) { - switch (e.key) { + handleKeyPress(key: string) { + switch (key) { case "1": { this.easeButtonPressed(0); break; @@ -92,6 +97,10 @@ export class ReviewerState { } } + onKeyDown(e: KeyboardEvent) { + this.handleKeyPress(e.key); + } + public registerShortcuts() { document.addEventListener("keydown", this.onKeyDown.bind(this)); } diff --git a/ts/routes/reviewer/reviewerRequest.ts b/ts/routes/reviewer/reviewerRequest.ts index 8ed9590cf..c57ea2c50 100644 --- a/ts/routes/reviewer/reviewerRequest.ts +++ b/ts/routes/reviewer/reviewerRequest.ts @@ -11,4 +11,9 @@ interface UpdateTypedAnswerMessage { value: string; } -export type ReviewerRequest = AudioMessage | UpdateTypedAnswerMessage; +interface KeyPressMessage { + type: "keyPress"; + key: string; +} + +export type ReviewerRequest = AudioMessage | UpdateTypedAnswerMessage | KeyPressMessage; From e16a49d376ad297b9f93a1d4d5d3710378b769d0 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 17:16:31 +0000 Subject: [PATCH 098/237] Added: New reviewer config bool --- ftl/core/preferences.ftl | 1 + proto/anki/config.proto | 2 ++ qt/aqt/forms/preferences.ui | 13 +++++++++++++ qt/aqt/main.py | 13 ++++++++----- qt/aqt/preferences.py | 3 +++ rslib/src/backend/config.rs | 1 + rslib/src/config/bool.rs | 1 + rslib/src/preferences.rs | 2 ++ 8 files changed, 31 insertions(+), 5 deletions(-) diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl index 23b72f267..a702cca4a 100644 --- a/ftl/core/preferences.ftl +++ b/ftl/core/preferences.ftl @@ -14,6 +14,7 @@ preferences-on-next-sync-force-changes-in = On next sync, force changes in one d preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting preferences-generate-latex-images-automatically = Generate LaTeX images (security risk) +preferences-use-new-reviewer = Use new reviewer preferences-latex-generation-disabled = LaTeX image generation is disabled in the preferences. preferences-periodically-sync-media = Periodically sync media preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change. diff --git a/proto/anki/config.proto b/proto/anki/config.proto index ea115f0fc..f7a04e38d 100644 --- a/proto/anki/config.proto +++ b/proto/anki/config.proto @@ -57,6 +57,7 @@ message ConfigKey { LOAD_BALANCER_ENABLED = 26; FSRS_SHORT_TERM_WITH_STEPS_ENABLED = 27; FSRS_LEGACY_EVALUATE = 28; + NEW_REVIEWER = 29; } enum String { SET_DUE_BROWSER = 0; @@ -120,6 +121,7 @@ message Preferences { uint32 time_limit_secs = 5; bool load_balancer_enabled = 6; bool fsrs_short_term_with_steps_enabled = 7; + bool new_reviewer = 8; } message Editing { bool adding_defaults_to_current_deck = 1; diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 0035e1f42..2a59b5f9d 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -451,6 +451,19 @@ + + + + + 0 + 0 + + + + preferences_use_new_reviewer + + + diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 3affb3a2e..98d4409a3 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -255,13 +255,11 @@ class AnkiQt(QMainWindow): # screens self.setupDeckBrowser() self.setupOverview() - self.setupReviewer() + # self.setupReviewer() def finish_ui_setup(self) -> None: "Actions that are deferred until after add-on loading." self.toolbar.draw() - # add-ons are only available here after setupAddons - gui_hooks.reviewer_did_init(self.reviewer) def setupProfileAfterWebviewsLoaded(self) -> None: for w in (self.web, self.bottomWeb): @@ -679,6 +677,8 @@ class AnkiQt(QMainWindow): # dump error to stderr so it gets picked up by errors.py traceback.print_exc() + self.setupReviewer(self.backend.get_config_bool(Config.Bool.NEW_REVIEWER)) + return True def _loadCollection(self) -> None: @@ -1074,10 +1074,13 @@ title="{}" {}>{}""".format( self.overview = Overview(self) - def setupReviewer(self) -> None: + def setupReviewer(self, new: bool) -> None: from aqt.reviewer import Reviewer, SvelteReviewer - self.reviewer = SvelteReviewer(self) if True else Reviewer(self) + self.reviewer = SvelteReviewer(self) if new else Reviewer(self) + + # add-ons are only available here after setupAddons + gui_hooks.reviewer_did_init(self.reviewer) # Syncing ########################################################################## diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 939dd8c2c..8895becb1 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -138,6 +138,7 @@ class Preferences(QDialog): form.showProgress.setChecked(reviewing.show_remaining_due_counts) form.showPlayButtons.setChecked(not reviewing.hide_audio_play_buttons) form.interrupt_audio.setChecked(reviewing.interrupt_audio_when_answering) + form.new_reviewer.setChecked(reviewing.new_reviewer) editing = self.prefs.editing form.useCurrent.setCurrentIndex( @@ -173,6 +174,8 @@ class Preferences(QDialog): reviewing.time_limit_secs = form.timeLimit.value() * 60 reviewing.hide_audio_play_buttons = not self.form.showPlayButtons.isChecked() reviewing.interrupt_audio_when_answering = self.form.interrupt_audio.isChecked() + reviewing.new_reviewer = form.new_reviewer.isChecked() + aqt.mw.setupReviewer(reviewing.new_reviewer) editing = self.prefs.editing editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex() diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index b6e81ce2a..9bc1140a2 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -40,6 +40,7 @@ impl From for BoolKey { BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled, BoolKeyProto::FsrsShortTermWithStepsEnabled => BoolKey::FsrsShortTermWithStepsEnabled, BoolKeyProto::FsrsLegacyEvaluate => BoolKey::FsrsLegacyEvaluate, + BoolKeyProto::NewReviewer => BoolKey::NewReviewer, } } } diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index c76787cb0..a5ba0e70f 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -44,6 +44,7 @@ pub enum BoolKey { FsrsLegacyEvaluate, LoadBalancerEnabled, FsrsShortTermWithStepsEnabled, + NewReviewer, #[strum(to_string = "normalize_note_text")] NormalizeNoteText, #[strum(to_string = "dayLearnFirst")] diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index 96be8e461..fa760baef 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -101,6 +101,7 @@ impl Collection { load_balancer_enabled: self.get_config_bool(BoolKey::LoadBalancerEnabled), fsrs_short_term_with_steps_enabled: self .get_config_bool(BoolKey::FsrsShortTermWithStepsEnabled), + new_reviewer: self.get_config_bool(BoolKey::NewReviewer), }) } @@ -125,6 +126,7 @@ impl Collection { BoolKey::FsrsShortTermWithStepsEnabled, s.fsrs_short_term_with_steps_enabled, )?; + self.set_config_bool_inner(BoolKey::NewReviewer, settings.new_reviewer)?; Ok(()) } From dae91bd1d84dc60a726d9ce5b522277aa4f6ebfa Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 17:26:07 +0000 Subject: [PATCH 099/237] keyPress -> keypress --- ts/routes/reviewer-inner/index.ts | 2 +- ts/routes/reviewer/reviewer.ts | 2 +- ts/routes/reviewer/reviewerRequest.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 9b581b198..bf2e85d27 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -59,7 +59,7 @@ addEventListener("message", async (e: MessageEvent) => { addEventListener("keydown", (e) => { if (e.key.length == 1 && "1234 ".includes(e.key) && !document.activeElement?.matches("#typeans")) { - postParentMessage({ type: "keyPress", key: e.key }); + postParentMessage({ type: "keypress", key: e.key }); } }); diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index cd0514ec3..ba138c08f 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -56,7 +56,7 @@ export class ReviewerState { this.currentTypedAnswer = e.data.value; break; } - case "keyPress": { + case "keypress": { this.handleKeyPress(e.data.key); break; } diff --git a/ts/routes/reviewer/reviewerRequest.ts b/ts/routes/reviewer/reviewerRequest.ts index c57ea2c50..bcd0f7fde 100644 --- a/ts/routes/reviewer/reviewerRequest.ts +++ b/ts/routes/reviewer/reviewerRequest.ts @@ -12,7 +12,7 @@ interface UpdateTypedAnswerMessage { } interface KeyPressMessage { - type: "keyPress"; + type: "keypress"; key: string; } From 4e8bf1381320bb5012648eadff3a3b333b1ebe59 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 31 Oct 2025 17:32:50 +0000 Subject: [PATCH 100/237] Fix: Check for focus with inner iframe keypress --- ts/routes/reviewer-inner/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index bf2e85d27..348e28068 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -58,7 +58,12 @@ addEventListener("message", async (e: MessageEvent) => { }); addEventListener("keydown", (e) => { - if (e.key.length == 1 && "1234 ".includes(e.key) && !document.activeElement?.matches("#typeans")) { + if (e.key === "Enter" && document.activeElement?.matches("#typeans")) { + postParentMessage({ type: "keypress", key: " " }); + } else if ( + e.key.length == 1 && "1234 ".includes(e.key) + && !document.activeElement?.matches("input[type=text], input[type=number], textarea") + ) { postParentMessage({ type: "keypress", key: e.key }); } }); From 7cb7d208c6a554bf537dd94d13fe698b5ed0dbd7 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 3 Nov 2025 23:07:03 +0000 Subject: [PATCH 101/237] Added: nc for typed answers --- proto/anki/scheduler.proto | 1 + rslib/src/scheduler/service/mod.rs | 12 +++++++++--- ts/routes/reviewer/reviewer.ts | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 2cf0108f8..dd8aaadb3 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -307,6 +307,7 @@ message NextCardDataResponse { string body_class = 6; bool autoplay = 7; optional string typed_answer = 12; + optional string typed_answer_args = 13; repeated card_rendering.AVTag question_av_tags = 8; repeated card_rendering.AVTag answer_av_tags = 9; diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 5ba32acb9..a686a76e3 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -431,7 +431,10 @@ impl crate::services::SchedulerService for Collection { let mut out = None; *text = ANSWER_REGEX .replace(text, |cap: ®ex::Captures<'_>| { - out = Some(cap[2].to_string()); + out = Some(( + cap.get(1).map(|g| g.as_str().to_string()), + cap[2].to_string(), + )); ANSWER_HTML }) .to_string(); @@ -441,12 +444,14 @@ impl crate::services::SchedulerService for Collection { } }); - let typed_answer = typed_answer_parent_node.map(|field| { + let typed_answer = typed_answer_parent_node.as_ref().map(|field| { let note = self.get_note(next_card.card.note_id.into()).unwrap(); let notetype = self.get_notetype(note.notetype_id.into()).unwrap().unwrap(); - note.fields[notetype.get_field_ord(&field).unwrap()].clone() + note.fields[notetype.get_field_ord(&field.1).unwrap()].clone() }); + dbg!(&typed_answer_parent_node); + Ok(NextCardDataResponse { next_card: Some(NextCardData { queue: Some(queue.into()), @@ -458,6 +463,7 @@ impl crate::services::SchedulerService for Collection { answer_buttons, autoplay: !config.inner.disable_autoplay, typed_answer, + typed_answer_args: typed_answer_parent_node.and_then(|v| v.0), // Filled by python front: "".to_string(), diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index ba138c08f..7aa411d31 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -137,13 +137,13 @@ export class ReviewerState { } async showTypedAnswer(html: string) { - if (!this._cardData?.typedAnswer) { + if (!this._cardData?.typedAnswer || !this._cardData.typedAnswerArgs) { return html; } const compareAnswerResp = await compareAnswer({ expected: this._cardData?.typedAnswer, provided: this.currentTypedAnswer, - combining: false, + combining: !this._cardData.typedAnswerArgs.includes("nc"), }); const display = compareAnswerResp.val; From b80468554a3cab2e1668d439d739117631ca6bc5 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 3 Nov 2025 23:11:48 +0000 Subject: [PATCH 102/237] remove dbg --- rslib/src/scheduler/service/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index a686a76e3..d535d8ae9 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -450,8 +450,6 @@ impl crate::services::SchedulerService for Collection { note.fields[notetype.get_field_ord(&field.1).unwrap()].clone() }); - dbg!(&typed_answer_parent_node); - Ok(NextCardDataResponse { next_card: Some(NextCardData { queue: Some(queue.into()), From 42325fd548bb1334d12273add6bfbb41bee69d1b Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 4 Nov 2025 18:37:09 +0000 Subject: [PATCH 103/237] Fix: Bottom bar remains for windows --- qt/aqt/reviewer.py | 9 ++++----- qt/aqt/toolbar.py | 6 +++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 63bb2ba9f..e816348d7 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1275,7 +1275,7 @@ class SvelteReviewer(Reviewer): return "" idx, counts = self._v3.counts() - self.bottom.web.eval(f"_updateRemaining({json.dumps(counts)},{idx})") + self.web.eval(f"_updateRemaining({json.dumps(counts)},{idx})") return "" def _showAnswerButton(self) -> None: @@ -1284,7 +1284,7 @@ class SvelteReviewer(Reviewer): else: maxTime = 0 self._remaining() - self.bottom.web.eval('showQuestion("",%d);' % (maxTime)) + self.web.eval('showQuestion("",%d);' % (maxTime)) def _buttonTime(self, i: int, v3_labels: Sequence[str]) -> str: return v3_labels[i - 1] if self.mw.col.conf["estTimes"] else "" @@ -1298,14 +1298,13 @@ class SvelteReviewer(Reviewer): def _initWeb(self) -> None: self._reps = 0 + # hide the bottom bar + self.bottom.web.setHtml("") # 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() def _shortcutKeys(self) -> Sequence[tuple[str, Callable] | tuple[Qt.Key, Callable]]: return [] diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index be547b5ba..7c617483f 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -211,7 +211,11 @@ class BottomWebView(ToolbarWebView): def animate_height(self, height: int) -> None: self.web_height = height - if self.mw.pm.reduce_motion() or height == self.height(): + if ( + self.mw.pm.reduce_motion() + or self.mw.col.conf.get("newReviewer") + or height == self.height() + ): self.setFixedHeight(height) else: # Collapse/Expand animation From a4a1783bc234bdf66ec166edab20b0c70e3b4db4 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 4 Nov 2025 21:13:29 +0000 Subject: [PATCH 104/237] Fix: Use small arrow instead of placeholder I tested this on my english windows 10 and seemed to cause no problems. --- ts/routes/reviewer/reviewer-bottom/More.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index d6783e2c6..d2f924ddd 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -37,7 +37,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }} title={tr.actionsShortcutKey({ val: "M" })} > - {tr.studyingMore()}↧ + {tr.studyingMore()}{"▾"}
From 65f38448573f6efe1bcb061eb4f3c2dc76afdbdb Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 4 Nov 2025 21:11:36 +0000 Subject: [PATCH 105/237] Fix: Enter key hotkey --- ts/routes/reviewer-inner/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 348e28068..a4c1df522 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -58,7 +58,7 @@ addEventListener("message", async (e: MessageEvent) => { }); addEventListener("keydown", (e) => { - if (e.key === "Enter" && document.activeElement?.matches("#typeans")) { + if (e.key === "Enter") { postParentMessage({ type: "keypress", key: " " }); } else if ( e.key.length == 1 && "1234 ".includes(e.key) From 71bdb006e4cdd117a80019df4e8bb8ffb85f8923 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 4 Nov 2025 21:20:32 +0000 Subject: [PATCH 106/237] disable pycmd --- qt/aqt/reviewer.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index e816348d7..f8cb9a754 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1290,11 +1290,7 @@ class SvelteReviewer(Reviewer): return v3_labels[i - 1] if self.mw.col.conf["estTimes"] else "" def _linkHandler(self, url: str) -> None: - if url == "bottomReady": - self._showQuestion() - self._remaining() - return - super()._linkHandler(url) + pass def _initWeb(self) -> None: self._reps = 0 From 1b3c918a9739b6135457f5693550e2464150492c Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 4 Nov 2025 21:28:28 +0000 Subject: [PATCH 107/237] Fix: add display count placeholder --- .../reviewer/reviewer-bottom/Remaining.svelte | 20 ++++++++++--------- .../reviewer-bottom/RemainingNumber.svelte | 7 +++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ts/routes/reviewer/reviewer-bottom/Remaining.svelte b/ts/routes/reviewer/reviewer-bottom/Remaining.svelte index 6e7ae9075..1e1854983 100644 --- a/ts/routes/reviewer/reviewer-bottom/Remaining.svelte +++ b/ts/routes/reviewer/reviewer-bottom/Remaining.svelte @@ -14,21 +14,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - {queue?.newCount} - + + + {"+"} - {queue?.learningCount} - + + count={queue?.learningCount} + > + {"+"} - {queue?.reviewCount} - + count={queue?.reviewCount} + > ") # main window - self.web.load_sveltekit_page("reviewer") + inner_port = self.mw.mediaServer.card_data_server.effective_port + self.web.load_sveltekit_page(f"reviewer?p={inner_port}") # block default drag & drop behavior while allowing drop events to be received by JS handlers self.web.allow_drops = True self.web.eval("_blockDefaultDragDropBehavior();") diff --git a/ts/routes/reviewer/Reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte index 7c958ecea..15150cb27 100644 --- a/ts/routes/reviewer/Reviewer.svelte +++ b/ts/routes/reviewer/Reviewer.svelte @@ -12,15 +12,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html state.registerIFrame(iframe); state.registerShortcuts(); } + + const innerPort = new URLSearchParams(window.location.search).get("p"); + + $: hostname = innerPort + ? `${window.location.protocol}//${window.location.hostname}:${innerPort}/reviewer-inner.html` + : "/_anki/pages/reviewer-inner.html"; // fallback + + $: sandboxAllowList = + "allow-scripts" + + (new URL(hostname).origin != window.location.origin + ? " allow-same-origin" + : "");
From cbecf66160677dffb365566960dab75d1f89a398 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 6 Nov 2025 17:10:59 +0000 Subject: [PATCH 110/237] Revert "different port localhost solution" This reverts commit 0bf871cadd6f67b98fd78e68e304f663586dc62c. --- qt/aqt/mediasrv.py | 29 ----------------------------- qt/aqt/reviewer.py | 3 +-- ts/routes/reviewer/Reviewer.svelte | 16 ++-------------- 3 files changed, 3 insertions(+), 45 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index cd80b816a..aab4e58c0 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -56,7 +56,6 @@ waitress.wasyncore._DISCONNECTED = waitress.wasyncore._DISCONNECTED.union({EPROT logger = logging.getLogger(__name__) app = flask.Flask(__name__, root_path="/fake") -card_data_app = flask.Flask(__name__, root_path="/fake") flask_cors.CORS(app, resources={r"/*": {"origins": "127.0.0.1"}}) @@ -116,35 +115,20 @@ class MediaServer(threading.Thread): try: desired_host = os.getenv("ANKI_API_HOST", "127.0.0.1") desired_port = int(os.getenv("ANKI_API_PORT") or 0) - desired_card_data_port = int(os.getenv("ANKI_CARD_DATA_PORT") or 0) self.server = create_server( app, host=desired_host, port=desired_port, clear_untrusted_proxy_headers=True, ) - self.card_data_server = create_server( - card_data_app, - host=desired_host, - port=desired_card_data_port, - clear_untrusted_proxy_headers=True, - ) logger.info( "Serving on http://%s:%s", self.server.effective_host, # type: ignore[union-attr] self.server.effective_port, # type: ignore[union-attr] ) - logger.info( - "Serving iframes on http://%s:%s", - self.card_data_server.effective_host, # type: ignore[union-attr] - self.card_data_server.effective_port, # type: ignore[union-attr] - ) self._ready.set() - card_data_thread = threading.Thread(target=self.card_data_server.run) - card_data_thread.start() self.server.run() - card_data_thread.join() except Exception: if not self.is_shutdown: @@ -195,19 +179,6 @@ def favicon() -> Response: return _handle_builtin_file_request(request) -@card_data_app.route("/reviewer-inner.") -def card_data(ext): - if ext not in ("html", "js", "css"): - abort(404) - response = _handle_builtin_file_request( - BundledFileRequest(path=os.path.join("pages", f"reviewer-inner.{ext}")) - ) - print(response.headers) - response.headers["Content-Security-Policy"] = "" - print(response.headers) - return response - - def _mime_for_path(path: str) -> str: "Mime type for provided path/filename." diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 3e552aedd..f8cb9a754 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1297,8 +1297,7 @@ class SvelteReviewer(Reviewer): # hide the bottom bar self.bottom.web.setHtml("") # main window - inner_port = self.mw.mediaServer.card_data_server.effective_port - self.web.load_sveltekit_page(f"reviewer?p={inner_port}") + 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();") diff --git a/ts/routes/reviewer/Reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte index 15150cb27..7c958ecea 100644 --- a/ts/routes/reviewer/Reviewer.svelte +++ b/ts/routes/reviewer/Reviewer.svelte @@ -12,27 +12,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html state.registerIFrame(iframe); state.registerShortcuts(); } - - const innerPort = new URLSearchParams(window.location.search).get("p"); - - $: hostname = innerPort - ? `${window.location.protocol}//${window.location.hostname}:${innerPort}/reviewer-inner.html` - : "/_anki/pages/reviewer-inner.html"; // fallback - - $: sandboxAllowList = - "allow-scripts" + - (new URL(hostname).origin != window.location.origin - ? " allow-same-origin" - : "");
From de9c2f7613e3c0942013a37e29749cb447826a75 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 6 Nov 2025 17:45:07 +0000 Subject: [PATCH 111/237] Fix:
From ef3a548832f8e4f2f4bb542284cbcb4c2633811b Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 6 Nov 2025 18:10:24 +0000 Subject: [PATCH 113/237] cleanup --- qt/aqt/reviewer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 3e552aedd..dd01abf45 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -444,7 +444,6 @@ class Reviewer: tooltip(tr.studying_question_time_elapsed()) def autoplay(self, card: Card) -> bool: - print("use card.autoplay() instead of reviewer.autoplay(card)") return card.autoplay() def _update_flag_icon(self) -> None: @@ -1297,7 +1296,7 @@ class SvelteReviewer(Reviewer): # hide the bottom bar self.bottom.web.setHtml("") # main window - inner_port = self.mw.mediaServer.card_data_server.effective_port + inner_port = self.mw.mediaServer.card_data_server.effective_port # type: ignore[union-attr] self.web.load_sveltekit_page(f"reviewer?p={inner_port}") # block default drag & drop behavior while allowing drop events to be received by JS handlers self.web.allow_drops = True From b31fe7721a0e356d30e4c06d7e591226b6dba6dc Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 6 Nov 2025 18:16:24 +0000 Subject: [PATCH 114/237] remove console.log --- ts/routes/reviewer/reviewer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 7aa411d31..36917d7b5 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -147,7 +147,6 @@ export class ReviewerState { }); const display = compareAnswerResp.val; - console.log({ typedAnswerRegex, html, display }); return html.replace(typedAnswerRegex, display); } From 8da27af264b7f5a8ef34ae547d357a6d2886a67b Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 6 Nov 2025 18:20:39 +0000 Subject: [PATCH 115/237] _blockDefaultDragDropBehavior --- qt/aqt/reviewer.py | 1 - ts/routes/reviewer/+page.svelte | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index dd01abf45..9c4df52a4 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1300,7 +1300,6 @@ class SvelteReviewer(Reviewer): self.web.load_sveltekit_page(f"reviewer?p={inner_port}") # block default drag & drop behavior while allowing drop events to be received by JS handlers self.web.allow_drops = True - self.web.eval("_blockDefaultDragDropBehavior();") def _shortcutKeys(self) -> Sequence[tuple[str, Callable] | tuple[Qt.Key, Callable]]: return [] diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte index 6b1b0b06f..cf574b00d 100644 --- a/ts/routes/reviewer/+page.svelte +++ b/ts/routes/reviewer/+page.svelte @@ -7,12 +7,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { ReviewerState, updateNightMode } from "./reviewer"; import ReviewerBottom from "./reviewer-bottom/ReviewerBottom.svelte"; import Reviewer from "./Reviewer.svelte"; + import { _blockDefaultDragDropBehavior } from "../../reviewer"; const state = new ReviewerState(); onMount(() => { updateNightMode(); globalThis.anki ??= {}; globalThis.anki.changeReceived = () => state.showQuestion(null); + _blockDefaultDragDropBehavior(); }); From 834c2b9a75b11e6ddad26b94711775534afb61c8 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 6 Nov 2025 18:28:40 +0000 Subject: [PATCH 116/237] Revert "Reapply "different port localhost solution"" This reverts commit 6bd72d9e4e57f9f0c84e0d4215dea6e6088e0f2c. --- qt/aqt/mediasrv.py | 29 ----------------------------- qt/aqt/reviewer.py | 3 +-- ts/routes/reviewer/Reviewer.svelte | 16 ++-------------- 3 files changed, 3 insertions(+), 45 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index cd80b816a..aab4e58c0 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -56,7 +56,6 @@ waitress.wasyncore._DISCONNECTED = waitress.wasyncore._DISCONNECTED.union({EPROT logger = logging.getLogger(__name__) app = flask.Flask(__name__, root_path="/fake") -card_data_app = flask.Flask(__name__, root_path="/fake") flask_cors.CORS(app, resources={r"/*": {"origins": "127.0.0.1"}}) @@ -116,35 +115,20 @@ class MediaServer(threading.Thread): try: desired_host = os.getenv("ANKI_API_HOST", "127.0.0.1") desired_port = int(os.getenv("ANKI_API_PORT") or 0) - desired_card_data_port = int(os.getenv("ANKI_CARD_DATA_PORT") or 0) self.server = create_server( app, host=desired_host, port=desired_port, clear_untrusted_proxy_headers=True, ) - self.card_data_server = create_server( - card_data_app, - host=desired_host, - port=desired_card_data_port, - clear_untrusted_proxy_headers=True, - ) logger.info( "Serving on http://%s:%s", self.server.effective_host, # type: ignore[union-attr] self.server.effective_port, # type: ignore[union-attr] ) - logger.info( - "Serving iframes on http://%s:%s", - self.card_data_server.effective_host, # type: ignore[union-attr] - self.card_data_server.effective_port, # type: ignore[union-attr] - ) self._ready.set() - card_data_thread = threading.Thread(target=self.card_data_server.run) - card_data_thread.start() self.server.run() - card_data_thread.join() except Exception: if not self.is_shutdown: @@ -195,19 +179,6 @@ def favicon() -> Response: return _handle_builtin_file_request(request) -@card_data_app.route("/reviewer-inner.") -def card_data(ext): - if ext not in ("html", "js", "css"): - abort(404) - response = _handle_builtin_file_request( - BundledFileRequest(path=os.path.join("pages", f"reviewer-inner.{ext}")) - ) - print(response.headers) - response.headers["Content-Security-Policy"] = "" - print(response.headers) - return response - - def _mime_for_path(path: str) -> str: "Mime type for provided path/filename." diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 9c4df52a4..32689b9a5 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1296,8 +1296,7 @@ class SvelteReviewer(Reviewer): # hide the bottom bar self.bottom.web.setHtml("") # main window - inner_port = self.mw.mediaServer.card_data_server.effective_port # type: ignore[union-attr] - self.web.load_sveltekit_page(f"reviewer?p={inner_port}") + 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 diff --git a/ts/routes/reviewer/Reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte index 15150cb27..7c958ecea 100644 --- a/ts/routes/reviewer/Reviewer.svelte +++ b/ts/routes/reviewer/Reviewer.svelte @@ -12,27 +12,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html state.registerIFrame(iframe); state.registerShortcuts(); } - - const innerPort = new URLSearchParams(window.location.search).get("p"); - - $: hostname = innerPort - ? `${window.location.protocol}//${window.location.hostname}:${innerPort}/reviewer-inner.html` - : "/_anki/pages/reviewer-inner.html"; // fallback - - $: sandboxAllowList = - "allow-scripts" + - (new URL(hostname).origin != window.location.origin - ? " allow-same-origin" - : "");
From 6b0b2aad56fe29f779a50b46ff4e0b7b12bff0a1 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 7 Nov 2025 15:48:14 +0000 Subject: [PATCH 117/237] Added: Monkey patch localStorage solution --- qt/aqt/mediasrv.py | 2 + ts/routes/reviewer-inner/index.ts | 67 ++++++++++++++++++- .../reviewer-inner/innerReviewerRequest.ts | 7 +- ts/routes/reviewer/reviewer.ts | 13 +++- ts/routes/reviewer/reviewerRequest.ts | 7 +- 5 files changed, 90 insertions(+), 6 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index aab4e58c0..232f5b383 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -723,6 +723,8 @@ exposed_backend_list = [ # CollectionService "latest_progress", "get_custom_colours", + "set_config_json", + "get_config_json", # DeckService "get_deck_names", # I18nService diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index c5b230943..143afaae9 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -17,12 +17,17 @@ function postParentMessage(message: ReviewerRequest) { declare const MathJax: any; const urlParams = new URLSearchParams(location.search); - +const decoder = new TextDecoder(); const style = document.createElement("style"); document.head.appendChild(style); addEventListener("message", async (e: MessageEvent) => { switch (e.data.type) { + case "setstorage": { + const json = JSON.parse(decoder.decode(e.data.json_buffer)); + Object.assign(storageObj, json); + break; + } case "html": { document.body.innerHTML = e.data.value; if (e.data.css) { @@ -56,7 +61,7 @@ addEventListener("message", async (e: MessageEvent) => { break; } default: { - console.warn(`Unknown message type: ${e.data.type}`); + // console.warn(`Unknown message type: ${e.data.type}`); break; } } @@ -99,3 +104,61 @@ function _typeAnsPress() { ); } globalThis._typeAnsPress = _typeAnsPress; + +const storageObj = {}; +const encoder = new TextEncoder(); + +function updateParentStorage() { + postParentMessage({ type: "setstorage", json_buffer: encoder.encode(JSON.stringify(storageObj)) }); +} + +function createStorageProxy() { + return new Proxy({}, { + get(_target, prop) { + switch (prop) { + case "getItem": + return (key) => key in storageObj ? storageObj[key] : null; + case "setItem": + return (key, value) => { + storageObj[key] = String(value); + updateParentStorage(); + }; + case "removeItem": + return (key) => { + delete storageObj[key]; + updateParentStorage(); + }; + case "clear": + return () => { + Object.keys(storageObj).forEach(key => delete storageObj[key]); + updateParentStorage(); + }; + case "key": + return (index) => Object.keys(storageObj)[index] ?? null; + case "length": + return Object.keys(storageObj).length; + default: + return storageObj[prop]; + } + }, + set(_target, prop, value) { + storageObj[prop] = String(value); + return true; + }, + ownKeys() { + return Object.keys(storageObj); + }, + getOwnPropertyDescriptor(_target, _prop) { + return { enumerable: true, configurable: true }; + }, + }); +} + +const ankiStorage = createStorageProxy(); + +Object.defineProperty(window, "localStorage", { + value: ankiStorage, + writable: false, + configurable: true, + enumerable: true, +}); diff --git a/ts/routes/reviewer-inner/innerReviewerRequest.ts b/ts/routes/reviewer-inner/innerReviewerRequest.ts index 16afcbb88..aa9de1867 100644 --- a/ts/routes/reviewer-inner/innerReviewerRequest.ts +++ b/ts/routes/reviewer-inner/innerReviewerRequest.ts @@ -7,4 +7,9 @@ interface HtmlMessage { bodyclass?: string; } -export type InnerReviewerRequest = HtmlMessage; +interface StorageUpdateMessage { + type: "setstorage"; + json_buffer: Uint8Array; +} + +export type InnerReviewerRequest = HtmlMessage | StorageUpdateMessage; diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 36917d7b5..06b86b442 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; -import { compareAnswer, nextCardData, playAvtags } from "@generated/backend"; +import { compareAnswer, getConfigJson, nextCardData, playAvtags, setConfigJson } from "@generated/backend"; import { derived, get, writable } from "svelte/store"; import type { InnerReviewerRequest } from "../reviewer-inner/innerReviewerRequest"; import type { ReviewerRequest } from "./reviewerRequest"; @@ -39,8 +39,10 @@ export class ReviewerState { iframe: HTMLIFrameElement | undefined = undefined; - onReady() { + async onReady() { this.iframe!.style.visibility = "visible"; + const { json } = await getConfigJson({ val: "reviewer_storage" }); + this.sendInnerRequest({ type: "setstorage", json_buffer: json }); this.showQuestion(null); addEventListener("message", this.onMessage.bind(this)); } @@ -60,6 +62,13 @@ export class ReviewerState { this.handleKeyPress(e.data.key); break; } + case "setstorage": { + setConfigJson({ + key: "reviewer_storage", + valueJson: e.data.json_buffer, + undoable: false, + }); + } } } diff --git a/ts/routes/reviewer/reviewerRequest.ts b/ts/routes/reviewer/reviewerRequest.ts index bcd0f7fde..8a3f04c9d 100644 --- a/ts/routes/reviewer/reviewerRequest.ts +++ b/ts/routes/reviewer/reviewerRequest.ts @@ -16,4 +16,9 @@ interface KeyPressMessage { key: string; } -export type ReviewerRequest = AudioMessage | UpdateTypedAnswerMessage | KeyPressMessage; +interface SetStorageMessage { + type: "setstorage"; + json_buffer: Uint8Array; +} + +export type ReviewerRequest = AudioMessage | UpdateTypedAnswerMessage | KeyPressMessage | SetStorageMessage; From c843ef135b37fb875dbacbd8196238c6ecc84361 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 7 Nov 2025 16:44:22 +0000 Subject: [PATCH 118/237] Added: Image occlusion --- ts/routes/reviewer-inner/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 143afaae9..67108f940 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -2,12 +2,22 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import "../base.scss"; import "../../reviewer/reviewer.scss"; + import "mathjax/es5/tex-chtml-full.js"; import { renderError } from "../../reviewer"; +import { addBrowserClasses } from "../../reviewer/browser_selector"; +import { imageOcclusionAPI } from "../image-occlusion/review"; import { enableNightMode } from "../reviewer/reviewer"; import type { ReviewerRequest } from "../reviewer/reviewerRequest"; import type { InnerReviewerRequest } from "./innerReviewerRequest"; +const anki = globalThis.anki || {}; +anki.imageOcclusion = imageOcclusionAPI; +anki.setupImageCloze = imageOcclusionAPI.setup; // deprecated +addBrowserClasses(); + +Object.defineProperty(window, "anki", { value: anki }); + function postParentMessage(message: ReviewerRequest) { window.parent.postMessage( message, From 7faa2a26e1aea84b908468caa3730c9823f3da99 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 7 Nov 2025 17:08:09 +0000 Subject: [PATCH 119/237] Fix: Typed answers with no arguments dont work --- proto/anki/scheduler.proto | 8 ++++++-- rslib/src/scheduler/service/mod.rs | 18 +++++++++++++----- ts/routes/reviewer/reviewer.ts | 7 ++++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index dd8aaadb3..581f6d046 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -297,6 +297,11 @@ message NextCardDataResponse { string due = 2; } + message TypedAnswer { + string text = 1; + string args = 2; + } + message NextCardData { QueuedCards queue = 1; repeated AnswerButton answer_buttons = 2; @@ -306,8 +311,7 @@ message NextCardDataResponse { string css = 5; string body_class = 6; bool autoplay = 7; - optional string typed_answer = 12; - optional string typed_answer_args = 13; + optional TypedAnswer typed_answer = 12; repeated card_rendering.AVTag question_av_tags = 8; repeated card_rendering.AVTag answer_av_tags = 9; diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index d535d8ae9..592065afb 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -11,6 +11,7 @@ use anki_proto::generic; use anki_proto::scheduler; use anki_proto::scheduler::next_card_data_response::AnswerButton; use anki_proto::scheduler::next_card_data_response::NextCardData; +use anki_proto::scheduler::next_card_data_response::TypedAnswer; use anki_proto::scheduler::ComputeFsrsParamsResponse; use anki_proto::scheduler::ComputeMemoryStateResponse; use anki_proto::scheduler::ComputeOptimalRetentionResponse; @@ -432,7 +433,9 @@ impl crate::services::SchedulerService for Collection { *text = ANSWER_REGEX .replace(text, |cap: ®ex::Captures<'_>| { out = Some(( - cap.get(1).map(|g| g.as_str().to_string()), + cap.get(1) + .map(|g| g.as_str().to_string()) + .unwrap_or("".to_string()), cap[2].to_string(), )); ANSWER_HTML @@ -444,10 +447,13 @@ impl crate::services::SchedulerService for Collection { } }); - let typed_answer = typed_answer_parent_node.as_ref().map(|field| { + let typed_answer = typed_answer_parent_node.map(|field| { let note = self.get_note(next_card.card.note_id.into()).unwrap(); let notetype = self.get_notetype(note.notetype_id.into()).unwrap().unwrap(); - note.fields[notetype.get_field_ord(&field.1).unwrap()].clone() + ( + field.0, + note.fields[notetype.get_field_ord(&field.1).unwrap()].clone(), + ) }); Ok(NextCardDataResponse { @@ -460,8 +466,10 @@ impl crate::services::SchedulerService for Collection { answer_buttons, autoplay: !config.inner.disable_autoplay, - typed_answer, - typed_answer_args: typed_answer_parent_node.and_then(|v| v.0), + typed_answer: typed_answer.map(|answer| TypedAnswer { + text: answer.1, + args: answer.0, + }), // Filled by python front: "".to_string(), diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 06b86b442..c1d9d40f7 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -146,13 +146,14 @@ export class ReviewerState { } async showTypedAnswer(html: string) { - if (!this._cardData?.typedAnswer || !this._cardData.typedAnswerArgs) { + console.log({ data: this._cardData }); + if (this._cardData?.typedAnswer === undefined) { return html; } const compareAnswerResp = await compareAnswer({ - expected: this._cardData?.typedAnswer, + expected: this._cardData.typedAnswer.text, provided: this.currentTypedAnswer, - combining: !this._cardData.typedAnswerArgs.includes("nc"), + combining: !this._cardData.typedAnswer.args.includes("nc"), }); const display = compareAnswerResp.val; From 75dd53dba8e8419217709cba9b8191b0f8bf2db5 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 7 Nov 2025 18:15:13 +0000 Subject: [PATCH 120/237] Fix: extract_cloze_for_typing --- rslib/src/scheduler/service/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 592065afb..d7116bc23 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -32,6 +32,7 @@ use regex::Regex; use crate::backend::Backend; use crate::card_rendering::service::rendered_nodes_to_proto; +use crate::cloze::extract_cloze_for_typing; use crate::prelude::*; use crate::scheduler::fsrs::params::ComputeParamsRequest; use crate::scheduler::new::NewCardDueOrder; @@ -450,10 +451,13 @@ impl crate::services::SchedulerService for Collection { let typed_answer = typed_answer_parent_node.map(|field| { let note = self.get_note(next_card.card.note_id.into()).unwrap(); let notetype = self.get_notetype(note.notetype_id.into()).unwrap().unwrap(); - ( - field.0, - note.fields[notetype.get_field_ord(&field.1).unwrap()].clone(), - ) + let ord = notetype.get_field_ord(&field.1).unwrap(); + let mut correct = note.fields[ord].clone(); + if field.0.contains("cloze") { + correct = extract_cloze_for_typing(&correct, (ord + 1).try_into().unwrap()) + .to_string() + } + (field.0, correct) }); Ok(NextCardDataResponse { From 666e22ed42b9a3c8df9e75aa0168ca17561224a8 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 7 Nov 2025 18:31:50 +0000 Subject: [PATCH 121/237] Better error handling for typed_answer --- rslib/src/scheduler/service/mod.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index d7116bc23..eb3e52d02 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -448,17 +448,22 @@ impl crate::services::SchedulerService for Collection { } }); - let typed_answer = typed_answer_parent_node.map(|field| { - let note = self.get_note(next_card.card.note_id.into()).unwrap(); - let notetype = self.get_notetype(note.notetype_id.into()).unwrap().unwrap(); - let ord = notetype.get_field_ord(&field.1).unwrap(); - let mut correct = note.fields[ord].clone(); - if field.0.contains("cloze") { - correct = extract_cloze_for_typing(&correct, (ord + 1).try_into().unwrap()) - .to_string() - } - (field.0, correct) - }); + let typed_answer = typed_answer_parent_node + .map(|field| -> Result<(String, String)> { + let note = self.get_note(next_card.card.note_id.into())?; + let notetype = self + .get_notetype(note.notetype_id.into())? + .or_not_found(note.notetype_id)?; + let ord = notetype.get_field_ord(&field.1).or_not_found(field.1)?; + let mut correct = note.fields[ord].clone(); + if field.0.contains("cloze") { + correct = + extract_cloze_for_typing(&correct, (ord + 1).try_into().unwrap_or(0)) + .to_string() + } + Ok((field.0, correct)) + }) + .transpose()?; Ok(NextCardDataResponse { next_card: Some(NextCardData { From 65954cb2a58d2772824a584ef216dd21507fb161 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 7 Nov 2025 18:41:50 +0000 Subject: [PATCH 122/237] Fix: Used wrong ord --- rslib/src/scheduler/service/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index eb3e52d02..ba4b02dd5 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -454,12 +454,11 @@ impl crate::services::SchedulerService for Collection { let notetype = self .get_notetype(note.notetype_id.into())? .or_not_found(note.notetype_id)?; - let ord = notetype.get_field_ord(&field.1).or_not_found(field.1)?; - let mut correct = note.fields[ord].clone(); + let field_ord = notetype.get_field_ord(&field.1).or_not_found(field.1)?; + let mut correct = note.fields[field_ord].clone(); if field.0.contains("cloze") { - correct = - extract_cloze_for_typing(&correct, (ord + 1).try_into().unwrap_or(0)) - .to_string() + let card_ord = queue.cards[0].card.template_idx; + correct = extract_cloze_for_typing(&correct, card_ord + 1).to_string() } Ok((field.0, correct)) }) From 0af6b4d2ef39f41f9a9ae1840ed2230065d8b8e9 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 7 Nov 2025 18:48:48 +0000 Subject: [PATCH 123/237] Fix: Close typed not on new line --- ts/routes/reviewer/reviewer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index c1d9d40f7..2c2e5a612 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -146,16 +146,18 @@ export class ReviewerState { } async showTypedAnswer(html: string) { - console.log({ data: this._cardData }); if (this._cardData?.typedAnswer === undefined) { return html; } + const args = this._cardData.typedAnswer.args; const compareAnswerResp = await compareAnswer({ expected: this._cardData.typedAnswer.text, provided: this.currentTypedAnswer, - combining: !this._cardData.typedAnswer.args.includes("nc"), + combining: !args.includes("nc"), }); - const display = compareAnswerResp.val; + + const prefix = args.includes("cloze") ? "
" : ""; + const display = prefix + compareAnswerResp.val; return html.replace(typedAnswerRegex, display); } From f91ba1075b13d76e858e83b0c91840ec16b73db5 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 7 Nov 2025 22:29:56 +0000 Subject: [PATCH 124/237] Added: Edit button functionality --- proto/anki/frontend.proto | 5 +++++ qt/aqt/mediasrv.py | 13 +++++++++++-- .../reviewer/reviewer-bottom/ReviewerBottom.svelte | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 358edfda6..7d56b0027 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -34,6 +34,7 @@ service FrontendService { // Plays the listed AV tags rpc PlayAVTags(PlayAVTagsRequest) returns (generic.Empty); + rpc displayEditMenu(DisplayEditMenuRequest) returns (generic.Empty); } service BackendFrontendService {} @@ -50,4 +51,8 @@ message SetSchedulingStatesRequest { message PlayAVTagsRequest { repeated card_rendering.AVTag tags = 1; +} + +message DisplayEditMenuRequest { + optional int64 cid = 1; } \ No newline at end of file diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 232f5b383..2986aea43 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -28,10 +28,10 @@ import aqt import aqt.main import aqt.operations from anki import hooks -from anki.cards import Card +from anki.cards import Card, CardId from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode from anki.decks import UpdateDeckConfigs -from anki.frontend_pb2 import PlayAVTagsRequest +from anki.frontend_pb2 import DisplayEditMenuRequest, PlayAVTagsRequest from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest from anki.scheduler_pb2 import NextCardDataResponse from anki.template import ( @@ -698,6 +698,14 @@ def play_avtags(): play_tags(av_tags_to_native(req.tags)) +def display_edit_menu(): + req = DisplayEditMenuRequest.FromString(request.data) + aqt.mw.reviewer.card = aqt.mw.col.get_card( + CardId(req.cid) if req.HasField("cid") else None + ) + aqt.mw.taskman.run_on_main(aqt.mw.onEditCurrent) + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -716,6 +724,7 @@ post_handler_list = [ save_custom_colours, next_card_data, play_avtags, + display_edit_menu, ] diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte index 39160026e..2442f1b86 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -6,11 +6,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import "./index.scss"; import AnswerButton from "./AnswerButton.svelte"; - import { bridgeCommand } from "@tslib/bridgecommand"; import * as tr from "@generated/ftl"; import type { ReviewerState } from "../reviewer"; import Remaining from "./Remaining.svelte"; import More from "./More.svelte"; + import { displayEditMenu } from "@generated/backend"; export let state: ReviewerState; @@ -26,7 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
From df58a2b36db7146b67c7b05db583e4fc62ed8913 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 09:06:45 +0000 Subject: [PATCH 125/237] Fix: no "reviewerStorage" config default --- rslib/src/backend/config.rs | 10 ++++++++-- ts/routes/reviewer/reviewer.ts | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index 9bc1140a2..88bc34601 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -58,8 +58,14 @@ impl From for StringKey { impl crate::services::ConfigService for Collection { fn get_config_json(&mut self, input: generic::String) -> Result { - let val: Option = self.get_config_optional(input.val.as_str()); - val.or_not_found(input.val) + let key = input.val.as_str(); + let val: Option = self.get_config_optional(key); + let default = match key { + "reviewerStorage" => Some(serde_json::from_str("{}").unwrap()), + _ => None, + }; + val.or(default) + .or_not_found(key) .and_then(|v| serde_json::to_vec(&v).map_err(Into::into)) .map(Into::into) } diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 2c2e5a612..6185d1b0c 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -41,7 +41,7 @@ export class ReviewerState { async onReady() { this.iframe!.style.visibility = "visible"; - const { json } = await getConfigJson({ val: "reviewer_storage" }); + const { json } = await getConfigJson({ val: "reviewerStorage" }); this.sendInnerRequest({ type: "setstorage", json_buffer: json }); this.showQuestion(null); addEventListener("message", this.onMessage.bind(this)); @@ -64,7 +64,7 @@ export class ReviewerState { } case "setstorage": { setConfigJson({ - key: "reviewer_storage", + key: "reviewerStorage", valueJson: e.data.json_buffer, undoable: false, }); From 7f256566c8fba37e7332917f062581b8c795d6d3 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 09:18:26 +0000 Subject: [PATCH 126/237] Fix: Enter key for outer reviewer --- ts/routes/reviewer-inner/index.ts | 2 +- ts/routes/reviewer/reviewer.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 67108f940..d94719d21 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -79,7 +79,7 @@ addEventListener("message", async (e: MessageEvent) => { addEventListener("keydown", (e) => { if (e.key === "Enter") { - postParentMessage({ type: "keypress", key: " " }); + postParentMessage({ type: "keypress", key: e.key }); } else if ( e.key.length == 1 && "1234 ".includes(e.key) && !document.activeElement?.matches("input[type=text], input[type=number], textarea") diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 6185d1b0c..e5516837a 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -95,7 +95,8 @@ export class ReviewerState { this.easeButtonPressed(3); break; } - case " ": { + case " ": + case "Enter": { if (!get(this.answerShown)) { this.showAnswer(); } else { From 8adcf30b632c11627c54ad14f10638cf3f6ad403 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 10:04:54 +0000 Subject: [PATCH 127/237] Added: Undo --- qt/aqt/mediasrv.py | 2 ++ ts/routes/reviewer-inner/index.ts | 12 +++++++----- ts/routes/reviewer/reviewer.ts | 27 ++++++++++++++++++++++----- ts/routes/reviewer/reviewerRequest.ts | 2 ++ 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 2986aea43..b7462f568 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -734,6 +734,8 @@ exposed_backend_list = [ "get_custom_colours", "set_config_json", "get_config_json", + "undo", + "redo", # DeckService "get_deck_names", # I18nService diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index d94719d21..48b90077e 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -78,13 +78,15 @@ addEventListener("message", async (e: MessageEvent) => { }); addEventListener("keydown", (e) => { - if (e.key === "Enter") { - postParentMessage({ type: "keypress", key: e.key }); - } else if ( + const keyInfo: ReviewerRequest = { type: "keypress", key: e.key, ctrl: e.ctrlKey, shift: e.shiftKey }; + if ( e.key.length == 1 && "1234 ".includes(e.key) - && !document.activeElement?.matches("input[type=text], input[type=number], textarea") ) { - postParentMessage({ type: "keypress", key: e.key }); + if (!document.activeElement?.matches("input[type=text], input[type=number], textarea")) { + postParentMessage(keyInfo); + } + } else { + postParentMessage(keyInfo); } }); diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index e5516837a..7d6729499 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; -import { compareAnswer, getConfigJson, nextCardData, playAvtags, setConfigJson } from "@generated/backend"; +import { compareAnswer, getConfigJson, nextCardData, playAvtags, redo, setConfigJson, undo } from "@generated/backend"; import { derived, get, writable } from "svelte/store"; import type { InnerReviewerRequest } from "../reviewer-inner/innerReviewerRequest"; import type { ReviewerRequest } from "./reviewerRequest"; @@ -33,6 +33,7 @@ export class ReviewerState { _cardData: NextCardDataResponse_NextCardData | undefined = undefined; beginAnsweringMs = Date.now(); readonly cardClass = writable(""); + readonly undoMessage = writable(""); readonly answerShown = writable(false); readonly cardData = writable(undefined); readonly answerButtons = derived(this.cardData, ($cardData) => $cardData?.answerButtons ?? []); @@ -59,7 +60,7 @@ export class ReviewerState { break; } case "keypress": { - this.handleKeyPress(e.data.key); + this.handleKeyPress(e.data.key, e.data.ctrl, e.data.shift); break; } case "setstorage": { @@ -77,7 +78,12 @@ export class ReviewerState { iframe.addEventListener("load", this.onReady.bind(this)); } - handleKeyPress(key: string) { + public refresh() { + this.showQuestion(null); + } + + async handleKeyPress(key: string, ctrl: boolean, shift: boolean) { + key = key.toLowerCase(); switch (key) { case "1": { this.easeButtonPressed(0); @@ -96,7 +102,7 @@ export class ReviewerState { break; } case " ": - case "Enter": { + case "enter": { if (!get(this.answerShown)) { this.showAnswer(); } else { @@ -104,11 +110,22 @@ export class ReviewerState { } break; } + case "z": { + if (ctrl) { + if (shift) { + redo({}); + } else { + await undo({}); + } + this.refresh(); + } + break; + } } } onKeyDown(e: KeyboardEvent) { - this.handleKeyPress(e.key); + this.handleKeyPress(e.key, e.ctrlKey, e.shiftKey); } public registerShortcuts() { diff --git a/ts/routes/reviewer/reviewerRequest.ts b/ts/routes/reviewer/reviewerRequest.ts index 8a3f04c9d..67c9d408c 100644 --- a/ts/routes/reviewer/reviewerRequest.ts +++ b/ts/routes/reviewer/reviewerRequest.ts @@ -14,6 +14,8 @@ interface UpdateTypedAnswerMessage { interface KeyPressMessage { type: "keypress"; key: string; + ctrl: boolean; + shift: boolean; } interface SetStorageMessage { From f3ecb8a1900d6dc904e7fd04697bb9d4d207e6e9 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 10:16:22 +0000 Subject: [PATCH 128/237] Fix: Missing redo await --- ts/routes/reviewer/reviewer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 7d6729499..a5ba0fa05 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -113,7 +113,7 @@ export class ReviewerState { case "z": { if (ctrl) { if (shift) { - redo({}); + await redo({}); } else { await undo({}); } From 49a65621f23bc865e5a2cb92ef279cdc97f86be6 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 10:49:31 +0000 Subject: [PATCH 129/237] Added:: ShowIntervalsAboveAnswerButtons --- rslib/src/scheduler/service/mod.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index ba4b02dd5..f6b5842e5 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -403,20 +403,21 @@ impl crate::services::SchedulerService for Collection { let next_card = queue.cards.first(); if let Some(next_card) = next_card { let cid = next_card.card.id; + let deck_config = self.deck_config_for_card(&next_card.card)?; let render = self.render_existing_card(cid, false, true)?; + let show_due = self.get_config_bool(BoolKey::ShowIntervalsAboveAnswerButtons); - let answer_buttons = self - .describe_next_states(&next_card.states)? - .into_iter() - .enumerate() - .map(|(i, due)| AnswerButton { - rating: i as i32, - due, - }) - .collect(); - - let config = self.deck_config_for_card(&next_card.card)?; + let answer_buttons = + self.describe_next_states(&next_card.states)? + .into_iter() + .enumerate() + .map(|(i, due)| AnswerButton { + rating: i as i32, + due: if show_due { due } else { "\u{00A0}".to_string() /*   */ } + }) + .collect(); + // Typed answer replacements static ANSWER_REGEX: LazyLock = @@ -473,7 +474,7 @@ impl crate::services::SchedulerService for Collection { partial_back: rendered_nodes_to_proto(render.anodes), answer_buttons, - autoplay: !config.inner.disable_autoplay, + autoplay: !deck_config.inner.disable_autoplay, typed_answer: typed_answer.map(|answer| TypedAnswer { text: answer.1, args: answer.0, From e9a02f29f559ad55756eb56e765201b73f37dda7 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 11:02:16 +0000 Subject: [PATCH 130/237] ./check --- rslib/src/scheduler/service/mod.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index f6b5842e5..d7bdae42d 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -406,18 +406,21 @@ impl crate::services::SchedulerService for Collection { let deck_config = self.deck_config_for_card(&next_card.card)?; let render = self.render_existing_card(cid, false, true)?; - let show_due = self.get_config_bool(BoolKey::ShowIntervalsAboveAnswerButtons); + let show_due = self.get_config_bool(BoolKey::ShowIntervalsAboveAnswerButtons); - let answer_buttons = - self.describe_next_states(&next_card.states)? - .into_iter() - .enumerate() - .map(|(i, due)| AnswerButton { - rating: i as i32, - due: if show_due { due } else { "\u{00A0}".to_string() /*   */ } - }) - .collect(); - + let answer_buttons = self + .describe_next_states(&next_card.states)? + .into_iter() + .enumerate() + .map(|(i, due)| AnswerButton { + rating: i as i32, + due: if show_due { + due + } else { + "\u{00A0}".to_string() /*   */ + }, + }) + .collect(); // Typed answer replacements static ANSWER_REGEX: LazyLock = From ef4500c7ef6b63a1d4e7309f3687844902a83961 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 15:38:39 +0000 Subject: [PATCH 131/237] Fix: Runopt image occlusion --- ts/routes/reviewer-inner/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 48b90077e..67223abba 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -1,5 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +"use-strict"; import "../base.scss"; import "../../reviewer/reviewer.scss"; @@ -11,12 +12,10 @@ import { enableNightMode } from "../reviewer/reviewer"; import type { ReviewerRequest } from "../reviewer/reviewerRequest"; import type { InnerReviewerRequest } from "./innerReviewerRequest"; -const anki = globalThis.anki || {}; -anki.imageOcclusion = imageOcclusionAPI; -anki.setupImageCloze = imageOcclusionAPI.setup; // deprecated addBrowserClasses(); -Object.defineProperty(window, "anki", { value: anki }); +export const imageOcclusion = imageOcclusionAPI; +export const setupImageCloze = imageOcclusionAPI.setup; // deprecated function postParentMessage(message: ReviewerRequest) { window.parent.postMessage( From 028b28584920598e8846637979f99048cfbdf26d Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 17:04:58 +0000 Subject: [PATCH 132/237] Added: Flag showing --- ts/routes/reviewer/+page.svelte | 8 ++++++++ ts/routes/reviewer/index.scss | 1 + 2 files changed, 9 insertions(+) create mode 100644 ts/routes/reviewer/index.scss diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte index cf574b00d..3eb014f18 100644 --- a/ts/routes/reviewer/+page.svelte +++ b/ts/routes/reviewer/+page.svelte @@ -3,6 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
@@ -23,6 +27,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+{#if flag} +
+{/if} + diff --git a/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte b/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte index 463a66b38..69ab0a576 100644 --- a/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte +++ b/ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte @@ -18,3 +18,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+ + From f13c27d21dd96ddb02187728531d1ba319e62683 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 11:53:42 +0000 Subject: [PATCH 139/237] Add: converted shortcut list --- .../reviewer/reviewer-bottom/More.svelte | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 49bcd0dd7..40739f631 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -23,8 +23,42 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html { colour: tr.actionsFlagPurple(), shortcut: "Ctrl+7" }, ]; + function todo() { + alert("Not Yet Implemented"); + } + const shortcuts = [ - { onClick: state.buryCurrentCard.bind(state), name: "bury", shortcut: "~" }, + { name: tr.studyingBuryCard(), shortcut: "-", onClick: state.buryCurrentCard.bind(state) }, + { name: tr.actionsForgetCard(), shortcut: "Ctrl+Alt+N", onClick: todo }, + { name: tr.actionsSetDueDate(), shortcut: "Ctrl+Shift+D", onClick: todo }, + { name: tr.actionsSuspendCard(), shortcut: "@", onClick: todo }, + { name: tr.actionsOptions(), shortcut: "O", onClick: todo }, + { name: tr.actionsCardInfo(), shortcut: "I", onClick: todo }, + { name: tr.actionsPreviousCardInfo(), shortcut: "Ctrl+Alt+I", onClick: todo }, + + // Notes + { name: tr.studyingMarkNote(), shortcut: "*", onClick: todo }, + { name: tr.studyingBuryNote(), shortcut: "=", onClick: todo }, + { name: tr.studyingSuspendNote(), shortcut: "!", onClick: todo }, + { name: tr.actionsCreateCopy(), shortcut: "Ctrl+Alt+E", onClick: todo }, + { + name: tr.studyingDeleteNote(), + shortcut: /* isMac ? "Ctrl+Backspace" :*/ "Ctrl+Delete", + onClick: todo, + }, + + // Audio + { name: tr.actionsReplayAudio(), shortcut: "R", onClick: todo }, + { name: tr.studyingPauseAudio(), shortcut: "5", onClick: todo }, + { name: tr.studyingAudio5s(), shortcut: "6", onClick: todo }, + { name: tr.studyingAudioAnd5s(), shortcut: "7", onClick: todo }, + { name: tr.studyingRecordOwnVoice(), shortcut: "Shift+V", onClick: todo }, + { name: tr.studyingReplayOwnVoice(), shortcut: "V", onClick: todo }, + { + name: tr.actionsAutoAdvance(), + shortcut: "Shift+A", + onClick: todo /* checked: autoAdvanceEnabled */, + }, ]; function changeFlag(index: number) { From 88756e899b6d1efcb38782353e26ebb5c4cf5d91 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 12:11:02 +0000 Subject: [PATCH 140/237] DisplayEditMenu -> OpenDialogMenu --- proto/anki/frontend.proto | 7 ++++--- qt/aqt/mediasrv.py | 17 +++++++++-------- ts/routes/reviewer/reviewer.ts | 8 ++++++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 7d56b0027..54be57942 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -34,7 +34,7 @@ service FrontendService { // Plays the listed AV tags rpc PlayAVTags(PlayAVTagsRequest) returns (generic.Empty); - rpc displayEditMenu(DisplayEditMenuRequest) returns (generic.Empty); + rpc openDialogMenu(OpenDialogMenuRequest) returns (generic.Empty); } service BackendFrontendService {} @@ -53,6 +53,7 @@ message PlayAVTagsRequest { repeated card_rendering.AVTag tags = 1; } -message DisplayEditMenuRequest { - optional int64 cid = 1; +message OpenDialogMenuRequest { + string name = 1; + optional int64 current_card_id = 2; } \ No newline at end of file diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index a61473b53..e3b1a8b68 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -31,7 +31,7 @@ from anki import hooks from anki.cards import Card, CardId from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode from anki.decks import UpdateDeckConfigs -from anki.frontend_pb2 import DisplayEditMenuRequest, PlayAVTagsRequest +from anki.frontend_pb2 import OpenDialogMenuRequest, PlayAVTagsRequest from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest from anki.scheduler_pb2 import NextCardDataResponse from anki.template import ( @@ -706,12 +706,13 @@ def play_avtags(): play_tags(av_tags_to_native(req.tags)) -def display_edit_menu(): - req = DisplayEditMenuRequest.FromString(request.data) - aqt.mw.reviewer.card = aqt.mw.col.get_card( - CardId(req.cid) if req.HasField("cid") else None - ) - aqt.mw.taskman.run_on_main(aqt.mw.onEditCurrent) +def open_dialog_menu(): + req = OpenDialogMenuRequest.FromString(request.data) + if req.HasField("current_card_id"): + aqt.mw.reviewer.card = aqt.mw.col.get_card( + CardId(req.current_card_id) + ) + aqt.mw.taskman.run_on_main(lambda: aqt.dialogs.open(req.name, aqt.mw)) post_handler_list = [ @@ -732,7 +733,7 @@ post_handler_list = [ save_custom_colours, next_card_data, play_avtags, - display_edit_menu, + open_dialog_menu, ] diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 6679e784b..3ea59fcc7 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -8,7 +8,7 @@ import { import { buryOrSuspendCards, compareAnswer, - displayEditMenu, + openDialogMenu, getConfigJson, nextCardData, playAvtags, @@ -96,8 +96,12 @@ export class ReviewerState { this.showQuestion(null); } + public displayMenu(name: string) { + openDialogMenu({name, currentCardId: this.currentCard?.card?.id }); + } + public displayEditMenu() { - displayEditMenu({ cid: this.currentCard?.card?.id }); + this.displayMenu("EditCurrent") } public buryCurrentCard() { From 38346578ca1c88905dacb94f1012b96812e620d1 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 12:35:37 +0000 Subject: [PATCH 141/237] format shortcut --- ts/routes/reviewer/reviewer-bottom/More.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 40739f631..9e92cb352 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -28,7 +28,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } const shortcuts = [ - { name: tr.studyingBuryCard(), shortcut: "-", onClick: state.buryCurrentCard.bind(state) }, + { + name: tr.studyingBuryCard(), + shortcut: "-", + onClick: state.buryCurrentCard.bind(state), + }, { name: tr.actionsForgetCard(), shortcut: "Ctrl+Alt+N", onClick: todo }, { name: tr.actionsSetDueDate(), shortcut: "Ctrl+Shift+D", onClick: todo }, { name: tr.actionsSuspendCard(), shortcut: "@", onClick: todo }, From 7c88768331f32f80aa5b83ffcf3d23f99a19f09a Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 12:40:35 +0000 Subject: [PATCH 142/237] OpenDialogMenu -> OpenReviewerMenu --- proto/anki/frontend.proto | 4 ++-- qt/aqt/mediasrv.py | 19 +++++++++++-------- ts/routes/reviewer/reviewer.ts | 6 +++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 54be57942..bcb155fab 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -34,7 +34,7 @@ service FrontendService { // Plays the listed AV tags rpc PlayAVTags(PlayAVTagsRequest) returns (generic.Empty); - rpc openDialogMenu(OpenDialogMenuRequest) returns (generic.Empty); + rpc OpenReviewerMenu(openReviewerMenuRequest) returns (generic.Empty); } service BackendFrontendService {} @@ -53,7 +53,7 @@ message PlayAVTagsRequest { repeated card_rendering.AVTag tags = 1; } -message OpenDialogMenuRequest { +message openReviewerMenuRequest { string name = 1; optional int64 current_card_id = 2; } \ No newline at end of file diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index e3b1a8b68..7e22ecaf2 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -31,7 +31,7 @@ from anki import hooks from anki.cards import Card, CardId from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode from anki.decks import UpdateDeckConfigs -from anki.frontend_pb2 import OpenDialogMenuRequest, PlayAVTagsRequest +from anki.frontend_pb2 import PlayAVTagsRequest, openReviewerMenuRequest from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest from anki.scheduler_pb2 import NextCardDataResponse from anki.template import ( @@ -706,13 +706,16 @@ def play_avtags(): play_tags(av_tags_to_native(req.tags)) -def open_dialog_menu(): - req = OpenDialogMenuRequest.FromString(request.data) +REVIEWER_MENUS = { + "EditCurrent": lambda: aqt.mw.taskman.run_on_main(aqt.mw.onEditCurrent), +} + + +def open_reviewer_menu(): + req = openReviewerMenuRequest.FromString(request.data) if req.HasField("current_card_id"): - aqt.mw.reviewer.card = aqt.mw.col.get_card( - CardId(req.current_card_id) - ) - aqt.mw.taskman.run_on_main(lambda: aqt.dialogs.open(req.name, aqt.mw)) + aqt.mw.reviewer.card = aqt.mw.col.get_card(CardId(req.current_card_id)) + REVIEWER_MENUS[req.name]() post_handler_list = [ @@ -733,7 +736,7 @@ post_handler_list = [ save_custom_colours, next_card_data, play_avtags, - open_dialog_menu, + open_reviewer_menu, ] diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 3ea59fcc7..c70420782 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -8,9 +8,9 @@ import { import { buryOrSuspendCards, compareAnswer, - openDialogMenu, getConfigJson, nextCardData, + openReviewerMenu, playAvtags, redo, setConfigJson, @@ -97,11 +97,11 @@ export class ReviewerState { } public displayMenu(name: string) { - openDialogMenu({name, currentCardId: this.currentCard?.card?.id }); + openReviewerMenu({ name, currentCardId: this.currentCard?.card?.id }); } public displayEditMenu() { - this.displayMenu("EditCurrent") + this.displayMenu("EditCurrent"); } public buryCurrentCard() { From ad80e0e6bfffc8f137e44919ae0af3c8279fa67c Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 12:50:48 +0000 Subject: [PATCH 143/237] Added: Menu Set due date --- qt/aqt/mediasrv.py | 12 ++++++------ ts/routes/reviewer/reviewer-bottom/More.svelte | 6 +++++- ts/routes/reviewer/reviewer.ts | 4 ++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 7e22ecaf2..ee2e6cf51 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -706,16 +706,16 @@ def play_avtags(): play_tags(av_tags_to_native(req.tags)) -REVIEWER_MENUS = { - "EditCurrent": lambda: aqt.mw.taskman.run_on_main(aqt.mw.onEditCurrent), -} - - def open_reviewer_menu(): + REVIEWER_MENUS = { + "EditCurrent": aqt.mw.onEditCurrent, + "SetDueDate": aqt.mw.reviewer.on_set_due, + } + req = openReviewerMenuRequest.FromString(request.data) if req.HasField("current_card_id"): aqt.mw.reviewer.card = aqt.mw.col.get_card(CardId(req.current_card_id)) - REVIEWER_MENUS[req.name]() + aqt.mw.taskman.run_on_main(REVIEWER_MENUS[req.name]) post_handler_list = [ diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 9e92cb352..f8c95e183 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -34,7 +34,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html onClick: state.buryCurrentCard.bind(state), }, { name: tr.actionsForgetCard(), shortcut: "Ctrl+Alt+N", onClick: todo }, - { name: tr.actionsSetDueDate(), shortcut: "Ctrl+Shift+D", onClick: todo }, + { + name: tr.actionsSetDueDate(), + shortcut: "Ctrl+Shift+D", + onClick: state.displaySetDueDateMenu.bind(state), + }, { name: tr.actionsSuspendCard(), shortcut: "@", onClick: todo }, { name: tr.actionsOptions(), shortcut: "O", onClick: todo }, { name: tr.actionsCardInfo(), shortcut: "I", onClick: todo }, diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index c70420782..f1422d433 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -104,6 +104,10 @@ export class ReviewerState { this.displayMenu("EditCurrent"); } + public displaySetDueDateMenu() { + this.displayMenu("SetDueDate"); + } + public buryCurrentCard() { if (this.currentCard?.card?.id) { buryOrSuspendCards({ From e1ce4264d9b2fca7b191005f5315a7c80c4461ad Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 12:56:34 +0000 Subject: [PATCH 144/237] Added: More menu "todo" style --- ts/routes/reviewer/reviewer-bottom/More.svelte | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index f8c95e183..7a438f4a2 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -24,7 +24,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ]; function todo() { - alert("Not Yet Implemented"); + alert("Not yet implemented in new reviewer."); } const shortcuts = [ @@ -113,9 +113,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{#each shortcuts as shortcut} - - {shortcut.name} - +
+ + {shortcut.name} + +
{/each}
From 7a64397eb7d4559d57027dd51f5395fd077e09ea Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 13:07:18 +0000 Subject: [PATCH 145/237] Added: Hr to shortcuts --- .../reviewer/reviewer-bottom/More.svelte | 31 +++++++++++++------ ts/routes/reviewer/reviewer-bottom/types.ts | 6 ++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 7a438f4a2..9029620b8 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import MoreItem from "./MoreItem.svelte"; import { setFlag } from "@generated/backend"; import type { ReviewerState } from "../reviewer"; + import type { MoreMenuItemInfo } from "./types"; let showFloating = false; let showFlags = false; @@ -27,7 +28,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html alert("Not yet implemented in new reviewer."); } - const shortcuts = [ + const shortcuts: MoreMenuItemInfo[] = [ { name: tr.studyingBuryCard(), shortcut: "-", @@ -44,6 +45,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html { name: tr.actionsCardInfo(), shortcut: "I", onClick: todo }, { name: tr.actionsPreviousCardInfo(), shortcut: "Ctrl+Alt+I", onClick: todo }, + "hr", // Notes { name: tr.studyingMarkNote(), shortcut: "*", onClick: todo }, { name: tr.studyingBuryNote(), shortcut: "=", onClick: todo }, @@ -55,6 +57,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html onClick: todo, }, + "hr", // Audio { name: tr.actionsReplayAudio(), shortcut: "R", onClick: todo }, { name: tr.studyingPauseAudio(), shortcut: "5", onClick: todo }, @@ -113,15 +116,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{#each shortcuts as shortcut} -
- - {shortcut.name} - -
+ {#if shortcut == "hr"} +
+ {:else} +
+ + {shortcut.name} + +
+ {/if} {/each}
@@ -140,6 +147,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } + hr { + margin: 0; + } + button { line-height: 18px; } diff --git a/ts/routes/reviewer/reviewer-bottom/types.ts b/ts/routes/reviewer/reviewer-bottom/types.ts index 4068ca1f9..a50c617a3 100644 --- a/ts/routes/reviewer/reviewer-bottom/types.ts +++ b/ts/routes/reviewer/reviewer-bottom/types.ts @@ -7,3 +7,9 @@ export interface AnswerButtonInfo { "label": string; "due": string; } + +export type MoreMenuItemInfo = { + name: string; + onClick: () => any; + shortcut: string; +} | "hr"; From d96809cdfa588c5d5567ba392862ca3ba28d061d Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 13:33:14 +0000 Subject: [PATCH 146/237] Added: Naive card info menu --- qt/aqt/mediasrv.py | 11 +++++++++-- ts/routes/reviewer/reviewer-bottom/More.svelte | 6 +++++- ts/routes/reviewer/reviewer.ts | 4 ++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index ee2e6cf51..3b4747940 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -707,14 +707,21 @@ def play_avtags(): def open_reviewer_menu(): + reviewer = aqt.mw.reviewer + + def on_card_info(): + reviewer._card_info.set_card(reviewer.card) + reviewer.on_card_info() + REVIEWER_MENUS = { "EditCurrent": aqt.mw.onEditCurrent, - "SetDueDate": aqt.mw.reviewer.on_set_due, + "SetDueDate": reviewer.on_set_due, + "CardInfo": on_card_info, } req = openReviewerMenuRequest.FromString(request.data) if req.HasField("current_card_id"): - aqt.mw.reviewer.card = aqt.mw.col.get_card(CardId(req.current_card_id)) + reviewer.card = aqt.mw.col.get_card(CardId(req.current_card_id)) aqt.mw.taskman.run_on_main(REVIEWER_MENUS[req.name]) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 9029620b8..1819f92f3 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -42,7 +42,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }, { name: tr.actionsSuspendCard(), shortcut: "@", onClick: todo }, { name: tr.actionsOptions(), shortcut: "O", onClick: todo }, - { name: tr.actionsCardInfo(), shortcut: "I", onClick: todo }, + { + name: tr.actionsCardInfo(), + shortcut: "I", + onClick: state.displayCardInfoMenu.bind(state), + }, { name: tr.actionsPreviousCardInfo(), shortcut: "Ctrl+Alt+I", onClick: todo }, "hr", diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index f1422d433..d8a12ce4e 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -108,6 +108,10 @@ export class ReviewerState { this.displayMenu("SetDueDate"); } + public displayCardInfoMenu() { + this.displayMenu("CardInfo"); + } + public buryCurrentCard() { if (this.currentCard?.card?.id) { buryOrSuspendCards({ From 723bd9a095b31a8201ed301e049e2a88475b496b Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 11 Nov 2025 14:14:13 +0000 Subject: [PATCH 147/237] Added: Bury/Suspend More menu --- .../reviewer/reviewer-bottom/More.svelte | 20 ++++++++++++++---- ts/routes/reviewer/reviewer.ts | 21 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 1819f92f3..3f11251a5 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -32,7 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html { name: tr.studyingBuryCard(), shortcut: "-", - onClick: state.buryCurrentCard.bind(state), + onClick: state.buryOrSuspendCurrentCard.bind(state, false), }, { name: tr.actionsForgetCard(), shortcut: "Ctrl+Alt+N", onClick: todo }, { @@ -40,7 +40,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html shortcut: "Ctrl+Shift+D", onClick: state.displaySetDueDateMenu.bind(state), }, - { name: tr.actionsSuspendCard(), shortcut: "@", onClick: todo }, + { + name: tr.actionsSuspendCard(), + shortcut: "@", + onClick: state.buryOrSuspendCurrentCard.bind(state, true), + }, { name: tr.actionsOptions(), shortcut: "O", onClick: todo }, { name: tr.actionsCardInfo(), @@ -52,8 +56,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html "hr", // Notes { name: tr.studyingMarkNote(), shortcut: "*", onClick: todo }, - { name: tr.studyingBuryNote(), shortcut: "=", onClick: todo }, - { name: tr.studyingSuspendNote(), shortcut: "!", onClick: todo }, + { + name: tr.studyingBuryNote(), + shortcut: "=", + onClick: state.buryOrSuspendCurrentNote.bind(state, false), + }, + { + name: tr.studyingSuspendNote(), + shortcut: "!", + onClick: state.buryOrSuspendCurrentNote.bind(state, true), + }, { name: tr.actionsCreateCopy(), shortcut: "Ctrl+Alt+E", onClick: todo }, { name: tr.studyingDeleteNote(), diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index d8a12ce4e..c67d4bff7 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -112,12 +112,25 @@ export class ReviewerState { this.displayMenu("CardInfo"); } - public buryCurrentCard() { + public buryOrSuspendCurrentCard(suspend: boolean) { + const mode = suspend ? BuryOrSuspendCardsRequest_Mode.SUSPEND : BuryOrSuspendCardsRequest_Mode.BURY_USER; if (this.currentCard?.card?.id) { buryOrSuspendCards({ - cardIds: [this.currentCard?.card?.id], + cardIds: [this.currentCard.card.id], noteIds: [], - mode: BuryOrSuspendCardsRequest_Mode.BURY_USER, + mode, + }); + this.refresh(); + } + } + + public buryOrSuspendCurrentNote(suspend: boolean) { + const mode = suspend ? BuryOrSuspendCardsRequest_Mode.SUSPEND : BuryOrSuspendCardsRequest_Mode.BURY_USER; + if (this.currentCard?.card?.noteId) { + buryOrSuspendCards({ + cardIds: [], + noteIds: [this.currentCard.card.noteId], + mode, }); this.refresh(); } @@ -167,7 +180,7 @@ export class ReviewerState { break; } case "-": { - this.buryCurrentCard(); + this.buryOrSuspendCurrentCard(false); break; } } From 16fe7d6b200cef95af1e5112ac0ccb63d4091c42 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 12 Nov 2025 14:01:27 +0000 Subject: [PATCH 148/237] Added: Flag toggle --- ts/routes/reviewer/reviewer-bottom/More.svelte | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 3f11251a5..29eec1f6e 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -88,12 +88,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }, ]; + const cardData = state.cardData; + function changeFlag(index: number) { - setFlag({ cardIds: [state.currentCard!.card!.id], flag: index }); - state.cardData.update(($cardData) => { - $cardData!.queue!.cards[0].card!.flags = index; - return $cardData; - }); + const card = $cardData!.queue!.cards[0].card!; + if (card.flags === index) { + index = 0; + } + setFlag({ cardIds: [card.id], flag: index }); + $cardData!.queue!.cards[0].card!.flags = index; } From fe4bdf519e40e090e69b5318df9b1507030352c7 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 12 Nov 2025 14:12:50 +0000 Subject: [PATCH 149/237] Added: Selected flag highlight --- .../reviewer/reviewer-bottom/More.svelte | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 29eec1f6e..7577c528b 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -89,13 +89,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ]; const cardData = state.cardData; + $: card = $cardData?.queue?.cards[0].card; function changeFlag(index: number) { - const card = $cardData!.queue!.cards[0].card!; - if (card.flags === index) { + if (card?.flags === index) { index = 0; } - setFlag({ cardIds: [card.id], flag: index }); + setFlag({ cardIds: [card!.id], flag: index }); $cardData!.queue!.cards[0].card!.flags = index; } @@ -124,12 +124,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html From fe753ed9d841d5c39c01d1678ff2102d28d15952 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 12 Nov 2025 15:16:42 +0000 Subject: [PATCH 150/237] Added: Tooltip --- ts/routes/reviewer/Reviewer.svelte | 21 +++++++++++++++++++-- ts/routes/reviewer/reviewer.ts | 22 ++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/ts/routes/reviewer/Reviewer.svelte b/ts/routes/reviewer/Reviewer.svelte index 7c958ecea..7ccd5b934 100644 --- a/ts/routes/reviewer/Reviewer.svelte +++ b/ts/routes/reviewer/Reviewer.svelte @@ -12,9 +12,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html state.registerIFrame(iframe); state.registerShortcuts(); } + $: tooltipMessage = state.tooltipMessage; -
+
+ +
+ {$tooltipMessage} +
From 312cbc2fbae54272144042f9f4263f996fece1b8 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 14 Nov 2025 17:46:41 +0000 Subject: [PATCH 167/237] Added: Timer limit --- proto/anki/scheduler.proto | 1 + rslib/src/scheduler/service/mod.rs | 1 + .../reviewer/reviewer-bottom/Timer.svelte | 43 +++++++++++++------ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index ecd68fa3e..418920ec7 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -313,6 +313,7 @@ message NextCardDataResponse { bool autoplay = 7; bool marked = 13; optional TypedAnswer typed_answer = 12; + uint32 max_time_ms = 14; repeated card_rendering.AVTag question_av_tags = 8; repeated card_rendering.AVTag answer_av_tags = 9; diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index f1cf80b80..74525549a 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -492,6 +492,7 @@ impl crate::services::SchedulerService for Collection { args: answer.0, }), marked, + max_time_ms: deck_config.inner.cap_answer_time_to_secs * 1000, // Filled by python front: "".to_string(), diff --git a/ts/routes/reviewer/reviewer-bottom/Timer.svelte b/ts/routes/reviewer/reviewer-bottom/Timer.svelte index 92bd7c67c..f9ea65819 100644 --- a/ts/routes/reviewer/reviewer-bottom/Timer.svelte +++ b/ts/routes/reviewer/reviewer-bottom/Timer.svelte @@ -4,21 +4,38 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> - -
- {text} -
-
+
+ {text} +
From f5cdf0f22f34e47a047b74c98716109bfecdbdfd Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 14 Nov 2025 18:45:16 +0000 Subject: [PATCH 168/237] Added: Show Timer + Stop timer on answer --- proto/anki/scheduler.proto | 7 ++++++- rslib/src/scheduler/service/mod.rs | 12 +++++++++--- .../reviewer/reviewer-bottom/ReviewerBottom.svelte | 5 +++-- ts/routes/reviewer/reviewer-bottom/Timer.svelte | 11 ++++++++--- ts/routes/reviewer/reviewer.ts | 3 +++ 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 418920ec7..9b489a037 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -302,6 +302,11 @@ message NextCardDataResponse { string args = 2; } + message TimerPreferences { + uint32 max_time_ms = 1; + bool stop_on_answer = 2; + } + message NextCardData { QueuedCards queue = 1; repeated AnswerButton answer_buttons = 2; @@ -313,7 +318,7 @@ message NextCardDataResponse { bool autoplay = 7; bool marked = 13; optional TypedAnswer typed_answer = 12; - uint32 max_time_ms = 14; + optional TimerPreferences timer = 14; repeated card_rendering.AVTag question_av_tags = 8; repeated card_rendering.AVTag answer_av_tags = 9; diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 74525549a..470e3d332 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -11,6 +11,7 @@ use anki_proto::generic; use anki_proto::scheduler; use anki_proto::scheduler::next_card_data_response::AnswerButton; use anki_proto::scheduler::next_card_data_response::NextCardData; +use anki_proto::scheduler::next_card_data_response::TimerPreferences; use anki_proto::scheduler::next_card_data_response::TypedAnswer; use anki_proto::scheduler::ComputeFsrsParamsResponse; use anki_proto::scheduler::ComputeMemoryStateResponse; @@ -403,7 +404,7 @@ impl crate::services::SchedulerService for Collection { let next_card = queue.cards.first(); if let Some(next_card) = next_card { let cid = next_card.card.id; - let deck_config = self.deck_config_for_card(&next_card.card)?; + let deck_config = self.deck_config_for_card(&next_card.card)?.inner; let note = self.get_note(next_card.card.note_id.into())?; let render = self.render_existing_card(cid, false, true)?; @@ -477,6 +478,11 @@ impl crate::services::SchedulerService for Collection { queue.new_count = 0; } + let timer = deck_config.show_timer.then_some(TimerPreferences { + max_time_ms: deck_config.cap_answer_time_to_secs * 1000, + stop_on_answer: deck_config.stop_timer_on_answer, + }); + Ok(NextCardDataResponse { next_card: Some(NextCardData { queue: Some(queue.into()), @@ -486,13 +492,13 @@ impl crate::services::SchedulerService for Collection { partial_back: rendered_nodes_to_proto(render.anodes), answer_buttons, - autoplay: !deck_config.inner.disable_autoplay, + autoplay: !deck_config.disable_autoplay, typed_answer: typed_answer.map(|answer| TypedAnswer { text: answer.1, args: answer.0, }), marked, - max_time_ms: deck_config.inner.cap_answer_time_to_secs * 1000, + timer, // Filled by python front: "".to_string(), diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte index 50c54fe75..6f8e0a55a 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -52,7 +52,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if}
- + {#if $cardData?.timer} + + {/if}
@@ -82,7 +84,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } .more { - // text-align: right; direction: rtl; } diff --git a/ts/routes/reviewer/reviewer-bottom/Timer.svelte b/ts/routes/reviewer/reviewer-bottom/Timer.svelte index f9ea65819..1ca7aae44 100644 --- a/ts/routes/reviewer/reviewer-bottom/Timer.svelte +++ b/ts/routes/reviewer/reviewer-bottom/Timer.svelte @@ -12,14 +12,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let cls = ""; function step() { - let time = Date.now() - state.beginAnsweringMs; - const maxTime = state._cardData?.maxTimeMs ?? 0; + const timerPreferences = state._cardData?.timer; + let time = Date.now(); + if (timerPreferences?.stopOnAnswer && state.answerMs !== undefined) { + time = state.answerMs; + } + time -= state.beginAnsweringMs; + const maxTime = state._cardData?.timer?.maxTimeMs ?? 0; if (time >= maxTime) { time = maxTime; cls = "overtime"; } else { cls = ""; } + text = formatTime(time); } @@ -37,7 +43,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html onDestroy(() => { clearInterval(interval); }); - step(); function formatTime(time: number) { const seconds = time / 1000; diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index eb53e71ca..fcf57662b 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -53,6 +53,7 @@ export class ReviewerState { currentTypedAnswer = ""; _cardData: NextCardDataResponse_NextCardData | undefined = undefined; beginAnsweringMs = Date.now(); + answerMs: number | undefined = undefined; readonly cardClass = writable(""); readonly answerShown = writable(false); readonly cardData = writable(undefined); @@ -310,6 +311,7 @@ export class ReviewerState { } this.beginAnsweringMs = Date.now(); + this.answerMs = undefined; } get currentCard() { @@ -338,6 +340,7 @@ export class ReviewerState { if (this._cardData?.autoplay) { playAvtags({ tags: this._cardData!.answerAvTags }); } + this.answerMs = Date.now(); this.updateHtml(await this.showTypedAnswer(this._cardData?.back || "")); } From acc672afcd67ef9c2b9c1fe3e973b278d01bfa21 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Sat, 15 Nov 2025 21:47:03 +0000 Subject: [PATCH 169/237] Use pre-existing shortcut methods --- ts/lib/tslib/shortcuts.ts | 6 +++- .../reviewer/reviewer-bottom/More.svelte | 30 ++++++++++++++++++- .../reviewer/reviewer-bottom/MoreItem.svelte | 1 - ts/routes/reviewer/reviewer.ts | 10 +++---- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/ts/lib/tslib/shortcuts.ts b/ts/lib/tslib/shortcuts.ts index 0cb0b2f2d..6e7061798 100644 --- a/ts/lib/tslib/shortcuts.ts +++ b/ts/lib/tslib/shortcuts.ts @@ -34,7 +34,10 @@ const keyCodeLookup = { ".": 190, "/": 191, "`": 192, -}; + "!": 49, + "*": 56, + "@": 50, +} as const; function isRequiredModifier(modifier: string): boolean { return !modifier.endsWith("?"); @@ -113,6 +116,7 @@ function keyCombinationToCheck( keyCombination.slice(0, -1), ); + console.log({ keyCode, required, optional }); return check(keyCode, required, optional); } diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 5220a9a02..8b82b228f 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -9,6 +9,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { setFlag } from "@generated/backend"; import type { ReviewerState } from "../reviewer"; import type { MoreMenuItemInfo } from "./types"; + import Shortcut from "$lib/components/Shortcut.svelte"; let showFloating = false; let showFlags = false; @@ -114,8 +115,30 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html setFlag({ cardIds: [card!.id], flag: index }); $cardData!.queue!.cards[0].card!.flags = index; } + + function prepKeycodeForShortcut(keycode: string) { + return keycode.replace("Ctrl", "Control"); + } +{#each shortcuts as shortcut} + {#if shortcut !== "hr"} + + {/if} +{/each} + +{#each flags as flag, i} + changeFlag(i + 1)} + /> +{/each} +
diff --git a/ts/routes/reviewer/reviewer-bottom/MoreItem.svelte b/ts/routes/reviewer/reviewer-bottom/MoreItem.svelte index 7ce0c2262..353adf74f 100644 --- a/ts/routes/reviewer/reviewer-bottom/MoreItem.svelte +++ b/ts/routes/reviewer/reviewer-bottom/MoreItem.svelte @@ -7,7 +7,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
- {shortcut}
diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index fcf57662b..6f5eb6734 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -259,12 +259,10 @@ export class ReviewerState { break; } case "e": { - this.displayEditMenu(); - break; - } - case "-": { - this.buryOrSuspendCurrentCard(false); - break; + if (!ctrl) { + this.displayEditMenu(); + break; + } } } } From 7f29a0d211998d52d873b93301bf95e76983f996 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Sat, 15 Nov 2025 22:20:19 +0000 Subject: [PATCH 170/237] Delete debug code --- ts/lib/tslib/shortcuts.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/lib/tslib/shortcuts.ts b/ts/lib/tslib/shortcuts.ts index 6e7061798..05006058b 100644 --- a/ts/lib/tslib/shortcuts.ts +++ b/ts/lib/tslib/shortcuts.ts @@ -116,7 +116,6 @@ function keyCombinationToCheck( keyCombination.slice(0, -1), ); - console.log({ keyCode, required, optional }); return check(keyCode, required, optional); } From f09cc303b650765cb1bcb3047e6c388ad7c30473 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Sat, 15 Nov 2025 22:21:19 +0000 Subject: [PATCH 171/237] Pass keyboard events from inner reviewer --- ts/routes/reviewer-inner/index.ts | 19 +++++++++++++------ ts/routes/reviewer/reviewer.ts | 4 ++-- ts/routes/reviewer/reviewerRequest.ts | 4 +--- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 8aaa92e69..1a71ac3eb 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -78,14 +78,21 @@ addEventListener("message", async (e: MessageEvent) => { }); addEventListener("keydown", (e) => { - const keyInfo: ReviewerRequest = { type: "keypress", key: e.key, ctrl: e.ctrlKey, shift: e.shiftKey }; + const keyInfo: ReviewerRequest = { + type: "keypress", + eventInit: { + key: e.key, + code: e.code, + altKey: e.altKey, + ctrlKey: e.ctrlKey, + shiftKey: e.shiftKey, + metaKey: e.metaKey, + repeat: e.repeat, + }, + }; if ( - e.key.length == 1 && "1234 ".includes(e.key) + !document.activeElement?.matches("input[type=text], input[type=number], textarea") && e.key !== "Enter" ) { - if (!document.activeElement?.matches("input[type=text], input[type=number], textarea")) { - postParentMessage(keyInfo); - } - } else { postParentMessage(keyInfo); } }); diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 6f5eb6734..6c344baf1 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -84,7 +84,7 @@ export class ReviewerState { break; } case "keypress": { - this.handleKeyPress(e.data.key, e.data.ctrl, e.data.shift); + dispatchEvent(new KeyboardEvent("keydown", e.data.eventInit)); break; } case "setstorage": { @@ -272,7 +272,7 @@ export class ReviewerState { } public registerShortcuts() { - document.addEventListener("keydown", this.onKeyDown.bind(this)); + window.addEventListener("keydown", this.onKeyDown.bind(this)); } sendInnerRequest(message: InnerReviewerRequest) { diff --git a/ts/routes/reviewer/reviewerRequest.ts b/ts/routes/reviewer/reviewerRequest.ts index 67c9d408c..3b9b51fab 100644 --- a/ts/routes/reviewer/reviewerRequest.ts +++ b/ts/routes/reviewer/reviewerRequest.ts @@ -13,9 +13,7 @@ interface UpdateTypedAnswerMessage { interface KeyPressMessage { type: "keypress"; - key: string; - ctrl: boolean; - shift: boolean; + eventInit: KeyboardEventInit; } interface SetStorageMessage { From 80f8ae5b56d7cf1bc493b7f0e749b8add8d21684 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 17 Nov 2025 11:31:15 +0000 Subject: [PATCH 172/237] Fix: Events not passed from inner frame --- ts/routes/reviewer-inner/index.ts | 3 +++ ts/routes/reviewer/reviewer.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ts/routes/reviewer-inner/index.ts b/ts/routes/reviewer-inner/index.ts index 1a71ac3eb..fdecb9e5e 100644 --- a/ts/routes/reviewer-inner/index.ts +++ b/ts/routes/reviewer-inner/index.ts @@ -83,11 +83,14 @@ addEventListener("keydown", (e) => { eventInit: { key: e.key, code: e.code, + keyCode: e.keyCode, + which: e.which, altKey: e.altKey, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, metaKey: e.metaKey, repeat: e.repeat, + bubbles: true, }, }; if ( diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index 6c344baf1..2f9d7f232 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -84,7 +84,7 @@ export class ReviewerState { break; } case "keypress": { - dispatchEvent(new KeyboardEvent("keydown", e.data.eventInit)); + document.dispatchEvent(new KeyboardEvent("keydown", e.data.eventInit)); break; } case "setstorage": { From 7a033964edbe92c9cd1da7d2053aff11699872af Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 17 Nov 2025 11:52:48 +0000 Subject: [PATCH 173/237] Fix: Depracated shortcut "which" code --- ts/lib/tslib/shortcuts.ts | 53 ++++++--------------------------------- 1 file changed, 8 insertions(+), 45 deletions(-) diff --git a/ts/lib/tslib/shortcuts.ts b/ts/lib/tslib/shortcuts.ts index 05006058b..22e639820 100644 --- a/ts/lib/tslib/shortcuts.ts +++ b/ts/lib/tslib/shortcuts.ts @@ -6,39 +6,6 @@ import type { Modifier } from "./keys"; import { checkIfModifierKey, checkModifiers, keyToPlatformString, modifiersToPlatformString } from "./keys"; import { registerPackage } from "./runtime-require"; -const keyCodeLookup = { - Backspace: 8, - Delete: 46, - Tab: 9, - Enter: 13, - F1: 112, - F2: 113, - F3: 114, - F4: 115, - F5: 116, - F6: 117, - F7: 118, - F8: 119, - F9: 120, - F10: 121, - F11: 122, - F12: 123, - "=": 187, - "-": 189, - "[": 219, - "]": 221, - "\\": 220, - ";": 186, - "'": 222, - ",": 188, - ".": 190, - "/": 191, - "`": 192, - "!": 49, - "*": 56, - "@": 50, -} as const; - function isRequiredModifier(modifier: string): boolean { return !modifier.endsWith("?"); } @@ -61,10 +28,8 @@ export function getPlatformString(keyCombinationString: string): string { .join(", "); } -function checkKey(event: KeyboardEvent, key: number): boolean { - // avoid deprecation warning - const which = event["which" + ""]; - return which === key; +function checkKey(event: KeyboardEvent, key: string): boolean { + return event.key.toLowerCase() === key.toLowerCase(); } function partition(predicate: (t: T) => boolean, items: T[]): [T[], T[]] { @@ -96,25 +61,23 @@ function separateRequiredOptionalModifiers( } const check = - (keyCode: number, requiredModifiers: Modifier[], optionalModifiers: Modifier[]) => - (event: KeyboardEvent): boolean => { + (key: string, requiredModifiers: Modifier[], optionalModifiers: Modifier[]) => (event: KeyboardEvent): boolean => { return ( - checkKey(event, keyCode) + checkKey(event, key) && checkModifiers(requiredModifiers, optionalModifiers)(event) ); }; -function keyToCode(key: string): number { - return keyCodeLookup[key] || key.toUpperCase().charCodeAt(0); -} - function keyCombinationToCheck( keyCombination: string[], ): (event: KeyboardEvent) => boolean { - const keyCode = keyToCode(keyCombination[keyCombination.length - 1]); + const keyCode = keyCombination[keyCombination.length - 1]; const [required, optional] = separateRequiredOptionalModifiers( keyCombination.slice(0, -1), ); + if ("@*!".includes(keyCode)) { + optional.push("Shift"); + } return check(keyCode, required, optional); } From 329edabcf12d86d59d3b53842ab5bf5c83a9d674 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 17 Nov 2025 13:27:49 +0000 Subject: [PATCH 174/237] Delete: Console.log --- ts/routes/reviewer/reviewer-bottom/Timer.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer-bottom/Timer.svelte b/ts/routes/reviewer/reviewer-bottom/Timer.svelte index 1ca7aae44..3e5c07470 100644 --- a/ts/routes/reviewer/reviewer-bottom/Timer.svelte +++ b/ts/routes/reviewer/reviewer-bottom/Timer.svelte @@ -35,7 +35,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html interval = setInterval(step, 1000); text = formatTime(0); cls = ""; - console.log("startTimer"); } state.cardData.subscribe(startTimer); From 3c924c66943fb8159d01308356e72839e6feed1f Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 17 Nov 2025 13:35:29 +0000 Subject: [PATCH 175/237] Fix: Rtl more menu --- ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte index 6f8e0a55a..97a04347f 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -84,7 +84,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } .more { - direction: rtl; + display: flex; + justify-content: flex-end; } @media (max-width: 583px) { From ed2ac660b476c7ee51bd2b1b3960dcaad612e125 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 17 Nov 2025 14:02:19 +0000 Subject: [PATCH 176/237] Added: Close more menu on click --- ts/routes/reviewer/reviewer-bottom/More.svelte | 2 +- .../reviewer/reviewer-bottom/MoreSubmenu.svelte | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ts/routes/reviewer/reviewer-bottom/More.svelte b/ts/routes/reviewer/reviewer-bottom/More.svelte index 8b82b228f..26bbbc94d 100644 --- a/ts/routes/reviewer/reviewer-bottom/More.svelte +++ b/ts/routes/reviewer/reviewer-bottom/More.svelte @@ -139,7 +139,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html /> {/each} - +