From e94c97a63af3ec4e36aa1646269d56ffdd09959c Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 12 May 2026 14:50:46 -0400 Subject: [PATCH] Split reviewer toggle and API access --- build/configure/src/web.rs | 11 +++++++++++ ftl/core/preferences.ftl | 1 + qt/aqt/forms/preferences.ui | 13 +++++++++++++ qt/aqt/main.py | 15 +++++++++------ qt/aqt/preferences.py | 2 ++ qt/aqt/webview.py | 24 ++++++++++++++++++++++-- 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index ef2d268bb89..fd169ac2207 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -228,6 +228,17 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> { ":sveltekit" ], )?; + build_page( + "reviewer-inner", + true, + inputs![ + // + ":ts:lib", + ":ts:components", + ":sass", + ":sveltekit" + ], + )?; Ok(()) } diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl index e7c23784997..62bc03e189f 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/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 67cd67a5feb..c4f0705d37c 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -461,6 +461,19 @@ + + + + + 0 + 0 + + + + preferences_use_new_reviewer + + + diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 48512c51aaf..bf61be058a4 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -255,13 +255,11 @@ def setupUI(self) -> None: # 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 @@ def loadCollection(self) -> bool: # 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: @@ -1079,10 +1079,13 @@ def setupOverview(self) -> None: self.overview = Overview(self) - def setupReviewer(self) -> None: - from aqt.reviewer import Reviewer + def setupReviewer(self, new: bool) -> None: + from aqt.reviewer import Reviewer, SvelteReviewer - self.reviewer = 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 f7f616cfa29..f26b603954b 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -136,6 +136,7 @@ def setup_collection(self) -> None: 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( @@ -171,6 +172,7 @@ def update_collection(self, on_done: Callable[[], None]) -> None: 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() editing = self.prefs.editing editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex() diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 8853558b09e..1fe669ac1da 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -12,14 +12,16 @@ from enum import Enum from typing import TYPE_CHECKING, Any, Type, cast +from google.protobuf.json_format import MessageToDict from typing_extensions import TypedDict, Unpack import anki import anki.lang from anki._legacy import deprecated from anki.lang import is_rtl -from anki.utils import hmr_mode, is_lin, is_mac, is_win +from anki.utils import hmr_mode, is_lin, is_mac, is_win, to_json_bytes from aqt import colors, gui_hooks +from aqt.operations import OpChanges from aqt.qt import * from aqt.qt import sip from aqt.theme import theme_manager @@ -131,6 +133,7 @@ def __init__( self._kind = kind self._setupBridge() self.open_links_externally = True + self.open_iframe_links_externally = False def _profileForPage(self, kind: AnkiWebViewKind) -> QWebEngineProfile: have_api_access = kind in ( @@ -142,6 +145,7 @@ def _profileForPage(self, kind: AnkiWebViewKind) -> QWebEngineProfile: AnkiWebViewKind.IMPORT_ANKI_PACKAGE, AnkiWebViewKind.IMPORT_CSV, AnkiWebViewKind.IMPORT_LOG, + AnkiWebViewKind.MAIN, ) global _profile_with_api_access, _profile_without_api_access @@ -252,7 +256,7 @@ def acceptNavigationRequest( ): return super().acceptNavigationRequest(url, navType, isMainFrame) - if not isMainFrame: + if not self.open_iframe_links_externally and not isMainFrame: return True # data: links generated by setHtml() if url.scheme() == "data": @@ -382,6 +386,7 @@ def __init__( self._filterSet = False gui_hooks.theme_did_change.append(self.on_theme_did_change) gui_hooks.body_classes_need_update.append(self.on_body_classes_need_update) + gui_hooks.operation_did_execute.append(self.on_operation_did_execute) qconnect(self.loadFinished, self._on_load_finished) @@ -437,6 +442,9 @@ def eventFilter(self, obj: QObject | None, evt: QEvent | None) -> bool: def set_open_links_externally(self, enable: bool) -> None: self.page().open_links_externally = enable + def set_open_iframe_links_externally(self, enable: bool) -> None: + self.page().open_iframe_links_externally = enable + def onEsc(self) -> None: w = self.parent() while w: @@ -911,6 +919,7 @@ def cleanup(self) -> None: gui_hooks.theme_did_change.remove(self.on_theme_did_change) gui_hooks.body_classes_need_update.remove(self.on_body_classes_need_update) + gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) # defer page cleanup so that in-flight requests have a chance to complete first # https://forums.ankiweb.net/t/error-when-exiting-browsing-when-the-software-is-installed-in-the-path-c-program-files-anki/38363 mw.progress.single_shot(5000, lambda: mw.mediaServer.clear_page_html(id(self))) @@ -952,6 +961,17 @@ def on_body_classes_need_update(self) -> None: f"""document.body.classList.toggle("reduce-motion", {json.dumps(mw.pm.reduce_motion())}); """ ) + def on_operation_did_execute( + self, changes: OpChanges, handler: object | None + ) -> None: + if handler is self.parentWidget(): + return + + changes_json = to_json_bytes(MessageToDict(changes)).decode() + self.eval( + f"if(globalThis.anki && globalThis.anki.onOperationDidExecute) globalThis.anki.onOperationDidExecute({changes_json})" + ) + @deprecated(info="use theme_manager.qcolor() instead") def get_window_bg_color(self, night_mode: bool | None = None) -> QColor: return theme_manager.qcolor(colors.CANVAS)