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)