From 6a7952672725f27af349fef8c7b92d0d7a0a4f83 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 23 Jun 2026 00:40:38 +0100 Subject: [PATCH 01/11] Added: Labs webview --- ftl/core/preferences.ftl | 1 + qt/aqt/forms/preferences.ui | 24 +++++++++++++++++++++--- qt/aqt/mediasrv.py | 1 + qt/aqt/preferences.py | 8 ++++++++ qt/aqt/webview.py | 1 + ts/routes/preferences/+page.svelte | 5 +++++ 6 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 ts/routes/preferences/+page.svelte diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl index e7c23784997..d71bbb81712 100644 --- a/ftl/core/preferences.ftl +++ b/ftl/core/preferences.ftl @@ -84,6 +84,7 @@ preferences-ankihub-not-logged-in = Not currently logged in to AnkiHub. preferences-ankiweb-intro = AnkiWeb is a free service that lets you keep your flashcard data in sync across your devices, and provides a way to recover the data if your device breaks or is lost. preferences-ankihub-intro = AnkiHub provides collaborative deck editing and additional study tools. A paid subscription is required to access certain features. preferences-third-party-description = Third-party services are unaffiliated with and not endorsed by Anki. Use of these services may require payment. +preferences-labs = Labs ## URL scheme related preferences-url-schemes = URL Schemes diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 67cd67a5feb..748d8023962 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -7,7 +7,7 @@ 0 0 636 - 638 + 710 @@ -20,7 +20,7 @@ Qt::FocusPolicy::StrongFocus - 0 + 6 @@ -1236,6 +1236,25 @@ + + + preferences_labs + + + + 0 + + + 0 + + + 0 + + + 0 + + + @@ -1306,7 +1325,6 @@ syncAnkiHubLogout syncAnkiHubLogin buttonBox - tabWidget diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 01dbd8708dc..c7f0d831cf1 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -422,6 +422,7 @@ def is_sveltekit_page(path: str) -> bool: "import-csv", "import-page", "image-occlusion", + "preferences", ] diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index f7f616cfa29..cb7be953b0b 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -32,6 +32,7 @@ showWarning, tr, ) +from aqt.webview import AnkiWebView, AnkiWebViewKind class Preferences(QDialog): @@ -61,6 +62,8 @@ def __init__(self, mw: AnkiQt) -> None: qconnect( self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES) ) + + self._setup_webview() self.silentlyClose = True self.setup_collection() self.setup_profile() @@ -68,6 +71,11 @@ def __init__(self, mw: AnkiQt) -> None: self.setup_configurable_answer_keys() self.show() + def _setup_webview(self) -> None: + self.web = AnkiWebView(kind=AnkiWebViewKind.PREFERENCES) + self.form.labsTab.layout().addWidget(self.web) + self.web.load_sveltekit_page("preferences") + def setup_configurable_answer_keys(self): """ Create a group box in Preferences with widgets that let the user edit answer keys. diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index e5b346b4655..1d55006ba5a 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -59,6 +59,7 @@ class AnkiWebViewKind(Enum): FIELDS = "fields" IMPORT_LOG = "import log" IMPORT_ANKI_PACKAGE = "anki package import" + PREFERENCES = "preferences" class AuthInterceptor(QWebEngineUrlRequestInterceptor): diff --git a/ts/routes/preferences/+page.svelte b/ts/routes/preferences/+page.svelte new file mode 100644 index 00000000000..369e99250b3 --- /dev/null +++ b/ts/routes/preferences/+page.svelte @@ -0,0 +1,5 @@ + + +Hello Labs \ No newline at end of file From 93515be1157e68e89c419e03b534c7eeaf621821 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 23 Jun 2026 01:24:25 +0100 Subject: [PATCH 02/11] Front-end mockup --- ts/routes/preferences/+page.svelte | 43 ++++++++++++++++++++++++++-- ts/routes/preferences/LabItem.svelte | 31 ++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 ts/routes/preferences/LabItem.svelte diff --git a/ts/routes/preferences/+page.svelte b/ts/routes/preferences/+page.svelte index 369e99250b3..e41798454d4 100644 --- a/ts/routes/preferences/+page.svelte +++ b/ts/routes/preferences/+page.svelte @@ -1,5 +1,44 @@ + -Hello Labs \ No newline at end of file +
+ + +
+ + + + +
+
+ + diff --git a/ts/routes/preferences/LabItem.svelte b/ts/routes/preferences/LabItem.svelte new file mode 100644 index 00000000000..a358c9c9319 --- /dev/null +++ b/ts/routes/preferences/LabItem.svelte @@ -0,0 +1,31 @@ + + + + + + From 00fdeca4768bbd810e2151cdbf23103785d60f5c Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 23 Jun 2026 11:08:59 +0100 Subject: [PATCH 03/11] appearance tweaks --- ts/routes/preferences/+page.svelte | 4 ++++ ts/routes/preferences/LabItem.svelte | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ts/routes/preferences/+page.svelte b/ts/routes/preferences/+page.svelte index e41798454d4..6032d7bd502 100644 --- a/ts/routes/preferences/+page.svelte +++ b/ts/routes/preferences/+page.svelte @@ -41,4 +41,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html margin: 1em; align-items: center; } + + :global(body) { + background-color: var(--canvas-elevated) !important; + } diff --git a/ts/routes/preferences/LabItem.svelte b/ts/routes/preferences/LabItem.svelte index a358c9c9319..03aec37b06b 100644 --- a/ts/routes/preferences/LabItem.svelte +++ b/ts/routes/preferences/LabItem.svelte @@ -12,7 +12,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html @@ -20,12 +22,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html From 6f8d528cb4bfb321e21623146763b622fe06ea28 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 23 Jun 2026 11:44:06 +0100 Subject: [PATCH 04/11] fix: assert layout --- qt/aqt/preferences.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index cb7be953b0b..6ef1f1d496d 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -73,7 +73,9 @@ def __init__(self, mw: AnkiQt) -> None: def _setup_webview(self) -> None: self.web = AnkiWebView(kind=AnkiWebViewKind.PREFERENCES) - self.form.labsTab.layout().addWidget(self.web) + layout = self.form.labsTab.layout() + assert layout is not None + layout.addWidget(self.web) self.web.load_sveltekit_page("preferences") def setup_configurable_answer_keys(self): From 6e12923f3085b63da1d3aa8442585a73611e58e4 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 23 Jun 2026 12:02:56 +0100 Subject: [PATCH 05/11] Fix: Wrong default preferences tab --- qt/aqt/forms/preferences.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 748d8023962..71e1bf31f91 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -20,7 +20,7 @@ Qt::FocusPolicy::StrongFocus - 6 + 0 From 9977621f7d31d250e88890d90fede54471478433 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 29 Jun 2026 10:28:29 +0100 Subject: [PATCH 06/11] =?UTF-8?q?Added:=20=E2=9A=A0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ts/routes/preferences/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/preferences/+page.svelte b/ts/routes/preferences/+page.svelte index 6032d7bd502..baac8d2c616 100644 --- a/ts/routes/preferences/+page.svelte +++ b/ts/routes/preferences/+page.svelte @@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
From 2a0091bb0cfe2b9a049adc487f525386c1368974 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 29 Jun 2026 17:59:38 +0100 Subject: [PATCH 07/11] Update qt/aqt/forms/preferences.ui --- qt/aqt/forms/preferences.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 71e1bf31f91..c97c3cadc84 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -6,7 +6,7 @@ 0 0 - 636 + 750 710 From 23807b7e0ffbbd41900e06f1fdc9844bb76d6d6a Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 30 Jun 2026 10:17:02 +0100 Subject: [PATCH 08/11] Added: Warning heading --- ts/routes/preferences/+page.svelte | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ts/routes/preferences/+page.svelte b/ts/routes/preferences/+page.svelte index baac8d2c616..61d5f620d42 100644 --- a/ts/routes/preferences/+page.svelte +++ b/ts/routes/preferences/+page.svelte @@ -3,15 +3,19 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
- + +
+ ⚠️ Experimental Features
+ These features may change, break, or be removed without notice, use at your own risk. +
+
Date: Tue, 30 Jun 2026 10:50:47 +0100 Subject: [PATCH 09/11] decrease font size ./check --- ts/routes/preferences/+page.svelte | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ts/routes/preferences/+page.svelte b/ts/routes/preferences/+page.svelte index 61d5f620d42..09855e9dca9 100644 --- a/ts/routes/preferences/+page.svelte +++ b/ts/routes/preferences/+page.svelte @@ -5,15 +5,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
- ⚠️ Experimental Features
- These features may change, break, or be removed without notice, use at your own risk. + ⚠️ Experimental Features +
+ These features may change, break, or be removed without notice, use at your own + risk.
@@ -27,7 +28,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html title="Example 3" description="This is a long winded description to help demonstrate the tiling of these elements. They should remain in line because I have used the css grid rather than flex boxes for easy use." > - +
@@ -48,5 +49,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html :global(body) { background-color: var(--canvas-elevated) !important; + font-size: 13px; } From 5f386da21b2db72be01545165d53f33ec0488c17 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 30 Jun 2026 17:19:25 +0100 Subject: [PATCH 10/11] feat: Labs interface backend (#5071) closes #5073 Saves the values set in - #5057 Saves it to the currently open collections json config. I'm not sure if this approach might have problems with https://github.com/ankitects/anki/pull/4289#discussion_r2544738518. A situation where the main window has Anki API access while also displaying user supplied JavaScript would be terrible so we should be careful with this. Test by enabling the "ping" labs option and then reload Anki or switch profile to the same profile. --- proto/anki/config.proto | 6 ++++++ pylib/anki/config.py | 13 +++++++++++++ qt/aqt/main.py | 3 +++ qt/aqt/mediasrv.py | 3 +++ qt/aqt/preferences.py | 4 ++++ qt/aqt/webview.py | 1 + rslib/src/backend/config.rs | 10 ++++++++-- ts/lib/components/Switch.svelte | 1 + ts/routes/preferences/+page.svelte | 17 ++++++++++++----- ts/routes/preferences/+page.ts | 16 ++++++++++++++++ ts/routes/preferences/LabItem.svelte | 12 +++++++++++- ts/routes/preferences/json.ts | 21 +++++++++++++++++++++ 12 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 ts/routes/preferences/+page.ts create mode 100644 ts/routes/preferences/json.ts diff --git a/proto/anki/config.proto b/proto/anki/config.proto index ea115f0fc81..354229789ee 100644 --- a/proto/anki/config.proto +++ b/proto/anki/config.proto @@ -66,6 +66,12 @@ message ConfigKey { } } +enum ExperimentalFeatureFlag { + TEST_FLAG = 0; + SVELTE_EDITOR = 1; + SVELTE_REVIEWER = 2; +} + message GetConfigBoolRequest { ConfigKey.Bool key = 1; } diff --git a/pylib/anki/config.py b/pylib/anki/config.py index 775557c848f..c72df83e79d 100644 --- a/pylib/anki/config.py +++ b/pylib/anki/config.py @@ -32,11 +32,14 @@ from anki.utils import from_json_bytes, to_json_bytes Config = config_pb2.ConfigKey +ExperimentFlag = config_pb2.ExperimentalFeatureFlag class ConfigManager: def __init__(self, col: anki.collection.Collection): self.col = col.weakref() + # Saved when the collection is loaded to prevent changes before restart. + self._experiments = self._get_experiments_dirty() def get_immutable(self, key: str) -> Any: try: @@ -44,6 +47,16 @@ def get_immutable(self, key: str) -> Any: except NotFoundError as exc: raise KeyError from exc + def experiment_enabled(self, key: ExperimentFlag.ValueType) -> bool: + return self._experiments.get(str(key), False) + + def _get_experiments_dirty(self) -> dict[str, bool]: + """This fetches the experiments in the state that they are saved in the database. + This should not be used to check if an experiment is enabled because this will update immediately without a restart. + + Use "experiment_enabled" to fetch an active experiment instead.""" + return self.get_immutable("experimentalFeatures") + def set(self, key: str, val: Any) -> None: self.col._backend.set_config_json_no_undo( key=key, diff --git a/qt/aqt/main.py b/qt/aqt/main.py index d25d5a79cb7..eecda929978 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -27,6 +27,7 @@ from anki._legacy import deprecated from anki.buildinfo import version as version_str from anki.collection import Collection, Config, GithubRelease, OpChanges, UndoStatus +from anki.config import ExperimentFlag from anki.decks import DeckDict, DeckId from anki.hooks import runHook from anki.notes import NoteId @@ -527,6 +528,8 @@ def _onsuccess(synced: bool) -> None: onsuccess() if not self.safeMode: self.maybe_check_for_addon_updates(self.setup_auto_update) + if self.col.conf.experiment_enabled(ExperimentFlag.TEST_FLAG): + showInfo('You have the "ping" experiment enabled') last_day_cutoff = self.col.sched.day_cutoff diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index c09ab01d7ed..5791aef27c0 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -766,6 +766,9 @@ def save_custom_colours() -> bytes: # DeckConfigService "get_ignored_before_count", "get_retention_workload", + # ConfigService + "set_config_json", + "get_config_json", ] diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 6ef1f1d496d..3ebc2c0bd71 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -403,6 +403,10 @@ def update_global(self) -> None: self.mw.pm.setUiScale(newScale) restart_required = True + conf = self.mw.col.conf + if conf._get_experiments_dirty() != conf._experiments: + restart_required = True + if restart_required: showInfo(tr.preferences_changes_will_take_effect_when_you()) diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 1d55006ba5a..c7fd8165796 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -143,6 +143,7 @@ def _profileForPage(self, kind: AnkiWebViewKind) -> QWebEngineProfile: AnkiWebViewKind.IMPORT_ANKI_PACKAGE, AnkiWebViewKind.IMPORT_CSV, AnkiWebViewKind.IMPORT_LOG, + AnkiWebViewKind.PREFERENCES, ) global _profile_with_api_access, _profile_without_api_access diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index b6e81ce2ad7..3b731067472 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -57,8 +57,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 { + "experimentalFeatures" => 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/lib/components/Switch.svelte b/ts/lib/components/Switch.svelte index d48f46fb338..343cddf3baa 100644 --- a/ts/lib/components/Switch.svelte +++ b/ts/lib/components/Switch.svelte @@ -20,6 +20,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html class:nightMode={$pageTheme.isDark} bind:checked={value} {disabled} + on:input />
diff --git a/ts/routes/preferences/+page.svelte b/ts/routes/preferences/+page.svelte index 09855e9dca9..a5e1200a16b 100644 --- a/ts/routes/preferences/+page.svelte +++ b/ts/routes/preferences/+page.svelte @@ -2,10 +2,15 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> -
@@ -22,13 +27,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -
diff --git a/ts/routes/preferences/+page.ts b/ts/routes/preferences/+page.ts new file mode 100644 index 00000000000..03d19aa0ee8 --- /dev/null +++ b/ts/routes/preferences/+page.ts @@ -0,0 +1,16 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import { autoSavingPrefs } from "$lib/sveltelib/preferences"; +import type { PageLoad } from "./$types"; +import { getConfigJsonObject, setConfigJsonObject } from "./json"; + +const CONFIG_KEY = "experimentalFeatures"; + +export const load = (async () => { + const labPerfs = await autoSavingPrefs( + () => getConfigJsonObject(CONFIG_KEY), + ($config) => setConfigJsonObject(CONFIG_KEY, $config), + ); + + return { labPerfs }; +}) satisfies PageLoad; diff --git a/ts/routes/preferences/LabItem.svelte b/ts/routes/preferences/LabItem.svelte index 03aec37b06b..ca285b977e9 100644 --- a/ts/routes/preferences/LabItem.svelte +++ b/ts/routes/preferences/LabItem.svelte @@ -4,16 +4,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->