Merge branch 'main' into Feat/90%-dr-message

This commit is contained in:
Luc Mcgrady 2025-05-15 13:52:31 +01:00 committed by GitHub
commit 3e1d95b24c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 223 additions and 53 deletions

View file

@ -0,0 +1,2 @@
- To build and check the project, use ./check in the root folder (or check.bat on Windows)
- This will format files, then run lints and unit tests.

7
.cursor/rules/i18n.md Normal file
View file

@ -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.

View file

@ -226,6 +226,8 @@ Jonathan Schoreels <https://github.com/JSchoreels>
JL710
Matt Brubeck <mbrubeck@limpet.net>
Yaoliang Chen <yaoliang.ch@gmail.com>
KolbyML <https://github.com/KolbyML>
Adnane Taghi <dev@soleuniverse.me>
********************

View file

@ -1,4 +1,4 @@
FROM rust:1.83.0-alpine3.20 AS builder
FROM rust:1.85.0-alpine3.20 AS builder
ARG ANKI_VERSION

View file

@ -1,4 +1,4 @@
FROM rust:1.83.0 AS builder
FROM rust:1.85.0 AS builder
ARG ANKI_VERSION

@ -1 +1 @@
Subproject commit 376ae99eb47eae3c5e6298150162715423bc2e4e
Subproject commit d2e9201d62b906b029570931e9567fe8cf1889b6

View file

@ -15,6 +15,7 @@ importing-colon = Colon
importing-comma = Comma
importing-empty-first-field = Empty first field: { $val }
importing-field-separator = Field separator
importing-field-separator-guessed = Field separator (guessed)
importing-field-mapping = Field mapping
importing-field-of-file-is = Field <b>{ $val }</b> of file is:
importing-fields-separated-by = Fields separated by: { $val }
@ -217,6 +218,9 @@ importing-field-separator-help =
Please note that if this character appears in any field itself, the field has to be
quoted accordingly to the CSV standard. Spreadsheet programs like LibreOffice will
do this automatically.
It cannot be changed if the text file forces use of a specific separator via a file header.
If a file header is not present, Anki will try to guess what the separator is.
importing-allow-html-in-fields-help =
Enable this if the file contains HTML formatting. E.g. if the file contains the string
'&lt;br&gt;', it will appear as a line break on your card. On the other hand, with this

View file

@ -83,6 +83,15 @@ 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 }.
preferences-url-scheme-allow-once = Allow Once
preferences-url-scheme-always-allow = Always Allow
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
preferences-basic = Basic

View file

@ -229,6 +229,7 @@ statistics-stability-day-single =
# hour range, eg "From 14:00-15:00"
statistics-hours-range = From { $hourStart }:00~{ $hourEnd }:00
statistics-hours-correct = { $correct }/{ $total } correct ({ $percent }%)
statistics-hours-correct-info = → (not 'Again')
# the emoji depicts the graph displaying this number
statistics-hours-reviews = 📊 { $reviews } reviews
# the emoji depicts the graph displaying this number

View file

@ -51,9 +51,9 @@ sync-account-required =
sync-sanity-check-failed = Please use the Check Database function, then sync again. If problems persist, please force a one-way sync in the preferences screen.
sync-clock-off = Unable to sync - your clock is not set to the correct time.
sync-upload-too-large =
Your collection file is too large to send to AnkiWeb. You can reduce its
size by removing any unwanted decks (optionally exporting them first), and
then using Check Database to shrink the file size down. ({ $details })
Your collection file is too large to send to AnkiWeb. You can reduce its size by removing any unwanted decks (optionally exporting them first), and then using Check Database to shrink the file size down.
{ $details } (uncompressed)
sync-sign-in = Sign in
sync-ankihub-dialog-heading = AnkiHub Login
sync-ankihub-username-label = Username or Email:

@ -1 +1 @@
Subproject commit 393bacec35703f91520e4d2feec37ed6953114f4
Subproject commit 5b855702dfe191e06cc3fbef4b684acc094500ec

View file

@ -221,6 +221,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
"Yuki",
"🦙 (siid)",
"Mukunda Madhav Dey",
"Adnane Taghi",
)
)

View file

@ -1244,11 +1244,13 @@ class Browser(QMainWindow):
self._line_edit().selectAll()
def onNote(self) -> None:
assert self.editor is not None
assert self.editor.web is not None
def cb():
assert self.editor is not None and self.editor.web is not None
self.editor.web.setFocus()
self.editor.loadNote(focusTo=0)
self.editor.web.setFocus()
self.editor.loadNote(focusTo=0)
assert self.editor is not None
self.editor.call_after_note_saved(cb)
def onCardList(self) -> None:
self.form.tableView.setFocus()

View file

@ -17,7 +17,7 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
<enum>Qt::FocusPolicy::StrongFocus</enum>
</property>
<property name="currentIndex">
<number>0</number>
@ -78,7 +78,7 @@
</sizepolicy>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
</widget>
</item>
@ -260,7 +260,7 @@
<item>
<spacer name="verticalSpacer_9">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -451,6 +451,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="url_schemes">
<property name="text">
<string>preferences_url_schemes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -466,7 +473,7 @@
<item>
<spacer name="verticalSpacer_12">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -518,10 +525,10 @@
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -614,10 +621,10 @@
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
<enum>QSizePolicy::Policy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -739,7 +746,7 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -827,7 +834,7 @@
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -840,7 +847,7 @@
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -918,10 +925,10 @@
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -953,7 +960,7 @@
<item row="1" column="3">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1020,7 +1027,7 @@
<item row="1" column="1">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1035,10 +1042,10 @@
<item>
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1080,7 +1087,7 @@
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1128,10 +1135,10 @@
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
<enum>QSizePolicy::Policy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1207,7 +1214,7 @@
<item>
<spacer name="verticalspacer_13">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1227,17 +1234,17 @@
<string>preferences_some_settings_will_take_effect_after</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close|QDialogButtonBox::Help</set>
<set>QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Help</set>
</property>
</widget>
</item>
@ -1266,6 +1273,7 @@
<tabstop>showEstimates</tabstop>
<tabstop>spacebar_rates_card</tabstop>
<tabstop>render_latex</tabstop>
<tabstop>url_schemes</tabstop>
<tabstop>pastePNG</tabstop>
<tabstop>paste_strips_formatting</tabstop>
<tabstop>useCurrent</tabstop>

View file

@ -30,6 +30,7 @@ from __future__ import annotations
import inspect
import json
import os
import platform
import select
import socket
import subprocess
@ -40,7 +41,7 @@ import time
from queue import Empty, Full, Queue
from shutil import which
from anki.utils import is_win
from anki.utils import is_mac, is_win
class MPVError(Exception):
@ -88,11 +89,13 @@ class MPVBase:
"--keep-open=no",
"--autoload-files=no",
"--gapless-audio=no",
"--no-ytdl",
]
if is_win:
default_argv += ["--af-add=lavfi=[apad=pad_dur=0.150]"]
if not is_mac or platform.machine() != "arm64":
# our arm64 mpv build doesn't support this option (compiled out)
default_argv += ["--no-ytdl"]
def __init__(self, window_id=None, debug=False):
self.window_id = window_id

View file

@ -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

View file

@ -744,3 +744,17 @@ 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
def always_allow_scheme(self, scheme: str) -> None:
schemes = self.allowed_url_schemes()
if scheme not in schemes:
schemes.append(scheme)
self.set_allowed_url_schemes(schemes)

View file

@ -551,9 +551,11 @@ class Reviewer:
def after_answer(changes: OpChanges) -> None:
if gui_hooks.reviewer_did_answer_card.count() > 0:
self.card.load()
# v3 scheduler doesn't report this
suspended = self.card is not None and self.card.queue < 0
self._after_answering(ease)
if sched.state_is_leech(answer.new_state):
self.onLeech()
self.onLeech(suspended)
self.state = "transition"
answer_card(parent=self.mw, answer=answer).success(
@ -949,11 +951,10 @@ timerStopped = false;
# Leeches
##########################################################################
def onLeech(self, card: Card | None = None) -> None:
def onLeech(self, suspended: bool = False) -> None:
# for now
s = tr.studying_card_was_a_leech()
# v3 scheduler doesn't report this
if card and card.queue < 0:
if suspended:
s += f" {tr.studying_it_has_been_suspended()}"
tooltip(s)

71
qt/aqt/url_schemes.py Normal file
View file

@ -0,0 +1,71 @@
# 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 QMessageBox, Qt, QUrl
from aqt.utils import MessageBox, 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 always_allow_scheme(url: QUrl) -> None:
from aqt import mw
scheme = url.scheme().lower()
mw.pm.always_allow_scheme(scheme)
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:
openLink(url)
elif idx == 1:
always_allow_scheme(url)
openLink(url)
msg = markdown(
tr.preferences_url_scheme_warning(link=url.toString(), scheme=url.scheme())
)
MessageBox(
msg,
buttons=[
tr.preferences_url_scheme_allow_once(),
tr.preferences_url_scheme_always_allow(),
(tr.actions_cancel(), QMessageBox.ButtonRole.RejectRole),
],
parent=mw,
callback=on_button,
textFormat=Qt.TextFormat.RichText,
default_button=2,
icon=QMessageBox.Icon.Warning,
)

View file

@ -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.

View file

@ -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:

View file

@ -158,7 +158,7 @@ impl SqliteStorage {
self.db
.prepare_cached(concat!(
include_str!("get.sql"),
" where ease between 1 and 4",
" where (ease between 1 and 4) or (ease = 0 and factor = 0)",
" order by cid, id"
))?
.query_and_then([], row_to_revlog_entry)?

View file

@ -119,9 +119,13 @@ pub enum UploadResponse {
}
pub fn check_upload_limit(size: usize, limit: usize) -> Result<()> {
let size_of_one_mb: f64 = 1024.0 * 1024.0;
let collection_size_in_mb: f64 = size as f64 / size_of_one_mb;
let limit_size_in_mb: f64 = limit as f64 / size_of_one_mb;
if size >= limit {
Err(AnkiError::sync_error(
format!("{size} > {limit}"),
format!("{collection_size_in_mb:.2} MB > {limit_size_in_mb:.2} MB"),
SyncErrorKind::UploadTooLarge,
))
} else {

View file

@ -75,6 +75,9 @@ input[type="radio"],
input[type="checkbox"] {
cursor: pointer;
}
textarea,
input[type="date"],
input[type="text"] {
border-radius: prop(border-radius);
outline: none;

View file

@ -29,10 +29,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
width: 100%;
-webkit-appearance: none;
appearance: none;
background: var(--canvas-inset);
border: 1px solid var(--border);
border-radius: var(--border-radius);
padding: 1px 0.5em;
outline: none !important;
}
</style>

View file

@ -3,11 +3,25 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { tick } from "svelte";
export let value: number[];
export let defaults: number[];
let stringValue: string;
$: stringValue = render(value);
let taRef: HTMLTextAreaElement;
function updateHeight() {
if (taRef) {
taRef.style.height = "auto";
taRef.style.height = `${taRef.scrollHeight}px`;
}
}
$: {
stringValue = render(value);
tick().then(updateHeight);
}
function render(params: number[]): string {
return params.map((v) => v.toFixed(4)).join(", ");
@ -22,7 +36,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
</script>
<svelte:window onresize={updateHeight} />
<textarea
bind:this={taRef}
value={stringValue}
on:blur={update}
class="w-100"

View file

@ -222,8 +222,23 @@ export function renderButtons(
const button = tr.statisticsAnswerButtonsButtonNumber();
const timesPressed = tr.statisticsAnswerButtonsButtonPressed();
const correctStr = tr.statisticsHoursCorrect(totalCorrect(d.group));
const correctStrInfo = tr.statisticsHoursCorrectInfo();
const pressedStr = `${timesPressed}: ${totalPressedStr(d)}`;
return `${button}: ${d.buttonNum}<br>${pressedStr}<br>${correctStr}`;
let buttonText: string;
if (d.buttonNum === 1) {
buttonText = tr.studyingAgain();
} else if (d.buttonNum === 2) {
buttonText = tr.studyingHard();
} else if (d.buttonNum === 3) {
buttonText = tr.studyingGood();
} else if (d.buttonNum === 4) {
buttonText = tr.studyingEasy();
} else {
buttonText = "";
}
return `${button}: ${d.buttonNum} (${buttonText})<br>${pressedStr}<br>${correctStr} ${correctStrInfo}`;
}
svg.select("g.hover-columns")

View file

@ -263,6 +263,8 @@ export function enableUniformScaling(canvas: fabric.Canvas, obj: fabric.Object):
export function addBorder(obj: fabric.Object): void {
obj.stroke = BORDER_COLOR;
obj.strokeWidth = 1;
obj.strokeUniform = true;
}
export const redraw = (canvas: fabric.Canvas): void => {

View file

@ -65,7 +65,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<SettingTitle
on:click={() => openHelpModal(Object.keys(settings).indexOf("delimiter"))}
>
{settings.delimiter.title}
{$metadata.forceDelimiter
? settings.delimiter.title
: tr.importingFieldSeparatorGuessed()}
</SettingTitle>
</EnumSelectorRow>