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 strict_optional = True
[mypy-aqt.operations.*] [mypy-aqt.operations.*]
strict_optional = True 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] [mypy-anki.scheduler.base]
strict_optional = True strict_optional = True
[mypy-anki._backend.rsbridge] [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> Taylor Obyen <162023405+taylorobyen@users.noreply.github.com>
Kris Cherven <krischerven@gmail.com> Kris Cherven <krischerven@gmail.com>
twwn <github.com/twwn> twwn <github.com/twwn>
Shirish Pokhrel <singurty@gmail.com> Cy Pokhrel <cy@cy7.sh>
Park Hyunwoo <phu54321@naver.com> Park Hyunwoo <phu54321@naver.com>
Tomas Fabrizio Orsi <torsi@fi.uba.ar> 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" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs] [workspace.dependencies.fsrs]
version = "=1.3.1" version = "=1.4.3"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941" # rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
# path = "../open-spaced-repetition/fsrs-rs" # path = "../open-spaced-repetition/fsrs-rs"
@ -55,19 +55,19 @@ unicase = "=2.6.0" # any changes could invalidate sqlite indexes
# normal # normal
ammonia = "4.0.0" ammonia = "4.0.0"
anyhow = "1.0.86" anyhow = "1.0.90"
apple-bundles = "0.17.0" apple-bundles = "0.17.0"
async-compression = { version = "0.4.12", features = ["zstd", "tokio"] } async-compression = { version = "0.4.17", features = ["zstd", "tokio"] }
async-stream = "0.3.5" async-stream = "0.3.6"
async-trait = "0.1.82" async-trait = "0.1.83"
axum = { version = "0.7", features = ["multipart", "macros"] } axum = { version = "0.7", features = ["multipart", "macros"] }
axum-client-ip = "0.6" 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" blake3 = "1.5.4"
bytes = "1.7.1" bytes = "1.7.2"
camino = "1.1.9" camino = "1.1.9"
chrono = { version = "0.4.38", default-features = false, features = ["std", "clock"] } 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" coarsetime = "0.1.34"
convert_case = "0.6.0" convert_case = "0.6.0"
criterion = { version = "0.5.1" } criterion = { version = "0.5.1" }
@ -77,14 +77,14 @@ difflib = "0.4.0"
dirs = "5.0.1" dirs = "5.0.1"
dunce = "1.0.5" dunce = "1.0.5"
envy = "0.4.2" envy = "0.4.2"
flate2 = "1.0.33" flate2 = "1.0.34"
fluent = "0.16.1" fluent = "0.16.1"
fluent-bundle = "0.15.3" fluent-bundle = "0.15.3"
fluent-syntax = "0.11.1" fluent-syntax = "0.11.1"
fnv = "1.0.7" fnv = "1.0.7"
futures = "0.3.30" futures = "0.3.31"
glob = "0.3.1" glob = "0.3.1"
globset = "0.4.14" globset = "0.4.15"
hex = "0.4.3" hex = "0.4.3"
htmlescape = "0.3.1" htmlescape = "0.3.1"
hyper = "1" hyper = "1"
@ -92,47 +92,47 @@ id_tree = "1.8.0"
inflections = "1.1.1" inflections = "1.1.1"
intl-memoizer = "0.5.2" intl-memoizer = "0.5.2"
itertools = "0.13.0" itertools = "0.13.0"
junction = "1.1.0" junction = "1.2.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
maplit = "1.0.2" maplit = "1.0.2"
nom = "7.1.3" nom = "7.1.3"
num-format = "0.4.4" num-format = "0.4.4"
num_cpus = "1.16.0" num_cpus = "1.16.0"
num_enum = "0.7.2" num_enum = "0.7.3"
once_cell = "1.19.0" once_cell = "1.20.2"
pbkdf2 = { version = "0.12", features = ["simple"] } pbkdf2 = { version = "0.12", features = ["simple"] }
phf = { version = "0.11.2", features = ["macros"] } phf = { version = "0.11.2", features = ["macros"] }
pin-project = "1.1.5" pin-project = "1.1.6"
plist = "1.5.1" plist = "1.7.0"
prettyplease = "0.2.22" prettyplease = "0.2.24"
prost = "0.12.3" prost = "0.13"
prost-build = "0.12.3" prost-build = "0.13"
prost-reflect = "0.12.0" prost-reflect = "0.14"
prost-types = "0.12.3" prost-types = "0.13"
pulldown-cmark = "0.9.6" 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" rand = "0.8.5"
regex = "1.10.6" regex = "1.11.0"
reqwest = { version = "0.12.7", default-features = false, features = ["json", "socks", "stream", "multipart"] } reqwest = { version = "0.12.8", default-features = false, features = ["json", "socks", "stream", "multipart"] }
rusqlite = { version = "0.30.0", features = ["trace", "functions", "collation", "bundled"] } rusqlite = { version = "0.30.0", features = ["trace", "functions", "collation", "bundled"] }
rustls-pemfile = "2.1.3" rustls-pemfile = "2.2.0"
scopeguard = "1.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-aux = "4.5.0"
serde_json = "1.0.127" serde_json = "1.0.132"
serde_repr = "0.1.19" serde_repr = "0.1.19"
serde_tuple = "0.5.0" serde_tuple = "0.5.0"
sha1 = "0.10.6" sha1 = "0.10.6"
sha2 = { version = "0.10.8" } sha2 = { version = "0.10.8" }
simple-file-manifest = "0.11.0" 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"] } strum = { version = "0.26.3", features = ["derive"] }
syn = { version = "2.0.77", features = ["parsing", "printing"] } syn = { version = "2.0.82", features = ["parsing", "printing"] }
tar = "0.4.41" tar = "0.4.42"
tempfile = "3.12.0" tempfile = "3.13.0"
termcolor = "1.4.1" termcolor = "1.4.1"
tokio = { version = "1.38", features = ["fs", "rt-multi-thread", "macros", "signal"] } tokio = { version = "1.40", features = ["fs", "rt-multi-thread", "macros", "signal"] }
tokio-util = { version = "0.7.11", features = ["io"] } tokio-util = { version = "0.7.12", features = ["io"] }
tower-http = { version = "0.5", features = ["trace"] } tower-http = { version = "0.5", features = ["trace"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] } tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracing-appender = "0.2.3" 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" tugger-windows-codesign = "0.10.0"
unic-langid = { version = "0.9.5", features = ["macros"] } unic-langid = { version = "0.9.5", features = ["macros"] }
unic-ucd-category = "0.9.0" unic-ucd-category = "0.9.0"
unicode-normalization = "0.1.23" unicode-normalization = "0.1.24"
utime = "0.3.1"
walkdir = "2.5.0" walkdir = "2.5.0"
which = "5.0.0" which = "5.0.0"
wiremock = "0.6.1" wiremock = "0.6.2"
xz2 = "0.1.7" xz2 = "0.1.7"
zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] } zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] }
zstd = { version = "0.13.2", features = ["zstdmt"] } 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 tag = if let Some(platform) = self.platform {
let platform = match 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::LinuxArm => "manylinux_2_31_aarch64",
Platform::MacX64 => "macosx_10_13_x86_64", Platform::MacX64 => "macosx_10_13_x86_64",
Platform::MacArm => "macosx_11_0_arm64", Platform::MacArm => "macosx_11_0_arm64",

View file

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

View file

@ -7,6 +7,6 @@ adding-history = History
adding-note-deleted = (Note deleted) adding-note-deleted = (Note deleted)
adding-shortcut = Shortcut: { $val } adding-shortcut = Shortcut: { $val }
adding-the-first-field-is-empty = The first field is empty. 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-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 notetypes. 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. 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-deck2 = Change Deck...
browsing-change-note-type = Change Note Type browsing-change-note-type = Change Note Type
browsing-change-note-type2 = 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-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-confirm-saved-search-overwrite = A saved search with the name { $name } already exists. Do you want to overwrite it?
browsing-created = Created 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-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-card = The name of a card's card template
browsing-tooltip-cards = The number of cards a note has 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-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-tooltip-answer = The back side of a card, customisable in the card template editor
browsing-studied-today = Studied 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-relearn = Relearn
card-stats-review-log-type-filtered = Filtered card-stats-review-log-type-filtered = Filtered
card-stats-review-log-type-manual = Manual card-stats-review-log-type-manual = Manual
card-stats-review-log-type-rescheduled = Rescheduled
card-stats-review-log-elapsed-time = Elapsed Time card-stats-review-log-elapsed-time = Elapsed Time
card-stats-no-card = (No card to display.) card-stats-no-card = (No card to display.)
card-stats-custom-data = Custom Data 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-month = First Month
card-stats-fsrs-forgetting-curve-first-year = First Year card-stats-fsrs-forgetting-curve-first-year = First Year
card-stats-fsrs-forgetting-curve-all-time = All Time 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 ## Window Titles

View file

@ -20,11 +20,11 @@ card-templates-night-mode = Night Mode
# on a mobile device. # on a mobile device.
card-templates-add-mobile-class = Add Mobile Class card-templates-add-mobile-class = Add Mobile Class
card-templates-preview-settings = Options 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-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-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-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-see-preview = See the preview for more information.
card-templates-field-not-found = Field '{ $field }' not found. card-templates-field-not-found = Field '{ $field }' not found.
card-templates-changes-saved = Changes saved. 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-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 = 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? 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-fields = Fields
change-notetype-templates = Templates change-notetype-templates = Templates
change-notetype-to-from-cloze = 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. 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. *[other] Fixed { $count } objects with timestamps in the future.
} }
# "db-check" is always in English # "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 ## Progress info

View file

@ -152,7 +152,7 @@ deck-config-new-gather-priority-tooltip-2 =
`Random cards`: Gathers cards in a random order. `Random cards`: Gathers cards in a random order.
deck-config-new-gather-priority-deck = Deck 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-lowest-first = Ascending position
deck-config-new-gather-priority-position-highest-first = Descending position deck-config-new-gather-priority-position-highest-first = Descending position
deck-config-new-gather-priority-random-notes = Random notes 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-descending-intervals = Descending intervals
deck-config-sort-order-ascending-ease = Ascending ease deck-config-sort-order-ascending-ease = Ascending ease
deck-config-sort-order-descending-ease = Descending ease deck-config-sort-order-descending-ease = Descending ease
deck-config-sort-order-ascending-difficulty = Ascending difficulty deck-config-sort-order-ascending-difficulty = Easy cards first
deck-config-sort-order-descending-difficulty = Descending difficulty deck-config-sort-order-descending-difficulty = Difficult cards first
deck-config-sort-order-relative-overdueness = Relative overdueness deck-config-sort-order-retrievability-ascending = Ascending retrievability
deck-config-sort-order-retrievability-descending = Descending retrievability
deck-config-display-order-will-use-current-deck = deck-config-display-order-will-use-current-deck =
Anki will use the display order from the deck you Anki will use the display order from the deck you
select to study, and not any subdecks it may have. 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-normal = Normal
deck-config-easy-days-reduced = Reduced deck-config-easy-days-reduced = Reduced
deck-config-easy-days-minimum = Minimum 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 ## Adding/renaming
@ -312,7 +315,7 @@ deck-config-confirm-remove-name = Remove { $name }?
deck-config-save-button = Save deck-config-save-button = Save
deck-config-save-to-all-subdecks = Save to All Subdecks deck-config-save-to-all-subdecks = Save to All Subdecks
deck-config-save-and-optimize = Optimize All Presets 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 ## These strings are shown via the Description button at the bottom of the
## overview screen. ## 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-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-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-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-predicted-minimum-recommended-retention = Minimum recommended retention: { $num }
deck-config-complete = { $num }% complete. deck-config-complete = { $num }% complete.
deck-config-iterations = Iteration: { $count }... deck-config-iterations = Iteration: { $count }...
@ -435,8 +435,8 @@ deck-config-compute-optimal-weights-tooltip2 =
deck-config-compute-optimal-retention-tooltip4 = deck-config-compute-optimal-retention-tooltip4 =
This tool will attempt to find the desired retention value 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 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 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 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. 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-please-save-your-changes-first = Please save your changes first.
deck-config-a-100-day-interval = 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-optimizing-preset = Optimizing preset { $current_count }/{ $total_count }...
deck-config-fsrs-must-be-enabled = FSRS must be enabled first. 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-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-wait-for-audio = Wait for audio
deck-config-show-reminder = Show Reminder 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. 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-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-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-order-due = Order due
decks-please-select-something = Please select something. decks-please-select-something = Please select something.
decks-random = Random decks-random = Random
decks-relative-overdueness = Relative overdueness
decks-repeat-failed-cards-after = Delay Repeat failed cards after decks-repeat-failed-cards-after = Delay Repeat failed cards after
# e.g. "Delay for Again", "Delay for Hard", "Delay for Good" # e.g. "Delay for Again", "Delay for Hard", "Delay for Good"
decks-delay-for-button = Delay for { $button } decks-delay-for-button = Delay for { $button }
@ -37,3 +36,8 @@ decks-learn-header = Learn
# The count of cards waiting to be reviewed # The count of cards waiting to be reviewed
decks-review-header = Due decks-review-header = Due
decks-zero-minutes-hint = (0 = return card to original deck) 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 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 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. 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-database = Please use the Check Database action, then try again.
errors-please-check-media = Please use the Check Media 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. 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... *[other] Processed { $count } media files...
} }
exporting-include-deck = Include deck name 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 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-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-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 = 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. You may still import the file with '{ importing-merge-notetypes }' disabled.
importing-change = Change importing-change = Change
importing-colon = Colon importing-colon = Colon
@ -33,13 +33,13 @@ importing-map-to = Map to { $val }
importing-map-to-tags = Map to Tags importing-map-to-tags = Map to Tags
importing-mapped-to = mapped to <b>{ $val }</b> importing-mapped-to = mapped to <b>{ $val }</b>
importing-mapped-to-tags = mapped to <b>Tags</b> importing-mapped-to-tags = mapped to <b>Tags</b>
# the action of combining two existing notetypes to create a new one # the action of combining two existing note types to create a new one
importing-merge-notetypes = Merge notetypes importing-merge-notetypes = Merge note types
importing-merge-notetypes-help = 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. 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. or changing the sort field.
As a counterexample, changing the front side of an existing template does *not* constitute As a counterexample, changing the front side of an existing template does *not* constitute
a schema change. 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-added-from-file = Notes added from file: { $val }
importing-notes-found-in-file = Notes found in 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-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-notes-updated-as-file-had-newer = Notes updated, as file had newer version: { $val }
importing-include-reviews = Include reviews importing-include-reviews = Include reviews
importing-also-import-progress = Import any learning progress importing-also-import-progress = Import any learning progress
@ -90,10 +90,10 @@ importing-update-notes = Update notes
importing-update-notes-help = importing-update-notes-help =
When to update an existing note in your collection. By default, this is only done When to update an existing note in your collection. By default, this is only done
if the matching imported note was more recently modified. if the matching imported note was more recently modified.
importing-update-notetypes = Update notetypes importing-update-notetypes = Update note types
importing-update-notetypes-help = importing-update-notetypes-help =
When to update an existing notetype in your collection. By default, this is only done When to update an existing note type in your collection. By default, this is only done
if the matching imported notetype was more recently modified. Changes to template text 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 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. fields has changed), the '{ importing-merge-notetypes }' option will also need to be enabled.
importing-note-added = importing-note-added =
@ -148,7 +148,7 @@ importing-file = File
# "Match scope: notetype / notetype and deck". Controls how duplicates are matched. # "Match scope: notetype / notetype and deck". Controls how duplicates are matched.
importing-match-scope = Match scope importing-match-scope = Match scope
# Used with the 'match scope' option # Used with the 'match scope' option
importing-notetype-and-deck = Notetype and deck importing-notetype-and-deck = Note type and deck
importing-cards-added = importing-cards-added =
{ $count -> { $count ->
[one] { $count } card added. [one] { $count } card added.
@ -182,8 +182,8 @@ importing-conflicting-notes-skipped =
} }
importing-conflicting-notes-skipped2 = importing-conflicting-notes-skipped2 =
{ $count -> { $count ->
[one] { $count } note was not imported, because its 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 notetype 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-import-log = Import Log
importing-no-notes-in-file = No notes found in file. 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-duplicate-note-added = Duplicate note added
importing-added-new-note = New 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-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-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 notetype has been modified since you first imported the note, and '{ importing-merge-notetypes }' was not enabled 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-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-notetype = Note skipped, as its notetype was missing
importing-note-skipped-due-to-missing-deck = Note skipped, as its deck 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 '&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. option disabled, the literal characters '&lt;br&gt;' will be rendered.
importing-notetype-help = importing-notetype-help =
Newly-imported notes will have this notetype, and only existing notes with this Newly-imported notes will have this note type, and only existing notes with this
notetype will be updated. 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. mapping tool.
importing-deck-help = Imported cards will be placed in this deck. importing-deck-help = Imported cards will be placed in this deck.
importing-existing-notes-help = importing-existing-notes-help =
@ -229,7 +229,7 @@ importing-existing-notes-help =
- `{ importing-preserve }`: Do nothing. - `{ importing-preserve }`: Do nothing.
- `{ importing-duplicate }`: Create a new note. - `{ importing-duplicate }`: Create a new note.
importing-match-scope-help = 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. additionally be restricted to notes with cards in the same deck.
importing-tag-all-notes-help = importing-tag-all-notes-help =
These tags will be added to both newly-imported and updated notes. 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 ## Default field names in newly created note types

View file

@ -48,6 +48,11 @@ statistics-cards =
[one] { $cards } card [one] { $cards } card
*[other] { $cards } cards *[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" # a count of how many cards have been answered, eg "Total: 34 reviews"
statistics-reviews = statistics-reviews =
{ $reviews -> { $reviews ->
@ -220,6 +225,7 @@ statistics-average-answer-time-label = Average answer time
statistics-average = Average statistics-average = Average
statistics-average-interval = Average interval statistics-average-interval = Average interval
statistics-due-tomorrow = Due tomorrow statistics-due-tomorrow = Due tomorrow
statistics-daily-load = Daily load
# eg 5 of 15 (33.3%) # eg 5 of 15 (33.3%)
statistics-amount-of-total-with-percentage = { $amount } of { $total } ({ $percent }%) statistics-amount-of-total-with-percentage = { $amount } of { $total } ({ $percent }%)
statistics-average-over-period = Average over period 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", "@poppanator/sveltekit-svg": "^5.0.0-svelte5.5",
"@sqltools/formatter": "^1.2.2", "@sqltools/formatter": "^1.2.2",
"@sveltejs/adapter-static": "^3.0.0", "@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.4.1", "@sveltejs/kit": "^2.8.3",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7", "@sveltejs/vite-plugin-svelte": "4.0.0",
"@types/bootstrap": "^5.0.12", "@types/bootstrap": "^5.0.12",
"@types/codemirror": "^5.60.0", "@types/codemirror": "^5.60.0",
"@types/d3": "^7.0.0", "@types/d3": "^7.0.0",
@ -47,7 +47,8 @@
"license-checker-rseidelsohn": "=4.3.0", "license-checker-rseidelsohn": "=4.3.0",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"prettier-plugin-svelte": "^3.2.6", "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-check": "^3.4.4",
"svelte-preprocess": "^5.0.4", "svelte-preprocess": "^5.0.4",
"svelte-preprocess-esbuild": "^3.0.1", "svelte-preprocess-esbuild": "^3.0.1",
@ -55,9 +56,8 @@
"tslib": "^2.0.3", "tslib": "^2.0.3",
"tsx": "^3.12.0", "tsx": "^3.12.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vite": "=5.4.7", "vite": "^5.4.10",
"vitest": "^1.2.1", "vitest": "^2"
"sass": "<1.77"
}, },
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^1.2.1", "@bufbuild/protobuf": "^1.2.1",
@ -81,7 +81,7 @@
}, },
"resolutions": { "resolutions": {
"canvas": "npm:empty-npm-package", "canvas": "npm:empty-npm-package",
"vite": "=5.4.7" "cookie": "0.7.0"
}, },
"browserslist": [ "browserslist": [
"defaults", "defaults",

View file

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

View file

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

View file

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

View file

@ -79,7 +79,8 @@ message DeckConfig {
REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 4; REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 4;
REVIEW_CARD_ORDER_EASE_ASCENDING = 5; REVIEW_CARD_ORDER_EASE_ASCENDING = 5;
REVIEW_CARD_ORDER_EASE_DESCENDING = 6; 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_RANDOM = 8;
REVIEW_CARD_ORDER_ADDED = 9; REVIEW_CARD_ORDER_ADDED = 9;
REVIEW_CARD_ORDER_REVERSE_ADDED = 10; REVIEW_CARD_ORDER_REVERSE_ADDED = 10;
@ -107,9 +108,11 @@ message DeckConfig {
repeated float learn_steps = 1; repeated float learn_steps = 1;
repeated float relearn_steps = 2; 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 new_per_day = 9;
uint32 reviews_per_day = 10; uint32 reviews_per_day = 10;
@ -163,7 +166,7 @@ message DeckConfig {
// used for fsrs_reschedule in the past // used for fsrs_reschedule in the past
reserved 39; reserved 39;
float historical_retention = 40; float historical_retention = 40;
string weight_search = 45; string param_search = 45;
bytes other = 255; bytes other = 255;
} }
@ -213,7 +216,7 @@ message DeckConfigsForUpdate {
enum UpdateDeckConfigsMode { enum UpdateDeckConfigsMode {
UPDATE_DECK_CONFIGS_MODE_NORMAL = 0; UPDATE_DECK_CONFIGS_MODE_NORMAL = 0;
UPDATE_DECK_CONFIGS_MODE_APPLY_TO_CHILDREN = 1; 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 { message UpdateDeckConfigsRequest {

View file

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

View file

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

View file

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

View file

@ -64,6 +64,7 @@ message CardStatsResponse {
string custom_data = 20; string custom_data = 20;
string preset = 21; string preset = 21;
optional string original_deck = 22; optional string original_deck = 22;
optional float desired_retention = 23;
} }
message GraphsRequest { message GraphsRequest {
@ -85,11 +86,13 @@ message GraphsResponse {
message Retrievability { message Retrievability {
map<uint32, uint32> retrievability = 1; map<uint32, uint32> retrievability = 1;
float average = 2; float average = 2;
float sum = 3; float sum_by_card = 3;
float sum_by_note = 4;
} }
message FutureDue { message FutureDue {
map<int32, uint32> future_due = 1; map<int32, uint32> future_due = 1;
bool have_backlog = 2; bool have_backlog = 2;
uint32 daily_load = 3;
} }
message Today { message Today {
uint32 answer_count = 1; uint32 answer_count = 1;
@ -205,6 +208,7 @@ message RevlogEntry {
RELEARNING = 2; RELEARNING = 2;
FILTERED = 3; FILTERED = 3;
MANUAL = 4; MANUAL = 4;
RESCHEDULED = 5;
} }
int64 id = 1; int64 id = 1;
int64 cid = 2; int64 cid = 2;
@ -217,7 +221,21 @@ message RevlogEntry {
ReviewKind review_kind = 9; ReviewKind review_kind = 9;
} }
message RevlogEntries { message CardEntry {
repeated RevlogEntry entries = 1; int64 id = 1;
int64 next_day_at = 2; 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) return self.format_timespan(seconds=seconds, context=context)
def compute_weights_from_items(self, items: Iterable[FsrsItem]) -> Sequence[float]: def compute_params_from_items(self, items: Iterable[FsrsItem]) -> Sequence[float]:
return self.compute_fsrs_weights_from_items(items).weights return self.compute_fsrs_params_from_items(items).params
def benchmark(self, train_set: Iterable[FsrsItem]) -> Sequence[float]: def benchmark(self, train_set: Iterable[FsrsItem]) -> Sequence[float]:
return self.fsrs_benchmark(train_set=train_set) 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: def import_json_string(self, json: str) -> ImportLogWithChanges:
return self._backend.import_json_string(json) 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 # Image Occlusion
########################################################################## ##########################################################################
@ -987,6 +992,16 @@ class Collection(DeprecatedNamesMixin):
fget=_get_enable_load_balancer, fset=_set_enable_load_balancer 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 # Stats
########################################################################## ##########################################################################
@ -1113,7 +1128,7 @@ class Collection(DeprecatedNamesMixin):
self._backend.abort_sync() self._backend.abort_sync()
def full_upload_or_download( 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: ) -> None:
self._backend.full_upload_or_download( self._backend.full_upload_or_download(
sync_pb2.FullUploadOrDownloadRequest( sync_pb2.FullUploadOrDownloadRequest(

View file

@ -43,10 +43,6 @@ from anki.models import NotetypeDict
from anki.sound import AVTag, SoundOrVideoTag, TTSTag from anki.sound import AVTag, SoundOrVideoTag, TTSTag
from anki.utils import to_json_bytes 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 @dataclass
class TemplateReplacement: class TemplateReplacement:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -144,7 +144,11 @@ class CustomStudy(QDialog):
form.spin.setValue(current_spinner_value) form.spin.setValue(current_spinner_value)
form.preSpin.setText(text_before_spinner) form.preSpin.setText(text_before_spinner)
form.postSpin.setText(text_after_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 self.radioIdx = idx
def accept(self) -> None: def accept(self) -> None:

View file

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

View file

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

View file

@ -99,7 +99,9 @@ class DeckChooser(QHBoxLayout):
def callback(ret: StudyDeck) -> None: def callback(ret: StudyDeck) -> None:
if not ret.name: if not ret.name:
return 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: if self.selected_deck_id != new_selected_deck_id:
self.selected_deck_id = new_selected_deck_id self.selected_deck_id = new_selected_deck_id
if func := self.on_deck_changed: if func := self.on_deck_changed:

View file

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

View file

@ -67,10 +67,10 @@ class DeckOptionsDialog(QDialog):
elif cmd == "_close": elif cmd == "_close":
self._close() self._close()
def closeEvent(self, evt: QCloseEvent) -> None: def closeEvent(self, evt: QCloseEvent | None) -> None:
if self._close_event_has_cleaned_up: if self._close_event_has_cleaned_up:
evt.accept() return super().closeEvent(evt)
return assert evt is not None
evt.ignore() evt.ignore()
self.check_pending_changes() self.check_pending_changes()
@ -98,7 +98,7 @@ class DeckOptionsDialog(QDialog):
def reject(self) -> None: def reject(self) -> None:
self.mw.col.set_wants_abort() self.mw.col.set_wants_abort()
self.web.cleanup() self.web.cleanup()
self.web = None self.web = None # type: ignore
saveGeom(self, self.TITLE) saveGeom(self, self.TITLE)
QDialog.reject(self) 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()] decks = [aqt.mw.col.decks.current()]
if card := active_card: if card := active_card:
if card.odid and card.odid != decks[0]["id"]: 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): 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: if len(decks) == 1:
display_options_for_deck(decks[0]) 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: 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: def display_options_for_deck(deck: DeckDict) -> None:
if not deck["dyn"]: if not deck["dyn"]:
if KeyboardModifiersPressed().shift or not aqt.mw.col.v3_scheduler(): if KeyboardModifiersPressed().shift or not aqt.mw.col.v3_scheduler():
deck_legacy = aqt.mw.col.decks.get(DeckId(deck["id"])) deck_legacy = aqt.mw.col.decks.get(DeckId(deck["id"]))
assert deck_legacy is not None
aqt.deckconf.DeckConf(aqt.mw, deck_legacy) aqt.deckconf.DeckConf(aqt.mw, deck_legacy)
else: else:
DeckOptionsDialog(aqt.mw, deck) DeckOptionsDialog(aqt.mw, deck)

View file

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

View file

@ -34,7 +34,7 @@ from anki.collection import Config, SearchNode
from anki.consts import MODEL_CLOZE from anki.consts import MODEL_CLOZE
from anki.hooks import runFilter from anki.hooks import runFilter
from anki.httpclient import HttpClient 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.notes import Note, NoteFieldsCheckResult, NoteId
from anki.utils import checksum, is_lin, is_mac, is_win, namedtmp from anki.utils import checksum, is_lin, is_mac, is_win, namedtmp
from aqt import AnkiQt, colors, gui_hooks 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, rightside: bool = True,
) -> str: ) -> str:
"""Assign func to bridge cmd, register shortcut, return button""" """Assign func to bridge cmd, register shortcut, return button"""
if func:
def wrapped_func(editor: Editor) -> None: def wrapped_func(editor: Editor) -> None:
self.call_after_note_saved( self.call_after_note_saved(functools.partial(func, editor), keepFocus=True)
functools.partial(func, editor), keepFocus=True
)
self._links[cmd] = wrapped_func self._links[cmd] = wrapped_func
if keys: if keys:
def on_activated() -> None: def on_activated() -> None:
wrapped_func(self) wrapped_func(self)
if toggleable: if toggleable:
# generate a random id for triggering toggle # generate a random id for triggering toggle
id = id or str(randrange(1_000_000)) id = id or str(randrange(1_000_000))
def on_hotkey() -> None: def on_hotkey() -> None:
on_activated() on_activated()
self.web.eval( self.web.eval(
f'toggleEditorButton(document.getElementById("{id}"));' f'toggleEditorButton(document.getElementById("{id}"));'
) )
else: else:
on_hotkey = on_activated on_hotkey = on_activated
QShortcut( # type: ignore QShortcut( # type: ignore
QKeySequence(keys), QKeySequence(keys),
self.widget, self.widget,
activated=on_hotkey, activated=on_hotkey,
) )
btn = self._addButton( btn = self._addButton(
icon, icon,
@ -363,7 +360,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def _onFields(self) -> None: def _onFields(self) -> None:
from aqt.fields import FieldDialog 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: def onCardLayout(self) -> None:
self.call_after_note_saved(self._onCardLayout) 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 ord = self.card.ord
else: else:
ord = 0 ord = 0
assert self.note is not None
CardLayout( CardLayout(
self.mw, self.mw,
self.note, 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) gui_hooks.editor_did_focus_field(self.note, self.currentField)
elif cmd.startswith("toggleStickyAll"): elif cmd.startswith("toggleStickyAll"):
model = self.note.note_type() model = self.note_type()
flds = model["flds"] flds = model["flds"]
any_sticky = any([fld["sticky"] for fld in 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) (type, num) = cmd.split(":", 1)
ord = int(num) ord = int(num)
model = self.note.note_type() model = self.note_type()
fld = model["flds"][ord] fld = model["flds"][ord]
new_state = not fld["sticky"] new_state = not fld["sticky"]
fld["sticky"] = new_state fld["sticky"] = new_state
@ -469,10 +468,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
elif cmd.startswith("lastTextColor"): elif cmd.startswith("lastTextColor"):
(_, textColor) = cmd.split(":", 1) (_, textColor) = cmd.split(":", 1)
assert self.mw.pm.profile is not None
self.mw.pm.profile["lastTextColor"] = textColor self.mw.pm.profile["lastTextColor"] = textColor
elif cmd.startswith("lastHighlightColor"): elif cmd.startswith("lastHighlightColor"):
(_, highlightColor) = cmd.split(":", 1) (_, highlightColor) = cmd.split(":", 1)
assert self.mw.pm.profile is not None
self.mw.pm.profile["lastHighlightColor"] = highlightColor self.mw.pm.profile["lastHighlightColor"] = highlightColor
elif cmd.startswith("saveTags"): 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() 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] collapsed = [fld["collapsed"] for fld in flds]
plain_texts = [fld.get("plainText", False) for fld in flds] plain_texts = [fld.get("plainText", False) for fld in flds]
descriptions = [fld.get("description", "") 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() self.widget.show()
@ -566,6 +568,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
self.web.setFocus() self.web.setFocus()
gui_hooks.editor_did_load_note(self) gui_hooks.editor_did_load_note(self)
assert self.mw.pm.profile is not None
text_color = self.mw.pm.profile.get("lastTextColor", "#0000ff") text_color = self.mw.pm.profile.get("lastTextColor", "#0000ff")
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#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: 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) js += " setSticky(%s);" % json.dumps(sticky)
if ( if (
@ -607,6 +610,9 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def _save_current_note(self) -> None: def _save_current_note(self) -> None:
"Call after note is updated with data from webview." "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( update_note(parent=self.widget, note=self.note).run_in_background(
initiator=self 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]]: def fonts(self) -> list[tuple[str, int, bool]]:
return [ return [
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"]) (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( 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 checkValid = _check_and_update_duplicate_display_async
def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -> None: def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -> None:
assert self.note is not None
cols = [""] * len(self.note.fields) cols = [""] * len(self.note.fields)
cloze_hint = "" cloze_hint = ""
if result == NoteFieldsCheckResult.DUPLICATE: if result == NoteFieldsCheckResult.DUPLICATE:
@ -665,13 +672,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
) )
def showDupes(self) -> None: def showDupes(self) -> None:
assert self.note is not None
aqt.dialogs.open( aqt.dialogs.open(
"Browser", "Browser",
self.mw, self.mw,
search=( search=(
SearchNode( SearchNode(
dupe=SearchNode.Dupe( dupe=SearchNode.Dupe(
notetype_id=self.note.note_type()["id"], notetype_id=self.note_type()["id"],
first_field=self.note.fields[0], 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: def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
if not self.note: if not self.note:
return True return True
m = self.note.note_type() m = self.note_type()
for c, f in enumerate(self.note.fields): for c, f in enumerate(self.note.fields):
f = f.replace("<br>", "").strip() f = f.replace("<br>", "").strip()
notChangedvalues = {"", "<br>"} 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 # prevent any remaining evalWithCallback() events from firing after C++ object deleted
if self.web: if self.web:
self.web.cleanup() self.web.cleanup()
self.web = None self.web = None # type: ignore
# legacy # legacy
@ -729,9 +737,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
if self.tags.col != self.mw.col: if self.tags.col != self.mw.col:
self.tags.setCol(self.mw.col) self.tags.setCol(self.mw.col)
if not self.tags.text() or not self.addMode: if not self.tags.text() or not self.addMode:
assert self.note is not None
self.tags.setText(self.note.string_tags().strip()) self.tags.setText(self.note.string_tags().strip())
def on_tag_focus_lost(self) -> None: def on_tag_focus_lost(self) -> None:
assert self.note is not None
self.note.tags = self.mw.col.tags.split(self.tags.text()) self.note.tags = self.mw.col.tags.split(self.tags.text())
gui_hooks.editor_did_update_tags(self.note) gui_hooks.editor_did_update_tags(self.note)
if not self.addMode: if not self.addMode:
@ -826,7 +836,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
# Media downloads # Media downloads
###################################################################### ######################################################################
def urlToLink(self, url: str) -> str | None: def urlToLink(self, url: str) -> str:
fname = self.urlToFile(url) fname = self.urlToFile(url)
if not fname: if not fname:
return '<a href="{}">{}</a>'.format( 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: def current_notetype_is_image_occlusion(self) -> bool:
return bool(self.note) and ( if not self.note:
self.note.note_type().get("originalStockKind", None) return False
return (
self.note_type().get("originalStockKind", None)
== StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION == 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 image_path=image_path, notetype_id=0
) )
else: else:
assert self.note is not None
self.setup_mask_editor_for_existing_note( self.setup_mask_editor_for_existing_note(
note_id=self.note.id, image_path=image_path 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: def select_image_from_clipboard_and_occlude(self) -> None:
"""Set up the mask editor for the image in the clipboard.""" """Set up the mask editor for the image in the clipboard."""
clipoard = self.mw.app.clipboard() clipboard = self.mw.app.clipboard()
mime = clipoard.mimeData() assert clipboard is not None
mime = clipboard.mimeData()
assert mime is not None
if not mime.hasImage(): if not mime.hasImage():
showWarning(tr.editing_no_image_found_on_clipboard()) showWarning(tr.editing_no_image_found_on_clipboard())
return return
@ -1160,6 +1176,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy) @deprecated(info=_js_legacy)
def _onHtmlEdit(self, field: int) -> None: def _onHtmlEdit(self, field: int) -> None:
assert self.note is not None
d = QDialog(self.widget, Qt.WindowType.Window) d = QDialog(self.widget, Qt.WindowType.Window)
form = aqt.forms.edithtml.Ui_Dialog() form = aqt.forms.edithtml.Ui_Dialog()
form.setupUi(d) form.setupUi(d)
@ -1223,7 +1240,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy) @deprecated(info=_js_legacy)
def _onCloze(self) -> None: def _onCloze(self) -> None:
# check that the model is set up for cloze deletion # 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: if self.addMode:
tooltip(tr.editing_warning_cloze_deletions_will_not_work()) tooltip(tr.editing_warning_cloze_deletions_will_not_work())
else: else:
@ -1231,7 +1248,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
return return
# find the highest existing cloze # find the highest existing cloze
highest = 0 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) m = re.findall(r"\{\{c(\d+)::", val)
if m: if m:
highest = max(highest, sorted(int(x) for x in m)[-1]) 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) self.web.eval("wrap('{{c%d::', '}}');" % highest)
def setupForegroundButton(self) -> None: def setupForegroundButton(self) -> None:
assert self.mw.pm.profile is not None
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f") self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
# use last colour # use last colour
@ -1276,6 +1295,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
@deprecated(info=_js_legacy) @deprecated(info=_js_legacy)
def onColourChanged(self) -> None: def onColourChanged(self) -> None:
self._updateForegroundButton() self._updateForegroundButton()
assert self.mw.pm.profile is not None
self.mw.pm.profile["lastColour"] = self.fcolour self.mw.pm.profile["lastColour"] = self.fcolour
@deprecated(info=_js_legacy) @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"), (tr.editing_edit_html(), self.onHtmlEdit, "Ctrl+Shift+X"),
): ):
a = m.addAction(text) a = m.addAction(text)
assert a is not None
qconnect(a.triggered, handler) qconnect(a.triggered, handler)
a.setShortcut(QKeySequence(shortcut)) 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, 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 # Pasting, drag & drop, and keyboard layouts
###################################################################### ######################################################################
@ -1403,6 +1430,7 @@ class EditorWebView(AnkiWebView):
self._internal_field_text_for_paste: str | None = None self._internal_field_text_for_paste: str | None = None
self._last_known_clipboard_mime: QMimeData | None = None self._last_known_clipboard_mime: QMimeData | None = None
clip = self.editor.mw.app.clipboard() clip = self.editor.mw.app.clipboard()
assert clip is not None
clip.dataChanged.connect(self._on_clipboard_change) clip.dataChanged.connect(self._on_clipboard_change)
gui_hooks.editor_web_view_did_init(self) 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._store_field_content_on_next_clipboard_change = True
self._internal_field_text_for_paste = None self._internal_field_text_for_paste = None
def _on_clipboard_change(self) -> None: def _on_clipboard_change(
self._last_known_clipboard_mime = self.editor.mw.app.clipboard().mimeData() 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 self._store_field_content_on_next_clipboard_change:
# if the flag was set, save the field data # 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 self._store_field_content_on_next_clipboard_change = False
elif ( elif 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
): ):
# if we've previously saved the field, blank it out if the clipboard state has changed # if we've previously saved the field, blank it out if the clipboard state has changed
self._internal_field_text_for_paste = None self._internal_field_text_for_paste = None
def _get_clipboard_html_for_field(self): def _get_clipboard_html_for_field(self, mode: QClipboard.Mode) -> str | None:
clip = self.editor.mw.app.clipboard() clip = self._clipboard()
mime = clip.mimeData() mime = clip.mimeData(mode)
assert mime is not None
if not mime.hasHtml(): if not mime.hasHtml():
return return None
return mime.html() return mime.html()
def onCut(self) -> None: def onCut(self) -> None:
@ -1440,6 +1473,7 @@ class EditorWebView(AnkiWebView):
def _opened_context_menu_on_image(self) -> bool: def _opened_context_menu_on_image(self) -> bool:
context_menu_request = self.lastContextMenuRequest() context_menu_request = self.lastContextMenuRequest()
assert context_menu_request is not None
return ( return (
context_menu_request.mediaType() context_menu_request.mediaType()
== context_menu_request.MediaType.MediaTypeImage == context_menu_request.MediaType.MediaTypeImage
@ -1455,15 +1489,17 @@ class EditorWebView(AnkiWebView):
def _onPaste(self, mode: QClipboard.Mode) -> None: 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 # 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(): clipboard = self._clipboard()
self._on_clipboard_change() if self._last_known_clipboard_mime != clipboard.mimeData(mode):
self._on_clipboard_change(mode)
extended = self._wantsExtendedPaste() extended = self._wantsExtendedPaste()
if html := self._internal_field_text_for_paste: if html := self._internal_field_text_for_paste:
print("reuse internal") print("reuse internal")
self.editor.doPaste(html, True, extended) self.editor.doPaste(html, True, extended)
else: else:
print("use clipboard") 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) html, internal = self._processMime(mime, extended)
if html: if html:
self.editor.doPaste(html, internal, extended) self.editor.doPaste(html, internal, extended)
@ -1474,12 +1510,15 @@ class EditorWebView(AnkiWebView):
def onMiddleClickPaste(self) -> None: def onMiddleClickPaste(self) -> None:
self._onPaste(QClipboard.Mode.Selection) 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() evt.accept()
def dropEvent(self, evt: QDropEvent) -> None: def dropEvent(self, evt: QDropEvent | None) -> None:
assert evt is not None
extended = self._wantsExtendedPaste() extended = self._wantsExtendedPaste()
mime = evt.mimeData() mime = evt.mimeData()
assert mime is not None
cursor_pos = self.mapFromGlobal(QCursor.pos()) cursor_pos = self.mapFromGlobal(QCursor.pos())
if evt.source() and mime.hasHtml(): if evt.source() and mime.hasHtml():
@ -1585,12 +1624,13 @@ class EditorWebView(AnkiWebView):
return fname return fname
def contextMenuEvent(self, evt: QContextMenuEvent) -> None: def contextMenuEvent(self, evt: QContextMenuEvent | None) -> None:
m = QMenu(self) m = QMenu(self)
if self.hasSelection(): if self.hasSelection():
self._add_cut_action(m) self._add_cut_action(m)
self._add_copy_action(m) self._add_copy_action(m)
a = m.addAction(tr.editing_paste()) a = m.addAction(tr.editing_paste())
assert a is not None
qconnect(a.triggered, self.onPaste) qconnect(a.triggered, self.onPaste)
if self._opened_context_menu_on_image(): if self._opened_context_menu_on_image():
self._add_image_menu(m) self._add_image_menu(m)
@ -1599,26 +1639,38 @@ class EditorWebView(AnkiWebView):
def _add_cut_action(self, menu: QMenu) -> None: def _add_cut_action(self, menu: QMenu) -> None:
a = menu.addAction(tr.editing_cut()) a = menu.addAction(tr.editing_cut())
assert a is not None
qconnect(a.triggered, self.onCut) qconnect(a.triggered, self.onCut)
def _add_copy_action(self, menu: QMenu) -> None: def _add_copy_action(self, menu: QMenu) -> None:
a = menu.addAction(tr.actions_copy()) a = menu.addAction(tr.actions_copy())
assert a is not None
qconnect(a.triggered, self.onCopy) qconnect(a.triggered, self.onCopy)
def _add_image_menu(self, menu: QMenu) -> None: def _add_image_menu(self, menu: QMenu) -> None:
a = menu.addAction(tr.editing_copy_image()) a = menu.addAction(tr.editing_copy_image())
assert a is not None
qconnect(a.triggered, self.on_copy_image) 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() file_name = url.fileName()
path = os.path.join(self.editor.mw.col.media.dir(), file_name) path = os.path.join(self.editor.mw.col.media.dir(), file_name)
a = menu.addAction(tr.editing_open_image()) a = menu.addAction(tr.editing_open_image())
assert a is not None
qconnect(a.triggered, lambda: openFolder(path)) qconnect(a.triggered, lambda: openFolder(path))
if is_win or is_mac: if is_win or is_mac:
a = menu.addAction(tr.editing_show_in_folder()) a = menu.addAction(tr.editing_show_in_folder())
assert a is not None
qconnect(a.triggered, lambda: show_in_folder(path)) 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" # 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 # - 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: 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( editor.web.eval(
'require("anki/ui").loaded.then(() =>' 'require("anki/ui").loaded.then(() =>'
f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("cloze")' 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: def __init__(self, mw: aqt.main.AnkiQt, report: EmptyCardsReport) -> None:
super().__init__(mw) super().__init__(mw)
self.mw = mw.weakref() self.mw = mw
self.mw.garbage_collect_on_dialog_finish(self) self.mw.garbage_collect_on_dialog_finish(self)
self.report = report self.report = report
self.form = aqt.forms.emptycards.Ui_Dialog() self.form = aqt.forms.emptycards.Ui_Dialog()
@ -63,7 +63,7 @@ class EmptyCardsDialog(QDialog):
def on_finished(code: Any) -> None: def on_finished(code: Any) -> None:
self.form.webview.cleanup() self.form.webview.cleanup()
self.form.webview = None self.form.webview = None # type: ignore
saveGeom(self, "emptycards") saveGeom(self, "emptycards")
qconnect(self.finished, on_finished) qconnect(self.finished, on_finished)
@ -71,6 +71,7 @@ class EmptyCardsDialog(QDialog):
self._delete_button = self.form.buttonBox.addButton( self._delete_button = self.form.buttonBox.addButton(
tr.empty_cards_delete_button(), QDialogButtonBox.ButtonRole.ActionRole tr.empty_cards_delete_button(), QDialogButtonBox.ButtonRole.ActionRole
) )
assert self._delete_button is not None
self._delete_button.setAutoDefault(False) self._delete_button.setAutoDefault(False)
qconnect(self._delete_button.clicked, self._on_delete) qconnect(self._delete_button.clicked, self._on_delete)

View file

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

View file

@ -130,9 +130,10 @@ class FilteredDeckConfigDialog(QDialog):
build_label = tr.actions_rebuild() build_label = tr.actions_rebuild()
else: else:
build_label = tr.decks_build() 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) form.resched.setChecked(config.reschedule)
self._onReschedToggled(0) self._onReschedToggled(0)

View file

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

View file

@ -611,28 +611,6 @@
</layout> </layout>
</widget> </widget>
</item> </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> <item>
<spacer name="verticalSpacer_3"> <spacer name="verticalSpacer_3">
<property name="orientation"> <property name="orientation">
@ -1293,7 +1271,6 @@
<tabstop>useCurrent</tabstop> <tabstop>useCurrent</tabstop>
<tabstop>default_search_text</tabstop> <tabstop>default_search_text</tabstop>
<tabstop>ignore_accents_in_search</tabstop> <tabstop>ignore_accents_in_search</tabstop>
<tabstop>legacy_import_export</tabstop>
<tabstop>syncMedia</tabstop> <tabstop>syncMedia</tabstop>
<tabstop>syncOnProgramOpen</tabstop> <tabstop>syncOnProgramOpen</tabstop>
<tabstop>autoSyncMedia</tabstop> <tabstop>autoSyncMedia</tabstop>

View file

@ -60,7 +60,7 @@ class ChangeMap(QDialog):
self.frm.fields.setCurrentRow(n + 1) self.frm.fields.setCurrentRow(n + 1)
self.field: str | None = None self.field: str | None = None
def getField(self) -> str: def getField(self) -> str | None:
self.exec() self.exec()
return self.field return self.field
@ -91,8 +91,10 @@ class ImportDialog(QDialog):
self.importer = importer self.importer = importer
self.frm = aqt.forms.importing.Ui_ImportDialog() self.frm = aqt.forms.importing.Ui_ImportDialog()
self.frm.setupUi(self) self.frm.setupUi(self)
help_button = self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help)
assert help_button is not None
qconnect( qconnect(
self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help).clicked, help_button.clicked,
self.helpRequested, self.helpRequested,
) )
disable_help_button(self) disable_help_button(self)
@ -103,6 +105,7 @@ class ImportDialog(QDialog):
gui_hooks.current_note_type_did_change.append(self.modelChanged) gui_hooks.current_note_type_did_change.append(self.modelChanged)
qconnect(self.frm.autoDetect.clicked, self.onDelimiter) qconnect(self.frm.autoDetect.clicked, self.onDelimiter)
self.updateDelimiterButtonText() self.updateDelimiterButtonText()
assert self.mw.pm.profile is not None
self.frm.allowHTML.setChecked(self.mw.pm.profile.get("allowHTML", True)) self.frm.allowHTML.setChecked(self.mw.pm.profile.get("allowHTML", True))
qconnect(self.frm.importMode.currentIndexChanged, self.importModeChanged) qconnect(self.frm.importMode.currentIndexChanged, self.importModeChanged)
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get("importMode", 1)) 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()) showWarning(tr.importing_the_first_field_of_the_note())
return return
self.importer.importMode = self.frm.importMode.currentIndex() self.importer.importMode = self.frm.importMode.currentIndex()
assert self.mw.pm.profile is not None
self.mw.pm.profile["importMode"] = self.importer.importMode self.mw.pm.profile["importMode"] = self.importer.importMode
self.importer.allowHTML = self.frm.allowHTML.isChecked() self.importer.allowHTML = self.frm.allowHTML.isChecked()
self.mw.pm.profile["allowHTML"] = self.importer.allowHTML self.mw.pm.profile["allowHTML"] = self.importer.allowHTML
@ -390,7 +394,7 @@ def importFile(mw: AnkiQt, file: str) -> None:
showWarning(invalidZipMsg()) showWarning(invalidZipMsg())
except MediaMapInvalid: except MediaMapInvalid:
showWarning( 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: except V2ImportIntoV1:
showWarning( showWarning(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -224,13 +224,14 @@ class Overview:
dyn = "" dyn = ""
return f'<div class="descfont descmid description {dyn}">{desc}</div>' 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()) counts = list(self.mw.col.sched.counts())
current_did = self.mw.col.decks.get_current_id() current_did = self.mw.col.decks.get_current_id()
deck_node = self.mw.col.sched.deck_due_tree(current_did) deck_node = self.mw.col.sched.deck_due_tree(current_did)
but = self.mw.button but = self.mw.button
if self.mw.col.v3_scheduler(): if self.mw.col.v3_scheduler():
assert deck_node is not None
buried_new = deck_node.new_count - counts[0] buried_new = deck_node.new_count - counts[0]
buried_learning = deck_node.learn_count - counts[1] buried_learning = deck_node.learn_count - counts[1]
buried_review = deck_node.review_count - counts[2] 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: def get_data_custom(package: str, resource: str) -> bytes | None:
try: try:
module = importlib.import_module(package) 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: with reader.open_resource(resource) as f:
return f.read() return f.read()
except Exception: except Exception:

View file

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

View file

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

View file

@ -245,7 +245,8 @@ av_player = AVPlayer()
def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]: def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
cmd = cmd[:] cmd = cmd[:]
env = os.environ.copy() 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"] del env["LD_LIBRARY_PATH"]
if is_win: if is_win:

View file

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

View file

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

View file

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

View file

@ -168,7 +168,7 @@ def full_sync(
def confirm_full_download( 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: ) -> None:
# confirmation step required, as some users customize their notetypes # confirmation step required, as some users customize their notetypes
# in an empty collection, then want to upload them # in an empty collection, then want to upload them
@ -184,7 +184,7 @@ def confirm_full_download(
def confirm_full_upload( 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: ) -> None:
# confirmation step required, as some users have reported an upload # confirmation step required, as some users have reported an upload
# happening despite having their AnkiWeb collection not being empty # 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( 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: ) -> None:
label = tr.sync_downloading_from_ankiweb() label = tr.sync_downloading_from_ankiweb()
@ -372,7 +372,9 @@ def get_id_and_pass_from_user(
l2.setBuddy(passwd) l2.setBuddy(passwd)
vbox.addLayout(g) vbox.addLayout(g)
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # type: ignore 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.accepted, diag.accept)
qconnect(bb.rejected, diag.reject) qconnect(bb.rejected, diag.reject)
vbox.addWidget(bb) 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()) l = (d.name for d in self.col.decks.all_names_and_ids())
self.model.setStringList(l) self.model.setStringList(l)
def focusInEvent(self, evt: QFocusEvent) -> None: def focusInEvent(self, evt: QFocusEvent | None) -> None:
QLineEdit.focusInEvent(self, evt) 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): if evt.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down):
# show completer on arrow key up/down # show completer on arrow key up/down
if not self._completer.popup().isVisible(): if not popup.isVisible():
self.showCompleter() self.showCompleter()
return return
if ( if (
@ -56,24 +60,21 @@ class TagEdit(QLineEdit):
and evt.modifiers() & Qt.KeyboardModifier.ControlModifier and evt.modifiers() & Qt.KeyboardModifier.ControlModifier
): ):
# select next completion # select next completion
if not self._completer.popup().isVisible(): if not popup.isVisible():
self.showCompleter() self.showCompleter()
index = self._completer.currentIndex() index = self._completer.currentIndex()
self._completer.popup().setCurrentIndex(index) popup.setCurrentIndex(index)
cur_row = index.row() cur_row = index.row()
if not self._completer.setCurrentRow(cur_row + 1): if not self._completer.setCurrentRow(cur_row + 1):
self._completer.setCurrentRow(0) self._completer.setCurrentRow(0)
return return
if ( if evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) and popup.isVisible():
evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return)
and self._completer.popup().isVisible()
):
# apply first completion if no suggestion selected # apply first completion if no suggestion selected
selected_row = self._completer.popup().currentIndex().row() selected_row = popup.currentIndex().row()
if selected_row == -1: if selected_row == -1:
self._completer.setCurrentRow(0) self._completer.setCurrentRow(0)
index = self._completer.currentIndex() index = self._completer.currentIndex()
self._completer.popup().setCurrentIndex(index) popup.setCurrentIndex(index)
self.hideCompleter() self.hideCompleter()
QWidget.keyPressEvent(self, evt) QWidget.keyPressEvent(self, evt)
return return
@ -97,15 +98,19 @@ class TagEdit(QLineEdit):
self._completer.setCompletionPrefix(self.text()) self._completer.setCompletionPrefix(self.text())
self._completer.complete() self._completer.complete()
def focusOutEvent(self, evt: QFocusEvent) -> None: def focusOutEvent(self, evt: QFocusEvent | None) -> None:
QLineEdit.focusOutEvent(self, evt) QLineEdit.focusOutEvent(self, evt)
self.lostFocus.emit() # type: ignore self.lostFocus.emit() # type: ignore
self._completer.popup().hide() popup = self._completer.popup()
assert popup is not None
popup.hide()
def hideCompleter(self) -> None: def hideCompleter(self) -> None:
if sip.isdeleted(self._completer): # type: ignore if sip.isdeleted(self._completer): # type: ignore
return return
self._completer.popup().hide() popup = self._completer.popup()
assert popup is not None
popup.hide()
class TagCompleter(QCompleter): class TagCompleter(QCompleter):
@ -120,7 +125,9 @@ class TagCompleter(QCompleter):
self.edit = edit self.edit = edit
self.cursor: int | None = None 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 = tags.strip()
stripped_tags = re.sub(" +", " ", stripped_tags) stripped_tags = re.sub(" +", " ", stripped_tags)
self.tags = self.edit.col.tags.split(stripped_tags) self.tags = self.edit.col.tags.split(stripped_tags)

View file

@ -50,7 +50,9 @@ class TagLimit(QDialog):
list.addItem(item) list.addItem(item)
if select: if select:
idx = list.indexFromItem(item) 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 idx, QItemSelectionModel.SelectionFlag.Select
) )
@ -77,12 +79,16 @@ class TagLimit(QDialog):
if want_active: if want_active:
item = self.form.activeList.item(c) item = self.form.activeList.item(c)
idx = self.form.activeList.indexFromItem(item) 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) include_tags.append(tag.name)
# inactive # inactive
item = self.form.inactiveList.item(c) item = self.form.inactiveList.item(c)
idx = self.form.inactiveList.indexFromItem(item) 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) exclude_tags.append(tag.name)
if (len(include_tags) + len(exclude_tags)) > 100: if (len(include_tags) + len(exclude_tags)) > 100:

View file

@ -231,7 +231,9 @@ class ThemeManager:
self._current_widget_style = new_widget_style self._current_widget_style = new_widget_style
app = aqt.mw.app app = aqt.mw.app
if not self._default_style: 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_palette(app)
self._apply_style(app) self._apply_style(app)
gui_hooks.theme_did_change() gui_hooks.theme_did_change()

View file

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

View file

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

View file

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

9
qt/bundle/Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -4,8 +4,11 @@
mod error; mod error;
use std::fs::File; use std::fs::File;
use std::fs::FileTimes;
use std::fs::OpenOptions;
use std::io::Read; use std::io::Read;
use std::io::Seek; use std::io::Seek;
use std::io::Write;
use std::path::Component; use std::path::Component;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; 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]. /// See [std::fs::write].
pub fn write_file(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> { pub fn write_file(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
std::fs::write(&path, contents).context(FileIoSnafu { 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]. /// See [std::fs::remove_file].
#[allow(dead_code)] #[allow(dead_code)]
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> { 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( .type_attribute(
"Deck.Normal.DayLimit", "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("HelpPageLinkRequest.HelpPage", "#[derive(strum::EnumIter)]")
.type_attribute("CsvMetadata.Delimiter", "#[derive(strum::EnumIter)]") .type_attribute("CsvMetadata.Delimiter", "#[derive(strum::EnumIter)]")
.type_attribute( .type_attribute(
"Preferences.BackupLimits", "Preferences.BackupLimits",
"#[derive(Copy, serde::Deserialize, serde::Serialize)]", "#[derive(serde::Deserialize, serde::Serialize)]",
) )
.type_attribute( .type_attribute(
"CsvMetadata.DupeResolution", "CsvMetadata.DupeResolution",

View file

@ -11,7 +11,7 @@ use serde::Serialize;
use crate::ankihub::login::LoginRequest; 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/"; static DEFAULT_API_URL: &str = "https://app.ankihub.net/api/";
#[derive(Clone)] #[derive(Clone)]

View file

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

View file

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

View file

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

View file

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

View file

@ -74,11 +74,12 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
bury_new: false, bury_new: false,
bury_reviews: false, bury_reviews: false,
bury_interday_learning: false, bury_interday_learning: false,
fsrs_weights: vec![], fsrs_params_4: vec![],
fsrs_params_5: vec![],
desired_retention: 0.9, desired_retention: 0.9,
other: Vec::new(), other: Vec::new(),
historical_retention: 0.9, historical_retention: 0.9,
weight_search: String::new(), param_search: String::new(),
ignore_revlogs_before_date: String::new(), ignore_revlogs_before_date: String::new(),
easy_days_percentages: Vec::new(), easy_days_percentages: Vec::new(),
}; };
@ -105,6 +106,15 @@ impl DeckConfig {
self.mtime_secs = TimestampSecs::now(); self.mtime_secs = TimestampSecs::now();
self.usn = usn; 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 { impl Collection {

View file

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

View file

@ -21,7 +21,7 @@ use crate::decks::NormalDeck;
use crate::prelude::*; use crate::prelude::*;
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry; use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry;
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest; 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::JoinSearches;
use crate::search::Negated; use crate::search::Negated;
use crate::search::SearchNode; use crate::search::SearchNode;
@ -50,7 +50,7 @@ impl Collection {
deck: DeckId, deck: DeckId,
) -> Result<anki_proto::deck_config::DeckConfigsForUpdate> { ) -> Result<anki_proto::deck_config::DeckConfigsForUpdate> {
let mut defaults = DeckConfig::default(); 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 last_optimize = self.get_config_i32(I32ConfigKey::LastFsrsOptimize) as u32;
let days_since_last_fsrs_optimize = if last_optimize > 0 { let days_since_last_fsrs_optimize = if last_optimize > 0 {
self.timing_today()? self.timing_today()?
@ -88,6 +88,12 @@ impl Collection {
// grab the config and sort it // grab the config and sort it
let mut config = self.storage.all_deck_config()?; let mut config = self.storage.all_deck_config()?;
config.sort_unstable_by(|a, b| a.name.cmp(&b.name)); 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 // combine with use counts
let counts = self.get_deck_config_use_counts()?; let counts = self.get_deck_config_use_counts()?;
@ -153,14 +159,20 @@ impl Collection {
configs_after_update.remove(dcid); configs_after_update.remove(dcid);
} }
if req.mode == UpdateDeckConfigsMode::ComputeAllWeights { if req.mode == UpdateDeckConfigsMode::ComputeAllParams {
self.compute_all_weights(&mut req)?; self.compute_all_params(&mut req)?;
} }
// add/update provided configs // add/update provided configs
for conf in &mut req.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 // 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)?; self.add_or_update_deck_config(conf)?;
configs_after_update.insert(conf.id, conf.clone()); configs_after_update.insert(conf.id, conf.clone());
} }
@ -195,13 +207,13 @@ impl Collection {
if let Ok(normal) = deck.normal() { if let Ok(normal) = deck.normal() {
let deck_id = deck.id; let deck_id = deck.id;
// previous order & weights // previous order & params
let previous_config_id = DeckConfigId(normal.config_id); let previous_config_id = DeckConfigId(normal.config_id);
let previous_config = configs_before_update.get(&previous_config_id); let previous_config = configs_before_update.get(&previous_config_id);
let previous_order = previous_config let previous_order = previous_config
.map(|c| c.inner.new_card_insert_order()) .map(|c| c.inner.new_card_insert_order())
.unwrap_or_default(); .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); 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 // 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)?; self.sort_deck(deck_id, current_order, usn)?;
} }
// if weights differ, memory state needs to be recomputed // if params differ, memory state needs to be recomputed
let current_weights = current_config.map(|c| &c.inner.fsrs_weights); let current_params = current_config.map(|c| c.fsrs_params());
let current_retention = current_config.map(|c| c.inner.desired_retention); let current_retention = current_config.map(|c| c.inner.desired_retention);
if fsrs_toggled if fsrs_toggled
|| previous_weights != current_weights || previous_params != current_params
|| previous_retention != current_retention || previous_retention != current_retention
{ {
decks_needing_memory_recompute decks_needing_memory_recompute
@ -249,10 +261,10 @@ impl Collection {
.into_iter() .into_iter()
.map(|(conf_id, search)| { .map(|(conf_id, search)| {
let config = configs_after_update.get(&conf_id); let config = configs_after_update.get(&conf_id);
let weights = config.and_then(|c| { let params = config.and_then(|c| {
if req.fsrs { if req.fsrs {
Some(UpdateMemoryStateRequest { Some(UpdateMemoryStateRequest {
weights: c.inner.fsrs_weights.clone(), params: c.fsrs_params().clone(),
desired_retention: c.inner.desired_retention, desired_retention: c.inner.desired_retention,
max_interval: c.inner.maximum_review_interval, max_interval: c.inner.maximum_review_interval,
reschedule: req.fsrs_reschedule, reschedule: req.fsrs_reschedule,
@ -262,12 +274,9 @@ impl Collection {
None None
} }
}); });
let search = SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search))
.and(SearchNode::State(StateKind::Suspended).negated())
.try_into_search()?;
Ok(UpdateMemoryStateEntry { Ok(UpdateMemoryStateEntry {
req: weights, req: params,
search, search: SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)),
ignore_before: config ignore_before: config
.map(ignore_revlogs_before_ms_from_config) .map(ignore_revlogs_before_ms_from_config)
.unwrap_or(Ok(0.into()))?, .unwrap_or(Ok(0.into()))?,
@ -320,7 +329,7 @@ impl Collection {
} }
Ok(()) 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"); require!(req.fsrs, "FSRS must be enabled");
// frontend didn't include any unmodified deck configs, so we need to fill them // 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 // other parts of the code expect the currently-selected preset to come last
req.configs.push(previous_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; let config_len = req.configs.len() as u32;
for (idx, config) in req.configs.iter_mut().enumerate() { 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()) SearchNode::Preset(config.name.clone())
.and(SearchNode::State(StateKind::Suspended).negated()) .and(SearchNode::State(StateKind::Suspended).negated())
.try_into_search()? .try_into_search()?
.to_string() .to_string()
} else { } else {
config.inner.weight_search.clone() config.inner.param_search.clone()
}; };
let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?; let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?;
match self.compute_weights( match self.compute_params(
&search, &search,
ignore_revlogs_before_ms, ignore_revlogs_before_ms,
idx as u32 + 1, idx as u32 + 1,
config_len, config_len,
&config.inner.fsrs_weights, config.fsrs_params(),
) { ) {
Ok(weights) => { Ok(params) => {
println!("{}: {:?}", config.name, weights.weights); println!("{}: {:?}", config.name, params.params);
config.inner.fsrs_weights = weights.weights; config.inner.fsrs_params_5 = params.params;
} }
Err(AnkiError::Interrupted) => return Err(AnkiError::Interrupted), Err(AnkiError::Interrupted) => return Err(AnkiError::Interrupted),
Err(err) => { Err(err) => {

View file

@ -60,7 +60,12 @@ fn search_order_label(order: FilteredSearchOrder, tr: &I18n) -> String {
FilteredSearchOrder::Added => tr.decks_order_added(), FilteredSearchOrder::Added => tr.decks_order_added(),
FilteredSearchOrder::Due => tr.decks_order_due(), FilteredSearchOrder::Due => tr.decks_order_due(),
FilteredSearchOrder::ReverseAdded => tr.decks_latest_added_first(), 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() .into()
} }

View file

@ -113,7 +113,7 @@ pub enum AnkiError {
}, },
InvalidMethodIndex, InvalidMethodIndex,
InvalidServiceIndex, InvalidServiceIndex,
FsrsWeightsInvalid, FsrsParamsInvalid,
/// Returned by fsrs-rs; may happen even if 400+ reviews /// Returned by fsrs-rs; may happen even if 400+ reviews
FsrsInsufficientData, FsrsInsufficientData,
/// Generated by our backend if count < 400 /// Generated by our backend if count < 400
@ -148,7 +148,6 @@ impl AnkiError {
tr.card_templates_identical_front(index + 1) tr.card_templates_identical_front(index + 1)
} }
CardTypeErrorDetails::MissingCloze => tr.card_templates_missing_cloze(), CardTypeErrorDetails::MissingCloze => tr.card_templates_missing_cloze(),
CardTypeErrorDetails::ExtraneousCloze => tr.card_templates_extraneous_cloze(),
}; };
format!("{}<br>{}", header, details) format!("{}<br>{}", header, details)
} }
@ -181,7 +180,7 @@ impl AnkiError {
AnkiError::FsrsInsufficientReviews { count } => { AnkiError::FsrsInsufficientReviews { count } => {
tr.deck_config_must_have_400_reviews(*count).into() 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 => { AnkiError::SchedulerUpgradeRequired => {
tr.scheduling_update_required().replace("V2", "v3") tr.scheduling_update_required().replace("V2", "v3")
} }
@ -203,7 +202,6 @@ impl AnkiError {
CardTypeErrorDetails::Duplicate { .. } => HelpPage::CardTypeDuplicate, CardTypeErrorDetails::Duplicate { .. } => HelpPage::CardTypeDuplicate,
CardTypeErrorDetails::NoFrontField => HelpPage::CardTypeNoFrontField, CardTypeErrorDetails::NoFrontField => HelpPage::CardTypeNoFrontField,
CardTypeErrorDetails::MissingCloze => HelpPage::CardTypeMissingCloze, CardTypeErrorDetails::MissingCloze => HelpPage::CardTypeMissingCloze,
CardTypeErrorDetails::ExtraneousCloze => HelpPage::CardTypeExtraneousCloze,
}), }),
_ => None, _ => None,
} }
@ -323,5 +321,4 @@ pub enum CardTypeErrorDetails {
NoFrontField, NoFrontField,
NoSuchField { field: String }, NoSuchField { field: String },
MissingCloze, 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 metadata.tags_column == 0 {
if let Some(CsvNotetype::GlobalNotetype(ref global)) = metadata.notetype { if let Some(CsvNotetype::GlobalNotetype(ref global)) = metadata.notetype {
let max_field = global.field_columns.iter().max().copied().unwrap_or(0); 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) { if !meta_columns.contains(&idx) {
metadata.tags_column = max_field + 1; metadata.tags_column = idx as u32;
break; break;
} }
} }
@ -843,4 +843,23 @@ pub(in crate::import_export) mod test {
); );
assert_eq!(metadata!(col, "\u{feff}tags:foo\n").global_tags, ["foo"]); 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" "templates/errors.html#no-field-replacement-on-front-side"
} }
HelpPage::CardTypeMissingCloze => "templates/errors.html#no-cloze-filter-on-cloze-notetype", 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", HelpPage::Troubleshooting => "troubleshooting.html",
} }
} }

View file

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

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