diff --git a/.cursor/rules/i18n.md b/.cursor/rules/i18n.md
new file mode 100644
index 000000000..336c9d995
--- /dev/null
+++ b/.cursor/rules/i18n.md
@@ -0,0 +1,7 @@
+- We use the fluent system+code generation for translation.
+- New strings should be added to rslib/core/. Ask for the appropriate file if you're not sure.
+- Assuming a string addons-you-have-count has been added to addons.ftl, that string is accessible in our different languages as follows:
+ - Python: from aqt.utils import tr; msg = tr.addons_you_have_count(count=3)
+ - TypeScript: import * as tr from "@generated/ftl"; tr.addonsYouHaveCount({count: 3})
+ - Rust: collection.tr.addons_you_have_count(3)
+- In Qt .ui files, strings that are marked as translatable will automatically use the registered ftl strings. So a QLabel with a title 'addons_you_have_count' that is marked as translatable will automatically use the translation defined in our addons.ftl file.
diff --git a/.cursor/rules/testing.md b/.cursor/rules/testing.md
new file mode 100644
index 000000000..47a530219
--- /dev/null
+++ b/.cursor/rules/testing.md
@@ -0,0 +1,2 @@
+- To build and check the project, use ./check(.bat)
+- This will format files, then run lints and unit tests.
diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl
index 69a9300a0..a0983dd6c 100644
--- a/ftl/core/preferences.ftl
+++ b/ftl/core/preferences.ftl
@@ -83,6 +83,13 @@ preferences-ankiweb-intro = AnkiWeb is a free service that lets you keep your fl
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.
+## URL scheme related
+preferences-url-schemes = URL Schemes
+preferences-url-scheme-prompt = Allowed { preferences-url-schemes } (space-separated):
+preferences-url-scheme-warning = Blocked attempt to open `{ $link }`, which may be a security issue.
+
+ If you trust the deck author and wish to proceed, you can add `{ $scheme }` to your allowed { preferences-url-schemes }.
+
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
preferences-basic = Basic
diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui
index 34de8c80e..807d4093c 100644
--- a/qt/aqt/forms/preferences.ui
+++ b/qt/aqt/forms/preferences.ui
@@ -17,7 +17,7 @@
-
- Qt::StrongFocus
+ Qt::FocusPolicy::StrongFocus
0
@@ -78,7 +78,7 @@
- QComboBox::AdjustToMinimumContentsLengthWithIcon
+ QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon
@@ -260,7 +260,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -451,6 +451,13 @@
+ -
+
+
+ preferences_url_schemes
+
+
+
@@ -466,7 +473,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -518,10 +525,10 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
- QSizePolicy::Fixed
+ QSizePolicy::Policy::Fixed
@@ -614,10 +621,10 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
- QSizePolicy::Expanding
+ QSizePolicy::Policy::Expanding
@@ -739,7 +746,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -827,7 +834,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -840,7 +847,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -918,10 +925,10 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
- QSizePolicy::Fixed
+ QSizePolicy::Policy::Fixed
@@ -953,7 +960,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -1020,7 +1027,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -1035,10 +1042,10 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
- QSizePolicy::Fixed
+ QSizePolicy::Policy::Fixed
@@ -1080,7 +1087,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -1128,10 +1135,10 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
- QSizePolicy::Maximum
+ QSizePolicy::Policy::Maximum
@@ -1207,7 +1214,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -1227,17 +1234,17 @@
preferences_some_settings_will_take_effect_after
- Qt::AlignCenter
+ Qt::AlignmentFlag::AlignCenter
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QDialogButtonBox::Close|QDialogButtonBox::Help
+ QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Help
@@ -1266,6 +1273,7 @@
showEstimates
spacebar_rates_card
render_latex
+ url_schemes
pastePNG
paste_strips_formatting
useCurrent
diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py
index 37483427a..bd87ef830 100644
--- a/qt/aqt/preferences.py
+++ b/qt/aqt/preferences.py
@@ -20,9 +20,11 @@ from aqt.profiles import VideoDriver
from aqt.qt import *
from aqt.sync import sync_login
from aqt.theme import Theme
+from aqt.url_schemes import show_url_schemes_dialog
from aqt.utils import (
HelpPage,
add_close_shortcut,
+ add_ellipsis_to_action_label,
askUser,
disable_help_button,
is_win,
@@ -152,6 +154,9 @@ class Preferences(QDialog):
form.monthly_backups.setValue(self.prefs.backups.monthly)
form.minutes_between_backups.setValue(self.prefs.backups.minimum_interval_mins)
+ add_ellipsis_to_action_label(self.form.url_schemes)
+ qconnect(self.form.url_schemes.clicked, show_url_schemes_dialog)
+
def update_collection(self, on_done: Callable[[], None]) -> None:
form = self.form
diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py
index d92d7f59f..0fd85ca8f 100644
--- a/qt/aqt/profiles.py
+++ b/qt/aqt/profiles.py
@@ -744,3 +744,9 @@ create table if not exists profiles
def ankihub_username(self) -> str | None:
return self.profile.get("thirdPartyAnkiHubUsername")
+
+ def allowed_url_schemes(self) -> list[str]:
+ return self.profile.get("allowedUrlSchemes", [])
+
+ def set_allowed_url_schemes(self, schemes: list[str]) -> None:
+ self.profile["allowedUrlSchemes"] = schemes
diff --git a/qt/aqt/url_schemes.py b/qt/aqt/url_schemes.py
new file mode 100644
index 000000000..f5ee1110d
--- /dev/null
+++ b/qt/aqt/url_schemes.py
@@ -0,0 +1,58 @@
+# Copyright: Ankitects Pty Ltd and contributors
+# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+from __future__ import annotations
+
+from markdown import markdown
+
+from aqt.qt import Qt, QUrl
+from aqt.utils import ask_user_dialog, getText, openLink, tr
+
+
+def show_url_schemes_dialog() -> None:
+ from aqt import mw
+
+ default = " ".join(mw.pm.allowed_url_schemes())
+ schemes, ok = getText(
+ prompt=tr.preferences_url_scheme_prompt(),
+ title=tr.preferences_url_schemes(),
+ default=default,
+ )
+ if ok:
+ mw.pm.set_allowed_url_schemes(schemes.split(" "))
+ mw.pm.save()
+
+
+def is_supported_scheme(url: QUrl) -> bool:
+ from aqt import mw
+
+ scheme = url.scheme().lower()
+ allowed_schemes = mw.pm.allowed_url_schemes()
+
+ return scheme in allowed_schemes or scheme in ["http", "https"]
+
+
+def open_url_if_supported_scheme(url: QUrl) -> None:
+ from aqt import mw
+
+ if is_supported_scheme(url):
+ openLink(url)
+ else:
+
+ def on_button(idx: int) -> None:
+ if idx == 0:
+ show_url_schemes_dialog()
+
+ msg = markdown(
+ tr.preferences_url_scheme_warning(link=url.toString(), scheme=url.scheme())
+ )
+ ask_user_dialog(
+ msg,
+ buttons=[
+ tr.actions_with_ellipsis(action=tr.preferences_url_schemes()),
+ tr.actions_close(),
+ ],
+ parent=mw,
+ callback=on_button,
+ textFormat=Qt.TextFormat.RichText,
+ )
diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py
index a11eb14b7..6ae8bace8 100644
--- a/qt/aqt/utils.py
+++ b/qt/aqt/utils.py
@@ -1188,7 +1188,7 @@ def disallow_full_screen() -> bool:
)
-def add_ellipsis_to_action_label(*actions: QAction) -> None:
+def add_ellipsis_to_action_label(*actions: QAction | QPushButton) -> None:
"""Pass actions to add '...' to their labels, indicating that more input is
required before they can be performed.
diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py
index 4db506e49..966d3de5a 100644
--- a/qt/aqt/webview.py
+++ b/qt/aqt/webview.py
@@ -266,7 +266,9 @@ class AnkiWebPage(QWebEnginePage):
print("onclick handler needs to return false")
return False
# load all other links in browser
- openLink(url)
+ from aqt.url_schemes import open_url_if_supported_scheme
+
+ open_url_if_supported_scheme(url)
return False
def _onCmd(self, str: str) -> Any: