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: