Merge branch 'main' into fsrs-simulator-fixes

This commit is contained in:
Luc 2024-12-16 22:02:21 +00:00
commit affe15f018
180 changed files with 2991 additions and 2157 deletions

View file

@ -38,6 +38,78 @@ strict_optional = True
strict_optional = True
[mypy-aqt.operations.*]
strict_optional = True
[mypy-aqt.editor]
strict_optional = True
[mypy-aqt.importing]
strict_optional = True
[mypy-aqt.preferences]
strict_optional = True
[mypy-aqt.overview]
strict_optional = True
[mypy-aqt.customstudy]
strict_optional = True
[mypy-aqt.taglimit]
strict_optional = True
[mypy-aqt.modelchooser]
strict_optional = True
[mypy-aqt.deckdescription]
strict_optional = True
[mypy-aqt.deckbrowser]
strict_optional = True
[mypy-aqt.studydeck]
strict_optional = True
[mypy-aqt.tts]
strict_optional = True
[mypy-aqt.mediasrv]
strict_optional = True
[mypy-aqt.changenotetype]
strict_optional = True
[mypy-aqt.clayout]
strict_optional = True
[mypy-aqt.fields]
strict_optional = True
[mypy-aqt.filtered_deck]
strict_optional = True
[mypy-aqt.editcurrent]
strict_optional = True
[mypy-aqt.deckoptions]
strict_optional = True
[mypy-aqt.notetypechooser]
strict_optional = True
[mypy-aqt.stats]
strict_optional = True
[mypy-aqt.switch]
strict_optional = True
[mypy-aqt.debug_console]
strict_optional = True
[mypy-aqt.emptycards]
strict_optional = True
[mypy-aqt.flags]
strict_optional = True
[mypy-aqt.mediacheck]
strict_optional = True
[mypy-aqt.theme]
strict_optional = True
[mypy-aqt.toolbar]
strict_optional = True
[mypy-aqt.deckchooser]
strict_optional = True
[mypy-aqt.about]
strict_optional = True
[mypy-aqt.webview]
strict_optional = True
[mypy-aqt.mediasync]
strict_optional = True
[mypy-aqt.package]
strict_optional = True
[mypy-aqt.progress]
strict_optional = True
[mypy-aqt.tagedit]
strict_optional = True
[mypy-aqt.utils]
strict_optional = True
[mypy-aqt.sync]
strict_optional = True
[mypy-anki.scheduler.base]
strict_optional = True
[mypy-anki._backend.rsbridge]

View file

@ -1 +1 @@
24.10
24.11

View file

@ -194,9 +194,13 @@ Gregory Abrasaldo <degeemon@gmail.com>
Taylor Obyen <162023405+taylorobyen@users.noreply.github.com>
Kris Cherven <krischerven@gmail.com>
twwn <github.com/twwn>
Shirish Pokhrel <singurty@gmail.com>
Cy Pokhrel <cy@cy7.sh>
Park Hyunwoo <phu54321@naver.com>
Tomas Fabrizio Orsi <torsi@fi.uba.ar>
Dongjin Ouyang <1113117424@qq.com>
Sawan Sunar <sawansunar24072002@gmail.com>
hideo aoyama <https://github.com/boukendesho>
Ross Brown <rbrownwsws@googlemail.com>
********************

740
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs]
version = "=1.3.1"
version = "=1.4.3"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
# path = "../open-spaced-repetition/fsrs-rs"
@ -55,19 +55,19 @@ unicase = "=2.6.0" # any changes could invalidate sqlite indexes
# normal
ammonia = "4.0.0"
anyhow = "1.0.86"
anyhow = "1.0.90"
apple-bundles = "0.17.0"
async-compression = { version = "0.4.12", features = ["zstd", "tokio"] }
async-stream = "0.3.5"
async-trait = "0.1.82"
async-compression = { version = "0.4.17", features = ["zstd", "tokio"] }
async-stream = "0.3.6"
async-trait = "0.1.83"
axum = { version = "0.7", features = ["multipart", "macros"] }
axum-client-ip = "0.6"
axum-extra = { version = "0.9.3", features = ["typed-header"] }
axum-extra = { version = "0.9.4", features = ["typed-header"] }
blake3 = "1.5.4"
bytes = "1.7.1"
bytes = "1.7.2"
camino = "1.1.9"
chrono = { version = "0.4.38", default-features = false, features = ["std", "clock"] }
clap = { version = "4.3.24", features = ["derive"] }
clap = { version = "4.5.20", features = ["derive"] }
coarsetime = "0.1.34"
convert_case = "0.6.0"
criterion = { version = "0.5.1" }
@ -77,14 +77,14 @@ difflib = "0.4.0"
dirs = "5.0.1"
dunce = "1.0.5"
envy = "0.4.2"
flate2 = "1.0.33"
flate2 = "1.0.34"
fluent = "0.16.1"
fluent-bundle = "0.15.3"
fluent-syntax = "0.11.1"
fnv = "1.0.7"
futures = "0.3.30"
futures = "0.3.31"
glob = "0.3.1"
globset = "0.4.14"
globset = "0.4.15"
hex = "0.4.3"
htmlescape = "0.3.1"
hyper = "1"
@ -92,47 +92,47 @@ id_tree = "1.8.0"
inflections = "1.1.1"
intl-memoizer = "0.5.2"
itertools = "0.13.0"
junction = "1.1.0"
junction = "1.2.0"
lazy_static = "1.5.0"
maplit = "1.0.2"
nom = "7.1.3"
num-format = "0.4.4"
num_cpus = "1.16.0"
num_enum = "0.7.2"
once_cell = "1.19.0"
num_enum = "0.7.3"
once_cell = "1.20.2"
pbkdf2 = { version = "0.12", features = ["simple"] }
phf = { version = "0.11.2", features = ["macros"] }
pin-project = "1.1.5"
plist = "1.5.1"
prettyplease = "0.2.22"
prost = "0.12.3"
prost-build = "0.12.3"
prost-reflect = "0.12.0"
prost-types = "0.12.3"
pin-project = "1.1.6"
plist = "1.7.0"
prettyplease = "0.2.24"
prost = "0.13"
prost-build = "0.13"
prost-reflect = "0.14"
prost-types = "0.13"
pulldown-cmark = "0.9.6"
pyo3 = { version = "0.22.2", features = ["extension-module", "abi3", "abi3-py39"] }
pyo3 = { version = "0.22.5", features = ["extension-module", "abi3", "abi3-py39"] }
rand = "0.8.5"
regex = "1.10.6"
reqwest = { version = "0.12.7", default-features = false, features = ["json", "socks", "stream", "multipart"] }
regex = "1.11.0"
reqwest = { version = "0.12.8", default-features = false, features = ["json", "socks", "stream", "multipart"] }
rusqlite = { version = "0.30.0", features = ["trace", "functions", "collation", "bundled"] }
rustls-pemfile = "2.1.3"
rustls-pemfile = "2.2.0"
scopeguard = "1.2.0"
serde = { version = "1.0.209", features = ["derive"] }
serde = { version = "1.0.210", features = ["derive"] }
serde-aux = "4.5.0"
serde_json = "1.0.127"
serde_json = "1.0.132"
serde_repr = "0.1.19"
serde_tuple = "0.5.0"
sha1 = "0.10.6"
sha2 = { version = "0.10.8" }
simple-file-manifest = "0.11.0"
snafu = { version = "0.8.4", features = ["rust_1_61"] }
snafu = { version = "0.8.5", features = ["rust_1_61"] }
strum = { version = "0.26.3", features = ["derive"] }
syn = { version = "2.0.77", features = ["parsing", "printing"] }
tar = "0.4.41"
tempfile = "3.12.0"
syn = { version = "2.0.82", features = ["parsing", "printing"] }
tar = "0.4.42"
tempfile = "3.13.0"
termcolor = "1.4.1"
tokio = { version = "1.38", features = ["fs", "rt-multi-thread", "macros", "signal"] }
tokio-util = { version = "0.7.11", features = ["io"] }
tokio = { version = "1.40", features = ["fs", "rt-multi-thread", "macros", "signal"] }
tokio-util = { version = "0.7.12", features = ["io"] }
tower-http = { version = "0.5", features = ["trace"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracing-appender = "0.2.3"
@ -140,11 +140,10 @@ tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
tugger-windows-codesign = "0.10.0"
unic-langid = { version = "0.9.5", features = ["macros"] }
unic-ucd-category = "0.9.0"
unicode-normalization = "0.1.23"
utime = "0.3.1"
unicode-normalization = "0.1.24"
walkdir = "2.5.0"
which = "5.0.0"
wiremock = "0.6.1"
wiremock = "0.6.2"
xz2 = "0.1.7"
zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] }
zstd = { version = "0.13.2", features = ["zstdmt"] }

View file

@ -142,7 +142,7 @@ impl BuildAction for BuildWheel {
let tag = if let Some(platform) = self.platform {
let platform = match platform {
Platform::LinuxX64 => "manylinux_2_28_x86_64",
Platform::LinuxX64 => "manylinux_2_31_x86_64",
Platform::LinuxArm => "manylinux_2_31_aarch64",
Platform::MacX64 => "macosx_10_13_x86_64",
Platform::MacArm => "macosx_11_0_arm64",

View file

@ -1,7 +1,7 @@
[
{
"name": "addr2line",
"version": "0.22.0",
"version": "0.24.2",
"authors": null,
"repository": "https://github.com/gimli-rs/addr2line",
"license": "Apache-2.0 OR MIT",
@ -136,7 +136,7 @@
},
{
"name": "anyhow",
"version": "1.0.86",
"version": "1.0.90",
"authors": "David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/dtolnay/anyhow",
"license": "Apache-2.0 OR MIT",
@ -145,7 +145,7 @@
},
{
"name": "arrayref",
"version": "0.3.8",
"version": "0.3.9",
"authors": "David Roundy <roundyd@physics.oregonstate.edu>",
"repository": "https://github.com/droundy/arrayref",
"license": "BSD-2-Clause",
@ -181,7 +181,7 @@
},
{
"name": "async-compression",
"version": "0.4.12",
"version": "0.4.17",
"authors": "Wim Looman <wim@nemo157.com>|Allen Bui <fairingrey@gmail.com>",
"repository": "https://github.com/Nullus157/async-compression",
"license": "Apache-2.0 OR MIT",
@ -190,7 +190,7 @@
},
{
"name": "async-stream",
"version": "0.3.5",
"version": "0.3.6",
"authors": "Carl Lerche <me@carllerche.com>",
"repository": "https://github.com/tokio-rs/async-stream",
"license": "MIT",
@ -199,7 +199,7 @@
},
{
"name": "async-stream-impl",
"version": "0.3.5",
"version": "0.3.6",
"authors": "Carl Lerche <me@carllerche.com>",
"repository": "https://github.com/tokio-rs/async-stream",
"license": "MIT",
@ -208,7 +208,7 @@
},
{
"name": "async-trait",
"version": "0.1.82",
"version": "0.1.83",
"authors": "David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/dtolnay/async-trait",
"license": "Apache-2.0 OR MIT",
@ -226,7 +226,7 @@
},
{
"name": "autocfg",
"version": "1.3.0",
"version": "1.4.0",
"authors": "Josh Stone <cuviper@gmail.com>",
"repository": "https://github.com/cuviper/autocfg",
"license": "Apache-2.0 OR MIT",
@ -235,7 +235,7 @@
},
{
"name": "axum",
"version": "0.7.5",
"version": "0.7.7",
"authors": null,
"repository": "https://github.com/tokio-rs/axum",
"license": "MIT",
@ -244,7 +244,7 @@
},
{
"name": "axum-client-ip",
"version": "0.6.0",
"version": "0.6.1",
"authors": null,
"repository": "https://github.com/imbolc/axum-client-ip",
"license": "MIT",
@ -253,7 +253,7 @@
},
{
"name": "axum-core",
"version": "0.4.3",
"version": "0.4.5",
"authors": null,
"repository": "https://github.com/tokio-rs/axum",
"license": "MIT",
@ -262,7 +262,7 @@
},
{
"name": "axum-extra",
"version": "0.9.3",
"version": "0.9.4",
"authors": null,
"repository": "https://github.com/tokio-rs/axum",
"license": "MIT",
@ -271,7 +271,7 @@
},
{
"name": "axum-macros",
"version": "0.4.1",
"version": "0.4.2",
"authors": null,
"repository": "https://github.com/tokio-rs/axum",
"license": "MIT",
@ -280,7 +280,7 @@
},
{
"name": "backtrace",
"version": "0.3.73",
"version": "0.3.74",
"authors": "The Rust Project Developers",
"repository": "https://github.com/rust-lang/backtrace-rs",
"license": "Apache-2.0 OR MIT",
@ -550,7 +550,7 @@
},
{
"name": "bytemuck",
"version": "1.17.1",
"version": "1.19.0",
"authors": "Lokathor <zefria@gmail.com>",
"repository": "https://github.com/Lokathor/bytemuck",
"license": "Apache-2.0 OR MIT OR Zlib",
@ -559,7 +559,7 @@
},
{
"name": "bytemuck_derive",
"version": "1.7.1",
"version": "1.8.0",
"authors": "Lokathor <zefria@gmail.com>",
"repository": "https://github.com/Lokathor/bytemuck",
"license": "Apache-2.0 OR MIT OR Zlib",
@ -577,7 +577,7 @@
},
{
"name": "bytes",
"version": "1.7.1",
"version": "1.7.2",
"authors": "Carl Lerche <me@carllerche.com>|Sean McArthur <sean@seanmonstar.com>",
"repository": "https://github.com/tokio-rs/bytes",
"license": "MIT",
@ -622,7 +622,7 @@
},
{
"name": "cc",
"version": "1.1.15",
"version": "1.1.31",
"authors": "Alex Crichton <alex@alexcrichton.com>",
"repository": "https://github.com/rust-lang/cc-rs",
"license": "Apache-2.0 OR MIT",
@ -775,7 +775,7 @@
},
{
"name": "cpufeatures",
"version": "0.2.13",
"version": "0.2.14",
"authors": "RustCrypto Developers",
"repository": "https://github.com/RustCrypto/utils",
"license": "Apache-2.0 OR MIT",
@ -1000,7 +1000,7 @@
},
{
"name": "enum-as-inner",
"version": "0.6.0",
"version": "0.6.1",
"authors": "Benjamin Fry <benjaminfry@me.com>",
"repository": "https://github.com/bluejekyll/enum-as-inner",
"license": "Apache-2.0 OR MIT",
@ -1081,7 +1081,7 @@
},
{
"name": "fdeflate",
"version": "0.3.4",
"version": "0.3.5",
"authors": "The image-rs Developers",
"repository": "https://github.com/image-rs/fdeflate",
"license": "Apache-2.0 OR MIT",
@ -1099,7 +1099,7 @@
},
{
"name": "flate2",
"version": "1.0.33",
"version": "1.0.34",
"authors": "Alex Crichton <alex@alexcrichton.com>|Josh Triplett <josh@joshtriplett.org>",
"repository": "https://github.com/rust-lang/flate2-rs",
"license": "Apache-2.0 OR MIT",
@ -1144,7 +1144,7 @@
},
{
"name": "flume",
"version": "0.11.0",
"version": "0.11.1",
"authors": "Joshua Barretto <joshua.s.barretto@gmail.com>",
"repository": "https://github.com/zesterer/flume",
"license": "Apache-2.0 OR MIT",
@ -1225,7 +1225,7 @@
},
{
"name": "fsrs",
"version": "1.3.1",
"version": "1.4.3",
"authors": "Open Spaced Repetition",
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
"license": "BSD-3-Clause",
@ -1243,7 +1243,7 @@
},
{
"name": "futures",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1252,7 +1252,7 @@
},
{
"name": "futures-channel",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1261,7 +1261,7 @@
},
{
"name": "futures-core",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1270,7 +1270,7 @@
},
{
"name": "futures-executor",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1288,7 +1288,7 @@
},
{
"name": "futures-io",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1297,7 +1297,7 @@
},
{
"name": "futures-macro",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1306,7 +1306,7 @@
},
{
"name": "futures-sink",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1315,7 +1315,7 @@
},
{
"name": "futures-task",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1324,7 +1324,7 @@
},
{
"name": "futures-util",
"version": "0.3.30",
"version": "0.3.31",
"authors": null,
"repository": "https://github.com/rust-lang/futures-rs",
"license": "Apache-2.0 OR MIT",
@ -1432,7 +1432,7 @@
},
{
"name": "gimli",
"version": "0.29.0",
"version": "0.31.1",
"authors": null,
"repository": "https://github.com/gimli-rs/gimli",
"license": "Apache-2.0 OR MIT",
@ -1477,7 +1477,7 @@
},
{
"name": "gix-trace",
"version": "0.1.9",
"version": "0.1.10",
"authors": "Sebastian Thiel <sebastian.thiel@icloud.com>",
"repository": "https://github.com/Byron/gitoxide",
"license": "Apache-2.0 OR MIT",
@ -1592,6 +1592,15 @@
"license_file": null,
"description": "A Rust port of Google's SwissTable hash map"
},
{
"name": "hashbrown",
"version": "0.15.2",
"authors": "Amanieu d'Antras <amanieu@gmail.com>",
"repository": "https://github.com/rust-lang/hashbrown",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "A Rust port of Google's SwissTable hash map"
},
{
"name": "hashlink",
"version": "0.8.4",
@ -1729,7 +1738,7 @@
},
{
"name": "httparse",
"version": "1.9.4",
"version": "1.9.5",
"authors": "Sean McArthur <sean@seanmonstar.com>",
"repository": "https://github.com/seanmonstar/httparse",
"license": "Apache-2.0 OR MIT",
@ -1747,7 +1756,7 @@
},
{
"name": "hyper",
"version": "1.4.1",
"version": "1.5.0",
"authors": "Sean McArthur <sean@seanmonstar.com>",
"repository": "https://github.com/hyperium/hyper",
"license": "MIT",
@ -1756,7 +1765,7 @@
},
{
"name": "hyper-rustls",
"version": "0.27.2",
"version": "0.27.3",
"authors": null,
"repository": "https://github.com/rustls/hyper-rustls",
"license": "Apache-2.0 OR ISC OR MIT",
@ -1774,7 +1783,7 @@
},
{
"name": "hyper-util",
"version": "0.1.7",
"version": "0.1.9",
"authors": "Sean McArthur <sean@seanmonstar.com>",
"repository": "https://github.com/hyperium/hyper-util",
"license": "MIT",
@ -1783,7 +1792,7 @@
},
{
"name": "iana-time-zone",
"version": "0.1.60",
"version": "0.1.61",
"authors": "Andrew Straw <strawman@astraw.com>|René Kijewski <rene.kijewski@fu-berlin.de>|Ryan Lopopolo <rjl@hyperbo.la>",
"repository": "https://github.com/strawlab/iana-time-zone",
"license": "Apache-2.0 OR MIT",
@ -1828,7 +1837,7 @@
},
{
"name": "indexmap",
"version": "2.5.0",
"version": "2.6.0",
"authors": null,
"repository": "https://github.com/indexmap-rs/indexmap",
"license": "Apache-2.0 OR MIT",
@ -1873,7 +1882,7 @@
},
{
"name": "ipnet",
"version": "2.9.0",
"version": "2.10.1",
"authors": "Kris Price <kris@krisprice.nz>",
"repository": "https://github.com/krisprice/ipnet",
"license": "Apache-2.0 OR MIT",
@ -1936,7 +1945,7 @@
},
{
"name": "js-sys",
"version": "0.3.70",
"version": "0.3.72",
"authors": "The wasm-bindgen Developers",
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys",
"license": "Apache-2.0 OR MIT",
@ -1981,7 +1990,7 @@
},
{
"name": "libc",
"version": "0.2.158",
"version": "0.2.161",
"authors": "The Rust Project Developers",
"repository": "https://github.com/rust-lang/libc",
"license": "Apache-2.0 OR MIT",
@ -2152,7 +2161,7 @@
},
{
"name": "memmap2",
"version": "0.9.4",
"version": "0.9.5",
"authors": "Dan Burkert <dan@danburkert.com>|Yevhenii Reizner <razrfalcon@gmail.com>",
"repository": "https://github.com/RazrFalcon/memmap2-rs",
"license": "Apache-2.0 OR MIT",
@ -2413,7 +2422,7 @@
},
{
"name": "object",
"version": "0.36.4",
"version": "0.36.5",
"authors": null,
"repository": "https://github.com/gimli-rs/object",
"license": "Apache-2.0 OR MIT",
@ -2422,7 +2431,7 @@
},
{
"name": "once_cell",
"version": "1.19.0",
"version": "1.20.2",
"authors": "Aleksey Kladov <aleksey.kladov@gmail.com>",
"repository": "https://github.com/matklad/once_cell",
"license": "Apache-2.0 OR MIT",
@ -2431,7 +2440,7 @@
},
{
"name": "openssl",
"version": "0.10.66",
"version": "0.10.68",
"authors": "Steven Fackler <sfackler@gmail.com>",
"repository": "https://github.com/sfackler/rust-openssl",
"license": "Apache-2.0",
@ -2458,7 +2467,7 @@
},
{
"name": "openssl-sys",
"version": "0.9.103",
"version": "0.9.104",
"authors": "Alex Crichton <alex@alexcrichton.com>|Steven Fackler <sfackler@gmail.com>",
"repository": "https://github.com/sfackler/rust-openssl",
"license": "MIT",
@ -2638,7 +2647,7 @@
},
{
"name": "pin-project",
"version": "1.1.5",
"version": "1.1.6",
"authors": null,
"repository": "https://github.com/taiki-e/pin-project",
"license": "Apache-2.0 OR MIT",
@ -2647,7 +2656,7 @@
},
{
"name": "pin-project-internal",
"version": "1.1.5",
"version": "1.1.6",
"authors": null,
"repository": "https://github.com/taiki-e/pin-project",
"license": "Apache-2.0 OR MIT",
@ -2674,7 +2683,7 @@
},
{
"name": "pkg-config",
"version": "0.3.30",
"version": "0.3.31",
"authors": "Alex Crichton <alex@alexcrichton.com>",
"repository": "https://github.com/rust-lang/pkg-config-rs",
"license": "Apache-2.0 OR MIT",
@ -2683,7 +2692,7 @@
},
{
"name": "png",
"version": "0.17.13",
"version": "0.17.14",
"authors": "The image-rs Developers",
"repository": "https://github.com/image-rs/image-png",
"license": "Apache-2.0 OR MIT",
@ -2737,13 +2746,22 @@
},
{
"name": "prettyplease",
"version": "0.2.22",
"version": "0.2.24",
"authors": "David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/dtolnay/prettyplease",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "A minimal `syn` syntax tree pretty-printer"
},
{
"name": "priority-queue",
"version": "2.1.1",
"authors": "Gianmarco Garrisi <gianmarcogarrisi@tutanota.com>",
"repository": "https://github.com/garro95/priority-queue",
"license": "LGPL-3.0-or-later OR MPL-2.0",
"license_file": null,
"description": "A Priority Queue implemented as a heap with a function to efficiently change the priority of an item."
},
{
"name": "proc-macro-crate",
"version": "3.2.0",
@ -2764,7 +2782,7 @@
},
{
"name": "proc-macro2",
"version": "1.0.86",
"version": "1.0.88",
"authors": "David Tolnay <dtolnay@gmail.com>|Alex Crichton <alex@alexcrichton.com>",
"repository": "https://github.com/dtolnay/proc-macro2",
"license": "Apache-2.0 OR MIT",
@ -2773,7 +2791,7 @@
},
{
"name": "profiling",
"version": "1.0.15",
"version": "1.0.16",
"authors": "Philip Degarmo <aclysma@gmail.com>",
"repository": "https://github.com/aclysma/profiling",
"license": "Apache-2.0 OR MIT",
@ -2782,7 +2800,7 @@
},
{
"name": "prost",
"version": "0.12.6",
"version": "0.13.3",
"authors": "Dan Burkert <dan@danburkert.com>|Lucio Franco <luciofranco14@gmail.com>|Casper Meijn <casper@meijn.net>|Tokio Contributors <team@tokio.rs>",
"repository": "https://github.com/tokio-rs/prost",
"license": "Apache-2.0",
@ -2791,7 +2809,7 @@
},
{
"name": "prost-build",
"version": "0.12.6",
"version": "0.13.3",
"authors": "Dan Burkert <dan@danburkert.com>|Lucio Franco <luciofranco14@gmail.com>|Casper Meijn <casper@meijn.net>|Tokio Contributors <team@tokio.rs>",
"repository": "https://github.com/tokio-rs/prost",
"license": "Apache-2.0",
@ -2800,7 +2818,7 @@
},
{
"name": "prost-derive",
"version": "0.12.6",
"version": "0.13.3",
"authors": "Dan Burkert <dan@danburkert.com>|Lucio Franco <luciofranco14@gmail.com>|Casper Meijn <casper@meijn.net>|Tokio Contributors <team@tokio.rs>",
"repository": "https://github.com/tokio-rs/prost",
"license": "Apache-2.0",
@ -2809,7 +2827,7 @@
},
{
"name": "prost-reflect",
"version": "0.12.0",
"version": "0.14.2",
"authors": "Andrew Hickman <andrew.hickman1@sky.com>",
"repository": "https://github.com/andrewhickman/prost-reflect",
"license": "Apache-2.0 OR MIT",
@ -2818,7 +2836,7 @@
},
{
"name": "prost-types",
"version": "0.12.6",
"version": "0.13.3",
"authors": "Dan Burkert <dan@danburkert.com>|Lucio Franco <luciofranco14@gmail.com>|Casper Meijn <casper@meijn.net>|Tokio Contributors <team@tokio.rs>",
"repository": "https://github.com/tokio-rs/prost",
"license": "Apache-2.0",
@ -3007,7 +3025,7 @@
},
{
"name": "redox_syscall",
"version": "0.5.3",
"version": "0.5.7",
"authors": "Jeremy Soller <jackpot51@gmail.com>",
"repository": "https://gitlab.redox-os.org/redox-os/syscall",
"license": "MIT",
@ -3025,7 +3043,7 @@
},
{
"name": "regex",
"version": "1.10.6",
"version": "1.11.0",
"authors": "The Rust Project Developers|Andrew Gallant <jamslam@gmail.com>",
"repository": "https://github.com/rust-lang/regex",
"license": "Apache-2.0 OR MIT",
@ -3043,7 +3061,7 @@
},
{
"name": "regex-automata",
"version": "0.4.7",
"version": "0.4.8",
"authors": "The Rust Project Developers|Andrew Gallant <jamslam@gmail.com>",
"repository": "https://github.com/rust-lang/regex/tree/master/regex-automata",
"license": "Apache-2.0 OR MIT",
@ -3061,7 +3079,7 @@
},
{
"name": "regex-syntax",
"version": "0.8.4",
"version": "0.8.5",
"authors": "The Rust Project Developers|Andrew Gallant <jamslam@gmail.com>",
"repository": "https://github.com/rust-lang/regex/tree/master/regex-syntax",
"license": "Apache-2.0 OR MIT",
@ -3079,7 +3097,7 @@
},
{
"name": "reqwest",
"version": "0.12.7",
"version": "0.12.8",
"authors": "Sean McArthur <sean@seanmonstar.com>",
"repository": "https://github.com/seanmonstar/reqwest",
"license": "Apache-2.0 OR MIT",
@ -3151,7 +3169,7 @@
},
{
"name": "rustix",
"version": "0.38.35",
"version": "0.38.37",
"authors": "Dan Gohman <dev@sunfishcode.online>|Jakub Konka <kubkon@jakubkonka.com>",
"repository": "https://github.com/bytecodealliance/rustix",
"license": "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
@ -3160,7 +3178,7 @@
},
{
"name": "rustls",
"version": "0.23.12",
"version": "0.23.18",
"authors": null,
"repository": "https://github.com/rustls/rustls",
"license": "Apache-2.0 OR ISC OR MIT",
@ -3169,7 +3187,7 @@
},
{
"name": "rustls-native-certs",
"version": "0.7.3",
"version": "0.8.0",
"authors": null,
"repository": "https://github.com/rustls/rustls-native-certs",
"license": "Apache-2.0 OR ISC OR MIT",
@ -3178,7 +3196,7 @@
},
{
"name": "rustls-pemfile",
"version": "2.1.3",
"version": "2.2.0",
"authors": null,
"repository": "https://github.com/rustls/pemfile",
"license": "Apache-2.0 OR ISC OR MIT",
@ -3187,7 +3205,7 @@
},
{
"name": "rustls-pki-types",
"version": "1.8.0",
"version": "1.10.0",
"authors": null,
"repository": "https://github.com/rustls/pki-types",
"license": "Apache-2.0 OR MIT",
@ -3196,7 +3214,7 @@
},
{
"name": "rustls-webpki",
"version": "0.102.7",
"version": "0.102.8",
"authors": null,
"repository": "https://github.com/rustls/webpki",
"license": "ISC",
@ -3205,7 +3223,7 @@
},
{
"name": "rustversion",
"version": "1.0.17",
"version": "1.0.18",
"authors": "David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/dtolnay/rustversion",
"license": "Apache-2.0 OR MIT",
@ -3232,7 +3250,7 @@
},
{
"name": "safetensors",
"version": "0.4.4",
"version": "0.4.5",
"authors": null,
"repository": "https://github.com/huggingface/safetensors",
"license": "Apache-2.0",
@ -3259,7 +3277,7 @@
},
{
"name": "schannel",
"version": "0.1.23",
"version": "0.1.26",
"authors": "Steven Fackler <sfackler@gmail.com>|Steffen Butzer <steffen.butzer@outlook.com>",
"repository": "https://github.com/steffengy/schannel-rs",
"license": "MIT",
@ -3295,7 +3313,7 @@
},
{
"name": "security-framework-sys",
"version": "2.11.1",
"version": "2.12.0",
"authors": "Steven Fackler <sfackler@gmail.com>|Kornel <kornel@geekhood.net>",
"repository": "https://github.com/kornelski/rust-security-framework",
"license": "Apache-2.0 OR MIT",
@ -3331,7 +3349,7 @@
},
{
"name": "serde",
"version": "1.0.209",
"version": "1.0.210",
"authors": "Erick Tryzelaar <erick.tryzelaar@gmail.com>|David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/serde-rs/serde",
"license": "Apache-2.0 OR MIT",
@ -3349,7 +3367,7 @@
},
{
"name": "serde_derive",
"version": "1.0.209",
"version": "1.0.210",
"authors": "Erick Tryzelaar <erick.tryzelaar@gmail.com>|David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/serde-rs/serde",
"license": "Apache-2.0 OR MIT",
@ -3358,7 +3376,7 @@
},
{
"name": "serde_json",
"version": "1.0.127",
"version": "1.0.132",
"authors": "Erick Tryzelaar <erick.tryzelaar@gmail.com>|David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/serde-rs/json",
"license": "Apache-2.0 OR MIT",
@ -3520,7 +3538,7 @@
},
{
"name": "snafu",
"version": "0.8.4",
"version": "0.8.5",
"authors": "Jake Goulding <jake.goulding@gmail.com>",
"repository": "https://github.com/shepmaster/snafu",
"license": "Apache-2.0 OR MIT",
@ -3529,7 +3547,7 @@
},
{
"name": "snafu-derive",
"version": "0.8.4",
"version": "0.8.5",
"authors": "Jake Goulding <jake.goulding@gmail.com>",
"repository": "https://github.com/shepmaster/snafu",
"license": "Apache-2.0 OR MIT",
@ -3664,7 +3682,7 @@
},
{
"name": "syn",
"version": "2.0.77",
"version": "2.0.82",
"authors": "David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/dtolnay/syn",
"license": "Apache-2.0 OR MIT",
@ -3718,7 +3736,7 @@
},
{
"name": "tempfile",
"version": "3.12.0",
"version": "3.13.0",
"authors": "Steven Allen <steven@stebalien.com>|The Rust Project Developers|Ashley Mannix <ashleymannix@live.com.au>|Jason White <me@jasonwhite.io>",
"repository": "https://github.com/Stebalien/tempfile",
"license": "Apache-2.0 OR MIT",
@ -3754,7 +3772,7 @@
},
{
"name": "thiserror",
"version": "1.0.63",
"version": "1.0.64",
"authors": "David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/dtolnay/thiserror",
"license": "Apache-2.0 OR MIT",
@ -3763,7 +3781,7 @@
},
{
"name": "thiserror-impl",
"version": "1.0.63",
"version": "1.0.64",
"authors": "David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/dtolnay/thiserror",
"license": "Apache-2.0 OR MIT",
@ -3898,7 +3916,7 @@
},
{
"name": "tokio-util",
"version": "0.7.11",
"version": "0.7.12",
"authors": "Tokio Contributors <team@tokio.rs>",
"repository": "https://github.com/tokio-rs/tokio",
"license": "MIT",
@ -3916,7 +3934,7 @@
},
{
"name": "toml_edit",
"version": "0.22.20",
"version": "0.22.22",
"authors": "Andronik Ordian <write@reusable.software>|Ed Page <eopage@gmail.com>",
"repository": "https://github.com/toml-rs/toml",
"license": "Apache-2.0 OR MIT",
@ -3934,7 +3952,7 @@
},
{
"name": "tower",
"version": "0.4.13",
"version": "0.5.1",
"authors": "Tower Maintainers <team@tower-rs.com>",
"repository": "https://github.com/tower-rs/tower",
"license": "MIT",
@ -4141,7 +4159,7 @@
},
{
"name": "unicode-bidi",
"version": "0.3.15",
"version": "0.3.17",
"authors": "The Servo Project Developers",
"repository": "https://github.com/servo/unicode-bidi",
"license": "Apache-2.0 OR MIT",
@ -4150,7 +4168,7 @@
},
{
"name": "unicode-ident",
"version": "1.0.12",
"version": "1.0.13",
"authors": "David Tolnay <dtolnay@gmail.com>",
"repository": "https://github.com/dtolnay/unicode-ident",
"license": "(MIT OR Apache-2.0) AND Unicode-DFS-2016",
@ -4159,7 +4177,7 @@
},
{
"name": "unicode-normalization",
"version": "0.1.23",
"version": "0.1.24",
"authors": "kwantam <kwantam@gmail.com>|Manish Goregaokar <manishsmail@gmail.com>",
"repository": "https://github.com/unicode-rs/unicode-normalization",
"license": "Apache-2.0 OR MIT",
@ -4168,7 +4186,7 @@
},
{
"name": "unicode-segmentation",
"version": "1.11.0",
"version": "1.12.0",
"authors": "kwantam <kwantam@gmail.com>|Manish Goregaokar <manishsmail@gmail.com>",
"repository": "https://github.com/unicode-rs/unicode-segmentation",
"license": "Apache-2.0 OR MIT",
@ -4177,7 +4195,7 @@
},
{
"name": "unicode-width",
"version": "0.1.13",
"version": "0.1.14",
"authors": "kwantam <kwantam@gmail.com>|Manish Goregaokar <manishsmail@gmail.com>",
"repository": "https://github.com/unicode-rs/unicode-width",
"license": "Apache-2.0 OR MIT",
@ -4186,7 +4204,7 @@
},
{
"name": "unicode-xid",
"version": "0.2.5",
"version": "0.2.6",
"authors": "erick.tryzelaar <erick.tryzelaar@gmail.com>|kwantam <kwantam@gmail.com>|Manish Goregaokar <manishsmail@gmail.com>",
"repository": "https://github.com/unicode-rs/unicode-xid",
"license": "Apache-2.0 OR MIT",
@ -4229,18 +4247,9 @@
"license_file": null,
"description": "Incremental, zero-copy UTF-8 decoding with error handling"
},
{
"name": "utime",
"version": "0.3.1",
"authors": "Hyeon Kim <simnalamburt@gmail.com>",
"repository": "https://github.com/simnalamburt/utime",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "A missing utime function for Rust."
},
{
"name": "uuid",
"version": "1.10.0",
"version": "1.11.0",
"authors": "Ashley Mannix<ashleymannix@live.com.au>|Dylan DPC<dylan.dpc@gmail.com>|Hunar Roop Kahlon<hunar.roop@gmail.com>",
"repository": "https://github.com/uuid-rs/uuid",
"license": "Apache-2.0 OR MIT",
@ -4312,7 +4321,7 @@
},
{
"name": "wasm-bindgen",
"version": "0.2.93",
"version": "0.2.95",
"authors": "The wasm-bindgen Developers",
"repository": "https://github.com/rustwasm/wasm-bindgen",
"license": "Apache-2.0 OR MIT",
@ -4321,7 +4330,7 @@
},
{
"name": "wasm-bindgen-backend",
"version": "0.2.93",
"version": "0.2.95",
"authors": "The wasm-bindgen Developers",
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend",
"license": "Apache-2.0 OR MIT",
@ -4330,7 +4339,7 @@
},
{
"name": "wasm-bindgen-futures",
"version": "0.4.43",
"version": "0.4.45",
"authors": "The wasm-bindgen Developers",
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures",
"license": "Apache-2.0 OR MIT",
@ -4339,7 +4348,7 @@
},
{
"name": "wasm-bindgen-macro",
"version": "0.2.93",
"version": "0.2.95",
"authors": "The wasm-bindgen Developers",
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro",
"license": "Apache-2.0 OR MIT",
@ -4348,7 +4357,7 @@
},
{
"name": "wasm-bindgen-macro-support",
"version": "0.2.93",
"version": "0.2.95",
"authors": "The wasm-bindgen Developers",
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro-support",
"license": "Apache-2.0 OR MIT",
@ -4357,7 +4366,7 @@
},
{
"name": "wasm-bindgen-shared",
"version": "0.2.93",
"version": "0.2.95",
"authors": "The wasm-bindgen Developers",
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/shared",
"license": "Apache-2.0 OR MIT",
@ -4366,7 +4375,7 @@
},
{
"name": "wasm-streams",
"version": "0.4.0",
"version": "0.4.1",
"authors": "Mattias Buelens <mattias@buelens.com>",
"repository": "https://github.com/MattiasBuelens/wasm-streams/",
"license": "Apache-2.0 OR MIT",
@ -4375,7 +4384,7 @@
},
{
"name": "web-sys",
"version": "0.3.70",
"version": "0.3.72",
"authors": "The wasm-bindgen Developers",
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys",
"license": "Apache-2.0 OR MIT",
@ -4393,7 +4402,7 @@
},
{
"name": "webpki-roots",
"version": "0.26.5",
"version": "0.26.6",
"authors": null,
"repository": "https://github.com/rustls/webpki-roots",
"license": "MPL-2.0",
@ -4762,7 +4771,7 @@
},
{
"name": "winnow",
"version": "0.6.18",
"version": "0.6.20",
"authors": null,
"repository": "https://github.com/winnow-rs/winnow",
"license": "MIT",
@ -4771,7 +4780,7 @@
},
{
"name": "wiremock",
"version": "0.6.1",
"version": "0.6.2",
"authors": "Luca Palmieri <rust@lpalmieri.com>",
"repository": "https://github.com/LukeMathWalker/wiremock-rs",
"license": "Apache-2.0 OR MIT",
@ -4780,7 +4789,7 @@
},
{
"name": "xml-rs",
"version": "0.8.21",
"version": "0.8.22",
"authors": "Vladimir Matveev <vmatveev@citrine.cc>",
"repository": "https://github.com/kornelski/xml-rs",
"license": "MIT",

View file

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

View file

@ -1,4 +1,4 @@
FROM rust:1.80.1 AS builder
FROM rust:1.82.0 AS builder
ARG ANKI_VERSION

@ -1 +1 @@
Subproject commit aa374dce927cb12ac6623f1bf3f5c5c14dddf95c
Subproject commit e1545f7f0ddeb617eeb1ca86e82862e552843578

View file

@ -48,9 +48,9 @@ actions-update-card = Update Card
actions-update-deck = Update Deck
actions-forget-card = Reset Card
actions-build-filtered-deck = Build Deck
actions-add-notetype = Add Notetype
actions-remove-notetype = Remove Notetype
actions-update-notetype = Update Notetype
actions-add-notetype = Add Note Type
actions-remove-notetype = Remove Note Type
actions-update-notetype = Update Note Type
actions-update-config = Update Config
actions-card-info = Card Info
actions-previous-card-info = Previous Card Info

View file

@ -7,6 +7,6 @@ adding-history = History
adding-note-deleted = (Note deleted)
adding-shortcut = Shortcut: { $val }
adding-the-first-field-is-empty = The first field is empty.
adding-you-have-a-cloze-deletion-note = You have a cloze notetype but have not made any cloze deletions. Proceed?
adding-cloze-outside-cloze-notetype = Cloze deletion can only be used on cloze notetypes.
adding-you-have-a-cloze-deletion-note = You have a cloze note type but have not made any cloze deletions. Proceed?
adding-cloze-outside-cloze-notetype = Cloze deletion can only be used on cloze note types.
adding-cloze-outside-cloze-field = Cloze deletion can only be used in fields which use the 'cloze:' filter. This is typically the first field.

View file

@ -29,7 +29,7 @@ browsing-change-deck = Change Deck
browsing-change-deck2 = Change Deck...
browsing-change-note-type = Change Note Type
browsing-change-note-type2 = Change Note Type...
browsing-change-notetype = Change Notetype
browsing-change-notetype = Change Note Type
browsing-clear-unused-tags = Clear Unused Tags
browsing-confirm-saved-search-overwrite = A saved search with the name { $name } already exists. Do you want to overwrite it?
browsing-created = Created
@ -142,7 +142,7 @@ browsing-tooltip-card-modified = The last time changes were made to a card, incl
browsing-tooltip-note-modified = The last time changes were made to a note, usually field content or tag edits
browsing-tooltip-card = The name of a card's card template
browsing-tooltip-cards = The number of cards a note has
browsing-tooltip-notetype = The name of a note's notetype
browsing-tooltip-notetype = The name of a note's note type
browsing-tooltip-question = The front side of a card, customisable in the card template editor
browsing-tooltip-answer = The back side of a card, customisable in the card template editor
browsing-studied-today = Studied

View file

@ -23,6 +23,7 @@ card-stats-review-log-type-review = Review
card-stats-review-log-type-relearn = Relearn
card-stats-review-log-type-filtered = Filtered
card-stats-review-log-type-manual = Manual
card-stats-review-log-type-rescheduled = Rescheduled
card-stats-review-log-elapsed-time = Elapsed Time
card-stats-no-card = (No card to display.)
card-stats-custom-data = Custom Data
@ -34,6 +35,8 @@ card-stats-fsrs-forgetting-curve-first-week = First Week
card-stats-fsrs-forgetting-curve-first-month = First Month
card-stats-fsrs-forgetting-curve-first-year = First Year
card-stats-fsrs-forgetting-curve-all-time = All Time
card-stats-fsrs-forgetting-curve-probability-of-recalling = Probability of Recall
card-stats-fsrs-forgetting-curve-desired-retention = Desired Retention
## Window Titles

View file

@ -20,11 +20,11 @@ card-templates-night-mode = Night Mode
# on a mobile device.
card-templates-add-mobile-class = Add Mobile Class
card-templates-preview-settings = Options
card-templates-invalid-template-number = Card template { $number } in notetype '{ $notetype }' has a problem.
card-templates-invalid-template-number = Card template { $number } in note type '{ $notetype }' has a problem.
card-templates-identical-front = The front side is identical to card template { $number }.
card-templates-no-front-field = Expected to find a field replacement on the front of the card template.
card-templates-missing-cloze = Expected to find '{ "{{" }cloze:Text{ "}}" }' or similar on the front and back of the card template.
card-templates-extraneous-cloze = 'cloze:' can only be used on cloze notetypes.
card-templates-extraneous-cloze = 'cloze:' can only be used on cloze note types.
card-templates-see-preview = See the preview for more information.
card-templates-field-not-found = Field '{ $field }' not found.
card-templates-changes-saved = Changes saved.
@ -59,7 +59,7 @@ card-templates-this-will-create-card-proceed =
}
card-templates-type-boxes-warning = Only one typing box per card template is supported.
card-templates-restore-to-default = Restore to Default
card-templates-restore-to-default-confirmation = This will reset all fields and templates in this notetype to their default
card-templates-restore-to-default-confirmation = This will reset all fields and templates in this note type to their default
values, removing any extra fields/templates and their content, and any custom styling. Do you wish to proceed?
card-templates-restored-to-default = Notetype has been restored to its original state.
card-templates-restored-to-default = Note type has been restored to its original state.

View file

@ -8,7 +8,7 @@ change-notetype-will-discard-cards = Will remove the following cards:
change-notetype-fields = Fields
change-notetype-templates = Templates
change-notetype-to-from-cloze =
When changing to or from a Cloze notetype, card numbers remain unchanged.
When changing to or from a Cloze note type, card numbers remain unchanged.
If changing to a regular notetype, and there are more cloze deletions
If changing to a regular note type, and there are more cloze deletions
than available card templates, any extra cards will be removed.

View file

@ -51,7 +51,7 @@ database-check-fixed-invalid-ids =
*[other] Fixed { $count } objects with timestamps in the future.
}
# "db-check" is always in English
database-check-notetypes-recovered = One or more notetypes were missing. The notes that used them have been given new notetypes starting with "db-check", but field names and card design have been lost, so you may be better off restoring from an automatic backup.
database-check-notetypes-recovered = One or more note types were missing. The notes that used them have been given new note types starting with "db-check", but field names and card design have been lost, so you may be better off restoring from an automatic backup.
## Progress info

View file

@ -152,7 +152,7 @@ deck-config-new-gather-priority-tooltip-2 =
`Random cards`: Gathers cards in a random order.
deck-config-new-gather-priority-deck = Deck
deck-config-new-gather-priority-deck-then-random-notes = Deck then random notes
deck-config-new-gather-priority-deck-then-random-notes = Deck, then random notes
deck-config-new-gather-priority-position-lowest-first = Ascending position
deck-config-new-gather-priority-position-highest-first = Descending position
deck-config-new-gather-priority-random-notes = Random notes
@ -207,9 +207,11 @@ deck-config-sort-order-ascending-intervals = Ascending intervals
deck-config-sort-order-descending-intervals = Descending intervals
deck-config-sort-order-ascending-ease = Ascending ease
deck-config-sort-order-descending-ease = Descending ease
deck-config-sort-order-ascending-difficulty = Ascending difficulty
deck-config-sort-order-descending-difficulty = Descending difficulty
deck-config-sort-order-relative-overdueness = Relative overdueness
deck-config-sort-order-ascending-difficulty = Easy cards first
deck-config-sort-order-descending-difficulty = Difficult cards first
deck-config-sort-order-retrievability-ascending = Ascending retrievability
deck-config-sort-order-retrievability-descending = Descending retrievability
deck-config-display-order-will-use-current-deck =
Anki will use the display order from the deck you
select to study, and not any subdecks it may have.
@ -290,6 +292,7 @@ deck-config-easy-days-sunday = Sunday
deck-config-easy-days-normal = Normal
deck-config-easy-days-reduced = Reduced
deck-config-easy-days-minimum = Minimum
deck-config-easy-days-no-normal-days = At least one day should be set to '{ deck-config-easy-days-normal }'.
## Adding/renaming
@ -312,7 +315,7 @@ deck-config-confirm-remove-name = Remove { $name }?
deck-config-save-button = Save
deck-config-save-to-all-subdecks = Save to All Subdecks
deck-config-save-and-optimize = Optimize All Presets
deck-config-revert-button-tooltip = Restore this setting to its default value.
deck-config-revert-button-tooltip = Restore this setting to its default value?
## These strings are shown via the Description button at the bottom of the
## overview screen.
@ -370,9 +373,6 @@ deck-config-historical-retention = Historical retention
deck-config-smaller-is-better = Smaller numbers indicate a better fit to your review history.
deck-config-steps-too-large-for-fsrs = When FSRS is enabled, steps of 1 day or more are not recommended.
deck-config-get-params = Get Params
deck-config-fsrs-on-all-clients =
Please ensure all of your Anki clients are Anki(Mobile) 23.10+ or AnkiDroid 2.17+. FSRS will
not work correctly if one of your clients is older.
deck-config-predicted-minimum-recommended-retention = Minimum recommended retention: { $num }
deck-config-complete = { $num }% complete.
deck-config-iterations = Iteration: { $count }...
@ -435,8 +435,8 @@ deck-config-compute-optimal-weights-tooltip2 =
deck-config-compute-optimal-retention-tooltip4 =
This tool will attempt to find the desired retention value
that will lead to the most material learnt, in the least amount of time. The calculated number can serve as a reference
when deciding what to set your desired retention to. You may wish to choose a higher desired retention, if youre
willing to trade more study time for a greater retention rate. Setting your desired retention lower than the minimum
when deciding what to set your desired retention to. You may wish to choose a higher desired retention if youre
willing to invest more study time to achieve it. Setting your desired retention lower than the minimum
is not recommended, as it will lead to a higher workload, because of the high forgetting rate.
deck-config-please-save-your-changes-first = Please save your changes first.
deck-config-a-100-day-interval =
@ -452,6 +452,7 @@ deck-config-percent-of-reviews =
deck-config-optimizing-preset = Optimizing preset { $current_count }/{ $total_count }...
deck-config-fsrs-must-be-enabled = FSRS must be enabled first.
deck-config-fsrs-params-optimal = The FSRS parameters currently appear to be optimal.
deck-config-fsrs-params-no-reviews = No reviews found. Please check that this preset is assigned to all decks you want to optimize (including subdecks) and try again.
deck-config-wait-for-audio = Wait for audio
deck-config-show-reminder = Show Reminder
@ -523,3 +524,6 @@ deck-config-compute-optimal-retention-tooltip3 =
lead to a higher workload, because of the high forgetting rate.
deck-config-seconds-to-show-question-tooltip-2 = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable.
deck-config-invalid-weights = Parameters must be either left blank to use the defaults, or must be 17 comma-separated numbers.
deck-config-fsrs-on-all-clients =
Please ensure all of your Anki clients are Anki(Mobile) 23.10+ or AnkiDroid 2.17+. FSRS will
not work correctly if one of your clients is older.

View file

@ -24,7 +24,6 @@ decks-order-added = Order added
decks-order-due = Order due
decks-please-select-something = Please select something.
decks-random = Random
decks-relative-overdueness = Relative overdueness
decks-repeat-failed-cards-after = Delay Repeat failed cards after
# e.g. "Delay for Again", "Delay for Hard", "Delay for Good"
decks-delay-for-button = Delay for { $button }
@ -37,3 +36,8 @@ decks-learn-header = Learn
# The count of cards waiting to be reviewed
decks-review-header = Due
decks-zero-minutes-hint = (0 = return card to original deck)
## These strings are no longer used - you do not need to translate them if they
## are not already translated.
decks-relative-overdueness = Relative overdueness

View file

@ -5,7 +5,7 @@ errors-100-tags-max =
A maximum of 100 tags can be selected. Listing the
tags you want instead of the ones you don't want is usually simpler, and there
is no need to select child tags if you have selected a parent tag.
errors-multiple-notetypes-selected = Please select notes from only one notetype.
errors-multiple-notetypes-selected = Please select notes from only one note type.
errors-please-check-database = Please use the Check Database action, then try again.
errors-please-check-media = Please use the Check Media action, then try again.
errors-collection-too-new = This collection requires a newer version of Anki to open.

View file

@ -40,5 +40,5 @@ exporting-processed-media-files =
*[other] Processed { $count } media files...
}
exporting-include-deck = Include deck name
exporting-include-notetype = Include notetype name
exporting-include-notetype = Include note type name
exporting-include-guid = Include unique identifier

View file

@ -8,7 +8,7 @@ importing-anki2-files-are-not-directly-importable = .anki2 files are not directl
importing-appeared-twice-in-file = Appeared twice in file: { $val }
importing-by-default-anki-will-detect-the = By default, Anki will detect the character between fields, such as a tab, comma, and so on. If Anki is detecting the character incorrectly, you can enter it here. Use \t to represent tab.
importing-cannot-merge-notetypes-of-different-kinds =
Cloze notetypes cannot be merged with regular notetypes.
Cloze note types cannot be merged with regular note types.
You may still import the file with '{ importing-merge-notetypes }' disabled.
importing-change = Change
importing-colon = Colon
@ -33,13 +33,13 @@ importing-map-to = Map to { $val }
importing-map-to-tags = Map to Tags
importing-mapped-to = mapped to <b>{ $val }</b>
importing-mapped-to-tags = mapped to <b>Tags</b>
# the action of combining two existing notetypes to create a new one
importing-merge-notetypes = Merge notetypes
# the action of combining two existing note types to create a new one
importing-merge-notetypes = Merge note types
importing-merge-notetypes-help =
If checked, and you or the deck author altered the schema of a notetype, Anki will
If checked, and you or the deck author altered the schema of a note type, Anki will
merge the two versions instead of keeping both.
Altering a notetype's schema means adding, removing, or reordering fields or templates,
Altering a note type's schema means adding, removing, or reordering fields or templates,
or changing the sort field.
As a counterexample, changing the front side of an existing template does *not* constitute
a schema change.
@ -50,7 +50,7 @@ importing-multicharacter-separators-are-not-supported-please = Multi-character s
importing-notes-added-from-file = Notes added from file: { $val }
importing-notes-found-in-file = Notes found in file: { $val }
importing-notes-skipped-as-theyre-already-in = Notes skipped, as up-to-date copies are already in your collection: { $val }
importing-notes-skipped-update-due-to-notetype = Notes not updated, as notetype has been modified since you first imported the notes: { $val }
importing-notes-skipped-update-due-to-notetype = Notes not updated, as note type has been modified since you first imported the notes: { $val }
importing-notes-updated-as-file-had-newer = Notes updated, as file had newer version: { $val }
importing-include-reviews = Include reviews
importing-also-import-progress = Import any learning progress
@ -90,10 +90,10 @@ importing-update-notes = Update notes
importing-update-notes-help =
When to update an existing note in your collection. By default, this is only done
if the matching imported note was more recently modified.
importing-update-notetypes = Update notetypes
importing-update-notetypes = Update note types
importing-update-notetypes-help =
When to update an existing notetype in your collection. By default, this is only done
if the matching imported notetype was more recently modified. Changes to template text
When to update an existing note type in your collection. By default, this is only done
if the matching imported note type was more recently modified. Changes to template text
and styling can always be imported, but for schema changes (e.g. the number or order of
fields has changed), the '{ importing-merge-notetypes }' option will also need to be enabled.
importing-note-added =
@ -148,7 +148,7 @@ importing-file = File
# "Match scope: notetype / notetype and deck". Controls how duplicates are matched.
importing-match-scope = Match scope
# Used with the 'match scope' option
importing-notetype-and-deck = Notetype and deck
importing-notetype-and-deck = Note type and deck
importing-cards-added =
{ $count ->
[one] { $count } card added.
@ -182,8 +182,8 @@ importing-conflicting-notes-skipped =
}
importing-conflicting-notes-skipped2 =
{ $count ->
[one] { $count } note was not imported, because its notetype has changed, and '{ importing-merge-notetypes }' was not enabled.
*[other] { $count } notes were not imported, because their notetype has changed, and '{ importing-merge-notetypes }' was not enabled.
[one] { $count } note was not imported, because its note type has changed, and '{ importing-merge-notetypes }' was not enabled.
*[other] { $count } notes were not imported, because their note type has changed, and '{ importing-merge-notetypes }' was not enabled.
}
importing-import-log = Import Log
importing-no-notes-in-file = No notes found in file.
@ -198,8 +198,8 @@ importing-status = Status
importing-duplicate-note-added = Duplicate note added
importing-added-new-note = New note added
importing-existing-note-skipped = Note skipped, as an up-to-date copy is already in your collection
importing-note-skipped-update-due-to-notetype = Note not updated, as notetype has been modified since you first imported the note
importing-note-skipped-update-due-to-notetype2 = Note not updated, as notetype has been modified since you first imported the note, and '{ importing-merge-notetypes }' was not enabled
importing-note-skipped-update-due-to-notetype = Note not updated, as note type has been modified since you first imported the note
importing-note-skipped-update-due-to-notetype2 = Note not updated, as note type has been modified since you first imported the note, and '{ importing-merge-notetypes }' was not enabled
importing-note-updated-as-file-had-newer = Note updated, as file had newer version
importing-note-skipped-due-to-missing-notetype = Note skipped, as its notetype was missing
importing-note-skipped-due-to-missing-deck = Note skipped, as its deck was missing
@ -216,10 +216,10 @@ importing-allow-html-in-fields-help =
'&lt;br&gt;', it will appear as a line break on your card. On the other hand, with this
option disabled, the literal characters '&lt;br&gt;' will be rendered.
importing-notetype-help =
Newly-imported notes will have this notetype, and only existing notes with this
notetype will be updated.
Newly-imported notes will have this note type, and only existing notes with this
note type will be updated.
You can choose which fields in the file correspond to which notetype fields with the
You can choose which fields in the file correspond to which note type fields with the
mapping tool.
importing-deck-help = Imported cards will be placed in this deck.
importing-existing-notes-help =
@ -229,7 +229,7 @@ importing-existing-notes-help =
- `{ importing-preserve }`: Do nothing.
- `{ importing-duplicate }`: Create a new note.
importing-match-scope-help =
Only existing notes with the same notetype will be checked for duplicates. This can
Only existing notes with the same note type will be checked for duplicates. This can
additionally be restricted to notes with cards in the same deck.
importing-tag-all-notes-help =
These tags will be added to both newly-imported and updated notes.

View file

@ -1,4 +1,4 @@
notetypes-notetype = Notetype
notetypes-notetype = Note Type
## Default field names in newly created note types

View file

@ -48,6 +48,11 @@ statistics-cards =
[one] { $cards } card
*[other] { $cards } cards
}
statistics-notes =
{ $notes ->
[one] { $notes } note
*[other] { $notes } notes
}
# a count of how many cards have been answered, eg "Total: 34 reviews"
statistics-reviews =
{ $reviews ->
@ -220,6 +225,7 @@ statistics-average-answer-time-label = Average answer time
statistics-average = Average
statistics-average-interval = Average interval
statistics-due-tomorrow = Due tomorrow
statistics-daily-load = Daily load
# eg 5 of 15 (33.3%)
statistics-amount-of-total-with-percentage = { $amount } of { $total } ({ $percent }%)
statistics-average-over-period = Average over period

@ -1 +1 @@
Subproject commit 2c629a5543be76a0136bffb428868d946b4c0e3a
Subproject commit e0f9724f75f6248f4e74558b25c3182d4f348bce

View file

@ -19,8 +19,8 @@
"@poppanator/sveltekit-svg": "^5.0.0-svelte5.5",
"@sqltools/formatter": "^1.2.2",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.4.1",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
"@sveltejs/kit": "^2.8.3",
"@sveltejs/vite-plugin-svelte": "4.0.0",
"@types/bootstrap": "^5.0.12",
"@types/codemirror": "^5.60.0",
"@types/d3": "^7.0.0",
@ -47,7 +47,8 @@
"license-checker-rseidelsohn": "=4.3.0",
"prettier": "^2.4.1",
"prettier-plugin-svelte": "^3.2.6",
"svelte": "5.0.0-next.179",
"sass": "<1.77",
"svelte": "5.0.0",
"svelte-check": "^3.4.4",
"svelte-preprocess": "^5.0.4",
"svelte-preprocess-esbuild": "^3.0.1",
@ -55,9 +56,8 @@
"tslib": "^2.0.3",
"tsx": "^3.12.0",
"typescript": "^5.0.4",
"vite": "=5.4.7",
"vitest": "^1.2.1",
"sass": "<1.77"
"vite": "^5.4.10",
"vitest": "^2"
},
"dependencies": {
"@bufbuild/protobuf": "^1.2.1",
@ -81,7 +81,7 @@
},
"resolutions": {
"canvas": "npm:empty-npm-package",
"vite": "=5.4.7"
"cookie": "0.7.0"
},
"browserslist": [
"defaults",

View file

@ -30,6 +30,7 @@ message BackendError {
DB_ERROR = 5;
NETWORK_ERROR = 6;
SYNC_AUTH_ERROR = 7;
SYNC_SERVER_MESSAGE = 23;
SYNC_OTHER_ERROR = 8;
JSON_ERROR = 9;
PROTO_ERROR = 10;

View file

@ -127,21 +127,21 @@ message Progress {
DatabaseCheck database_check = 6;
string importing = 7;
string exporting = 8;
ComputeWeightsProgress compute_weights = 9;
ComputeParamsProgress compute_params = 9;
ComputeRetentionProgress compute_retention = 10;
ComputeMemoryProgress compute_memory = 11;
}
}
message ComputeWeightsProgress {
message ComputeParamsProgress {
// Current iteration
uint32 current = 1;
// Total iterations
uint32 total = 2;
uint32 reviews = 3;
// Only used in 'compute all weights' case
// Only used in 'compute all params' case
uint32 current_preset = 4;
// Only used in 'compute all weights' case
// Only used in 'compute all params' case
uint32 total_presets = 5;
}

View file

@ -55,6 +55,7 @@ message ConfigKey {
SHIFT_POSITION_OF_EXISTING_CARDS = 24;
RENDER_LATEX = 25;
LOAD_BALANCER_ENABLED = 26;
FSRS_SHORT_TERM_WITH_STEPS_ENABLED = 27;
}
enum String {
SET_DUE_BROWSER = 0;
@ -117,6 +118,7 @@ message Preferences {
bool show_intervals_on_buttons = 4;
uint32 time_limit_secs = 5;
bool load_balancer_enabled = 6;
bool fsrs_short_term_with_steps_enabled = 7;
}
message Editing {
bool adding_defaults_to_current_deck = 1;

View file

@ -79,7 +79,8 @@ message DeckConfig {
REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 4;
REVIEW_CARD_ORDER_EASE_ASCENDING = 5;
REVIEW_CARD_ORDER_EASE_DESCENDING = 6;
REVIEW_CARD_ORDER_RELATIVE_OVERDUENESS = 7;
REVIEW_CARD_ORDER_RETRIEVABILITY_ASCENDING = 7;
REVIEW_CARD_ORDER_RETRIEVABILITY_DESCENDING = 11;
REVIEW_CARD_ORDER_RANDOM = 8;
REVIEW_CARD_ORDER_ADDED = 9;
REVIEW_CARD_ORDER_REVERSE_ADDED = 10;
@ -107,9 +108,11 @@ message DeckConfig {
repeated float learn_steps = 1;
repeated float relearn_steps = 2;
repeated float fsrs_weights = 3;
repeated float fsrs_params_4 = 3;
repeated float fsrs_params_5 = 5;
reserved 5 to 8;
// consider saving remaining ones for fsrs param changes
reserved 6 to 8;
uint32 new_per_day = 9;
uint32 reviews_per_day = 10;
@ -163,7 +166,7 @@ message DeckConfig {
// used for fsrs_reschedule in the past
reserved 39;
float historical_retention = 40;
string weight_search = 45;
string param_search = 45;
bytes other = 255;
}
@ -213,7 +216,7 @@ message DeckConfigsForUpdate {
enum UpdateDeckConfigsMode {
UPDATE_DECK_CONFIGS_MODE_NORMAL = 0;
UPDATE_DECK_CONFIGS_MODE_APPLY_TO_CHILDREN = 1;
UPDATE_DECK_CONFIGS_MODE_COMPUTE_ALL_WEIGHTS = 2;
UPDATE_DECK_CONFIGS_MODE_COMPUTE_ALL_PARAMS = 2;
}
message UpdateDeckConfigsRequest {

View file

@ -97,7 +97,8 @@ message Deck {
ADDED = 5;
DUE = 6;
REVERSE_ADDED = 7;
DUE_PRIORITY = 8;
RETRIEVABILITY_ASCENDING = 8;
RETRIEVABILITY_DESCENDING = 9;
}
string search = 1;

View file

@ -40,9 +40,8 @@ message HelpPageLinkRequest {
CARD_TYPE_DUPLICATE = 18;
CARD_TYPE_NO_FRONT_FIELD = 19;
CARD_TYPE_MISSING_CLOZE = 20;
CARD_TYPE_EXTRANEOUS_CLOZE = 21;
TROUBLESHOOTING = 21;
CARD_TYPE_TEMPLATE_ERROR = 22;
TROUBLESHOOTING = 23;
}
HelpPage page = 1;
}

View file

@ -45,15 +45,15 @@ service SchedulerService {
rpc CustomStudyDefaults(CustomStudyDefaultsRequest)
returns (CustomStudyDefaultsResponse);
rpc RepositionDefaults(generic.Empty) returns (RepositionDefaultsResponse);
rpc ComputeFsrsWeights(ComputeFsrsWeightsRequest)
returns (ComputeFsrsWeightsResponse);
rpc ComputeFsrsParams(ComputeFsrsParamsRequest)
returns (ComputeFsrsParamsResponse);
rpc GetOptimalRetentionParameters(GetOptimalRetentionParametersRequest)
returns (GetOptimalRetentionParametersResponse);
rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest)
returns (ComputeOptimalRetentionResponse);
rpc SimulateFsrsReview(SimulateFsrsReviewRequest)
returns (SimulateFsrsReviewResponse);
rpc EvaluateWeights(EvaluateWeightsRequest) returns (EvaluateWeightsResponse);
rpc EvaluateParams(EvaluateParamsRequest) returns (EvaluateParamsResponse);
rpc ComputeMemoryState(cards.CardId) returns (ComputeMemoryStateResponse);
// The number of days the calculated interval was fuzzed by on the previous
// review (if any). Utilized by the FSRS add-on.
@ -63,10 +63,12 @@ service SchedulerService {
// Implicitly includes any of the above methods that are not listed in the
// backend service.
service BackendSchedulerService {
rpc ComputeFsrsWeightsFromItems(ComputeFsrsWeightsFromItemsRequest)
returns (ComputeFsrsWeightsResponse);
rpc ComputeFsrsParamsFromItems(ComputeFsrsParamsFromItemsRequest)
returns (ComputeFsrsParamsResponse);
// Generates parameters used for FSRS's scheduler benchmarks.
rpc FsrsBenchmark(FsrsBenchmarkRequest) returns (FsrsBenchmarkResponse);
// Used for exporting revlogs for algorithm research.
rpc ExportDataset(ExportDatasetRequest) returns (generic.Empty);
}
message SchedulingState {
@ -339,19 +341,19 @@ message RepositionDefaultsResponse {
bool shift = 2;
}
message ComputeFsrsWeightsRequest {
message ComputeFsrsParamsRequest {
/// The search used to gather cards for training
string search = 1;
repeated float current_weights = 2;
repeated float current_params = 2;
int64 ignore_revlogs_before_ms = 3;
}
message ComputeFsrsWeightsResponse {
repeated float weights = 1;
message ComputeFsrsParamsResponse {
repeated float params = 1;
uint32 fsrs_items = 2;
}
message ComputeFsrsWeightsFromItemsRequest {
message ComputeFsrsParamsFromItemsRequest {
repeated FsrsItem items = 1;
}
@ -360,7 +362,12 @@ message FsrsBenchmarkRequest {
}
message FsrsBenchmarkResponse {
repeated float weights = 1;
repeated float params = 1;
}
message ExportDatasetRequest {
uint32 min_entries = 1;
string target_path = 2;
}
message FsrsItem {
@ -373,7 +380,7 @@ message FsrsReview {
}
message SimulateFsrsReviewRequest {
repeated float weights = 1;
repeated float params = 1;
float desired_retention = 2;
uint32 deck_size = 3;
uint32 days_to_simulate = 4;
@ -391,7 +398,7 @@ message SimulateFsrsReviewResponse {
}
message ComputeOptimalRetentionRequest {
repeated float weights = 1;
repeated float params = 1;
uint32 days_to_simulate = 2;
uint32 max_interval = 3;
string search = 4;
@ -424,13 +431,13 @@ message GetOptimalRetentionParametersResponse {
uint32 review_limit = 15;
}
message EvaluateWeightsRequest {
repeated float weights = 1;
message EvaluateParamsRequest {
repeated float params = 1;
string search = 2;
int64 ignore_revlogs_before_ms = 3;
}
message EvaluateWeightsResponse {
message EvaluateParamsResponse {
float log_loss = 1;
float rmse_bins = 2;
}

View file

@ -64,6 +64,7 @@ message CardStatsResponse {
string custom_data = 20;
string preset = 21;
optional string original_deck = 22;
optional float desired_retention = 23;
}
message GraphsRequest {
@ -85,11 +86,13 @@ message GraphsResponse {
message Retrievability {
map<uint32, uint32> retrievability = 1;
float average = 2;
float sum = 3;
float sum_by_card = 3;
float sum_by_note = 4;
}
message FutureDue {
map<int32, uint32> future_due = 1;
bool have_backlog = 2;
uint32 daily_load = 3;
}
message Today {
uint32 answer_count = 1;
@ -205,6 +208,7 @@ message RevlogEntry {
RELEARNING = 2;
FILTERED = 3;
MANUAL = 4;
RESCHEDULED = 5;
}
int64 id = 1;
int64 cid = 2;
@ -217,7 +221,21 @@ message RevlogEntry {
ReviewKind review_kind = 9;
}
message RevlogEntries {
repeated RevlogEntry entries = 1;
int64 next_day_at = 2;
message CardEntry {
int64 id = 1;
int64 note_id = 2;
int64 deck_id = 3;
}
message DeckEntry {
int64 id = 1;
int64 parent_id = 2;
int64 preset_id = 3;
}
message Dataset {
repeated RevlogEntry revlogs = 1;
repeated CardEntry cards = 2;
repeated DeckEntry decks = 3;
int64 next_day_at = 4;
}

View file

@ -149,8 +149,8 @@ class RustBackend(RustBackendGenerated):
)
return self.format_timespan(seconds=seconds, context=context)
def compute_weights_from_items(self, items: Iterable[FsrsItem]) -> Sequence[float]:
return self.compute_fsrs_weights_from_items(items).weights
def compute_params_from_items(self, items: Iterable[FsrsItem]) -> Sequence[float]:
return self.compute_fsrs_params_from_items(items).params
def benchmark(self, train_set: Iterable[FsrsItem]) -> Sequence[float]:
return self.fsrs_benchmark(train_set=train_set)

View file

@ -420,6 +420,11 @@ class Collection(DeprecatedNamesMixin):
def import_json_string(self, json: str) -> ImportLogWithChanges:
return self._backend.import_json_string(json)
def export_dataset_for_research(
self, target_path: str, min_entries: int = 0
) -> None:
self._backend.export_dataset(min_entries=min_entries, target_path=target_path)
# Image Occlusion
##########################################################################
@ -987,6 +992,16 @@ class Collection(DeprecatedNamesMixin):
fget=_get_enable_load_balancer, fset=_set_enable_load_balancer
)
def _get_enable_fsrs_short_term_with_steps(self) -> bool:
return self.get_config_bool(Config.Bool.FSRS_SHORT_TERM_WITH_STEPS_ENABLED)
def _set_enable_fsrs_short_term_with_steps(self, value: bool) -> None:
self.set_config_bool(Config.Bool.FSRS_SHORT_TERM_WITH_STEPS_ENABLED, value)
fsrs_short_term_with_steps_enabled = property(
fget=_get_enable_fsrs_short_term_with_steps,
fset=_set_enable_fsrs_short_term_with_steps,
)
# Stats
##########################################################################
@ -1113,7 +1128,7 @@ class Collection(DeprecatedNamesMixin):
self._backend.abort_sync()
def full_upload_or_download(
self, *, auth: SyncAuth, server_usn: int | None, upload: bool
self, *, auth: SyncAuth | None, server_usn: int | None, upload: bool
) -> None:
self._backend.full_upload_or_download(
sync_pb2.FullUploadOrDownloadRequest(

View file

@ -43,10 +43,6 @@ from anki.models import NotetypeDict
from anki.sound import AVTag, SoundOrVideoTag, TTSTag
from anki.utils import to_json_bytes
CARD_BLANK_HELP = (
"https://anki.tenderapp.com/kb/card-appearance/the-front-of-this-card-is-blank"
)
@dataclass
class TemplateReplacement:

View file

@ -386,13 +386,13 @@ urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
# via requests
waitress==3.0.0 \
--hash=sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1 \
--hash=sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669
waitress==3.0.1 \
--hash=sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac \
--hash=sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3
# via -r requirements.aqt.in
werkzeug==3.0.4 \
--hash=sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c \
--hash=sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306
werkzeug==3.0.6 \
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
# via flask
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \

View file

@ -608,17 +608,17 @@ urllib3==2.2.2 \
# via
# requests
# types-requests
waitress==3.0.0 \
--hash=sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1 \
--hash=sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669
waitress==3.0.1 \
--hash=sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac \
--hash=sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3
# via -r requirements.aqt.in
websocket-client==1.8.0 \
--hash=sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526 \
--hash=sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da
# via pychromedevtools
werkzeug==3.0.4 \
--hash=sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c \
--hash=sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306
werkzeug==3.0.6 \
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
# via flask
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import atexit
import logging
import sys
from collections.abc import Callable
@ -525,6 +526,7 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None:
print(f"Qt {category}: {msg} {context}")
qInstallMessageHandler(msgHandler)
atexit.register(qInstallMessageHandler, None)
if driver == VideoDriver.OpenGL:
# Leaving QT_OPENGL unset appears to sometimes produce different results

View file

@ -37,14 +37,21 @@ def show(mw: aqt.AnkiQt) -> QDialog:
txt = supportText()
if mw.addonManager.dirty:
txt += "\n" + addon_debug_info()
QApplication.clipboard().setText(txt)
clipboard = QApplication.clipboard()
assert clipboard is not None
clipboard.setText(txt)
tooltip(tr.about_copied_to_clipboard(), parent=dialog)
btn = QPushButton(tr.about_copy_debug_info())
qconnect(btn.clicked, on_copy)
abt.buttonBox.addButton(btn, QDialogButtonBox.ButtonRole.ActionRole)
abt.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setFocus()
ok_button = abt.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
assert ok_button is not None
ok_button.setFocus()
btnLayout = abt.buttonBox.layout()
assert btnLayout is not None
btnLayout.setContentsMargins(12, 12, 12, 12)
# WebView cleanup
@ -52,7 +59,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
def on_dialog_destroyed() -> None:
abt.label.cleanup()
abt.label = None
abt.label = None # type: ignore
qconnect(dialog.destroyed, on_dialog_destroyed)

View file

@ -152,10 +152,13 @@ class AddCards(QMainWindow):
def on_deck_changed(self, deck_id: int) -> None:
gui_hooks.add_cards_did_change_deck(deck_id)
def on_notetype_change(self, notetype_id: NotetypeId) -> None:
def on_notetype_change(
self, notetype_id: NotetypeId, update_deck: bool = True
) -> None:
# need to adjust current deck?
if deck_id := self.col.default_deck_for_notetype(notetype_id):
self.deck_chooser.selected_deck_id = deck_id
if update_deck:
if deck_id := self.col.default_deck_for_notetype(notetype_id):
self.deck_chooser.selected_deck_id = deck_id
# only used for detecting changed sticky fields on close
self._last_added_note = None
@ -224,7 +227,8 @@ class AddCards(QMainWindow):
self.col.defaults_for_adding(
current_review_card=self.mw.reviewer.card
).notetype_id
)
),
update_deck=False,
)
def _new_note(self) -> Note:

View file

@ -63,7 +63,7 @@ class ChangeNotetypeDialog(QDialog):
def reject(self) -> None:
self.web.cleanup()
self.web = None
self.web = None # type: ignore
saveGeom(self, self.TITLE)
QDialog.reject(self)

View file

@ -61,7 +61,9 @@ class CardLayout(QDialog):
self.ord = ord
self.col = self.mw.col.weakref()
self.mm = self.mw.col.models
self.model = note.note_type()
note_type = note.note_type()
assert note_type is not None
self.model = note_type
self.templates = self.model["tmpls"]
self.fill_empty_action_toggled = fill_empty
self.night_mode_is_enabled = theme_manager.night_mode
@ -404,6 +406,7 @@ class CardLayout(QDialog):
m = QMenu(self)
a = m.addAction(tr.card_templates_fill_empty())
assert a is not None
a.setCheckable(True)
a.setChecked(self.fill_empty_action_toggled)
qconnect(a.triggered, self.on_fill_empty_action_toggled)
@ -411,11 +414,13 @@ class CardLayout(QDialog):
a.setVisible(False)
a = m.addAction(tr.card_templates_night_mode())
assert a is not None
a.setCheckable(True)
a.setChecked(self.night_mode_is_enabled)
qconnect(a.triggered, self.on_night_mode_action_toggled)
a = m.addAction(tr.card_templates_add_mobile_class())
assert a is not None
a.setCheckable(True)
a.setChecked(self.mobile_emulation_enabled)
qconnect(a.toggled, self.on_mobile_class_action_toggled)
@ -754,6 +759,7 @@ class CardLayout(QDialog):
a = m.addAction(
tr.actions_with_ellipsis(action=tr.card_templates_restore_to_default())
)
assert a is not None
qconnect(
a.triggered,
lambda: self.on_restore_to_default(), # pylint: disable=unnecessary-lambda
@ -761,15 +767,19 @@ class CardLayout(QDialog):
if not self._isCloze():
a = m.addAction(tr.card_templates_add_card_type())
assert a is not None
qconnect(a.triggered, self.onAddCard)
a = m.addAction(tr.card_templates_remove_card_type())
assert a is not None
qconnect(a.triggered, self.onRemove)
a = m.addAction(tr.card_templates_rename_card_type())
assert a is not None
qconnect(a.triggered, self.onRename)
a = m.addAction(tr.card_templates_reposition_card_type())
assert a is not None
qconnect(a.triggered, self.onReorder)
m.addSeparator()
@ -780,9 +790,11 @@ class CardLayout(QDialog):
else:
s = tr.card_templates_off()
a = m.addAction(tr.card_templates_deck_override() + s)
assert a is not None
qconnect(a.triggered, self.onTargetDeck)
a = m.addAction(tr.card_templates_browser_appearance())
assert a is not None
qconnect(a.triggered, self.onBrowserDisplay)
m.popup(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0)))
@ -834,7 +846,9 @@ class CardLayout(QDialog):
te.setCol(self.col)
l.addWidget(te)
if t["did"]:
te.setText(self.col.decks.get(t["did"])["name"])
deck = self.col.decks.get(t["did"])
assert deck is not None
te.setText(deck["name"])
te.selectAll()
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
qconnect(bb.rejected, d.close)
@ -927,10 +941,10 @@ class CardLayout(QDialog):
saveGeom(self, "CardLayout")
saveSplitter(self.mainArea, "CardLayoutMainArea")
self.preview_web.cleanup()
self.preview_web = None
self.model = None
self.rendered_card = None
self.mw = None
self.preview_web = None # type: ignore
self.model = None # type: ignore
self.rendered_card = None # type: ignore
self.mw = None # type: ignore
def onHelp(self) -> None:
openHelp(HelpPage.TEMPLATES)

View file

@ -144,7 +144,11 @@ class CustomStudy(QDialog):
form.spin.setValue(current_spinner_value)
form.preSpin.setText(text_before_spinner)
form.postSpin.setText(text_after_spinner)
form.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(ok)
ok_button = form.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
assert ok_button is not None
ok_button.setText(ok)
self.radioIdx = idx
def accept(self) -> None:

View file

@ -72,8 +72,10 @@ class DebugConsole(QDialog):
qconnect(self._script.currentIndexChanged, self._on_script_change)
def _setup_text_edits(self):
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
font.setPointSize(self._text.font().pointSize() + 1)
font = QFont("Consolas")
if not font.exactMatch():
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
font.setPointSize(self._text.font().pointSize())
self._text.setFont(font)
self._log.setFont(font)
@ -196,6 +198,7 @@ class DebugConsole(QDialog):
def _on_context_menu(self, text_edit: QPlainTextEdit) -> None:
menu = text_edit.createStandardContextMenu()
assert menu is not None
menu.addSeparator()
for action in self._actions():
entry = menu.addAction(action.name)
@ -227,7 +230,7 @@ class DebugConsole(QDialog):
sys.stderr = self._oldStderr
sys.stdout = self._oldStdout
def _card_repr(self, card: anki.cards.Card) -> None:
def _card_repr(self, card: anki.cards.Card | None) -> None:
import copy
import pprint
@ -316,6 +319,7 @@ class DebugConsole(QDialog):
)
self._log.appendPlainText(to_append)
slider = self._log.verticalScrollBar()
assert slider is not None
slider.setValue(slider.maximum())
self._log.ensureCursorVisible()

View file

@ -309,12 +309,16 @@ class DeckBrowser:
def _showOptions(self, did: str) -> None:
m = QMenu(self.mw)
a = m.addAction(tr.actions_rename())
assert a is not None
qconnect(a.triggered, lambda b, did=did: self._rename(DeckId(int(did))))
a = m.addAction(tr.actions_options())
assert a is not None
qconnect(a.triggered, lambda b, did=did: self._options(DeckId(int(did))))
a = m.addAction(tr.actions_export())
assert a is not None
qconnect(a.triggered, lambda b, did=did: self._export(DeckId(int(did))))
a = m.addAction(tr.actions_delete())
assert a is not None
qconnect(a.triggered, lambda b, did=did: self._delete(DeckId(int(did))))
gui_hooks.deck_browser_will_show_options_menu(m, int(did))
m.popup(QCursor.pos())
@ -357,9 +361,9 @@ class DeckBrowser:
).run_in_background()
def _delete(self, did: DeckId) -> None:
deck_name = self.mw.col.decks.find_deck_in_tree(
self._render_data.tree, did
).name
deck = self.mw.col.decks.find_deck_in_tree(self._render_data.tree, did)
assert deck is not None
deck_name = deck.name
remove_decks(
parent=self.mw, deck_ids=[did], deck_name=deck_name
).run_in_background()

View file

@ -99,7 +99,9 @@ class DeckChooser(QHBoxLayout):
def callback(ret: StudyDeck) -> None:
if not ret.name:
return
new_selected_deck_id = self.mw.col.decks.by_name(ret.name)["id"]
deck = self.mw.col.decks.by_name(ret.name)
assert deck is not None
new_selected_deck_id = deck["id"]
if self.selected_deck_id != new_selected_deck_id:
self.selected_deck_id = new_selected_deck_id
if func := self.on_deck_changed:

View file

@ -60,6 +60,7 @@ class DeckDescriptionDialog(QDialog):
button_box = QDialogButtonBox()
ok = button_box.addButton(QDialogButtonBox.StandardButton.Ok)
assert ok is not None
qconnect(ok.clicked, self.save_and_accept)
box.addWidget(button_box)

View file

@ -67,10 +67,10 @@ class DeckOptionsDialog(QDialog):
elif cmd == "_close":
self._close()
def closeEvent(self, evt: QCloseEvent) -> None:
def closeEvent(self, evt: QCloseEvent | None) -> None:
if self._close_event_has_cleaned_up:
evt.accept()
return
return super().closeEvent(evt)
assert evt is not None
evt.ignore()
self.check_pending_changes()
@ -98,7 +98,7 @@ class DeckOptionsDialog(QDialog):
def reject(self) -> None:
self.mw.col.set_wants_abort()
self.web.cleanup()
self.web = None
self.web = None # type: ignore
saveGeom(self, self.TITLE)
QDialog.reject(self)
@ -113,10 +113,14 @@ def confirm_deck_then_display_options(active_card: Card | None = None) -> None:
decks = [aqt.mw.col.decks.current()]
if card := active_card:
if card.odid and card.odid != decks[0]["id"]:
decks.append(aqt.mw.col.decks.get(card.odid))
deck = aqt.mw.col.decks.get(card.odid)
assert deck is not None
decks.append(deck)
if not any(d["id"] == card.did for d in decks):
decks.append(aqt.mw.col.decks.get(card.did))
deck = aqt.mw.col.decks.get(card.did)
assert deck is not None
decks.append(deck)
if len(decks) == 1:
display_options_for_deck(decks[0])
@ -143,13 +147,16 @@ def _deck_prompt_dialog(decks: list[DeckDict]) -> None:
def display_options_for_deck_id(deck_id: DeckId) -> None:
display_options_for_deck(aqt.mw.col.decks.get(deck_id))
deck = aqt.mw.col.decks.get(deck_id)
assert deck is not None
display_options_for_deck(deck)
def display_options_for_deck(deck: DeckDict) -> None:
if not deck["dyn"]:
if KeyboardModifiersPressed().shift or not aqt.mw.col.v3_scheduler():
deck_legacy = aqt.mw.col.decks.get(DeckId(deck["id"]))
assert deck_legacy is not None
aqt.deckconf.DeckConf(aqt.mw, deck_legacy)
else:
DeckOptionsDialog(aqt.mw, deck)

View file

@ -28,10 +28,12 @@ class EditCurrent(QMainWindow):
self,
editor_mode=aqt.editor.EditorMode.EDIT_CURRENT,
)
assert self.mw.reviewer.card is not None
self.editor.card = self.mw.reviewer.card
self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0)
restoreGeom(self, "editcurrent")
close_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close)
assert close_button is not None
close_button.setShortcut(QKeySequence("Ctrl+Return"))
# qt5.14+ doesn't handle numpad enter on Windows
self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self)
@ -46,6 +48,7 @@ class EditCurrent(QMainWindow):
# reload note
note = self.editor.note
try:
assert note is not None
note.load()
except NotFoundError:
# note's been deleted
@ -65,7 +68,7 @@ class EditCurrent(QMainWindow):
if card := self.mw.reviewer.card:
self.editor.set_note(card.note())
def closeEvent(self, evt: QCloseEvent) -> None:
def closeEvent(self, evt: QCloseEvent | None) -> None:
self.editor.call_after_note_saved(self.cleanup)
def _saveAndClose(self) -> None:

View file

@ -34,7 +34,7 @@ from anki.collection import Config, SearchNode
from anki.consts import MODEL_CLOZE
from anki.hooks import runFilter
from anki.httpclient import HttpClient
from anki.models import NotetypeId, StockNotetype
from anki.models import NotetypeDict, NotetypeId, StockNotetype
from anki.notes import Note, NoteFieldsCheckResult, NoteId
from anki.utils import checksum, is_lin, is_mac, is_win, namedtmp
from aqt import AnkiQt, colors, gui_hooks
@ -242,38 +242,35 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
rightside: bool = True,
) -> str:
"""Assign func to bridge cmd, register shortcut, return button"""
if func:
def wrapped_func(editor: Editor) -> None:
self.call_after_note_saved(
functools.partial(func, editor), keepFocus=True
)
def wrapped_func(editor: Editor) -> None:
self.call_after_note_saved(functools.partial(func, editor), keepFocus=True)
self._links[cmd] = wrapped_func
self._links[cmd] = wrapped_func
if keys:
if keys:
def on_activated() -> None:
wrapped_func(self)
def on_activated() -> None:
wrapped_func(self)
if toggleable:
# generate a random id for triggering toggle
id = id or str(randrange(1_000_000))
if toggleable:
# generate a random id for triggering toggle
id = id or str(randrange(1_000_000))
def on_hotkey() -> None:
on_activated()
self.web.eval(
f'toggleEditorButton(document.getElementById("{id}"));'
)
def on_hotkey() -> None:
on_activated()
self.web.eval(
f'toggleEditorButton(document.getElementById("{id}"));'
)
else:
on_hotkey = on_activated
else:
on_hotkey = on_activated
QShortcut( # type: ignore
QKeySequence(keys),
self.widget,
activated=on_hotkey,
)
QShortcut( # type: ignore
QKeySequence(keys),
self.widget,
activated=on_hotkey,
)
btn = self._addButton(
icon,
@ -363,7 +360,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def _onFields(self) -> None:
from aqt.fields import FieldDialog
FieldDialog(self.mw, self.note.note_type(), parent=self.parentWindow)
FieldDialog(self.mw, self.note_type(), parent=self.parentWindow)
def onCardLayout(self) -> None:
self.call_after_note_saved(self._onCardLayout)
@ -375,6 +372,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
ord = self.card.ord
else:
ord = 0
assert self.note is not None
CardLayout(
self.mw,
self.note,
@ -435,7 +434,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
gui_hooks.editor_did_focus_field(self.note, self.currentField)
elif cmd.startswith("toggleStickyAll"):
model = self.note.note_type()
model = self.note_type()
flds = model["flds"]
any_sticky = any([fld["sticky"] for fld in flds])
@ -456,7 +455,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
(type, num) = cmd.split(":", 1)
ord = int(num)
model = self.note.note_type()
model = self.note_type()
fld = model["flds"][ord]
new_state = not fld["sticky"]
fld["sticky"] = new_state
@ -469,10 +468,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
elif cmd.startswith("lastTextColor"):
(_, textColor) = cmd.split(":", 1)
assert self.mw.pm.profile is not None
self.mw.pm.profile["lastTextColor"] = textColor
elif cmd.startswith("lastHighlightColor"):
(_, highlightColor) = cmd.split(":", 1)
assert self.mw.pm.profile is not None
self.mw.pm.profile["lastHighlightColor"] = highlightColor
elif cmd.startswith("saveTags"):
@ -545,11 +546,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
for fld, val in self.note.items()
]
flds = self.note.note_type()["flds"]
note_type = self.note_type()
flds = note_type["flds"]
collapsed = [fld["collapsed"] for fld in flds]
plain_texts = [fld.get("plainText", False) for fld in flds]
descriptions = [fld.get("description", "") for fld in flds]
notetype_meta = {"id": self.note.mid, "modTime": self.note.note_type()["mod"]}
notetype_meta = {"id": self.note.mid, "modTime": note_type["mod"]}
self.widget.show()
@ -566,6 +568,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
self.web.setFocus()
gui_hooks.editor_did_load_note(self)
assert self.mw.pm.profile is not None
text_color = self.mw.pm.profile.get("lastTextColor", "#0000ff")
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#0000ff")
@ -590,7 +593,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
"""
if self.addMode:
sticky = [field["sticky"] for field in self.note.note_type()["flds"]]
sticky = [field["sticky"] for field in self.note_type()["flds"]]
js += " setSticky(%s);" % json.dumps(sticky)
if (
@ -607,6 +610,9 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def _save_current_note(self) -> None:
"Call after note is updated with data from webview."
if not self.note:
return
update_note(parent=self.widget, note=self.note).run_in_background(
initiator=self
)
@ -614,7 +620,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def fonts(self) -> list[tuple[str, int, bool]]:
return [
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
for f in self.note.note_type()["flds"]
for f in self.note_type()["flds"]
]
def call_after_note_saved(
@ -648,6 +654,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
checkValid = _check_and_update_duplicate_display_async
def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -> None:
assert self.note is not None
cols = [""] * len(self.note.fields)
cloze_hint = ""
if result == NoteFieldsCheckResult.DUPLICATE:
@ -665,13 +672,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
)
def showDupes(self) -> None:
assert self.note is not None
aqt.dialogs.open(
"Browser",
self.mw,
search=(
SearchNode(
dupe=SearchNode.Dupe(
notetype_id=self.note.note_type()["id"],
notetype_id=self.note_type()["id"],
first_field=self.note.fields[0],
)
),
@ -681,7 +689,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
if not self.note:
return True
m = self.note.note_type()
m = self.note_type()
for c, f in enumerate(self.note.fields):
f = f.replace("<br>", "").strip()
notChangedvalues = {"", "<br>"}
@ -696,7 +704,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
if self.web:
self.web.cleanup()
self.web = None
self.web = None # type: ignore
# legacy
@ -729,9 +737,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
if self.tags.col != self.mw.col:
self.tags.setCol(self.mw.col)
if not self.tags.text() or not self.addMode:
assert self.note is not None
self.tags.setText(self.note.string_tags().strip())
def on_tag_focus_lost(self) -> None:
assert self.note is not None
self.note.tags = self.mw.col.tags.split(self.tags.text())
gui_hooks.editor_did_update_tags(self.note)
if not self.addMode:
@ -826,7 +836,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
# Media downloads
######################################################################
def urlToLink(self, url: str) -> str | None:
def urlToLink(self, url: str) -> str:
fname = self.urlToFile(url)
if not fname:
return '<a href="{}">{}</a>'.format(
@ -1037,8 +1047,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
######################################################################
def current_notetype_is_image_occlusion(self) -> bool:
return bool(self.note) and (
self.note.note_type().get("originalStockKind", None)
if not self.note:
return False
return (
self.note_type().get("originalStockKind", None)
== StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION
)
@ -1049,6 +1062,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
image_path=image_path, notetype_id=0
)
else:
assert self.note is not None
self.setup_mask_editor_for_existing_note(
note_id=self.note.id, image_path=image_path
)
@ -1075,8 +1089,10 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def select_image_from_clipboard_and_occlude(self) -> None:
"""Set up the mask editor for the image in the clipboard."""
clipoard = self.mw.app.clipboard()
mime = clipoard.mimeData()
clipboard = self.mw.app.clipboard()
assert clipboard is not None
mime = clipboard.mimeData()
assert mime is not None
if not mime.hasImage():
showWarning(tr.editing_no_image_found_on_clipboard())
return
@ -1160,6 +1176,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy)
def _onHtmlEdit(self, field: int) -> None:
assert self.note is not None
d = QDialog(self.widget, Qt.WindowType.Window)
form = aqt.forms.edithtml.Ui_Dialog()
form.setupUi(d)
@ -1223,7 +1240,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy)
def _onCloze(self) -> None:
# check that the model is set up for cloze deletion
if self.note.note_type()["type"] != MODEL_CLOZE:
if self.note_type()["type"] != MODEL_CLOZE:
if self.addMode:
tooltip(tr.editing_warning_cloze_deletions_will_not_work())
else:
@ -1231,7 +1248,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
return
# find the highest existing cloze
highest = 0
for name, val in list(self.note.items()):
assert self.note is not None
for _, val in list(self.note.items()):
m = re.findall(r"\{\{c(\d+)::", val)
if m:
highest = max(highest, sorted(int(x) for x in m)[-1])
@ -1243,6 +1261,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
self.web.eval("wrap('{{c%d::', '}}');" % highest)
def setupForegroundButton(self) -> None:
assert self.mw.pm.profile is not None
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
# use last colour
@ -1276,6 +1295,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy)
def onColourChanged(self) -> None:
self._updateForegroundButton()
assert self.mw.pm.profile is not None
self.mw.pm.profile["lastColour"] = self.fcolour
@deprecated(info=_js_legacy)
@ -1300,6 +1320,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
(tr.editing_edit_html(), self.onHtmlEdit, "Ctrl+Shift+X"),
):
a = m.addAction(text)
assert a is not None
qconnect(a.triggered, handler)
a.setShortcut(QKeySequence(shortcut))
@ -1387,6 +1408,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
addImageForOcclusionFromClipboard=Editor.select_image_from_clipboard_and_occlude,
)
def note_type(self) -> NotetypeDict:
assert self.note is not None
note_type = self.note.note_type()
assert note_type is not None
return note_type
# Pasting, drag & drop, and keyboard layouts
######################################################################
@ -1403,6 +1430,7 @@ class EditorWebView(AnkiWebView):
self._internal_field_text_for_paste: str | None = None
self._last_known_clipboard_mime: QMimeData | None = None
clip = self.editor.mw.app.clipboard()
assert clip is not None
clip.dataChanged.connect(self._on_clipboard_change)
gui_hooks.editor_web_view_did_init(self)
@ -1410,23 +1438,28 @@ class EditorWebView(AnkiWebView):
self._store_field_content_on_next_clipboard_change = True
self._internal_field_text_for_paste = None
def _on_clipboard_change(self) -> None:
self._last_known_clipboard_mime = self.editor.mw.app.clipboard().mimeData()
def _on_clipboard_change(
self, mode: QClipboard.Mode = QClipboard.Mode.Clipboard
) -> None:
self._last_known_clipboard_mime = self._clipboard().mimeData(mode)
if self._store_field_content_on_next_clipboard_change:
# if the flag was set, save the field data
self._internal_field_text_for_paste = self._get_clipboard_html_for_field()
self._internal_field_text_for_paste = self._get_clipboard_html_for_field(
mode
)
self._store_field_content_on_next_clipboard_change = False
elif (
self._internal_field_text_for_paste != self._get_clipboard_html_for_field()
elif self._internal_field_text_for_paste != self._get_clipboard_html_for_field(
mode
):
# if we've previously saved the field, blank it out if the clipboard state has changed
self._internal_field_text_for_paste = None
def _get_clipboard_html_for_field(self):
clip = self.editor.mw.app.clipboard()
mime = clip.mimeData()
def _get_clipboard_html_for_field(self, mode: QClipboard.Mode) -> str | None:
clip = self._clipboard()
mime = clip.mimeData(mode)
assert mime is not None
if not mime.hasHtml():
return
return None
return mime.html()
def onCut(self) -> None:
@ -1440,6 +1473,7 @@ class EditorWebView(AnkiWebView):
def _opened_context_menu_on_image(self) -> bool:
context_menu_request = self.lastContextMenuRequest()
assert context_menu_request is not None
return (
context_menu_request.mediaType()
== context_menu_request.MediaType.MediaTypeImage
@ -1455,15 +1489,17 @@ class EditorWebView(AnkiWebView):
def _onPaste(self, mode: QClipboard.Mode) -> None:
# Since _on_clipboard_change doesn't always trigger properly on macOS, we do a double check if any changes were made before pasting
if self._last_known_clipboard_mime != self.editor.mw.app.clipboard().mimeData():
self._on_clipboard_change()
clipboard = self._clipboard()
if self._last_known_clipboard_mime != clipboard.mimeData(mode):
self._on_clipboard_change(mode)
extended = self._wantsExtendedPaste()
if html := self._internal_field_text_for_paste:
print("reuse internal")
self.editor.doPaste(html, True, extended)
else:
print("use clipboard")
mime = self.editor.mw.app.clipboard().mimeData(mode=mode)
mime = clipboard.mimeData(mode=mode)
assert mime is not None
html, internal = self._processMime(mime, extended)
if html:
self.editor.doPaste(html, internal, extended)
@ -1474,12 +1510,15 @@ class EditorWebView(AnkiWebView):
def onMiddleClickPaste(self) -> None:
self._onPaste(QClipboard.Mode.Selection)
def dragEnterEvent(self, evt: QDragEnterEvent) -> None:
def dragEnterEvent(self, evt: QDragEnterEvent | None) -> None:
assert evt is not None
evt.accept()
def dropEvent(self, evt: QDropEvent) -> None:
def dropEvent(self, evt: QDropEvent | None) -> None:
assert evt is not None
extended = self._wantsExtendedPaste()
mime = evt.mimeData()
assert mime is not None
cursor_pos = self.mapFromGlobal(QCursor.pos())
if evt.source() and mime.hasHtml():
@ -1585,12 +1624,13 @@ class EditorWebView(AnkiWebView):
return fname
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
def contextMenuEvent(self, evt: QContextMenuEvent | None) -> None:
m = QMenu(self)
if self.hasSelection():
self._add_cut_action(m)
self._add_copy_action(m)
a = m.addAction(tr.editing_paste())
assert a is not None
qconnect(a.triggered, self.onPaste)
if self._opened_context_menu_on_image():
self._add_image_menu(m)
@ -1599,26 +1639,38 @@ class EditorWebView(AnkiWebView):
def _add_cut_action(self, menu: QMenu) -> None:
a = menu.addAction(tr.editing_cut())
assert a is not None
qconnect(a.triggered, self.onCut)
def _add_copy_action(self, menu: QMenu) -> None:
a = menu.addAction(tr.actions_copy())
assert a is not None
qconnect(a.triggered, self.onCopy)
def _add_image_menu(self, menu: QMenu) -> None:
a = menu.addAction(tr.editing_copy_image())
assert a is not None
qconnect(a.triggered, self.on_copy_image)
url = self.lastContextMenuRequest().mediaUrl()
context_menu_request = self.lastContextMenuRequest()
assert context_menu_request is not None
url = context_menu_request.mediaUrl()
file_name = url.fileName()
path = os.path.join(self.editor.mw.col.media.dir(), file_name)
a = menu.addAction(tr.editing_open_image())
assert a is not None
qconnect(a.triggered, lambda: openFolder(path))
if is_win or is_mac:
a = menu.addAction(tr.editing_show_in_folder())
assert a is not None
qconnect(a.triggered, lambda: show_in_folder(path))
def _clipboard(self) -> QClipboard:
clipboard = self.editor.mw.app.clipboard()
assert clipboard is not None
return clipboard
# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light"
# - there may be other cases like a trailing 'Bold' that need fixing, but will
@ -1648,7 +1700,7 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
def set_cloze_button(editor: Editor) -> None:
action = "show" if editor.note.note_type()["type"] == MODEL_CLOZE else "hide"
action = "show" if editor.note_type()["type"] == MODEL_CLOZE else "hide"
editor.web.eval(
'require("anki/ui").loaded.then(() =>'
f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("cloze")'

View file

@ -38,7 +38,7 @@ class EmptyCardsDialog(QDialog):
def __init__(self, mw: aqt.main.AnkiQt, report: EmptyCardsReport) -> None:
super().__init__(mw)
self.mw = mw.weakref()
self.mw = mw
self.mw.garbage_collect_on_dialog_finish(self)
self.report = report
self.form = aqt.forms.emptycards.Ui_Dialog()
@ -63,7 +63,7 @@ class EmptyCardsDialog(QDialog):
def on_finished(code: Any) -> None:
self.form.webview.cleanup()
self.form.webview = None
self.form.webview = None # type: ignore
saveGeom(self, "emptycards")
qconnect(self.finished, on_finished)
@ -71,6 +71,7 @@ class EmptyCardsDialog(QDialog):
self._delete_button = self.form.buttonBox.addButton(
tr.empty_cards_delete_button(), QDialogButtonBox.ButtonRole.ActionRole
)
assert self._delete_button is not None
self._delete_button.setAutoDefault(False)
qconnect(self._delete_button.clicked, self._on_delete)

View file

@ -41,25 +41,30 @@ class FieldDialog(QDialog):
self.model = nt
self.mm._remove_from_cache(self.model["id"])
self.change_tracker = ChangeTracker(self.mw)
self.webview = None
self.form = aqt.forms.fields.Ui_Dialog()
self.form.setupUi(self)
self.setWindowTitle(
without_unicode_isolation(tr.fields_fields_for(val=self.model["name"]))
)
self.form = aqt.forms.fields.Ui_Dialog()
self.form.setupUi(self)
self.webview = None
disable_help_button(self)
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help).setAutoDefault(
False
)
self.form.buttonBox.button(
help_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help)
assert help_button is not None
help_button.setAutoDefault(False)
cancel_button = self.form.buttonBox.button(
QDialogButtonBox.StandardButton.Cancel
).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save).setAutoDefault(
False
)
assert cancel_button is not None
cancel_button.setAutoDefault(False)
save_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save)
assert save_button is not None
save_button.setAutoDefault(False)
self.currentIdx: int | None = None
self.fillFields()
self.setupSignals()
@ -112,6 +117,7 @@ class FieldDialog(QDialog):
# for pylint
return
# the item in idx is removed thus subtract 1.
assert idx is not None
if idx < dropPos:
movePos -= 1
self.moveField(movePos + 1) # convert to 1 based.
@ -144,6 +150,9 @@ class FieldDialog(QDialog):
return txt
def onRename(self) -> None:
if self.currentIdx is None:
return
idx = self.currentIdx
f = self.model["flds"][idx]
name = self._uniqueName(tr.actions_new_name(), self.currentIdx, f["name"])
@ -195,6 +204,7 @@ class FieldDialog(QDialog):
def onPosition(self, delta: int = -1) -> None:
idx = self.currentIdx
assert idx is not None
l = len(self.model["flds"])
txt = getOnlyText(tr.fields_new_position_1(val=l), default=str(idx + 1))
if not txt:

View file

@ -130,9 +130,10 @@ class FilteredDeckConfigDialog(QDialog):
build_label = tr.actions_rebuild()
else:
build_label = tr.decks_build()
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(
build_label
)
ok_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
assert ok_button is not None
ok_button.setText(build_label)
form.resched.setChecked(config.reschedule)
self._onReschedToggled(0)

View file

@ -34,11 +34,11 @@ class Flag:
class FlagManager:
def __init__(self, mw: aqt.main.AnkiQt) -> None:
self.mw = mw
self._flags: list[Flag] | None = None
self._flags: list[Flag] = []
def all(self) -> list[Flag]:
"""Return a list of all flags."""
if self._flags is None:
if not self._flags:
self._load_flags()
return self._flags
@ -57,7 +57,7 @@ class FlagManager:
def require_refresh(self) -> None:
"Discard cached labels."
self._flags = None
self._flags = []
def _load_flags(self) -> None:
labels = cast(dict[str, str], self.mw.col.get_config("flagLabels", {}))

View file

@ -611,28 +611,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>preferences_import_export</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<widget class="QCheckBox" name="legacy_import_export">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">Legacy import/export handling</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
@ -1293,7 +1271,6 @@
<tabstop>useCurrent</tabstop>
<tabstop>default_search_text</tabstop>
<tabstop>ignore_accents_in_search</tabstop>
<tabstop>legacy_import_export</tabstop>
<tabstop>syncMedia</tabstop>
<tabstop>syncOnProgramOpen</tabstop>
<tabstop>autoSyncMedia</tabstop>

View file

@ -60,7 +60,7 @@ class ChangeMap(QDialog):
self.frm.fields.setCurrentRow(n + 1)
self.field: str | None = None
def getField(self) -> str:
def getField(self) -> str | None:
self.exec()
return self.field
@ -91,8 +91,10 @@ class ImportDialog(QDialog):
self.importer = importer
self.frm = aqt.forms.importing.Ui_ImportDialog()
self.frm.setupUi(self)
help_button = self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help)
assert help_button is not None
qconnect(
self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help).clicked,
help_button.clicked,
self.helpRequested,
)
disable_help_button(self)
@ -103,6 +105,7 @@ class ImportDialog(QDialog):
gui_hooks.current_note_type_did_change.append(self.modelChanged)
qconnect(self.frm.autoDetect.clicked, self.onDelimiter)
self.updateDelimiterButtonText()
assert self.mw.pm.profile is not None
self.frm.allowHTML.setChecked(self.mw.pm.profile.get("allowHTML", True))
qconnect(self.frm.importMode.currentIndexChanged, self.importModeChanged)
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get("importMode", 1))
@ -187,6 +190,7 @@ class ImportDialog(QDialog):
showWarning(tr.importing_the_first_field_of_the_note())
return
self.importer.importMode = self.frm.importMode.currentIndex()
assert self.mw.pm.profile is not None
self.mw.pm.profile["importMode"] = self.importer.importMode
self.importer.allowHTML = self.frm.allowHTML.isChecked()
self.mw.pm.profile["allowHTML"] = self.importer.allowHTML
@ -390,7 +394,7 @@ def importFile(mw: AnkiQt, file: str) -> None:
showWarning(invalidZipMsg())
except MediaMapInvalid:
showWarning(
"Unable to read file. It probably requires a newer version of Anki to import. Try unchecking 'Legacy import/export Handling' under Preferences > Editing > Import/Export and see if the problem persists."
"Unable to read file. It probably requires a newer version of Anki to import."
)
except V2ImportIntoV1:
showWarning(

View file

@ -112,7 +112,6 @@ class MainWebView(AnkiWebView):
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
self.setMinimumWidth(400)
self.setAcceptDrops(True)
print("todo: windows paths in import screen")
# Importing files via drag & drop
##########################################################################

View file

@ -80,6 +80,7 @@ class MediaChecker:
label = progress.media_check
try:
assert self.progress_dialog is not None
if self.progress_dialog.wantCancel:
self.mw.col.set_wants_abort()
except AttributeError:
@ -165,6 +166,7 @@ class MediaChecker:
def _on_render_latex(self) -> None:
self.progress_dialog = self.mw.progress.start()
assert self.progress_dialog is not None
try:
out = self.mw.col.media.render_all_latex(self._on_render_latex_progress)
if self.progress_dialog.wantCancel:
@ -181,6 +183,7 @@ class MediaChecker:
tooltip(tr.media_check_all_latex_rendered())
def _on_render_latex_progress(self, count: int) -> bool:
assert self.progress_dialog is not None
if self.progress_dialog.wantCancel:
return False

View file

@ -457,8 +457,8 @@ def update_deck_configs() -> bytes:
update.max = val.total_cards
update.value = val.current_cards
update.label = val.label
elif progress.HasField("compute_weights"):
val2 = progress.compute_weights
elif progress.HasField("compute_params"):
val2 = progress.compute_params
# prevent an indeterminate progress bar from appearing at the start of each preset
update.max = max(val2.total, 1)
update.value = val2.current
@ -621,10 +621,10 @@ exposed_backend_list = [
"update_image_occlusion_note",
"get_image_occlusion_fields",
# SchedulerService
"compute_fsrs_weights",
"compute_fsrs_params",
"compute_optimal_retention",
"set_wants_abort",
"evaluate_weights",
"evaluate_params",
"get_optimal_retention_parameters",
"simulate_fsrs_review",
]
@ -735,8 +735,12 @@ def _extract_page_context() -> PageContext:
return PageContext.NON_LEGACY_PAGE
elif referer.path == "/_anki/legacyPageData":
query_params = parse_qs(referer.query)
id = int(query_params.get("id", [None])[0])
return aqt.mw.mediaServer.get_page_context(id)
query_id = query_params.get("id")
if not query_id:
return PageContext.UNKNOWN
id = int(query_id[0])
page_context = aqt.mw.mediaServer.get_page_context(id)
return page_context if page_context else PageContext.UNKNOWN
else:
return PageContext.UNKNOWN

View file

@ -119,7 +119,7 @@ class MediaSyncer:
diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True)
diag.show()
timer: QTimer | None = None
timer: QTimer
def check_finished() -> None:
if not self.is_syncing():

View file

@ -84,6 +84,7 @@ class ModelChooser(QHBoxLayout):
if not ret.name:
return
m = self.deck.models.by_name(ret.name)
assert m is not None
self.deck.conf["curModel"] = m["id"]
cdeck = self.deck.decks.current()
cdeck["mid"] = m["id"]

View file

@ -111,6 +111,7 @@ class NotetypeChooser(QHBoxLayout):
if not ret.name:
return
notetype = self.mw.col.models.by_name(ret.name)
assert notetype is not None
if (id := notetype["id"]) != self._selected_notetype_id:
self.selected_notetype_id = id
@ -146,7 +147,9 @@ class NotetypeChooser(QHBoxLayout):
func(self._selected_notetype_id)
def selected_notetype_name(self) -> str:
return self.mw.col.models.get(self.selected_notetype_id)["name"]
selected_notetype = self.mw.col.models.get(self.selected_notetype_id)
assert selected_notetype is not None
return selected_notetype["name"]
def _ensure_selected_notetype_valid(self) -> None:
if not self.mw.col.models.get(self._selected_notetype_id):

View file

@ -224,13 +224,14 @@ class Overview:
dyn = ""
return f'<div class="descfont descmid description {dyn}">{desc}</div>'
def _table(self) -> str | None:
def _table(self) -> str:
counts = list(self.mw.col.sched.counts())
current_did = self.mw.col.decks.get_current_id()
deck_node = self.mw.col.sched.deck_due_tree(current_did)
but = self.mw.button
if self.mw.col.v3_scheduler():
assert deck_node is not None
buried_new = deck_node.new_count - counts[0]
buried_learning = deck_node.learn_count - counts[1]
buried_review = deck_node.review_count - counts[2]

View file

@ -41,7 +41,7 @@ def _patch_pkgutil() -> None:
def get_data_custom(package: str, resource: str) -> bytes | None:
try:
module = importlib.import_module(package)
reader = module.__loader__.get_resource_reader(package) # type: ignore[attr-defined]
reader = module.__loader__.get_resource_reader(package) # type: ignore
with reader.open_resource(resource) as f:
return f.read()
except Exception:

View file

@ -46,13 +46,16 @@ class Preferences(QDialog):
self.form.network_timeout,
):
spinbox.setSuffix(f" {spinbox.suffix()}")
disable_help_button(self)
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help).setAutoDefault(
False
)
self.form.buttonBox.button(
QDialogButtonBox.StandardButton.Close
).setAutoDefault(False)
help_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help)
assert help_button is not None
help_button.setAutoDefault(False)
close_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close)
assert close_button is not None
close_button.setAutoDefault(False)
qconnect(
self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES)
)
@ -218,6 +221,7 @@ class Preferences(QDialog):
qconnect(self.form.syncAnkiHubLogin.clicked, self.ankihub_sync_login)
def update_login_status(self) -> None:
assert self.prof is not None
if not self.prof.get("syncKey"):
self.form.syncUser.setText(tr.preferences_ankiweb_intro())
self.form.syncLogin.setVisible(True)
@ -241,6 +245,7 @@ class Preferences(QDialog):
def sync_login(self) -> None:
def on_success():
assert self.prof is not None
if self.prof.get("syncKey"):
self.update_login_status()
self.confirm_sync_after_login()
@ -251,6 +256,7 @@ class Preferences(QDialog):
if self.mw.media_syncer.is_syncing():
showWarning("Can't log out while sync in progress.")
return
assert self.prof is not None
self.prof["syncKey"] = None
self.mw.col.media.force_resync()
self.update_login_status()
@ -263,7 +269,10 @@ class Preferences(QDialog):
ankihub_login(self.mw, on_success)
def ankihub_sync_logout(self) -> None:
ankihub_logout(self.mw, self.update_login_status, self.mw.pm.ankihub_token())
ankihub_token = self.mw.pm.ankihub_token()
if ankihub_token is None:
return
ankihub_logout(self.mw, self.update_login_status, ankihub_token)
def confirm_sync_after_login(self) -> None:
from aqt import mw
@ -272,6 +281,7 @@ class Preferences(QDialog):
self.accept_with_callback(self.mw.on_sync_button_clicked)
def update_network(self) -> None:
assert self.prof is not None
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
self.mw.pm.set_periodic_sync_media_minutes(
@ -349,7 +359,6 @@ class Preferences(QDialog):
)
self.form.styleLabel.setVisible(not is_win)
self.form.styleComboBox.setVisible(not is_win)
self.form.legacy_import_export.setChecked(self.mw.pm.legacy_import_export())
qconnect(self.form.resetWindowSizes.clicked, self.on_reset_window_sizes)
self.setup_language()
@ -367,8 +376,6 @@ class Preferences(QDialog):
self.mw.pm.setUiScale(newScale)
restart_required = True
self.mw.pm.set_legacy_import_export(self.form.legacy_import_export.isChecked())
if restart_required:
showInfo(tr.preferences_changes_will_take_effect_when_you())
@ -378,6 +385,7 @@ class Preferences(QDialog):
self.mw.set_theme(Theme(index))
def on_reset_window_sizes(self) -> None:
assert self.prof is not None
regexp = re.compile(r"(Geom(etry)?|State|Splitter|Header)(\d+.\d+)?$")
for key in list(self.prof.keys()):
if regexp.search(key):

View file

@ -636,7 +636,8 @@ create table if not exists profiles
self.meta[f"{self.editor_key(mode)}TagsCollapsed"] = collapsed
def legacy_import_export(self) -> bool:
return self.meta.get("legacy_import", False)
"Always returns False so users with this option enabled are not stuck on the legacy importer after the UI option is removed."
return False
def set_legacy_import_export(self, enabled: bool) -> None:
self.meta["legacy_import"] = enabled

View file

@ -216,6 +216,8 @@ class ProgressManager:
self._maybeShow()
if not self._shown:
return
assert self._win is not None
if label:
self._win.form.label.setText(label)
@ -290,6 +292,7 @@ class ProgressManager:
self._showWin()
def _showWin(self) -> None:
assert self._win is not None
self._shown = time.monotonic()
self._win.show()
@ -297,6 +300,7 @@ class ProgressManager:
# if the parent window has been deleted, the progress dialog may have
# already been dropped; delete it if it hasn't been
if not sip.isdeleted(self._win):
assert self._win is not None
self._win.cancel()
self._win = None
self._shown = 0
@ -314,6 +318,7 @@ class ProgressManager:
def _on_show_timer(self) -> None:
if self.mw.app.focusWindow() is None:
# if no window is focused (eg app is minimized), defer display
assert self._show_timer is not None
self._show_timer.start(10)
return
@ -334,7 +339,7 @@ class ProgressManager:
class ProgressDialog(QDialog):
def __init__(self, parent: QWidget) -> None:
def __init__(self, parent: QWidget | None) -> None:
QDialog.__init__(self, parent)
disable_help_button(self)
self.form = aqt.forms.progress.Ui_Dialog()
@ -349,14 +354,16 @@ class ProgressDialog(QDialog):
self.hide()
self.deleteLater()
def closeEvent(self, evt: QCloseEvent) -> None:
def closeEvent(self, evt: QCloseEvent | None) -> None:
assert evt is not None
if self._closingDown:
evt.accept()
else:
self.wantCancel = True
evt.ignore()
def keyPressEvent(self, evt: QKeyEvent) -> None:
def keyPressEvent(self, evt: QKeyEvent | None) -> None:
assert evt is not None
if evt.key() == Qt.Key.Key_Escape:
evt.ignore()
self.wantCancel = True

View file

@ -245,7 +245,8 @@ av_player = AVPlayer()
def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
cmd = cmd[:]
env = os.environ.copy()
if "LD_LIBRARY_PATH" in env:
# keep LD_LIBRARY_PATH when in snap environment
if "LD_LIBRARY_PATH" in env and "SNAP" not in env:
del env["LD_LIBRARY_PATH"]
if is_win:

View file

@ -61,9 +61,11 @@ class NewDeckStats(QDialog):
b = f.buttonBox.addButton(
tr.statistics_save_pdf(), QDialogButtonBox.ButtonRole.ActionRole
)
assert b is not None
qconnect(b.clicked, self.saveImage)
b.setAutoDefault(False)
b = f.buttonBox.button(QDialogButtonBox.StandardButton.Close)
assert b is not None
b.setAutoDefault(False)
maybeHideClose(self.form.buttonBox)
addCloseShortcut(self)
@ -78,7 +80,7 @@ class NewDeckStats(QDialog):
def reject(self) -> None:
self.deck_chooser.cleanup()
self.form.web.cleanup()
self.form.web = None
self.form.web = None # type: ignore
saveGeom(self, self.name)
aqt.dialogs.markClosed("NewDeckStats")
QDialog.reject(self)
@ -92,7 +94,7 @@ class NewDeckStats(QDialog):
lambda _: self.refresh()
).run_in_background()
def _imagePath(self) -> str:
def _imagePath(self) -> str | None:
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
name = f"anki-{tr.statistics_stats()}{name}"
file = getSaveFile(
@ -115,7 +117,9 @@ class NewDeckStats(QDialog):
# unreadable. A simple fix for now is to scroll to the top of the
# page first.
def after_scroll(arg: Any) -> None:
self.form.web.page().printToPdf(path)
form_web_page = self.form.web.page()
assert form_web_page is not None
form_web_page.printToPdf(path)
tooltip(tr.statistics_saved())
self.form.web.evalWithCallback("window.scrollTo(0, 0);", after_scroll)
@ -165,6 +169,7 @@ class DeckStats(QDialog):
b = f.buttonBox.addButton(
tr.statistics_save_pdf(), QDialogButtonBox.ButtonRole.ActionRole
)
assert b is not None
qconnect(b.clicked, self.saveImage)
b.setAutoDefault(False)
qconnect(f.groups.clicked, lambda: self.changeScope("deck"))
@ -182,7 +187,7 @@ class DeckStats(QDialog):
def reject(self) -> None:
self.form.web.cleanup()
self.form.web = None
self.form.web = None # type: ignore
saveGeom(self, self.name)
aqt.dialogs.markClosed("DeckStats")
QDialog.reject(self)
@ -191,7 +196,7 @@ class DeckStats(QDialog):
self.reject()
callback()
def _imagePath(self) -> str:
def _imagePath(self) -> str | None:
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
name = f"anki-{tr.statistics_stats()}{name}"
file = getSaveFile(
@ -208,7 +213,9 @@ class DeckStats(QDialog):
path = self._imagePath()
if not path:
return
self.form.web.page().printToPdf(path)
form_web_page = self.form.web.page()
assert form_web_page is not None
form_web_page.printToPdf(path)
tooltip(tr.statistics_saved())
def changePeriod(self, n: int) -> None:

View file

@ -101,7 +101,7 @@ class StudyDeck(QDialog):
else:
self.exec()
def eventFilter(self, obj: QObject, evt: QEvent) -> bool:
def eventFilter(self, obj: QObject | None, evt: QEvent | None) -> bool:
if isinstance(evt, QKeyEvent) and evt.type() == QEvent.Type.KeyPress:
new_row = current_row = self.form.list.currentRow()
rows_count = self.form.list.count()
@ -178,6 +178,7 @@ class StudyDeck(QDialog):
def success(out: OpChangesWithId) -> None:
deck = self.mw.col.decks.get(DeckId(out.id))
assert deck is not None
self.name = deck["name"]
self.accept_with_callback()

View file

@ -103,7 +103,7 @@ class Switch(QAbstractButton):
self._position = self.end_position
self.update()
def paintEvent(self, _event: QPaintEvent) -> None:
def paintEvent(self, _event: QPaintEvent | None) -> None:
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
painter.setPen(Qt.PenStyle.NoPen)
@ -162,12 +162,13 @@ class Switch(QAbstractButton):
self._current_label_rectangle(), Qt.AlignmentFlag.AlignCenter, self.label
)
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
def mouseReleaseEvent(self, event: QMouseEvent | None) -> None:
super().mouseReleaseEvent(event)
assert event is not None
if event.button() == Qt.MouseButton.LeftButton:
self._animate_toggle()
def enterEvent(self, event: QEnterEvent) -> None:
def enterEvent(self, event: QEnterEvent | None) -> None:
self.setCursor(Qt.CursorShape.PointingHandCursor)
super().enterEvent(event)

View file

@ -168,7 +168,7 @@ def full_sync(
def confirm_full_download(
mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None]
mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None]
) -> None:
# confirmation step required, as some users customize their notetypes
# in an empty collection, then want to upload them
@ -184,7 +184,7 @@ def confirm_full_download(
def confirm_full_upload(
mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None]
mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None]
) -> None:
# confirmation step required, as some users have reported an upload
# happening despite having their AnkiWeb collection not being empty
@ -220,7 +220,7 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt, label: str) -> None:
def full_download(
mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None]
mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None]
) -> None:
label = tr.sync_downloading_from_ankiweb()
@ -372,7 +372,9 @@ def get_id_and_pass_from_user(
l2.setBuddy(passwd)
vbox.addLayout(g)
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # type: ignore
bb.button(QDialogButtonBox.StandardButton.Ok).setAutoDefault(True)
ok_button = bb.button(QDialogButtonBox.StandardButton.Ok)
assert ok_button is not None
ok_button.setAutoDefault(True)
qconnect(bb.accepted, diag.accept)
qconnect(bb.rejected, diag.reject)
vbox.addWidget(bb)

View file

@ -42,13 +42,17 @@ class TagEdit(QLineEdit):
l = (d.name for d in self.col.decks.all_names_and_ids())
self.model.setStringList(l)
def focusInEvent(self, evt: QFocusEvent) -> None:
def focusInEvent(self, evt: QFocusEvent | None) -> None:
QLineEdit.focusInEvent(self, evt)
def keyPressEvent(self, evt: QKeyEvent) -> None:
def keyPressEvent(self, evt: QKeyEvent | None) -> None:
assert evt is not None
popup = self._completer.popup()
assert popup is not None
if evt.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down):
# show completer on arrow key up/down
if not self._completer.popup().isVisible():
if not popup.isVisible():
self.showCompleter()
return
if (
@ -56,24 +60,21 @@ class TagEdit(QLineEdit):
and evt.modifiers() & Qt.KeyboardModifier.ControlModifier
):
# select next completion
if not self._completer.popup().isVisible():
if not popup.isVisible():
self.showCompleter()
index = self._completer.currentIndex()
self._completer.popup().setCurrentIndex(index)
popup.setCurrentIndex(index)
cur_row = index.row()
if not self._completer.setCurrentRow(cur_row + 1):
self._completer.setCurrentRow(0)
return
if (
evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return)
and self._completer.popup().isVisible()
):
if evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) and popup.isVisible():
# apply first completion if no suggestion selected
selected_row = self._completer.popup().currentIndex().row()
selected_row = popup.currentIndex().row()
if selected_row == -1:
self._completer.setCurrentRow(0)
index = self._completer.currentIndex()
self._completer.popup().setCurrentIndex(index)
popup.setCurrentIndex(index)
self.hideCompleter()
QWidget.keyPressEvent(self, evt)
return
@ -97,15 +98,19 @@ class TagEdit(QLineEdit):
self._completer.setCompletionPrefix(self.text())
self._completer.complete()
def focusOutEvent(self, evt: QFocusEvent) -> None:
def focusOutEvent(self, evt: QFocusEvent | None) -> None:
QLineEdit.focusOutEvent(self, evt)
self.lostFocus.emit() # type: ignore
self._completer.popup().hide()
popup = self._completer.popup()
assert popup is not None
popup.hide()
def hideCompleter(self) -> None:
if sip.isdeleted(self._completer): # type: ignore
return
self._completer.popup().hide()
popup = self._completer.popup()
assert popup is not None
popup.hide()
class TagCompleter(QCompleter):
@ -120,7 +125,9 @@ class TagCompleter(QCompleter):
self.edit = edit
self.cursor: int | None = None
def splitPath(self, tags: str) -> list[str]:
def splitPath(self, tags: str | None) -> list[str]:
assert tags is not None
assert self.edit.col is not None
stripped_tags = tags.strip()
stripped_tags = re.sub(" +", " ", stripped_tags)
self.tags = self.edit.col.tags.split(stripped_tags)

View file

@ -50,7 +50,9 @@ class TagLimit(QDialog):
list.addItem(item)
if select:
idx = list.indexFromItem(item)
list.selectionModel().select(
list_selection_model = list.selectionModel()
assert list_selection_model is not None
list_selection_model.select(
idx, QItemSelectionModel.SelectionFlag.Select
)
@ -77,12 +79,16 @@ class TagLimit(QDialog):
if want_active:
item = self.form.activeList.item(c)
idx = self.form.activeList.indexFromItem(item)
if self.form.activeList.selectionModel().isSelected(idx):
active_list_selection_model = self.form.activeList.selectionModel()
assert active_list_selection_model is not None
if active_list_selection_model.isSelected(idx):
include_tags.append(tag.name)
# inactive
item = self.form.inactiveList.item(c)
idx = self.form.inactiveList.indexFromItem(item)
if self.form.inactiveList.selectionModel().isSelected(idx):
inactive_list_selection_model = self.form.inactiveList.selectionModel()
assert inactive_list_selection_model is not None
if inactive_list_selection_model.isSelected(idx):
exclude_tags.append(tag.name)
if (len(include_tags) + len(exclude_tags)) > 100:

View file

@ -231,7 +231,9 @@ class ThemeManager:
self._current_widget_style = new_widget_style
app = aqt.mw.app
if not self._default_style:
self._default_style = app.style().objectName()
style = app.style()
assert style is not None
self._default_style = style.objectName()
self._apply_palette(app)
self._apply_style(app)
gui_hooks.theme_did_change()

View file

@ -37,7 +37,9 @@ class BottomToolbar:
class ToolbarWebView(AnkiWebView):
hide_condition: Callable[..., bool]
def __init__(self, mw: aqt.AnkiQt, kind: AnkiWebViewKind | None = None) -> None:
def __init__(
self, mw: aqt.AnkiQt, kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT
) -> None:
AnkiWebView.__init__(self, mw, kind=kind)
self.mw = mw
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
@ -172,7 +174,7 @@ class TopWebView(ToolbarWebView):
self.eval("""document.body.style.setProperty("min-height", "0px"); """)
self.evalWithCallback("document.documentElement.offsetHeight", self._onHeight)
def resizeEvent(self, event: QResizeEvent) -> None:
def resizeEvent(self, event: QResizeEvent | None) -> None:
super().resizeEvent(event)
self.mw.web.evalWithCallback(

View file

@ -189,6 +189,7 @@ class MacTTSPlayer(TTSProcessPlayer):
stderr=subprocess.DEVNULL,
)
# write the input text to stdin
assert self._process.stdin is not None
self._process.stdin.write(tag.field_text.encode("utf8"))
self._process.stdin.close()
self._wait_for_termination(tag)
@ -247,6 +248,7 @@ class MacTTSFilePlayer(MacTTSPlayer):
stderr=subprocess.DEVNULL,
)
# write the input text to stdin
assert self._process.stdin is not None
self._process.stdin.write(tag.field_text.encode("utf8"))
self._process.stdin.close()
self._wait_for_termination(tag)

View file

@ -52,7 +52,7 @@ def check_for_update() -> None:
QueryOp(parent=mw, op=do_check, success=on_done).failure(
on_fail
).run_in_background()
).without_collection().run_in_background()
def prompt_to_update(mw: aqt.AnkiQt, ver: str) -> None:

View file

@ -118,10 +118,13 @@ HelpPageArgument = Union["HelpPage.V", str]
def openHelp(section: HelpPageArgument) -> None:
assert tr.backend is not None
backend = tr.backend()
assert backend is not None
if isinstance(section, str):
link = tr.backend().help_page_link(page=HelpPage.INDEX) + section
link = backend.help_page_link(page=HelpPage.INDEX) + section
else:
link = tr.backend().help_page_link(page=section)
link = backend.help_page_link(page=section)
openLink(link)
@ -170,17 +173,20 @@ class MessageBox(QMessageBox):
b = self.addButton(button)
# a translator has complained the default Qt translation is inappropriate, so we override it
if button == QMessageBox.StandardButton.Discard:
assert b is not None
b.setText(tr.actions_discard())
elif isinstance(button, tuple):
b = self.addButton(button[0], button[1])
else:
continue
if callback is not None:
assert b is not None
qconnect(b.clicked, partial(callback, i))
if i == default_button:
self.setDefaultButton(b)
if help is not None:
b = self.addButton(QMessageBox.StandardButton.Help)
assert b is not None
qconnect(b.clicked, lambda: openHelp(help))
self.open()
@ -316,9 +322,11 @@ def showInfo(
mb.setDefaultButton(default)
else:
b = mb.addButton(QMessageBox.StandardButton.Ok)
assert b is not None
b.setDefault(True)
if help is not None:
b = mb.addButton(QMessageBox.StandardButton.Help)
assert b is not None
qconnect(b.clicked, lambda: openHelp(help))
b.setAutoDefault(False)
return mb.exec()
@ -363,7 +371,9 @@ def showText(
if copyBtn:
def onCopy() -> None:
QApplication.clipboard().setText(text.toPlainText())
clipboard = QApplication.clipboard()
assert clipboard is not None
clipboard.setText(text.toPlainText())
btn = QPushButton(tr.qt_misc_copy_to_clipboard())
qconnect(btn.clicked, onCopy)
@ -415,6 +425,7 @@ def askUser(
default = QMessageBox.StandardButton.Yes
r = msgfunc(parent, title, text, sb, default)
if r == QMessageBox.StandardButton.Help:
assert help is not None
openHelp(help)
else:
break
@ -431,7 +442,7 @@ class ButtonedDialog(QMessageBox):
title: str = "Anki",
):
QMessageBox.__init__(self, parent)
self._buttons: list[QPushButton] = []
self._buttons: list[QPushButton | None] = []
self.setWindowTitle(title)
self.help = help
self.setIcon(QMessageBox.Icon.Warning)
@ -444,11 +455,13 @@ class ButtonedDialog(QMessageBox):
def run(self) -> str:
self.exec()
but = self.clickedButton().text()
if but == "Help":
clicked_button = self.clickedButton()
assert clicked_button is not None
txt = clicked_button.text()
if txt == "Help":
# FIXME stop dialog closing?
assert self.help is not None
openHelp(self.help)
txt = self.clickedButton().text()
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
return txt.replace("&", "")
@ -504,13 +517,18 @@ class GetTextDialog(QDialog):
b = QDialogButtonBox(buts) # type: ignore
v.addWidget(b)
self.setLayout(v)
qconnect(b.button(QDialogButtonBox.StandardButton.Ok).clicked, self.accept)
qconnect(b.button(QDialogButtonBox.StandardButton.Cancel).clicked, self.reject)
ok_button = b.button(QDialogButtonBox.StandardButton.Ok)
assert ok_button is not None
qconnect(ok_button.clicked, self.accept)
cancel_button = b.button(QDialogButtonBox.StandardButton.Cancel)
assert cancel_button is not None
qconnect(cancel_button.clicked, self.reject)
if help:
qconnect(
b.button(QDialogButtonBox.StandardButton.Help).clicked,
self.helpRequested,
)
help_button = b.button(QDialogButtonBox.StandardButton.Help)
assert help_button is not None
qconnect(help_button.clicked, self.helpRequested)
self.l.setFocus()
def accept(self) -> None:
@ -520,7 +538,8 @@ class GetTextDialog(QDialog):
return QDialog.reject(self)
def helpRequested(self) -> None:
openHelp(self.help)
if self.help is not None:
openHelp(self.help)
def getText(
@ -624,6 +643,7 @@ def getFile(
if dir and key:
raise Exception("expected dir or key")
if not dir:
assert aqt.mw.pm.profile is not None
dirkey = f"{key}Directory"
dir = aqt.mw.pm.profile.get(dirkey, "")
else:
@ -635,6 +655,7 @@ def getFile(
else QFileDialog.FileMode.ExistingFile
)
d.setFileMode(mode)
assert dir is not None
if os.path.exists(dir):
d.setDirectory(dir)
d.setWindowTitle(title)
@ -644,6 +665,7 @@ def getFile(
def accept() -> None:
files = list(d.selectedFiles())
if dirkey:
assert aqt.mw.pm.profile is not None
dir = os.path.dirname(files[0])
aqt.mw.pm.profile[dirkey] = dir
result = files if multi else files[0]
@ -683,10 +705,11 @@ def getSaveFile(
dir_description: str,
key: str,
ext: str,
fname: str | None = None,
) -> str:
fname: str = "",
) -> str | None:
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
variable. The file dialog will default to open with FNAME."""
assert aqt.mw.pm.profile is not None
config_key = f"{dir_description}Directory"
defaultPath = QStandardPaths.writableLocation(
@ -709,9 +732,10 @@ def getSaveFile(
dir = os.path.dirname(file)
aqt.mw.pm.profile[config_key] = dir
# check if it exists
if os.path.exists(file):
if not askUser(tr.qt_misc_this_file_exists_are_you_sure(), parent):
return None
if os.path.exists(file) and not askUser(
tr.qt_misc_this_file_exists_are_you_sure(), parent
):
return None
return file
@ -735,6 +759,7 @@ def _qt_state_key(kind: _QtStateKeyKind, key: str) -> str:
def saveGeom(widget: QWidget, key: str) -> None:
# restoring a fullscreen window breaks the tab functionality of 5.15
if not widget.isFullScreen() or qtmajor == 6:
assert aqt.mw.pm.profile is not None
key = _qt_state_key(_QtStateKeyKind.GEOMETRY, key)
aqt.mw.pm.profile[key] = widget.saveGeometry()
@ -745,6 +770,7 @@ def restoreGeom(
adjustSize: bool = False,
default_size: tuple[int, int] | None = None,
) -> None:
assert aqt.mw.pm.profile is not None
key = _qt_state_key(_QtStateKeyKind.GEOMETRY, key)
if existing_geom := aqt.mw.pm.profile.get(key):
widget.restoreGeometry(existing_geom)
@ -756,7 +782,9 @@ def restoreGeom(
def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
handle = widget.window().windowHandle()
window = widget.window()
assert window is not None
handle = window.windowHandle()
if not handle:
# window has not yet been shown, retry later
aqt.mw.progress.timer(
@ -765,7 +793,9 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
return
# ensure widget is smaller than screen bounds
geom = handle.screen().availableGeometry()
screen = handle.screen()
assert screen is not None
geom = screen.availableGeometry()
wsize = widget.size()
cappedWidth = min(geom.width(), wsize.width())
cappedHeight = min(geom.height(), wsize.height())
@ -784,44 +814,52 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
def saveState(widget: QFileDialog | QMainWindow, key: str) -> None:
assert aqt.mw.pm.profile is not None
key = _qt_state_key(_QtStateKeyKind.STATE, key)
aqt.mw.pm.profile[key] = widget.saveState()
def restoreState(widget: QFileDialog | QMainWindow, key: str) -> None:
assert aqt.mw.pm.profile is not None
key = _qt_state_key(_QtStateKeyKind.STATE, key)
if data := aqt.mw.pm.profile.get(key):
widget.restoreState(data)
def saveSplitter(widget: QSplitter, key: str) -> None:
assert aqt.mw.pm.profile is not None
key = _qt_state_key(_QtStateKeyKind.SPLITTER, key)
aqt.mw.pm.profile[key] = widget.saveState()
def restoreSplitter(widget: QSplitter, key: str) -> None:
assert aqt.mw.pm.profile is not None
key = _qt_state_key(_QtStateKeyKind.SPLITTER, key)
if data := aqt.mw.pm.profile.get(key):
widget.restoreState(data)
def saveHeader(widget: QHeaderView, key: str) -> None:
assert aqt.mw.pm.profile is not None
key = _qt_state_key(_QtStateKeyKind.HEADER, key)
aqt.mw.pm.profile[key] = widget.saveState()
def restoreHeader(widget: QHeaderView, key: str) -> None:
assert aqt.mw.pm.profile is not None
key = _qt_state_key(_QtStateKeyKind.HEADER, key)
if state := aqt.mw.pm.profile.get(key):
widget.restoreState(state)
def save_is_checked(widget: QCheckBox, key: str) -> None:
assert aqt.mw.pm.profile is not None
key += "IsChecked"
aqt.mw.pm.profile[key] = widget.isChecked()
def restore_is_checked(widget: QCheckBox, key: str) -> None:
assert aqt.mw.pm.profile is not None
key += "IsChecked"
if aqt.mw.pm.profile.get(key) is not None:
widget.setChecked(aqt.mw.pm.profile[key])
@ -847,8 +885,11 @@ def restore_combo_index_for_session(
def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> str:
assert aqt.mw.pm.profile is not None
name += "BoxHistory"
text_input = comboBox.lineEdit().text()
line_edit = comboBox.lineEdit()
assert line_edit is not None
text_input = line_edit.text()
if text_input in history:
history.remove(text_input)
history.insert(0, text_input)
@ -861,14 +902,17 @@ def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> st
def restore_combo_history(comboBox: QComboBox, name: str) -> list[str]:
assert aqt.mw.pm.profile is not None
name += "BoxHistory"
history = aqt.mw.pm.profile.get(name, [])
comboBox.addItems([""] + history)
if history:
session_input = aqt.mw.pm.session.get(name)
if session_input and session_input == history[0]:
comboBox.lineEdit().setText(session_input)
comboBox.lineEdit().selectAll()
line_edit = comboBox.lineEdit()
assert line_edit is not None
line_edit.setText(session_input)
line_edit.selectAll()
return history
@ -980,7 +1024,7 @@ def send_to_trash(path: Path) -> None:
except Exception as exc:
# Linux users may not have a trash folder set up
print("trash failure:", path, exc)
if path.is_dir:
if path.is_dir():
shutil.rmtree(path)
else:
path.unlink()
@ -1005,7 +1049,8 @@ def tooltip(
class CustomLabel(QLabel):
silentlyClose = True
def mousePressEvent(self, evt: QMouseEvent) -> None:
def mousePressEvent(self, evt: QMouseEvent | None) -> None:
assert evt is not None
evt.accept()
self.hide()
@ -1074,7 +1119,7 @@ class MenuList:
print(
"MenuList will be removed; please copy it into your add-on's code if you need it."
)
self.children: list[MenuListChild] = []
self.children: list[MenuListChild | None] = []
def addItem(self, title: str, func: Callable) -> MenuItem:
item = MenuItem(title, func)
@ -1114,6 +1159,7 @@ class SubMenu(MenuList):
def renderTo(self, menu: QMenu) -> None:
submenu = menu.addMenu(self.title)
assert submenu is not None
super().renderTo(submenu)
@ -1124,6 +1170,7 @@ class MenuItem:
def renderTo(self, qmenu: QMenu) -> None:
a = qmenu.addAction(self.title)
assert a is not None
qconnect(a.triggered, self.func)

View file

@ -8,9 +8,9 @@ import json
import os
import re
import sys
from collections.abc import Callable
from collections.abc import Callable, Sequence
from enum import Enum
from typing import TYPE_CHECKING, Any, Optional, cast
from typing import TYPE_CHECKING, Any, cast
import anki
import anki.lang
@ -89,17 +89,23 @@ class AnkiWebPage(QWebEnginePage):
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
script.setRunsOnSubFrames(False)
self.profile().scripts().insert(script)
profile = self.profile()
assert profile is not None
scripts = profile.scripts()
assert scripts is not None
scripts.insert(script)
def javaScriptConsoleMessage(
self,
level: QWebEnginePage.JavaScriptConsoleMessageLevel,
msg: str,
msg: str | None,
line: int,
srcID: str,
srcID: str | None,
) -> None:
# not translated because console usually not visible,
# and may only accept ascii text
assert srcID is not None
if srcID.startswith("data"):
srcID = ""
else:
@ -162,10 +168,16 @@ class AnkiWebPage(QWebEnginePage):
def _onCmd(self, str: str) -> Any:
return self._onBridgeCmd(str)
def javaScriptAlert(self, frame: Any, text: str) -> None:
def javaScriptAlert(self, frame: Any, text: str | None) -> None:
if text is None:
return
showInfo(text)
def javaScriptConfirm(self, frame: Any, text: str) -> bool:
def javaScriptConfirm(self, frame: Any, text: str | None) -> bool:
if text is None:
return False
return askUser(text)
@ -285,7 +297,7 @@ class AnkiWebView(QWebEngineView):
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
self._domDone = True
self._pendingActions: list[Callable[[], None]] = []
self._pendingActions: list[tuple[str, Sequence[Any]]] = []
self.requiresCol = True
self.setPage(self._page)
self._disable_zoom = False
@ -328,7 +340,9 @@ class AnkiWebView(QWebEngineView):
# with target="_blank") and return view
return AnkiWebView()
def eventFilter(self, obj: QObject, evt: QEvent) -> bool:
def eventFilter(self, obj: QObject | None, evt: QEvent | None) -> bool:
if evt is None:
return False
if self._disable_zoom and is_gesture_or_zoom_event(evt):
return True
@ -377,31 +391,34 @@ class AnkiWebView(QWebEngineView):
def onSelectAll(self) -> None:
self.triggerPageAction(QWebEnginePage.WebAction.SelectAll)
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
def contextMenuEvent(self, evt: QContextMenuEvent | None) -> None:
m = QMenu(self)
self._maybe_add_copy_action(m)
gui_hooks.webview_will_show_context_menu(self, m)
m.popup(QCursor.pos())
if m.actions():
m.popup(QCursor.pos())
def _maybe_add_copy_action(self, menu: QMenu) -> None:
if self.hasSelection():
a = menu.addAction(tr.actions_copy())
assert a is not None
qconnect(a.triggered, self.onCopy)
def dropEvent(self, evt: QDropEvent) -> None:
def dropEvent(self, evt: QDropEvent | None) -> None:
if self.allow_drops:
super().dropEvent(evt)
def setHtml( # type: ignore[override]
self, html: str, context: PageContext | None = None
) -> None:
from aqt.mediasrv import PageContext
# discard any previous pending actions
self._pendingActions = []
self._domDone = True
if context is None:
context = PageContext.UNKNOWN
self._queueAction(lambda: self._setHtml(html, context))
self._queueAction("setHtml", html, context)
self.set_open_links_externally(True)
self.allow_drops = False
self.show()
@ -453,7 +470,9 @@ class AnkiWebView(QWebEngineView):
return 1
def setPlaybackRequiresGesture(self, value: bool) -> None:
self.settings().setAttribute(
settings = self.settings()
assert settings is not None
settings.setAttribute(
QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture, value
)
@ -630,10 +649,13 @@ html {{ {font} }}
def eval(self, js: str) -> None:
self.evalWithCallback(js, None)
def evalWithCallback(self, js: str, cb: Optional[Callable]) -> None:
self._queueAction(lambda: self._evalWithCallback(js, cb))
def evalWithCallback(self, js: str, cb: Callable | None) -> None:
self._queueAction("eval", js, cb)
def _evalWithCallback(self, js: str, cb: Callable[[Any], Any] | None) -> None:
page = self.page()
assert page is not None
def _evalWithCallback(self, js: str, cb: Optional[Callable[[Any], Any]]) -> None:
if cb:
def handler(val: Any) -> None:
@ -642,20 +664,26 @@ html {{ {font} }}
return
cb(val)
self.page().runJavaScript(js, handler)
page.runJavaScript(js, handler)
else:
self.page().runJavaScript(js)
page.runJavaScript(js)
def _queueAction(self, action: Callable[[], None]) -> None:
self._pendingActions.append(action)
def _queueAction(self, name: str, *args: Any) -> None:
self._pendingActions.append((name, args))
self._maybeRunActions()
def _maybeRunActions(self) -> None:
if sip.isdeleted(self):
return
while self._pendingActions and self._domDone:
action = self._pendingActions.pop(0)
action()
name, args = self._pendingActions.pop(0)
if name == "eval":
self._evalWithCallback(*args)
elif name == "setHtml":
self._setHtml(*args)
else:
raise Exception(f"unknown action: {name}")
def _openLinksExternally(self, url: str) -> None:
openLink(url)
@ -677,7 +705,9 @@ html {{ {font} }}
return
if not self._filterSet:
self.focusProxy().installEventFilter(self)
focus_proxy = self.focusProxy()
assert focus_proxy is not None
focus_proxy.installEventFilter(self)
self._filterSet = True
if cmd == "domDone":

9
qt/bundle/Cargo.lock generated
View file

@ -202,12 +202,13 @@ dependencies = [
[[package]]
name = "libmimalloc-sys"
version = "0.1.22"
version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1b8479c593dba88c2741fc50b92e13dbabbbe0bd504d979f244ccc1a5b1c01"
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
dependencies = [
"cc",
"cty",
"libc",
]
[[package]]
@ -267,9 +268,9 @@ dependencies = [
[[package]]
name = "mimalloc"
version = "0.1.26"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb74897ce508e6c49156fd1476fc5922cbc6e75183c65e399c765a09122e5130"
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
dependencies = [
"libmimalloc-sys",
]

View file

@ -103,7 +103,6 @@ tracing-subscriber.workspace = true
unic-ucd-category.workspace = true
unicase.workspace = true
unicode-normalization.workspace = true
utime.workspace = true
zip.workspace = true
zstd.workspace = true

View file

@ -35,6 +35,7 @@ pub enum FileOp {
Sync,
Metadata,
DecodeUtf8Filename,
SetFileTimes,
/// For legacy errors without any context.
Unknown,
}
@ -61,6 +62,7 @@ impl FileIoError {
FileOp::Sync => "sync".into(),
FileOp::Metadata => "get metadata".into(),
FileOp::DecodeUtf8Filename => "decode utf8 filename".into(),
FileOp::SetFileTimes => "set file times".into(),
},
self.path.to_string_lossy(),
self.source

View file

@ -4,8 +4,11 @@
mod error;
use std::fs::File;
use std::fs::FileTimes;
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Seek;
use std::io::Write;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
@ -37,6 +40,13 @@ pub fn open_file(path: impl AsRef<Path>) -> Result<File> {
})
}
pub fn open_file_ext(path: impl AsRef<Path>, options: OpenOptions) -> Result<File> {
options.open(&path).context(FileIoSnafu {
path: path.as_ref(),
op: FileOp::Open,
})
}
/// See [std::fs::write].
pub fn write_file(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
std::fs::write(&path, contents).context(FileIoSnafu {
@ -45,6 +55,45 @@ pub fn write_file(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<
})
}
pub fn write_file_and_flush(
path: impl AsRef<Path> + Clone,
contents: impl AsRef<[u8]>,
) -> Result<()> {
let mut file = create_file(path.clone())?;
file.write_all(contents.as_ref()).context(FileIoSnafu {
path: path.clone().as_ref(),
op: FileOp::Write,
})?;
file.sync_all().context(FileIoSnafu {
path: path.as_ref(),
op: FileOp::Sync,
})
}
/// See [File::set_times].
pub fn set_file_times(path: impl AsRef<Path>, times: FileTimes) -> Result<()> {
#[cfg(not(windows))]
let file = open_file(&path)?;
#[cfg(windows)]
let file = {
use std::os::windows::fs::OpenOptionsExt;
open_file_ext(
&path,
OpenOptions::new()
.write(true)
// It's required to modify the time attributes of a directory in windows system.
.custom_flags(0x02000000) // FILE_FLAG_BACKUP_SEMANTICS
.to_owned(),
)?
};
file.set_times(times).context(FileIoSnafu {
path: path.as_ref(),
op: FileOp::SetFileTimes,
})
}
/// See [std::fs::remove_file].
#[allow(dead_code)]
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {

View file

@ -29,13 +29,13 @@ pub fn write_rust_protos(descriptors_path: PathBuf) -> Result<DescriptorPool> {
)
.type_attribute(
"Deck.Normal.DayLimit",
"#[derive(Copy, Eq, serde::Deserialize, serde::Serialize)]",
"#[derive(Eq, serde::Deserialize, serde::Serialize)]",
)
.type_attribute("HelpPageLinkRequest.HelpPage", "#[derive(strum::EnumIter)]")
.type_attribute("CsvMetadata.Delimiter", "#[derive(strum::EnumIter)]")
.type_attribute(
"Preferences.BackupLimits",
"#[derive(Copy, serde::Deserialize, serde::Serialize)]",
"#[derive(serde::Deserialize, serde::Serialize)]",
)
.type_attribute(
"CsvMetadata.DupeResolution",

View file

@ -11,7 +11,7 @@ use serde::Serialize;
use crate::ankihub::login::LoginRequest;
static API_VERSION: &str = "18.0";
static API_VERSION: &str = "19.0";
static DEFAULT_API_URL: &str = "https://app.ankihub.net/api/";
#[derive(Clone)]

View file

@ -38,6 +38,7 @@ impl From<BoolKeyProto> for BoolKey {
BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards,
BoolKeyProto::RenderLatex => BoolKey::RenderLatex,
BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled,
BoolKeyProto::FsrsShortTermWithStepsEnabled => BoolKey::FsrsShortTermWithStepsEnabled,
}
}
}

View file

@ -42,7 +42,7 @@ impl AnkiError {
AnkiError::InvalidId => Kind::InvalidInput,
AnkiError::InvalidMethodIndex
| AnkiError::InvalidServiceIndex
| AnkiError::FsrsWeightsInvalid
| AnkiError::FsrsParamsInvalid
| AnkiError::FsrsUnableToDetermineDesiredRetention
| AnkiError::FsrsInsufficientData => Kind::InvalidInput,
#[cfg(windows)]
@ -66,6 +66,7 @@ impl From<SyncErrorKind> for Kind {
fn from(err: SyncErrorKind) -> Self {
match err {
SyncErrorKind::AuthFailed => Kind::SyncAuthError,
SyncErrorKind::ServerMessage => Kind::SyncServerMessage,
_ => Kind::SyncOtherError,
}
}

View file

@ -111,12 +111,12 @@ impl Card {
/// Returns the card's due date as a timestamp if it has one.
fn due_time(&self, timing: &SchedTimingToday) -> Option<TimestampSecs> {
if self.queue == CardQueue::Learn {
Some(TimestampSecs(self.due as i64))
Some(TimestampSecs(self.original_or_current_due() as i64))
} else if self.is_due_in_days() {
Some(
TimestampSecs::now().adding_secs(
((self.original_or_current_due() - timing.days_elapsed as i32)
.saturating_mul(86400)) as i64,
(self.original_or_current_due() as i64 - timing.days_elapsed as i64)
.saturating_mul(86400),
),
)
} else {
@ -128,12 +128,15 @@ impl Card {
/// date' or an add-on has changed the due date, this won't be accurate.
pub(crate) fn days_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> {
if !self.is_due_in_days() {
Some((timing.next_day_at.0 as u32).saturating_sub(self.due.max(0) as u32) / 86_400)
Some(
(timing.next_day_at.0 as u32).saturating_sub(self.original_or_current_due() as u32)
/ 86_400,
)
} else {
self.due_time(timing).map(|due| {
due.adding_secs(-86_400 * self.interval as i64)
.elapsed_secs() as u32
/ 86_400
(due.adding_secs(-86_400 * self.interval as i64)
.elapsed_secs()
/ 86_400) as u32
})
}
}

View file

@ -41,6 +41,7 @@ pub enum BoolKey {
WithDeckConfigs,
Fsrs,
LoadBalancerEnabled,
FsrsShortTermWithStepsEnabled,
#[strum(to_string = "normalize_note_text")]
NormalizeNoteText,
#[strum(to_string = "dayLearnFirst")]

View file

@ -74,11 +74,12 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
bury_new: false,
bury_reviews: false,
bury_interday_learning: false,
fsrs_weights: vec![],
fsrs_params_4: vec![],
fsrs_params_5: vec![],
desired_retention: 0.9,
other: Vec::new(),
historical_retention: 0.9,
weight_search: String::new(),
param_search: String::new(),
ignore_revlogs_before_date: String::new(),
easy_days_percentages: Vec::new(),
};
@ -105,6 +106,15 @@ impl DeckConfig {
self.mtime_secs = TimestampSecs::now();
self.usn = usn;
}
/// Retrieve the FSRS 5.0 params, falling back on 4.x ones.
pub fn fsrs_params(&self) -> &Vec<f32> {
if self.inner.fsrs_params_5.len() == 19 {
&self.inner.fsrs_params_5
} else {
&self.inner.fsrs_params_4
}
}
}
impl Collection {

View file

@ -69,8 +69,10 @@ pub struct DeckConfSchema11 {
#[serde(default)]
bury_interday_learning: bool,
#[serde(default, rename = "fsrsWeights")]
fsrs_params_4: Vec<f32>,
#[serde(default)]
fsrs_weights: Vec<f32>,
fsrs_params_5: Vec<f32>,
#[serde(default)]
desired_retention: f32,
#[serde(default)]
@ -92,8 +94,8 @@ pub struct DeckConfSchema11 {
#[serde(default)]
/// historical retention
sm2_retention: f32,
#[serde(default)]
weight_search: String,
#[serde(default, rename = "weightSearch")]
param_search: String,
#[serde(flatten)]
other: HashMap<String, Value>,
@ -306,10 +308,11 @@ impl Default for DeckConfSchema11 {
new_sort_order: 0,
new_gather_priority: 0,
bury_interday_learning: false,
fsrs_weights: vec![],
fsrs_params_4: vec![],
fsrs_params_5: vec![],
desired_retention: 0.9,
sm2_retention: 0.9,
weight_search: "".to_string(),
param_search: "".to_string(),
ignore_revlogs_before_date: "".to_string(),
easy_days_percentages: vec![1.0; 7],
}
@ -386,12 +389,13 @@ impl From<DeckConfSchema11> for DeckConfig {
bury_new: c.new.bury,
bury_reviews: c.rev.bury,
bury_interday_learning: c.bury_interday_learning,
fsrs_weights: c.fsrs_weights,
fsrs_params_4: c.fsrs_params_4,
fsrs_params_5: c.fsrs_params_5,
ignore_revlogs_before_date: c.ignore_revlogs_before_date,
easy_days_percentages: c.easy_days_percentages,
desired_retention: c.desired_retention,
historical_retention: c.sm2_retention,
weight_search: c.weight_search,
param_search: c.param_search,
other: other_bytes,
},
}
@ -498,10 +502,11 @@ impl From<DeckConfig> for DeckConfSchema11 {
new_sort_order: i.new_card_sort_order,
new_gather_priority: i.new_card_gather_priority,
bury_interday_learning: i.bury_interday_learning,
fsrs_weights: i.fsrs_weights,
fsrs_params_4: i.fsrs_params_4,
fsrs_params_5: i.fsrs_params_5,
desired_retention: i.desired_retention,
sm2_retention: i.historical_retention,
weight_search: i.weight_search,
param_search: i.param_search,
ignore_revlogs_before_date: i.ignore_revlogs_before_date,
easy_days_percentages: i.easy_days_percentages,
}
@ -526,6 +531,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
"interdayLearningMix",
"newGatherPriority",
"fsrsWeights",
"fsrsParams5",
"desiredRetention",
"stopTimerOnAnswer",
"secondsToShowQuestion",

View file

@ -21,7 +21,7 @@ use crate::decks::NormalDeck;
use crate::prelude::*;
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry;
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
use crate::scheduler::fsrs::weights::ignore_revlogs_before_ms_from_config;
use crate::scheduler::fsrs::params::ignore_revlogs_before_ms_from_config;
use crate::search::JoinSearches;
use crate::search::Negated;
use crate::search::SearchNode;
@ -50,7 +50,7 @@ impl Collection {
deck: DeckId,
) -> Result<anki_proto::deck_config::DeckConfigsForUpdate> {
let mut defaults = DeckConfig::default();
defaults.inner.fsrs_weights = DEFAULT_PARAMETERS.into();
defaults.inner.fsrs_params_5 = DEFAULT_PARAMETERS.into();
let last_optimize = self.get_config_i32(I32ConfigKey::LastFsrsOptimize) as u32;
let days_since_last_fsrs_optimize = if last_optimize > 0 {
self.timing_today()?
@ -88,6 +88,12 @@ impl Collection {
// grab the config and sort it
let mut config = self.storage.all_deck_config()?;
config.sort_unstable_by(|a, b| a.name.cmp(&b.name));
// pre-fill empty fsrs 5 params with 4 params
config.iter_mut().for_each(|c| {
if c.inner.fsrs_params_5.is_empty() {
c.inner.fsrs_params_5 = c.inner.fsrs_params_4.clone();
}
});
// combine with use counts
let counts = self.get_deck_config_use_counts()?;
@ -153,14 +159,20 @@ impl Collection {
configs_after_update.remove(dcid);
}
if req.mode == UpdateDeckConfigsMode::ComputeAllWeights {
self.compute_all_weights(&mut req)?;
if req.mode == UpdateDeckConfigsMode::ComputeAllParams {
self.compute_all_params(&mut req)?;
}
// add/update provided configs
for conf in &mut req.configs {
// If the user has provided empty FSRS5 params, zero out any
// old params as well, so we don't fall back on them, which would
// be surprising as they're not shown in the GUI.
if conf.inner.fsrs_params_5.is_empty() {
conf.inner.fsrs_params_4.clear();
}
// check the provided parameters are valid before we save them
FSRS::new(Some(&conf.inner.fsrs_weights))?;
FSRS::new(Some(conf.fsrs_params()))?;
self.add_or_update_deck_config(conf)?;
configs_after_update.insert(conf.id, conf.clone());
}
@ -195,13 +207,13 @@ impl Collection {
if let Ok(normal) = deck.normal() {
let deck_id = deck.id;
// previous order & weights
// previous order & params
let previous_config_id = DeckConfigId(normal.config_id);
let previous_config = configs_before_update.get(&previous_config_id);
let previous_order = previous_config
.map(|c| c.inner.new_card_insert_order())
.unwrap_or_default();
let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights);
let previous_params = previous_config.map(|c| c.fsrs_params());
let previous_retention = previous_config.map(|c| c.inner.desired_retention);
// if a selected (sub)deck, or its old config was removed, update deck to point
@ -227,11 +239,11 @@ impl Collection {
self.sort_deck(deck_id, current_order, usn)?;
}
// if weights differ, memory state needs to be recomputed
let current_weights = current_config.map(|c| &c.inner.fsrs_weights);
// if params differ, memory state needs to be recomputed
let current_params = current_config.map(|c| c.fsrs_params());
let current_retention = current_config.map(|c| c.inner.desired_retention);
if fsrs_toggled
|| previous_weights != current_weights
|| previous_params != current_params
|| previous_retention != current_retention
{
decks_needing_memory_recompute
@ -249,10 +261,10 @@ impl Collection {
.into_iter()
.map(|(conf_id, search)| {
let config = configs_after_update.get(&conf_id);
let weights = config.and_then(|c| {
let params = config.and_then(|c| {
if req.fsrs {
Some(UpdateMemoryStateRequest {
weights: c.inner.fsrs_weights.clone(),
params: c.fsrs_params().clone(),
desired_retention: c.inner.desired_retention,
max_interval: c.inner.maximum_review_interval,
reschedule: req.fsrs_reschedule,
@ -262,12 +274,9 @@ impl Collection {
None
}
});
let search = SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search))
.and(SearchNode::State(StateKind::Suspended).negated())
.try_into_search()?;
Ok(UpdateMemoryStateEntry {
req: weights,
search,
req: params,
search: SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)),
ignore_before: config
.map(ignore_revlogs_before_ms_from_config)
.unwrap_or(Ok(0.into()))?,
@ -320,7 +329,7 @@ impl Collection {
}
Ok(())
}
fn compute_all_weights(&mut self, req: &mut UpdateDeckConfigsRequest) -> Result<()> {
fn compute_all_params(&mut self, req: &mut UpdateDeckConfigsRequest) -> Result<()> {
require!(req.fsrs, "FSRS must be enabled");
// frontend didn't include any unmodified deck configs, so we need to fill them
@ -335,28 +344,28 @@ impl Collection {
// other parts of the code expect the currently-selected preset to come last
req.configs.push(previous_last);
// calculate and apply weights to each preset
// calculate and apply params to each preset
let config_len = req.configs.len() as u32;
for (idx, config) in req.configs.iter_mut().enumerate() {
let search = if config.inner.weight_search.trim().is_empty() {
let search = if config.inner.param_search.trim().is_empty() {
SearchNode::Preset(config.name.clone())
.and(SearchNode::State(StateKind::Suspended).negated())
.try_into_search()?
.to_string()
} else {
config.inner.weight_search.clone()
config.inner.param_search.clone()
};
let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?;
match self.compute_weights(
match self.compute_params(
&search,
ignore_revlogs_before_ms,
idx as u32 + 1,
config_len,
&config.inner.fsrs_weights,
config.fsrs_params(),
) {
Ok(weights) => {
println!("{}: {:?}", config.name, weights.weights);
config.inner.fsrs_weights = weights.weights;
Ok(params) => {
println!("{}: {:?}", config.name, params.params);
config.inner.fsrs_params_5 = params.params;
}
Err(AnkiError::Interrupted) => return Err(AnkiError::Interrupted),
Err(err) => {

View file

@ -60,7 +60,12 @@ fn search_order_label(order: FilteredSearchOrder, tr: &I18n) -> String {
FilteredSearchOrder::Added => tr.decks_order_added(),
FilteredSearchOrder::Due => tr.decks_order_due(),
FilteredSearchOrder::ReverseAdded => tr.decks_latest_added_first(),
FilteredSearchOrder::DuePriority => tr.decks_relative_overdueness(),
FilteredSearchOrder::RetrievabilityAscending => {
tr.deck_config_sort_order_retrievability_ascending()
}
FilteredSearchOrder::RetrievabilityDescending => {
tr.deck_config_sort_order_retrievability_descending()
}
}
.into()
}

View file

@ -113,7 +113,7 @@ pub enum AnkiError {
},
InvalidMethodIndex,
InvalidServiceIndex,
FsrsWeightsInvalid,
FsrsParamsInvalid,
/// Returned by fsrs-rs; may happen even if 400+ reviews
FsrsInsufficientData,
/// Generated by our backend if count < 400
@ -148,7 +148,6 @@ impl AnkiError {
tr.card_templates_identical_front(index + 1)
}
CardTypeErrorDetails::MissingCloze => tr.card_templates_missing_cloze(),
CardTypeErrorDetails::ExtraneousCloze => tr.card_templates_extraneous_cloze(),
};
format!("{}<br>{}", header, details)
}
@ -181,7 +180,7 @@ impl AnkiError {
AnkiError::FsrsInsufficientReviews { count } => {
tr.deck_config_must_have_400_reviews(*count).into()
}
AnkiError::FsrsWeightsInvalid => tr.deck_config_invalid_parameters().into(),
AnkiError::FsrsParamsInvalid => tr.deck_config_invalid_parameters().into(),
AnkiError::SchedulerUpgradeRequired => {
tr.scheduling_update_required().replace("V2", "v3")
}
@ -203,7 +202,6 @@ impl AnkiError {
CardTypeErrorDetails::Duplicate { .. } => HelpPage::CardTypeDuplicate,
CardTypeErrorDetails::NoFrontField => HelpPage::CardTypeNoFrontField,
CardTypeErrorDetails::MissingCloze => HelpPage::CardTypeMissingCloze,
CardTypeErrorDetails::ExtraneousCloze => HelpPage::CardTypeExtraneousCloze,
}),
_ => None,
}
@ -323,5 +321,4 @@ pub enum CardTypeErrorDetails {
NoFrontField,
NoSuchField { field: String },
MissingCloze,
ExtraneousCloze,
}

View file

@ -497,9 +497,9 @@ fn maybe_set_tags_column(metadata: &mut CsvMetadata, meta_columns: &HashSet<usiz
if metadata.tags_column == 0 {
if let Some(CsvNotetype::GlobalNotetype(ref global)) = metadata.notetype {
let max_field = global.field_columns.iter().max().copied().unwrap_or(0);
for idx in (max_field + 1) as usize..metadata.column_labels.len() {
for idx in (max_field + 1) as usize..=metadata.column_labels.len() {
if !meta_columns.contains(&idx) {
metadata.tags_column = max_field + 1;
metadata.tags_column = idx as u32;
break;
}
}
@ -843,4 +843,23 @@ pub(in crate::import_export) mod test {
);
assert_eq!(metadata!(col, "\u{feff}tags:foo\n").global_tags, ["foo"]);
}
#[test]
fn should_not_set_tags_column_if_all_are_field_columns() {
let meta_columns = Default::default();
let mut metadata = CsvMetadata::defaults_for_testing();
maybe_set_tags_column(&mut metadata, &meta_columns);
assert_eq!(metadata.tags_column, 0);
}
#[test]
fn should_set_tags_column_to_next_unused_column() {
let mut meta_columns = HashSet::default();
meta_columns.insert(3);
let mut metadata = CsvMetadata::defaults_for_testing();
metadata.column_labels.push(String::new());
metadata.column_labels.push(String::new());
maybe_set_tags_column(&mut metadata, &meta_columns);
assert_eq!(metadata.tags_column, 4);
}
}

View file

@ -38,9 +38,6 @@ pub fn help_page_link_suffix(page: HelpPage) -> &'static str {
"templates/errors.html#no-field-replacement-on-front-side"
}
HelpPage::CardTypeMissingCloze => "templates/errors.html#no-cloze-filter-on-cloze-notetype",
HelpPage::CardTypeExtraneousCloze => {
"templates/errors.html#cloze-filter-outside-cloze-notetype"
}
HelpPage::Troubleshooting => "troubleshooting.html",
}
}

View file

@ -546,6 +546,7 @@ pub(crate) mod test {
use anki_io::create_dir;
use anki_io::read_to_string;
use anki_io::write_file;
use anki_io::write_file_and_flush;
use tempfile::tempdir;
use tempfile::TempDir;
@ -696,7 +697,7 @@ Unused: unused.jpg
fn unicode_normalization() -> Result<()> {
let (_dir, mgr, mut col) = common_setup()?;
write_file(mgr.media_folder.join("ぱぱ.jpg"), "nfd encoding")?;
write_file_and_flush(mgr.media_folder.join("ぱぱ.jpg"), "nfd encoding")?;
let mut output = {
let mut checker = col.media_checker()?;

Some files were not shown because too many files have changed in this diff Show more