mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 08:46:37 -04:00
Merge branch 'main' into fsrs-simulator-fixes
This commit is contained in:
commit
affe15f018
180 changed files with 2991 additions and 2157 deletions
72
.mypy.ini
72
.mypy.ini
|
@ -38,6 +38,78 @@ strict_optional = True
|
|||
strict_optional = True
|
||||
[mypy-aqt.operations.*]
|
||||
strict_optional = True
|
||||
[mypy-aqt.editor]
|
||||
strict_optional = True
|
||||
[mypy-aqt.importing]
|
||||
strict_optional = True
|
||||
[mypy-aqt.preferences]
|
||||
strict_optional = True
|
||||
[mypy-aqt.overview]
|
||||
strict_optional = True
|
||||
[mypy-aqt.customstudy]
|
||||
strict_optional = True
|
||||
[mypy-aqt.taglimit]
|
||||
strict_optional = True
|
||||
[mypy-aqt.modelchooser]
|
||||
strict_optional = True
|
||||
[mypy-aqt.deckdescription]
|
||||
strict_optional = True
|
||||
[mypy-aqt.deckbrowser]
|
||||
strict_optional = True
|
||||
[mypy-aqt.studydeck]
|
||||
strict_optional = True
|
||||
[mypy-aqt.tts]
|
||||
strict_optional = True
|
||||
[mypy-aqt.mediasrv]
|
||||
strict_optional = True
|
||||
[mypy-aqt.changenotetype]
|
||||
strict_optional = True
|
||||
[mypy-aqt.clayout]
|
||||
strict_optional = True
|
||||
[mypy-aqt.fields]
|
||||
strict_optional = True
|
||||
[mypy-aqt.filtered_deck]
|
||||
strict_optional = True
|
||||
[mypy-aqt.editcurrent]
|
||||
strict_optional = True
|
||||
[mypy-aqt.deckoptions]
|
||||
strict_optional = True
|
||||
[mypy-aqt.notetypechooser]
|
||||
strict_optional = True
|
||||
[mypy-aqt.stats]
|
||||
strict_optional = True
|
||||
[mypy-aqt.switch]
|
||||
strict_optional = True
|
||||
[mypy-aqt.debug_console]
|
||||
strict_optional = True
|
||||
[mypy-aqt.emptycards]
|
||||
strict_optional = True
|
||||
[mypy-aqt.flags]
|
||||
strict_optional = True
|
||||
[mypy-aqt.mediacheck]
|
||||
strict_optional = True
|
||||
[mypy-aqt.theme]
|
||||
strict_optional = True
|
||||
[mypy-aqt.toolbar]
|
||||
strict_optional = True
|
||||
[mypy-aqt.deckchooser]
|
||||
strict_optional = True
|
||||
[mypy-aqt.about]
|
||||
strict_optional = True
|
||||
[mypy-aqt.webview]
|
||||
strict_optional = True
|
||||
[mypy-aqt.mediasync]
|
||||
strict_optional = True
|
||||
[mypy-aqt.package]
|
||||
strict_optional = True
|
||||
[mypy-aqt.progress]
|
||||
strict_optional = True
|
||||
[mypy-aqt.tagedit]
|
||||
strict_optional = True
|
||||
[mypy-aqt.utils]
|
||||
strict_optional = True
|
||||
[mypy-aqt.sync]
|
||||
strict_optional = True
|
||||
[mypy-anki.scheduler.base]
|
||||
strict_optional = True
|
||||
[mypy-anki._backend.rsbridge]
|
||||
|
|
2
.version
2
.version
|
@ -1 +1 @@
|
|||
24.10
|
||||
24.11
|
|
@ -194,9 +194,13 @@ Gregory Abrasaldo <degeemon@gmail.com>
|
|||
Taylor Obyen <162023405+taylorobyen@users.noreply.github.com>
|
||||
Kris Cherven <krischerven@gmail.com>
|
||||
twwn <github.com/twwn>
|
||||
Shirish Pokhrel <singurty@gmail.com>
|
||||
Cy Pokhrel <cy@cy7.sh>
|
||||
Park Hyunwoo <phu54321@naver.com>
|
||||
Tomas Fabrizio Orsi <torsi@fi.uba.ar>
|
||||
Dongjin Ouyang <1113117424@qq.com>
|
||||
Sawan Sunar <sawansunar24072002@gmail.com>
|
||||
hideo aoyama <https://github.com/boukendesho>
|
||||
Ross Brown <rbrownwsws@googlemail.com>
|
||||
|
||||
********************
|
||||
|
||||
|
|
740
Cargo.lock
generated
740
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
71
Cargo.toml
71
Cargo.toml
|
@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
|
|||
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
||||
|
||||
[workspace.dependencies.fsrs]
|
||||
version = "=1.3.1"
|
||||
version = "=1.4.3"
|
||||
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
||||
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
|
||||
# path = "../open-spaced-repetition/fsrs-rs"
|
||||
|
@ -55,19 +55,19 @@ unicase = "=2.6.0" # any changes could invalidate sqlite indexes
|
|||
|
||||
# normal
|
||||
ammonia = "4.0.0"
|
||||
anyhow = "1.0.86"
|
||||
anyhow = "1.0.90"
|
||||
apple-bundles = "0.17.0"
|
||||
async-compression = { version = "0.4.12", features = ["zstd", "tokio"] }
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.82"
|
||||
async-compression = { version = "0.4.17", features = ["zstd", "tokio"] }
|
||||
async-stream = "0.3.6"
|
||||
async-trait = "0.1.83"
|
||||
axum = { version = "0.7", features = ["multipart", "macros"] }
|
||||
axum-client-ip = "0.6"
|
||||
axum-extra = { version = "0.9.3", features = ["typed-header"] }
|
||||
axum-extra = { version = "0.9.4", features = ["typed-header"] }
|
||||
blake3 = "1.5.4"
|
||||
bytes = "1.7.1"
|
||||
bytes = "1.7.2"
|
||||
camino = "1.1.9"
|
||||
chrono = { version = "0.4.38", default-features = false, features = ["std", "clock"] }
|
||||
clap = { version = "4.3.24", features = ["derive"] }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
coarsetime = "0.1.34"
|
||||
convert_case = "0.6.0"
|
||||
criterion = { version = "0.5.1" }
|
||||
|
@ -77,14 +77,14 @@ difflib = "0.4.0"
|
|||
dirs = "5.0.1"
|
||||
dunce = "1.0.5"
|
||||
envy = "0.4.2"
|
||||
flate2 = "1.0.33"
|
||||
flate2 = "1.0.34"
|
||||
fluent = "0.16.1"
|
||||
fluent-bundle = "0.15.3"
|
||||
fluent-syntax = "0.11.1"
|
||||
fnv = "1.0.7"
|
||||
futures = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
glob = "0.3.1"
|
||||
globset = "0.4.14"
|
||||
globset = "0.4.15"
|
||||
hex = "0.4.3"
|
||||
htmlescape = "0.3.1"
|
||||
hyper = "1"
|
||||
|
@ -92,47 +92,47 @@ id_tree = "1.8.0"
|
|||
inflections = "1.1.1"
|
||||
intl-memoizer = "0.5.2"
|
||||
itertools = "0.13.0"
|
||||
junction = "1.1.0"
|
||||
junction = "1.2.0"
|
||||
lazy_static = "1.5.0"
|
||||
maplit = "1.0.2"
|
||||
nom = "7.1.3"
|
||||
num-format = "0.4.4"
|
||||
num_cpus = "1.16.0"
|
||||
num_enum = "0.7.2"
|
||||
once_cell = "1.19.0"
|
||||
num_enum = "0.7.3"
|
||||
once_cell = "1.20.2"
|
||||
pbkdf2 = { version = "0.12", features = ["simple"] }
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
pin-project = "1.1.5"
|
||||
plist = "1.5.1"
|
||||
prettyplease = "0.2.22"
|
||||
prost = "0.12.3"
|
||||
prost-build = "0.12.3"
|
||||
prost-reflect = "0.12.0"
|
||||
prost-types = "0.12.3"
|
||||
pin-project = "1.1.6"
|
||||
plist = "1.7.0"
|
||||
prettyplease = "0.2.24"
|
||||
prost = "0.13"
|
||||
prost-build = "0.13"
|
||||
prost-reflect = "0.14"
|
||||
prost-types = "0.13"
|
||||
pulldown-cmark = "0.9.6"
|
||||
pyo3 = { version = "0.22.2", features = ["extension-module", "abi3", "abi3-py39"] }
|
||||
pyo3 = { version = "0.22.5", features = ["extension-module", "abi3", "abi3-py39"] }
|
||||
rand = "0.8.5"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.12.7", default-features = false, features = ["json", "socks", "stream", "multipart"] }
|
||||
regex = "1.11.0"
|
||||
reqwest = { version = "0.12.8", default-features = false, features = ["json", "socks", "stream", "multipart"] }
|
||||
rusqlite = { version = "0.30.0", features = ["trace", "functions", "collation", "bundled"] }
|
||||
rustls-pemfile = "2.1.3"
|
||||
rustls-pemfile = "2.2.0"
|
||||
scopeguard = "1.2.0"
|
||||
serde = { version = "1.0.209", features = ["derive"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde-aux = "4.5.0"
|
||||
serde_json = "1.0.127"
|
||||
serde_json = "1.0.132"
|
||||
serde_repr = "0.1.19"
|
||||
serde_tuple = "0.5.0"
|
||||
sha1 = "0.10.6"
|
||||
sha2 = { version = "0.10.8" }
|
||||
simple-file-manifest = "0.11.0"
|
||||
snafu = { version = "0.8.4", features = ["rust_1_61"] }
|
||||
snafu = { version = "0.8.5", features = ["rust_1_61"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
syn = { version = "2.0.77", features = ["parsing", "printing"] }
|
||||
tar = "0.4.41"
|
||||
tempfile = "3.12.0"
|
||||
syn = { version = "2.0.82", features = ["parsing", "printing"] }
|
||||
tar = "0.4.42"
|
||||
tempfile = "3.13.0"
|
||||
termcolor = "1.4.1"
|
||||
tokio = { version = "1.38", features = ["fs", "rt-multi-thread", "macros", "signal"] }
|
||||
tokio-util = { version = "0.7.11", features = ["io"] }
|
||||
tokio = { version = "1.40", features = ["fs", "rt-multi-thread", "macros", "signal"] }
|
||||
tokio-util = { version = "0.7.12", features = ["io"] }
|
||||
tower-http = { version = "0.5", features = ["trace"] }
|
||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-appender = "0.2.3"
|
||||
|
@ -140,11 +140,10 @@ tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
|||
tugger-windows-codesign = "0.10.0"
|
||||
unic-langid = { version = "0.9.5", features = ["macros"] }
|
||||
unic-ucd-category = "0.9.0"
|
||||
unicode-normalization = "0.1.23"
|
||||
utime = "0.3.1"
|
||||
unicode-normalization = "0.1.24"
|
||||
walkdir = "2.5.0"
|
||||
which = "5.0.0"
|
||||
wiremock = "0.6.1"
|
||||
wiremock = "0.6.2"
|
||||
xz2 = "0.1.7"
|
||||
zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] }
|
||||
zstd = { version = "0.13.2", features = ["zstdmt"] }
|
||||
|
|
|
@ -142,7 +142,7 @@ impl BuildAction for BuildWheel {
|
|||
|
||||
let tag = if let Some(platform) = self.platform {
|
||||
let platform = match platform {
|
||||
Platform::LinuxX64 => "manylinux_2_28_x86_64",
|
||||
Platform::LinuxX64 => "manylinux_2_31_x86_64",
|
||||
Platform::LinuxArm => "manylinux_2_31_aarch64",
|
||||
Platform::MacX64 => "macosx_10_13_x86_64",
|
||||
Platform::MacArm => "macosx_11_0_arm64",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[
|
||||
{
|
||||
"name": "addr2line",
|
||||
"version": "0.22.0",
|
||||
"version": "0.24.2",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/gimli-rs/addr2line",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -136,7 +136,7 @@
|
|||
},
|
||||
{
|
||||
"name": "anyhow",
|
||||
"version": "1.0.86",
|
||||
"version": "1.0.90",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/dtolnay/anyhow",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -145,7 +145,7 @@
|
|||
},
|
||||
{
|
||||
"name": "arrayref",
|
||||
"version": "0.3.8",
|
||||
"version": "0.3.9",
|
||||
"authors": "David Roundy <roundyd@physics.oregonstate.edu>",
|
||||
"repository": "https://github.com/droundy/arrayref",
|
||||
"license": "BSD-2-Clause",
|
||||
|
@ -181,7 +181,7 @@
|
|||
},
|
||||
{
|
||||
"name": "async-compression",
|
||||
"version": "0.4.12",
|
||||
"version": "0.4.17",
|
||||
"authors": "Wim Looman <wim@nemo157.com>|Allen Bui <fairingrey@gmail.com>",
|
||||
"repository": "https://github.com/Nullus157/async-compression",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -190,7 +190,7 @@
|
|||
},
|
||||
{
|
||||
"name": "async-stream",
|
||||
"version": "0.3.5",
|
||||
"version": "0.3.6",
|
||||
"authors": "Carl Lerche <me@carllerche.com>",
|
||||
"repository": "https://github.com/tokio-rs/async-stream",
|
||||
"license": "MIT",
|
||||
|
@ -199,7 +199,7 @@
|
|||
},
|
||||
{
|
||||
"name": "async-stream-impl",
|
||||
"version": "0.3.5",
|
||||
"version": "0.3.6",
|
||||
"authors": "Carl Lerche <me@carllerche.com>",
|
||||
"repository": "https://github.com/tokio-rs/async-stream",
|
||||
"license": "MIT",
|
||||
|
@ -208,7 +208,7 @@
|
|||
},
|
||||
{
|
||||
"name": "async-trait",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/dtolnay/async-trait",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -226,7 +226,7 @@
|
|||
},
|
||||
{
|
||||
"name": "autocfg",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"authors": "Josh Stone <cuviper@gmail.com>",
|
||||
"repository": "https://github.com/cuviper/autocfg",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -235,7 +235,7 @@
|
|||
},
|
||||
{
|
||||
"name": "axum",
|
||||
"version": "0.7.5",
|
||||
"version": "0.7.7",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/tokio-rs/axum",
|
||||
"license": "MIT",
|
||||
|
@ -244,7 +244,7 @@
|
|||
},
|
||||
{
|
||||
"name": "axum-client-ip",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/imbolc/axum-client-ip",
|
||||
"license": "MIT",
|
||||
|
@ -253,7 +253,7 @@
|
|||
},
|
||||
{
|
||||
"name": "axum-core",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.5",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/tokio-rs/axum",
|
||||
"license": "MIT",
|
||||
|
@ -262,7 +262,7 @@
|
|||
},
|
||||
{
|
||||
"name": "axum-extra",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.4",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/tokio-rs/axum",
|
||||
"license": "MIT",
|
||||
|
@ -271,7 +271,7 @@
|
|||
},
|
||||
{
|
||||
"name": "axum-macros",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/tokio-rs/axum",
|
||||
"license": "MIT",
|
||||
|
@ -280,7 +280,7 @@
|
|||
},
|
||||
{
|
||||
"name": "backtrace",
|
||||
"version": "0.3.73",
|
||||
"version": "0.3.74",
|
||||
"authors": "The Rust Project Developers",
|
||||
"repository": "https://github.com/rust-lang/backtrace-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -550,7 +550,7 @@
|
|||
},
|
||||
{
|
||||
"name": "bytemuck",
|
||||
"version": "1.17.1",
|
||||
"version": "1.19.0",
|
||||
"authors": "Lokathor <zefria@gmail.com>",
|
||||
"repository": "https://github.com/Lokathor/bytemuck",
|
||||
"license": "Apache-2.0 OR MIT OR Zlib",
|
||||
|
@ -559,7 +559,7 @@
|
|||
},
|
||||
{
|
||||
"name": "bytemuck_derive",
|
||||
"version": "1.7.1",
|
||||
"version": "1.8.0",
|
||||
"authors": "Lokathor <zefria@gmail.com>",
|
||||
"repository": "https://github.com/Lokathor/bytemuck",
|
||||
"license": "Apache-2.0 OR MIT OR Zlib",
|
||||
|
@ -577,7 +577,7 @@
|
|||
},
|
||||
{
|
||||
"name": "bytes",
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"authors": "Carl Lerche <me@carllerche.com>|Sean McArthur <sean@seanmonstar.com>",
|
||||
"repository": "https://github.com/tokio-rs/bytes",
|
||||
"license": "MIT",
|
||||
|
@ -622,7 +622,7 @@
|
|||
},
|
||||
{
|
||||
"name": "cc",
|
||||
"version": "1.1.15",
|
||||
"version": "1.1.31",
|
||||
"authors": "Alex Crichton <alex@alexcrichton.com>",
|
||||
"repository": "https://github.com/rust-lang/cc-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -775,7 +775,7 @@
|
|||
},
|
||||
{
|
||||
"name": "cpufeatures",
|
||||
"version": "0.2.13",
|
||||
"version": "0.2.14",
|
||||
"authors": "RustCrypto Developers",
|
||||
"repository": "https://github.com/RustCrypto/utils",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1000,7 +1000,7 @@
|
|||
},
|
||||
{
|
||||
"name": "enum-as-inner",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"authors": "Benjamin Fry <benjaminfry@me.com>",
|
||||
"repository": "https://github.com/bluejekyll/enum-as-inner",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1081,7 +1081,7 @@
|
|||
},
|
||||
{
|
||||
"name": "fdeflate",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"authors": "The image-rs Developers",
|
||||
"repository": "https://github.com/image-rs/fdeflate",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1099,7 +1099,7 @@
|
|||
},
|
||||
{
|
||||
"name": "flate2",
|
||||
"version": "1.0.33",
|
||||
"version": "1.0.34",
|
||||
"authors": "Alex Crichton <alex@alexcrichton.com>|Josh Triplett <josh@joshtriplett.org>",
|
||||
"repository": "https://github.com/rust-lang/flate2-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1144,7 +1144,7 @@
|
|||
},
|
||||
{
|
||||
"name": "flume",
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.1",
|
||||
"authors": "Joshua Barretto <joshua.s.barretto@gmail.com>",
|
||||
"repository": "https://github.com/zesterer/flume",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1225,7 +1225,7 @@
|
|||
},
|
||||
{
|
||||
"name": "fsrs",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.3",
|
||||
"authors": "Open Spaced Repetition",
|
||||
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
|
||||
"license": "BSD-3-Clause",
|
||||
|
@ -1243,7 +1243,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1252,7 +1252,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures-channel",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1261,7 +1261,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures-core",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1270,7 +1270,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures-executor",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1288,7 +1288,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures-io",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1297,7 +1297,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures-macro",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1306,7 +1306,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures-sink",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1315,7 +1315,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures-task",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1324,7 +1324,7 @@
|
|||
},
|
||||
{
|
||||
"name": "futures-util",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rust-lang/futures-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1432,7 +1432,7 @@
|
|||
},
|
||||
{
|
||||
"name": "gimli",
|
||||
"version": "0.29.0",
|
||||
"version": "0.31.1",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/gimli-rs/gimli",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1477,7 +1477,7 @@
|
|||
},
|
||||
{
|
||||
"name": "gix-trace",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"authors": "Sebastian Thiel <sebastian.thiel@icloud.com>",
|
||||
"repository": "https://github.com/Byron/gitoxide",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1592,6 +1592,15 @@
|
|||
"license_file": null,
|
||||
"description": "A Rust port of Google's SwissTable hash map"
|
||||
},
|
||||
{
|
||||
"name": "hashbrown",
|
||||
"version": "0.15.2",
|
||||
"authors": "Amanieu d'Antras <amanieu@gmail.com>",
|
||||
"repository": "https://github.com/rust-lang/hashbrown",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"license_file": null,
|
||||
"description": "A Rust port of Google's SwissTable hash map"
|
||||
},
|
||||
{
|
||||
"name": "hashlink",
|
||||
"version": "0.8.4",
|
||||
|
@ -1729,7 +1738,7 @@
|
|||
},
|
||||
{
|
||||
"name": "httparse",
|
||||
"version": "1.9.4",
|
||||
"version": "1.9.5",
|
||||
"authors": "Sean McArthur <sean@seanmonstar.com>",
|
||||
"repository": "https://github.com/seanmonstar/httparse",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1747,7 +1756,7 @@
|
|||
},
|
||||
{
|
||||
"name": "hyper",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.0",
|
||||
"authors": "Sean McArthur <sean@seanmonstar.com>",
|
||||
"repository": "https://github.com/hyperium/hyper",
|
||||
"license": "MIT",
|
||||
|
@ -1756,7 +1765,7 @@
|
|||
},
|
||||
{
|
||||
"name": "hyper-rustls",
|
||||
"version": "0.27.2",
|
||||
"version": "0.27.3",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rustls/hyper-rustls",
|
||||
"license": "Apache-2.0 OR ISC OR MIT",
|
||||
|
@ -1774,7 +1783,7 @@
|
|||
},
|
||||
{
|
||||
"name": "hyper-util",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.9",
|
||||
"authors": "Sean McArthur <sean@seanmonstar.com>",
|
||||
"repository": "https://github.com/hyperium/hyper-util",
|
||||
"license": "MIT",
|
||||
|
@ -1783,7 +1792,7 @@
|
|||
},
|
||||
{
|
||||
"name": "iana-time-zone",
|
||||
"version": "0.1.60",
|
||||
"version": "0.1.61",
|
||||
"authors": "Andrew Straw <strawman@astraw.com>|René Kijewski <rene.kijewski@fu-berlin.de>|Ryan Lopopolo <rjl@hyperbo.la>",
|
||||
"repository": "https://github.com/strawlab/iana-time-zone",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1828,7 +1837,7 @@
|
|||
},
|
||||
{
|
||||
"name": "indexmap",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.0",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/indexmap-rs/indexmap",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1873,7 +1882,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ipnet",
|
||||
"version": "2.9.0",
|
||||
"version": "2.10.1",
|
||||
"authors": "Kris Price <kris@krisprice.nz>",
|
||||
"repository": "https://github.com/krisprice/ipnet",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1936,7 +1945,7 @@
|
|||
},
|
||||
{
|
||||
"name": "js-sys",
|
||||
"version": "0.3.70",
|
||||
"version": "0.3.72",
|
||||
"authors": "The wasm-bindgen Developers",
|
||||
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -1981,7 +1990,7 @@
|
|||
},
|
||||
{
|
||||
"name": "libc",
|
||||
"version": "0.2.158",
|
||||
"version": "0.2.161",
|
||||
"authors": "The Rust Project Developers",
|
||||
"repository": "https://github.com/rust-lang/libc",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2152,7 +2161,7 @@
|
|||
},
|
||||
{
|
||||
"name": "memmap2",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"authors": "Dan Burkert <dan@danburkert.com>|Yevhenii Reizner <razrfalcon@gmail.com>",
|
||||
"repository": "https://github.com/RazrFalcon/memmap2-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2413,7 +2422,7 @@
|
|||
},
|
||||
{
|
||||
"name": "object",
|
||||
"version": "0.36.4",
|
||||
"version": "0.36.5",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/gimli-rs/object",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2422,7 +2431,7 @@
|
|||
},
|
||||
{
|
||||
"name": "once_cell",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.2",
|
||||
"authors": "Aleksey Kladov <aleksey.kladov@gmail.com>",
|
||||
"repository": "https://github.com/matklad/once_cell",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2431,7 +2440,7 @@
|
|||
},
|
||||
{
|
||||
"name": "openssl",
|
||||
"version": "0.10.66",
|
||||
"version": "0.10.68",
|
||||
"authors": "Steven Fackler <sfackler@gmail.com>",
|
||||
"repository": "https://github.com/sfackler/rust-openssl",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -2458,7 +2467,7 @@
|
|||
},
|
||||
{
|
||||
"name": "openssl-sys",
|
||||
"version": "0.9.103",
|
||||
"version": "0.9.104",
|
||||
"authors": "Alex Crichton <alex@alexcrichton.com>|Steven Fackler <sfackler@gmail.com>",
|
||||
"repository": "https://github.com/sfackler/rust-openssl",
|
||||
"license": "MIT",
|
||||
|
@ -2638,7 +2647,7 @@
|
|||
},
|
||||
{
|
||||
"name": "pin-project",
|
||||
"version": "1.1.5",
|
||||
"version": "1.1.6",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/taiki-e/pin-project",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2647,7 +2656,7 @@
|
|||
},
|
||||
{
|
||||
"name": "pin-project-internal",
|
||||
"version": "1.1.5",
|
||||
"version": "1.1.6",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/taiki-e/pin-project",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2674,7 +2683,7 @@
|
|||
},
|
||||
{
|
||||
"name": "pkg-config",
|
||||
"version": "0.3.30",
|
||||
"version": "0.3.31",
|
||||
"authors": "Alex Crichton <alex@alexcrichton.com>",
|
||||
"repository": "https://github.com/rust-lang/pkg-config-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2683,7 +2692,7 @@
|
|||
},
|
||||
{
|
||||
"name": "png",
|
||||
"version": "0.17.13",
|
||||
"version": "0.17.14",
|
||||
"authors": "The image-rs Developers",
|
||||
"repository": "https://github.com/image-rs/image-png",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2737,13 +2746,22 @@
|
|||
},
|
||||
{
|
||||
"name": "prettyplease",
|
||||
"version": "0.2.22",
|
||||
"version": "0.2.24",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/dtolnay/prettyplease",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"license_file": null,
|
||||
"description": "A minimal `syn` syntax tree pretty-printer"
|
||||
},
|
||||
{
|
||||
"name": "priority-queue",
|
||||
"version": "2.1.1",
|
||||
"authors": "Gianmarco Garrisi <gianmarcogarrisi@tutanota.com>",
|
||||
"repository": "https://github.com/garro95/priority-queue",
|
||||
"license": "LGPL-3.0-or-later OR MPL-2.0",
|
||||
"license_file": null,
|
||||
"description": "A Priority Queue implemented as a heap with a function to efficiently change the priority of an item."
|
||||
},
|
||||
{
|
||||
"name": "proc-macro-crate",
|
||||
"version": "3.2.0",
|
||||
|
@ -2764,7 +2782,7 @@
|
|||
},
|
||||
{
|
||||
"name": "proc-macro2",
|
||||
"version": "1.0.86",
|
||||
"version": "1.0.88",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>|Alex Crichton <alex@alexcrichton.com>",
|
||||
"repository": "https://github.com/dtolnay/proc-macro2",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2773,7 +2791,7 @@
|
|||
},
|
||||
{
|
||||
"name": "profiling",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.16",
|
||||
"authors": "Philip Degarmo <aclysma@gmail.com>",
|
||||
"repository": "https://github.com/aclysma/profiling",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2782,7 +2800,7 @@
|
|||
},
|
||||
{
|
||||
"name": "prost",
|
||||
"version": "0.12.6",
|
||||
"version": "0.13.3",
|
||||
"authors": "Dan Burkert <dan@danburkert.com>|Lucio Franco <luciofranco14@gmail.com>|Casper Meijn <casper@meijn.net>|Tokio Contributors <team@tokio.rs>",
|
||||
"repository": "https://github.com/tokio-rs/prost",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -2791,7 +2809,7 @@
|
|||
},
|
||||
{
|
||||
"name": "prost-build",
|
||||
"version": "0.12.6",
|
||||
"version": "0.13.3",
|
||||
"authors": "Dan Burkert <dan@danburkert.com>|Lucio Franco <luciofranco14@gmail.com>|Casper Meijn <casper@meijn.net>|Tokio Contributors <team@tokio.rs>",
|
||||
"repository": "https://github.com/tokio-rs/prost",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -2800,7 +2818,7 @@
|
|||
},
|
||||
{
|
||||
"name": "prost-derive",
|
||||
"version": "0.12.6",
|
||||
"version": "0.13.3",
|
||||
"authors": "Dan Burkert <dan@danburkert.com>|Lucio Franco <luciofranco14@gmail.com>|Casper Meijn <casper@meijn.net>|Tokio Contributors <team@tokio.rs>",
|
||||
"repository": "https://github.com/tokio-rs/prost",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -2809,7 +2827,7 @@
|
|||
},
|
||||
{
|
||||
"name": "prost-reflect",
|
||||
"version": "0.12.0",
|
||||
"version": "0.14.2",
|
||||
"authors": "Andrew Hickman <andrew.hickman1@sky.com>",
|
||||
"repository": "https://github.com/andrewhickman/prost-reflect",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2818,7 +2836,7 @@
|
|||
},
|
||||
{
|
||||
"name": "prost-types",
|
||||
"version": "0.12.6",
|
||||
"version": "0.13.3",
|
||||
"authors": "Dan Burkert <dan@danburkert.com>|Lucio Franco <luciofranco14@gmail.com>|Casper Meijn <casper@meijn.net>|Tokio Contributors <team@tokio.rs>",
|
||||
"repository": "https://github.com/tokio-rs/prost",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -3007,7 +3025,7 @@
|
|||
},
|
||||
{
|
||||
"name": "redox_syscall",
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.7",
|
||||
"authors": "Jeremy Soller <jackpot51@gmail.com>",
|
||||
"repository": "https://gitlab.redox-os.org/redox-os/syscall",
|
||||
"license": "MIT",
|
||||
|
@ -3025,7 +3043,7 @@
|
|||
},
|
||||
{
|
||||
"name": "regex",
|
||||
"version": "1.10.6",
|
||||
"version": "1.11.0",
|
||||
"authors": "The Rust Project Developers|Andrew Gallant <jamslam@gmail.com>",
|
||||
"repository": "https://github.com/rust-lang/regex",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3043,7 +3061,7 @@
|
|||
},
|
||||
{
|
||||
"name": "regex-automata",
|
||||
"version": "0.4.7",
|
||||
"version": "0.4.8",
|
||||
"authors": "The Rust Project Developers|Andrew Gallant <jamslam@gmail.com>",
|
||||
"repository": "https://github.com/rust-lang/regex/tree/master/regex-automata",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3061,7 +3079,7 @@
|
|||
},
|
||||
{
|
||||
"name": "regex-syntax",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.5",
|
||||
"authors": "The Rust Project Developers|Andrew Gallant <jamslam@gmail.com>",
|
||||
"repository": "https://github.com/rust-lang/regex/tree/master/regex-syntax",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3079,7 +3097,7 @@
|
|||
},
|
||||
{
|
||||
"name": "reqwest",
|
||||
"version": "0.12.7",
|
||||
"version": "0.12.8",
|
||||
"authors": "Sean McArthur <sean@seanmonstar.com>",
|
||||
"repository": "https://github.com/seanmonstar/reqwest",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3151,7 +3169,7 @@
|
|||
},
|
||||
{
|
||||
"name": "rustix",
|
||||
"version": "0.38.35",
|
||||
"version": "0.38.37",
|
||||
"authors": "Dan Gohman <dev@sunfishcode.online>|Jakub Konka <kubkon@jakubkonka.com>",
|
||||
"repository": "https://github.com/bytecodealliance/rustix",
|
||||
"license": "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
|
||||
|
@ -3160,7 +3178,7 @@
|
|||
},
|
||||
{
|
||||
"name": "rustls",
|
||||
"version": "0.23.12",
|
||||
"version": "0.23.18",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rustls/rustls",
|
||||
"license": "Apache-2.0 OR ISC OR MIT",
|
||||
|
@ -3169,7 +3187,7 @@
|
|||
},
|
||||
{
|
||||
"name": "rustls-native-certs",
|
||||
"version": "0.7.3",
|
||||
"version": "0.8.0",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rustls/rustls-native-certs",
|
||||
"license": "Apache-2.0 OR ISC OR MIT",
|
||||
|
@ -3178,7 +3196,7 @@
|
|||
},
|
||||
{
|
||||
"name": "rustls-pemfile",
|
||||
"version": "2.1.3",
|
||||
"version": "2.2.0",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rustls/pemfile",
|
||||
"license": "Apache-2.0 OR ISC OR MIT",
|
||||
|
@ -3187,7 +3205,7 @@
|
|||
},
|
||||
{
|
||||
"name": "rustls-pki-types",
|
||||
"version": "1.8.0",
|
||||
"version": "1.10.0",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rustls/pki-types",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3196,7 +3214,7 @@
|
|||
},
|
||||
{
|
||||
"name": "rustls-webpki",
|
||||
"version": "0.102.7",
|
||||
"version": "0.102.8",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rustls/webpki",
|
||||
"license": "ISC",
|
||||
|
@ -3205,7 +3223,7 @@
|
|||
},
|
||||
{
|
||||
"name": "rustversion",
|
||||
"version": "1.0.17",
|
||||
"version": "1.0.18",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/dtolnay/rustversion",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3232,7 +3250,7 @@
|
|||
},
|
||||
{
|
||||
"name": "safetensors",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/huggingface/safetensors",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -3259,7 +3277,7 @@
|
|||
},
|
||||
{
|
||||
"name": "schannel",
|
||||
"version": "0.1.23",
|
||||
"version": "0.1.26",
|
||||
"authors": "Steven Fackler <sfackler@gmail.com>|Steffen Butzer <steffen.butzer@outlook.com>",
|
||||
"repository": "https://github.com/steffengy/schannel-rs",
|
||||
"license": "MIT",
|
||||
|
@ -3295,7 +3313,7 @@
|
|||
},
|
||||
{
|
||||
"name": "security-framework-sys",
|
||||
"version": "2.11.1",
|
||||
"version": "2.12.0",
|
||||
"authors": "Steven Fackler <sfackler@gmail.com>|Kornel <kornel@geekhood.net>",
|
||||
"repository": "https://github.com/kornelski/rust-security-framework",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3331,7 +3349,7 @@
|
|||
},
|
||||
{
|
||||
"name": "serde",
|
||||
"version": "1.0.209",
|
||||
"version": "1.0.210",
|
||||
"authors": "Erick Tryzelaar <erick.tryzelaar@gmail.com>|David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/serde-rs/serde",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3349,7 +3367,7 @@
|
|||
},
|
||||
{
|
||||
"name": "serde_derive",
|
||||
"version": "1.0.209",
|
||||
"version": "1.0.210",
|
||||
"authors": "Erick Tryzelaar <erick.tryzelaar@gmail.com>|David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/serde-rs/serde",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3358,7 +3376,7 @@
|
|||
},
|
||||
{
|
||||
"name": "serde_json",
|
||||
"version": "1.0.127",
|
||||
"version": "1.0.132",
|
||||
"authors": "Erick Tryzelaar <erick.tryzelaar@gmail.com>|David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/serde-rs/json",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3520,7 +3538,7 @@
|
|||
},
|
||||
{
|
||||
"name": "snafu",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.5",
|
||||
"authors": "Jake Goulding <jake.goulding@gmail.com>",
|
||||
"repository": "https://github.com/shepmaster/snafu",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3529,7 +3547,7 @@
|
|||
},
|
||||
{
|
||||
"name": "snafu-derive",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.5",
|
||||
"authors": "Jake Goulding <jake.goulding@gmail.com>",
|
||||
"repository": "https://github.com/shepmaster/snafu",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3664,7 +3682,7 @@
|
|||
},
|
||||
{
|
||||
"name": "syn",
|
||||
"version": "2.0.77",
|
||||
"version": "2.0.82",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/dtolnay/syn",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3718,7 +3736,7 @@
|
|||
},
|
||||
{
|
||||
"name": "tempfile",
|
||||
"version": "3.12.0",
|
||||
"version": "3.13.0",
|
||||
"authors": "Steven Allen <steven@stebalien.com>|The Rust Project Developers|Ashley Mannix <ashleymannix@live.com.au>|Jason White <me@jasonwhite.io>",
|
||||
"repository": "https://github.com/Stebalien/tempfile",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3754,7 +3772,7 @@
|
|||
},
|
||||
{
|
||||
"name": "thiserror",
|
||||
"version": "1.0.63",
|
||||
"version": "1.0.64",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/dtolnay/thiserror",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3763,7 +3781,7 @@
|
|||
},
|
||||
{
|
||||
"name": "thiserror-impl",
|
||||
"version": "1.0.63",
|
||||
"version": "1.0.64",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/dtolnay/thiserror",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3898,7 +3916,7 @@
|
|||
},
|
||||
{
|
||||
"name": "tokio-util",
|
||||
"version": "0.7.11",
|
||||
"version": "0.7.12",
|
||||
"authors": "Tokio Contributors <team@tokio.rs>",
|
||||
"repository": "https://github.com/tokio-rs/tokio",
|
||||
"license": "MIT",
|
||||
|
@ -3916,7 +3934,7 @@
|
|||
},
|
||||
{
|
||||
"name": "toml_edit",
|
||||
"version": "0.22.20",
|
||||
"version": "0.22.22",
|
||||
"authors": "Andronik Ordian <write@reusable.software>|Ed Page <eopage@gmail.com>",
|
||||
"repository": "https://github.com/toml-rs/toml",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -3934,7 +3952,7 @@
|
|||
},
|
||||
{
|
||||
"name": "tower",
|
||||
"version": "0.4.13",
|
||||
"version": "0.5.1",
|
||||
"authors": "Tower Maintainers <team@tower-rs.com>",
|
||||
"repository": "https://github.com/tower-rs/tower",
|
||||
"license": "MIT",
|
||||
|
@ -4141,7 +4159,7 @@
|
|||
},
|
||||
{
|
||||
"name": "unicode-bidi",
|
||||
"version": "0.3.15",
|
||||
"version": "0.3.17",
|
||||
"authors": "The Servo Project Developers",
|
||||
"repository": "https://github.com/servo/unicode-bidi",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4150,7 +4168,7 @@
|
|||
},
|
||||
{
|
||||
"name": "unicode-ident",
|
||||
"version": "1.0.12",
|
||||
"version": "1.0.13",
|
||||
"authors": "David Tolnay <dtolnay@gmail.com>",
|
||||
"repository": "https://github.com/dtolnay/unicode-ident",
|
||||
"license": "(MIT OR Apache-2.0) AND Unicode-DFS-2016",
|
||||
|
@ -4159,7 +4177,7 @@
|
|||
},
|
||||
{
|
||||
"name": "unicode-normalization",
|
||||
"version": "0.1.23",
|
||||
"version": "0.1.24",
|
||||
"authors": "kwantam <kwantam@gmail.com>|Manish Goregaokar <manishsmail@gmail.com>",
|
||||
"repository": "https://github.com/unicode-rs/unicode-normalization",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4168,7 +4186,7 @@
|
|||
},
|
||||
{
|
||||
"name": "unicode-segmentation",
|
||||
"version": "1.11.0",
|
||||
"version": "1.12.0",
|
||||
"authors": "kwantam <kwantam@gmail.com>|Manish Goregaokar <manishsmail@gmail.com>",
|
||||
"repository": "https://github.com/unicode-rs/unicode-segmentation",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4177,7 +4195,7 @@
|
|||
},
|
||||
{
|
||||
"name": "unicode-width",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"authors": "kwantam <kwantam@gmail.com>|Manish Goregaokar <manishsmail@gmail.com>",
|
||||
"repository": "https://github.com/unicode-rs/unicode-width",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4186,7 +4204,7 @@
|
|||
},
|
||||
{
|
||||
"name": "unicode-xid",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"authors": "erick.tryzelaar <erick.tryzelaar@gmail.com>|kwantam <kwantam@gmail.com>|Manish Goregaokar <manishsmail@gmail.com>",
|
||||
"repository": "https://github.com/unicode-rs/unicode-xid",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4229,18 +4247,9 @@
|
|||
"license_file": null,
|
||||
"description": "Incremental, zero-copy UTF-8 decoding with error handling"
|
||||
},
|
||||
{
|
||||
"name": "utime",
|
||||
"version": "0.3.1",
|
||||
"authors": "Hyeon Kim <simnalamburt@gmail.com>",
|
||||
"repository": "https://github.com/simnalamburt/utime",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"license_file": null,
|
||||
"description": "A missing utime function for Rust."
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"version": "1.10.0",
|
||||
"version": "1.11.0",
|
||||
"authors": "Ashley Mannix<ashleymannix@live.com.au>|Dylan DPC<dylan.dpc@gmail.com>|Hunar Roop Kahlon<hunar.roop@gmail.com>",
|
||||
"repository": "https://github.com/uuid-rs/uuid",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4312,7 +4321,7 @@
|
|||
},
|
||||
{
|
||||
"name": "wasm-bindgen",
|
||||
"version": "0.2.93",
|
||||
"version": "0.2.95",
|
||||
"authors": "The wasm-bindgen Developers",
|
||||
"repository": "https://github.com/rustwasm/wasm-bindgen",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4321,7 +4330,7 @@
|
|||
},
|
||||
{
|
||||
"name": "wasm-bindgen-backend",
|
||||
"version": "0.2.93",
|
||||
"version": "0.2.95",
|
||||
"authors": "The wasm-bindgen Developers",
|
||||
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4330,7 +4339,7 @@
|
|||
},
|
||||
{
|
||||
"name": "wasm-bindgen-futures",
|
||||
"version": "0.4.43",
|
||||
"version": "0.4.45",
|
||||
"authors": "The wasm-bindgen Developers",
|
||||
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4339,7 +4348,7 @@
|
|||
},
|
||||
{
|
||||
"name": "wasm-bindgen-macro",
|
||||
"version": "0.2.93",
|
||||
"version": "0.2.95",
|
||||
"authors": "The wasm-bindgen Developers",
|
||||
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4348,7 +4357,7 @@
|
|||
},
|
||||
{
|
||||
"name": "wasm-bindgen-macro-support",
|
||||
"version": "0.2.93",
|
||||
"version": "0.2.95",
|
||||
"authors": "The wasm-bindgen Developers",
|
||||
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro-support",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4357,7 +4366,7 @@
|
|||
},
|
||||
{
|
||||
"name": "wasm-bindgen-shared",
|
||||
"version": "0.2.93",
|
||||
"version": "0.2.95",
|
||||
"authors": "The wasm-bindgen Developers",
|
||||
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/shared",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4366,7 +4375,7 @@
|
|||
},
|
||||
{
|
||||
"name": "wasm-streams",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"authors": "Mattias Buelens <mattias@buelens.com>",
|
||||
"repository": "https://github.com/MattiasBuelens/wasm-streams/",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4375,7 +4384,7 @@
|
|||
},
|
||||
{
|
||||
"name": "web-sys",
|
||||
"version": "0.3.70",
|
||||
"version": "0.3.72",
|
||||
"authors": "The wasm-bindgen Developers",
|
||||
"repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4393,7 +4402,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webpki-roots",
|
||||
"version": "0.26.5",
|
||||
"version": "0.26.6",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/rustls/webpki-roots",
|
||||
"license": "MPL-2.0",
|
||||
|
@ -4762,7 +4771,7 @@
|
|||
},
|
||||
{
|
||||
"name": "winnow",
|
||||
"version": "0.6.18",
|
||||
"version": "0.6.20",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/winnow-rs/winnow",
|
||||
"license": "MIT",
|
||||
|
@ -4771,7 +4780,7 @@
|
|||
},
|
||||
{
|
||||
"name": "wiremock",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"authors": "Luca Palmieri <rust@lpalmieri.com>",
|
||||
"repository": "https://github.com/LukeMathWalker/wiremock-rs",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -4780,7 +4789,7 @@
|
|||
},
|
||||
{
|
||||
"name": "xml-rs",
|
||||
"version": "0.8.21",
|
||||
"version": "0.8.22",
|
||||
"authors": "Vladimir Matveev <vmatveev@citrine.cc>",
|
||||
"repository": "https://github.com/kornelski/xml-rs",
|
||||
"license": "MIT",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM rust:1.80.1 AS builder
|
||||
FROM rust:1.82.0 AS builder
|
||||
|
||||
ARG ANKI_VERSION
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit aa374dce927cb12ac6623f1bf3f5c5c14dddf95c
|
||||
Subproject commit e1545f7f0ddeb617eeb1ca86e82862e552843578
|
|
@ -48,9 +48,9 @@ actions-update-card = Update Card
|
|||
actions-update-deck = Update Deck
|
||||
actions-forget-card = Reset Card
|
||||
actions-build-filtered-deck = Build Deck
|
||||
actions-add-notetype = Add Notetype
|
||||
actions-remove-notetype = Remove Notetype
|
||||
actions-update-notetype = Update Notetype
|
||||
actions-add-notetype = Add Note Type
|
||||
actions-remove-notetype = Remove Note Type
|
||||
actions-update-notetype = Update Note Type
|
||||
actions-update-config = Update Config
|
||||
actions-card-info = Card Info
|
||||
actions-previous-card-info = Previous Card Info
|
||||
|
|
|
@ -7,6 +7,6 @@ adding-history = History
|
|||
adding-note-deleted = (Note deleted)
|
||||
adding-shortcut = Shortcut: { $val }
|
||||
adding-the-first-field-is-empty = The first field is empty.
|
||||
adding-you-have-a-cloze-deletion-note = You have a cloze notetype but have not made any cloze deletions. Proceed?
|
||||
adding-cloze-outside-cloze-notetype = Cloze deletion can only be used on cloze notetypes.
|
||||
adding-you-have-a-cloze-deletion-note = You have a cloze note type but have not made any cloze deletions. Proceed?
|
||||
adding-cloze-outside-cloze-notetype = Cloze deletion can only be used on cloze note types.
|
||||
adding-cloze-outside-cloze-field = Cloze deletion can only be used in fields which use the 'cloze:' filter. This is typically the first field.
|
||||
|
|
|
@ -29,7 +29,7 @@ browsing-change-deck = Change Deck
|
|||
browsing-change-deck2 = Change Deck...
|
||||
browsing-change-note-type = Change Note Type
|
||||
browsing-change-note-type2 = Change Note Type...
|
||||
browsing-change-notetype = Change Notetype
|
||||
browsing-change-notetype = Change Note Type
|
||||
browsing-clear-unused-tags = Clear Unused Tags
|
||||
browsing-confirm-saved-search-overwrite = A saved search with the name { $name } already exists. Do you want to overwrite it?
|
||||
browsing-created = Created
|
||||
|
@ -142,7 +142,7 @@ browsing-tooltip-card-modified = The last time changes were made to a card, incl
|
|||
browsing-tooltip-note-modified = The last time changes were made to a note, usually field content or tag edits
|
||||
browsing-tooltip-card = The name of a card's card template
|
||||
browsing-tooltip-cards = The number of cards a note has
|
||||
browsing-tooltip-notetype = The name of a note's notetype
|
||||
browsing-tooltip-notetype = The name of a note's note type
|
||||
browsing-tooltip-question = The front side of a card, customisable in the card template editor
|
||||
browsing-tooltip-answer = The back side of a card, customisable in the card template editor
|
||||
browsing-studied-today = Studied
|
||||
|
|
|
@ -23,6 +23,7 @@ card-stats-review-log-type-review = Review
|
|||
card-stats-review-log-type-relearn = Relearn
|
||||
card-stats-review-log-type-filtered = Filtered
|
||||
card-stats-review-log-type-manual = Manual
|
||||
card-stats-review-log-type-rescheduled = Rescheduled
|
||||
card-stats-review-log-elapsed-time = Elapsed Time
|
||||
card-stats-no-card = (No card to display.)
|
||||
card-stats-custom-data = Custom Data
|
||||
|
@ -34,6 +35,8 @@ card-stats-fsrs-forgetting-curve-first-week = First Week
|
|||
card-stats-fsrs-forgetting-curve-first-month = First Month
|
||||
card-stats-fsrs-forgetting-curve-first-year = First Year
|
||||
card-stats-fsrs-forgetting-curve-all-time = All Time
|
||||
card-stats-fsrs-forgetting-curve-probability-of-recalling = Probability of Recall
|
||||
card-stats-fsrs-forgetting-curve-desired-retention = Desired Retention
|
||||
|
||||
## Window Titles
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ card-templates-night-mode = Night Mode
|
|||
# on a mobile device.
|
||||
card-templates-add-mobile-class = Add Mobile Class
|
||||
card-templates-preview-settings = Options
|
||||
card-templates-invalid-template-number = Card template { $number } in notetype '{ $notetype }' has a problem.
|
||||
card-templates-invalid-template-number = Card template { $number } in note type '{ $notetype }' has a problem.
|
||||
card-templates-identical-front = The front side is identical to card template { $number }.
|
||||
card-templates-no-front-field = Expected to find a field replacement on the front of the card template.
|
||||
card-templates-missing-cloze = Expected to find '{ "{{" }cloze:Text{ "}}" }' or similar on the front and back of the card template.
|
||||
card-templates-extraneous-cloze = 'cloze:' can only be used on cloze notetypes.
|
||||
card-templates-extraneous-cloze = 'cloze:' can only be used on cloze note types.
|
||||
card-templates-see-preview = See the preview for more information.
|
||||
card-templates-field-not-found = Field '{ $field }' not found.
|
||||
card-templates-changes-saved = Changes saved.
|
||||
|
@ -59,7 +59,7 @@ card-templates-this-will-create-card-proceed =
|
|||
}
|
||||
card-templates-type-boxes-warning = Only one typing box per card template is supported.
|
||||
card-templates-restore-to-default = Restore to Default
|
||||
card-templates-restore-to-default-confirmation = This will reset all fields and templates in this notetype to their default
|
||||
card-templates-restore-to-default-confirmation = This will reset all fields and templates in this note type to their default
|
||||
values, removing any extra fields/templates and their content, and any custom styling. Do you wish to proceed?
|
||||
card-templates-restored-to-default = Notetype has been restored to its original state.
|
||||
card-templates-restored-to-default = Note type has been restored to its original state.
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ change-notetype-will-discard-cards = Will remove the following cards:
|
|||
change-notetype-fields = Fields
|
||||
change-notetype-templates = Templates
|
||||
change-notetype-to-from-cloze =
|
||||
When changing to or from a Cloze notetype, card numbers remain unchanged.
|
||||
When changing to or from a Cloze note type, card numbers remain unchanged.
|
||||
|
||||
If changing to a regular notetype, and there are more cloze deletions
|
||||
If changing to a regular note type, and there are more cloze deletions
|
||||
than available card templates, any extra cards will be removed.
|
||||
|
|
|
@ -51,7 +51,7 @@ database-check-fixed-invalid-ids =
|
|||
*[other] Fixed { $count } objects with timestamps in the future.
|
||||
}
|
||||
# "db-check" is always in English
|
||||
database-check-notetypes-recovered = One or more notetypes were missing. The notes that used them have been given new notetypes starting with "db-check", but field names and card design have been lost, so you may be better off restoring from an automatic backup.
|
||||
database-check-notetypes-recovered = One or more note types were missing. The notes that used them have been given new note types starting with "db-check", but field names and card design have been lost, so you may be better off restoring from an automatic backup.
|
||||
|
||||
## Progress info
|
||||
|
||||
|
|
|
@ -152,7 +152,7 @@ deck-config-new-gather-priority-tooltip-2 =
|
|||
|
||||
`Random cards`: Gathers cards in a random order.
|
||||
deck-config-new-gather-priority-deck = Deck
|
||||
deck-config-new-gather-priority-deck-then-random-notes = Deck then random notes
|
||||
deck-config-new-gather-priority-deck-then-random-notes = Deck, then random notes
|
||||
deck-config-new-gather-priority-position-lowest-first = Ascending position
|
||||
deck-config-new-gather-priority-position-highest-first = Descending position
|
||||
deck-config-new-gather-priority-random-notes = Random notes
|
||||
|
@ -207,9 +207,11 @@ deck-config-sort-order-ascending-intervals = Ascending intervals
|
|||
deck-config-sort-order-descending-intervals = Descending intervals
|
||||
deck-config-sort-order-ascending-ease = Ascending ease
|
||||
deck-config-sort-order-descending-ease = Descending ease
|
||||
deck-config-sort-order-ascending-difficulty = Ascending difficulty
|
||||
deck-config-sort-order-descending-difficulty = Descending difficulty
|
||||
deck-config-sort-order-relative-overdueness = Relative overdueness
|
||||
deck-config-sort-order-ascending-difficulty = Easy cards first
|
||||
deck-config-sort-order-descending-difficulty = Difficult cards first
|
||||
deck-config-sort-order-retrievability-ascending = Ascending retrievability
|
||||
deck-config-sort-order-retrievability-descending = Descending retrievability
|
||||
|
||||
deck-config-display-order-will-use-current-deck =
|
||||
Anki will use the display order from the deck you
|
||||
select to study, and not any subdecks it may have.
|
||||
|
@ -290,6 +292,7 @@ deck-config-easy-days-sunday = Sunday
|
|||
deck-config-easy-days-normal = Normal
|
||||
deck-config-easy-days-reduced = Reduced
|
||||
deck-config-easy-days-minimum = Minimum
|
||||
deck-config-easy-days-no-normal-days = At least one day should be set to '{ deck-config-easy-days-normal }'.
|
||||
|
||||
## Adding/renaming
|
||||
|
||||
|
@ -312,7 +315,7 @@ deck-config-confirm-remove-name = Remove { $name }?
|
|||
deck-config-save-button = Save
|
||||
deck-config-save-to-all-subdecks = Save to All Subdecks
|
||||
deck-config-save-and-optimize = Optimize All Presets
|
||||
deck-config-revert-button-tooltip = Restore this setting to its default value.
|
||||
deck-config-revert-button-tooltip = Restore this setting to its default value?
|
||||
|
||||
## These strings are shown via the Description button at the bottom of the
|
||||
## overview screen.
|
||||
|
@ -370,9 +373,6 @@ deck-config-historical-retention = Historical retention
|
|||
deck-config-smaller-is-better = Smaller numbers indicate a better fit to your review history.
|
||||
deck-config-steps-too-large-for-fsrs = When FSRS is enabled, steps of 1 day or more are not recommended.
|
||||
deck-config-get-params = Get Params
|
||||
deck-config-fsrs-on-all-clients =
|
||||
Please ensure all of your Anki clients are Anki(Mobile) 23.10+ or AnkiDroid 2.17+. FSRS will
|
||||
not work correctly if one of your clients is older.
|
||||
deck-config-predicted-minimum-recommended-retention = Minimum recommended retention: { $num }
|
||||
deck-config-complete = { $num }% complete.
|
||||
deck-config-iterations = Iteration: { $count }...
|
||||
|
@ -435,8 +435,8 @@ deck-config-compute-optimal-weights-tooltip2 =
|
|||
deck-config-compute-optimal-retention-tooltip4 =
|
||||
This tool will attempt to find the desired retention value
|
||||
that will lead to the most material learnt, in the least amount of time. The calculated number can serve as a reference
|
||||
when deciding what to set your desired retention to. You may wish to choose a higher desired retention, if you’re
|
||||
willing to trade more study time for a greater retention rate. Setting your desired retention lower than the minimum
|
||||
when deciding what to set your desired retention to. You may wish to choose a higher desired retention if you’re
|
||||
willing to invest more study time to achieve it. Setting your desired retention lower than the minimum
|
||||
is not recommended, as it will lead to a higher workload, because of the high forgetting rate.
|
||||
deck-config-please-save-your-changes-first = Please save your changes first.
|
||||
deck-config-a-100-day-interval =
|
||||
|
@ -452,6 +452,7 @@ deck-config-percent-of-reviews =
|
|||
deck-config-optimizing-preset = Optimizing preset { $current_count }/{ $total_count }...
|
||||
deck-config-fsrs-must-be-enabled = FSRS must be enabled first.
|
||||
deck-config-fsrs-params-optimal = The FSRS parameters currently appear to be optimal.
|
||||
deck-config-fsrs-params-no-reviews = No reviews found. Please check that this preset is assigned to all decks you want to optimize (including subdecks) and try again.
|
||||
|
||||
deck-config-wait-for-audio = Wait for audio
|
||||
deck-config-show-reminder = Show Reminder
|
||||
|
@ -523,3 +524,6 @@ deck-config-compute-optimal-retention-tooltip3 =
|
|||
lead to a higher workload, because of the high forgetting rate.
|
||||
deck-config-seconds-to-show-question-tooltip-2 = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable.
|
||||
deck-config-invalid-weights = Parameters must be either left blank to use the defaults, or must be 17 comma-separated numbers.
|
||||
deck-config-fsrs-on-all-clients =
|
||||
Please ensure all of your Anki clients are Anki(Mobile) 23.10+ or AnkiDroid 2.17+. FSRS will
|
||||
not work correctly if one of your clients is older.
|
||||
|
|
|
@ -24,7 +24,6 @@ decks-order-added = Order added
|
|||
decks-order-due = Order due
|
||||
decks-please-select-something = Please select something.
|
||||
decks-random = Random
|
||||
decks-relative-overdueness = Relative overdueness
|
||||
decks-repeat-failed-cards-after = Delay Repeat failed cards after
|
||||
# e.g. "Delay for Again", "Delay for Hard", "Delay for Good"
|
||||
decks-delay-for-button = Delay for { $button }
|
||||
|
@ -37,3 +36,8 @@ decks-learn-header = Learn
|
|||
# The count of cards waiting to be reviewed
|
||||
decks-review-header = Due
|
||||
decks-zero-minutes-hint = (0 = return card to original deck)
|
||||
|
||||
## These strings are no longer used - you do not need to translate them if they
|
||||
## are not already translated.
|
||||
|
||||
decks-relative-overdueness = Relative overdueness
|
||||
|
|
|
@ -5,7 +5,7 @@ errors-100-tags-max =
|
|||
A maximum of 100 tags can be selected. Listing the
|
||||
tags you want instead of the ones you don't want is usually simpler, and there
|
||||
is no need to select child tags if you have selected a parent tag.
|
||||
errors-multiple-notetypes-selected = Please select notes from only one notetype.
|
||||
errors-multiple-notetypes-selected = Please select notes from only one note type.
|
||||
errors-please-check-database = Please use the Check Database action, then try again.
|
||||
errors-please-check-media = Please use the Check Media action, then try again.
|
||||
errors-collection-too-new = This collection requires a newer version of Anki to open.
|
||||
|
|
|
@ -40,5 +40,5 @@ exporting-processed-media-files =
|
|||
*[other] Processed { $count } media files...
|
||||
}
|
||||
exporting-include-deck = Include deck name
|
||||
exporting-include-notetype = Include notetype name
|
||||
exporting-include-notetype = Include note type name
|
||||
exporting-include-guid = Include unique identifier
|
||||
|
|
|
@ -8,7 +8,7 @@ importing-anki2-files-are-not-directly-importable = .anki2 files are not directl
|
|||
importing-appeared-twice-in-file = Appeared twice in file: { $val }
|
||||
importing-by-default-anki-will-detect-the = By default, Anki will detect the character between fields, such as a tab, comma, and so on. If Anki is detecting the character incorrectly, you can enter it here. Use \t to represent tab.
|
||||
importing-cannot-merge-notetypes-of-different-kinds =
|
||||
Cloze notetypes cannot be merged with regular notetypes.
|
||||
Cloze note types cannot be merged with regular note types.
|
||||
You may still import the file with '{ importing-merge-notetypes }' disabled.
|
||||
importing-change = Change
|
||||
importing-colon = Colon
|
||||
|
@ -33,13 +33,13 @@ importing-map-to = Map to { $val }
|
|||
importing-map-to-tags = Map to Tags
|
||||
importing-mapped-to = mapped to <b>{ $val }</b>
|
||||
importing-mapped-to-tags = mapped to <b>Tags</b>
|
||||
# the action of combining two existing notetypes to create a new one
|
||||
importing-merge-notetypes = Merge notetypes
|
||||
# the action of combining two existing note types to create a new one
|
||||
importing-merge-notetypes = Merge note types
|
||||
importing-merge-notetypes-help =
|
||||
If checked, and you or the deck author altered the schema of a notetype, Anki will
|
||||
If checked, and you or the deck author altered the schema of a note type, Anki will
|
||||
merge the two versions instead of keeping both.
|
||||
|
||||
Altering a notetype's schema means adding, removing, or reordering fields or templates,
|
||||
Altering a note type's schema means adding, removing, or reordering fields or templates,
|
||||
or changing the sort field.
|
||||
As a counterexample, changing the front side of an existing template does *not* constitute
|
||||
a schema change.
|
||||
|
@ -50,7 +50,7 @@ importing-multicharacter-separators-are-not-supported-please = Multi-character s
|
|||
importing-notes-added-from-file = Notes added from file: { $val }
|
||||
importing-notes-found-in-file = Notes found in file: { $val }
|
||||
importing-notes-skipped-as-theyre-already-in = Notes skipped, as up-to-date copies are already in your collection: { $val }
|
||||
importing-notes-skipped-update-due-to-notetype = Notes not updated, as notetype has been modified since you first imported the notes: { $val }
|
||||
importing-notes-skipped-update-due-to-notetype = Notes not updated, as note type has been modified since you first imported the notes: { $val }
|
||||
importing-notes-updated-as-file-had-newer = Notes updated, as file had newer version: { $val }
|
||||
importing-include-reviews = Include reviews
|
||||
importing-also-import-progress = Import any learning progress
|
||||
|
@ -90,10 +90,10 @@ importing-update-notes = Update notes
|
|||
importing-update-notes-help =
|
||||
When to update an existing note in your collection. By default, this is only done
|
||||
if the matching imported note was more recently modified.
|
||||
importing-update-notetypes = Update notetypes
|
||||
importing-update-notetypes = Update note types
|
||||
importing-update-notetypes-help =
|
||||
When to update an existing notetype in your collection. By default, this is only done
|
||||
if the matching imported notetype was more recently modified. Changes to template text
|
||||
When to update an existing note type in your collection. By default, this is only done
|
||||
if the matching imported note type was more recently modified. Changes to template text
|
||||
and styling can always be imported, but for schema changes (e.g. the number or order of
|
||||
fields has changed), the '{ importing-merge-notetypes }' option will also need to be enabled.
|
||||
importing-note-added =
|
||||
|
@ -148,7 +148,7 @@ importing-file = File
|
|||
# "Match scope: notetype / notetype and deck". Controls how duplicates are matched.
|
||||
importing-match-scope = Match scope
|
||||
# Used with the 'match scope' option
|
||||
importing-notetype-and-deck = Notetype and deck
|
||||
importing-notetype-and-deck = Note type and deck
|
||||
importing-cards-added =
|
||||
{ $count ->
|
||||
[one] { $count } card added.
|
||||
|
@ -182,8 +182,8 @@ importing-conflicting-notes-skipped =
|
|||
}
|
||||
importing-conflicting-notes-skipped2 =
|
||||
{ $count ->
|
||||
[one] { $count } note was not imported, because its notetype has changed, and '{ importing-merge-notetypes }' was not enabled.
|
||||
*[other] { $count } notes were not imported, because their notetype has changed, and '{ importing-merge-notetypes }' was not enabled.
|
||||
[one] { $count } note was not imported, because its note type has changed, and '{ importing-merge-notetypes }' was not enabled.
|
||||
*[other] { $count } notes were not imported, because their note type has changed, and '{ importing-merge-notetypes }' was not enabled.
|
||||
}
|
||||
importing-import-log = Import Log
|
||||
importing-no-notes-in-file = No notes found in file.
|
||||
|
@ -198,8 +198,8 @@ importing-status = Status
|
|||
importing-duplicate-note-added = Duplicate note added
|
||||
importing-added-new-note = New note added
|
||||
importing-existing-note-skipped = Note skipped, as an up-to-date copy is already in your collection
|
||||
importing-note-skipped-update-due-to-notetype = Note not updated, as notetype has been modified since you first imported the note
|
||||
importing-note-skipped-update-due-to-notetype2 = Note not updated, as notetype has been modified since you first imported the note, and '{ importing-merge-notetypes }' was not enabled
|
||||
importing-note-skipped-update-due-to-notetype = Note not updated, as note type has been modified since you first imported the note
|
||||
importing-note-skipped-update-due-to-notetype2 = Note not updated, as note type has been modified since you first imported the note, and '{ importing-merge-notetypes }' was not enabled
|
||||
importing-note-updated-as-file-had-newer = Note updated, as file had newer version
|
||||
importing-note-skipped-due-to-missing-notetype = Note skipped, as its notetype was missing
|
||||
importing-note-skipped-due-to-missing-deck = Note skipped, as its deck was missing
|
||||
|
@ -216,10 +216,10 @@ importing-allow-html-in-fields-help =
|
|||
'<br>', it will appear as a line break on your card. On the other hand, with this
|
||||
option disabled, the literal characters '<br>' will be rendered.
|
||||
importing-notetype-help =
|
||||
Newly-imported notes will have this notetype, and only existing notes with this
|
||||
notetype will be updated.
|
||||
Newly-imported notes will have this note type, and only existing notes with this
|
||||
note type will be updated.
|
||||
|
||||
You can choose which fields in the file correspond to which notetype fields with the
|
||||
You can choose which fields in the file correspond to which note type fields with the
|
||||
mapping tool.
|
||||
importing-deck-help = Imported cards will be placed in this deck.
|
||||
importing-existing-notes-help =
|
||||
|
@ -229,7 +229,7 @@ importing-existing-notes-help =
|
|||
- `{ importing-preserve }`: Do nothing.
|
||||
- `{ importing-duplicate }`: Create a new note.
|
||||
importing-match-scope-help =
|
||||
Only existing notes with the same notetype will be checked for duplicates. This can
|
||||
Only existing notes with the same note type will be checked for duplicates. This can
|
||||
additionally be restricted to notes with cards in the same deck.
|
||||
importing-tag-all-notes-help =
|
||||
These tags will be added to both newly-imported and updated notes.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
notetypes-notetype = Notetype
|
||||
notetypes-notetype = Note Type
|
||||
|
||||
## Default field names in newly created note types
|
||||
|
||||
|
|
|
@ -48,6 +48,11 @@ statistics-cards =
|
|||
[one] { $cards } card
|
||||
*[other] { $cards } cards
|
||||
}
|
||||
statistics-notes =
|
||||
{ $notes ->
|
||||
[one] { $notes } note
|
||||
*[other] { $notes } notes
|
||||
}
|
||||
# a count of how many cards have been answered, eg "Total: 34 reviews"
|
||||
statistics-reviews =
|
||||
{ $reviews ->
|
||||
|
@ -220,6 +225,7 @@ statistics-average-answer-time-label = Average answer time
|
|||
statistics-average = Average
|
||||
statistics-average-interval = Average interval
|
||||
statistics-due-tomorrow = Due tomorrow
|
||||
statistics-daily-load = Daily load
|
||||
# eg 5 of 15 (33.3%)
|
||||
statistics-amount-of-total-with-percentage = { $amount } of { $total } ({ $percent }%)
|
||||
statistics-average-over-period = Average over period
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 2c629a5543be76a0136bffb428868d946b4c0e3a
|
||||
Subproject commit e0f9724f75f6248f4e74558b25c3182d4f348bce
|
14
package.json
14
package.json
|
@ -19,8 +19,8 @@
|
|||
"@poppanator/sveltekit-svg": "^5.0.0-svelte5.5",
|
||||
"@sqltools/formatter": "^1.2.2",
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.4.1",
|
||||
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
|
||||
"@sveltejs/kit": "^2.8.3",
|
||||
"@sveltejs/vite-plugin-svelte": "4.0.0",
|
||||
"@types/bootstrap": "^5.0.12",
|
||||
"@types/codemirror": "^5.60.0",
|
||||
"@types/d3": "^7.0.0",
|
||||
|
@ -47,7 +47,8 @@
|
|||
"license-checker-rseidelsohn": "=4.3.0",
|
||||
"prettier": "^2.4.1",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "5.0.0-next.179",
|
||||
"sass": "<1.77",
|
||||
"svelte": "5.0.0",
|
||||
"svelte-check": "^3.4.4",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"svelte-preprocess-esbuild": "^3.0.1",
|
||||
|
@ -55,9 +56,8 @@
|
|||
"tslib": "^2.0.3",
|
||||
"tsx": "^3.12.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "=5.4.7",
|
||||
"vitest": "^1.2.1",
|
||||
"sass": "<1.77"
|
||||
"vite": "^5.4.10",
|
||||
"vitest": "^2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^1.2.1",
|
||||
|
@ -81,7 +81,7 @@
|
|||
},
|
||||
"resolutions": {
|
||||
"canvas": "npm:empty-npm-package",
|
||||
"vite": "=5.4.7"
|
||||
"cookie": "0.7.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults",
|
||||
|
|
|
@ -30,6 +30,7 @@ message BackendError {
|
|||
DB_ERROR = 5;
|
||||
NETWORK_ERROR = 6;
|
||||
SYNC_AUTH_ERROR = 7;
|
||||
SYNC_SERVER_MESSAGE = 23;
|
||||
SYNC_OTHER_ERROR = 8;
|
||||
JSON_ERROR = 9;
|
||||
PROTO_ERROR = 10;
|
||||
|
|
|
@ -127,21 +127,21 @@ message Progress {
|
|||
DatabaseCheck database_check = 6;
|
||||
string importing = 7;
|
||||
string exporting = 8;
|
||||
ComputeWeightsProgress compute_weights = 9;
|
||||
ComputeParamsProgress compute_params = 9;
|
||||
ComputeRetentionProgress compute_retention = 10;
|
||||
ComputeMemoryProgress compute_memory = 11;
|
||||
}
|
||||
}
|
||||
|
||||
message ComputeWeightsProgress {
|
||||
message ComputeParamsProgress {
|
||||
// Current iteration
|
||||
uint32 current = 1;
|
||||
// Total iterations
|
||||
uint32 total = 2;
|
||||
uint32 reviews = 3;
|
||||
// Only used in 'compute all weights' case
|
||||
// Only used in 'compute all params' case
|
||||
uint32 current_preset = 4;
|
||||
// Only used in 'compute all weights' case
|
||||
// Only used in 'compute all params' case
|
||||
uint32 total_presets = 5;
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ message ConfigKey {
|
|||
SHIFT_POSITION_OF_EXISTING_CARDS = 24;
|
||||
RENDER_LATEX = 25;
|
||||
LOAD_BALANCER_ENABLED = 26;
|
||||
FSRS_SHORT_TERM_WITH_STEPS_ENABLED = 27;
|
||||
}
|
||||
enum String {
|
||||
SET_DUE_BROWSER = 0;
|
||||
|
@ -117,6 +118,7 @@ message Preferences {
|
|||
bool show_intervals_on_buttons = 4;
|
||||
uint32 time_limit_secs = 5;
|
||||
bool load_balancer_enabled = 6;
|
||||
bool fsrs_short_term_with_steps_enabled = 7;
|
||||
}
|
||||
message Editing {
|
||||
bool adding_defaults_to_current_deck = 1;
|
||||
|
|
|
@ -79,7 +79,8 @@ message DeckConfig {
|
|||
REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 4;
|
||||
REVIEW_CARD_ORDER_EASE_ASCENDING = 5;
|
||||
REVIEW_CARD_ORDER_EASE_DESCENDING = 6;
|
||||
REVIEW_CARD_ORDER_RELATIVE_OVERDUENESS = 7;
|
||||
REVIEW_CARD_ORDER_RETRIEVABILITY_ASCENDING = 7;
|
||||
REVIEW_CARD_ORDER_RETRIEVABILITY_DESCENDING = 11;
|
||||
REVIEW_CARD_ORDER_RANDOM = 8;
|
||||
REVIEW_CARD_ORDER_ADDED = 9;
|
||||
REVIEW_CARD_ORDER_REVERSE_ADDED = 10;
|
||||
|
@ -107,9 +108,11 @@ message DeckConfig {
|
|||
repeated float learn_steps = 1;
|
||||
repeated float relearn_steps = 2;
|
||||
|
||||
repeated float fsrs_weights = 3;
|
||||
repeated float fsrs_params_4 = 3;
|
||||
repeated float fsrs_params_5 = 5;
|
||||
|
||||
reserved 5 to 8;
|
||||
// consider saving remaining ones for fsrs param changes
|
||||
reserved 6 to 8;
|
||||
|
||||
uint32 new_per_day = 9;
|
||||
uint32 reviews_per_day = 10;
|
||||
|
@ -163,7 +166,7 @@ message DeckConfig {
|
|||
// used for fsrs_reschedule in the past
|
||||
reserved 39;
|
||||
float historical_retention = 40;
|
||||
string weight_search = 45;
|
||||
string param_search = 45;
|
||||
|
||||
bytes other = 255;
|
||||
}
|
||||
|
@ -213,7 +216,7 @@ message DeckConfigsForUpdate {
|
|||
enum UpdateDeckConfigsMode {
|
||||
UPDATE_DECK_CONFIGS_MODE_NORMAL = 0;
|
||||
UPDATE_DECK_CONFIGS_MODE_APPLY_TO_CHILDREN = 1;
|
||||
UPDATE_DECK_CONFIGS_MODE_COMPUTE_ALL_WEIGHTS = 2;
|
||||
UPDATE_DECK_CONFIGS_MODE_COMPUTE_ALL_PARAMS = 2;
|
||||
}
|
||||
|
||||
message UpdateDeckConfigsRequest {
|
||||
|
|
|
@ -97,7 +97,8 @@ message Deck {
|
|||
ADDED = 5;
|
||||
DUE = 6;
|
||||
REVERSE_ADDED = 7;
|
||||
DUE_PRIORITY = 8;
|
||||
RETRIEVABILITY_ASCENDING = 8;
|
||||
RETRIEVABILITY_DESCENDING = 9;
|
||||
}
|
||||
|
||||
string search = 1;
|
||||
|
|
|
@ -40,9 +40,8 @@ message HelpPageLinkRequest {
|
|||
CARD_TYPE_DUPLICATE = 18;
|
||||
CARD_TYPE_NO_FRONT_FIELD = 19;
|
||||
CARD_TYPE_MISSING_CLOZE = 20;
|
||||
CARD_TYPE_EXTRANEOUS_CLOZE = 21;
|
||||
TROUBLESHOOTING = 21;
|
||||
CARD_TYPE_TEMPLATE_ERROR = 22;
|
||||
TROUBLESHOOTING = 23;
|
||||
}
|
||||
HelpPage page = 1;
|
||||
}
|
||||
|
|
|
@ -45,15 +45,15 @@ service SchedulerService {
|
|||
rpc CustomStudyDefaults(CustomStudyDefaultsRequest)
|
||||
returns (CustomStudyDefaultsResponse);
|
||||
rpc RepositionDefaults(generic.Empty) returns (RepositionDefaultsResponse);
|
||||
rpc ComputeFsrsWeights(ComputeFsrsWeightsRequest)
|
||||
returns (ComputeFsrsWeightsResponse);
|
||||
rpc ComputeFsrsParams(ComputeFsrsParamsRequest)
|
||||
returns (ComputeFsrsParamsResponse);
|
||||
rpc GetOptimalRetentionParameters(GetOptimalRetentionParametersRequest)
|
||||
returns (GetOptimalRetentionParametersResponse);
|
||||
rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest)
|
||||
returns (ComputeOptimalRetentionResponse);
|
||||
rpc SimulateFsrsReview(SimulateFsrsReviewRequest)
|
||||
returns (SimulateFsrsReviewResponse);
|
||||
rpc EvaluateWeights(EvaluateWeightsRequest) returns (EvaluateWeightsResponse);
|
||||
rpc EvaluateParams(EvaluateParamsRequest) returns (EvaluateParamsResponse);
|
||||
rpc ComputeMemoryState(cards.CardId) returns (ComputeMemoryStateResponse);
|
||||
// The number of days the calculated interval was fuzzed by on the previous
|
||||
// review (if any). Utilized by the FSRS add-on.
|
||||
|
@ -63,10 +63,12 @@ service SchedulerService {
|
|||
// Implicitly includes any of the above methods that are not listed in the
|
||||
// backend service.
|
||||
service BackendSchedulerService {
|
||||
rpc ComputeFsrsWeightsFromItems(ComputeFsrsWeightsFromItemsRequest)
|
||||
returns (ComputeFsrsWeightsResponse);
|
||||
rpc ComputeFsrsParamsFromItems(ComputeFsrsParamsFromItemsRequest)
|
||||
returns (ComputeFsrsParamsResponse);
|
||||
// Generates parameters used for FSRS's scheduler benchmarks.
|
||||
rpc FsrsBenchmark(FsrsBenchmarkRequest) returns (FsrsBenchmarkResponse);
|
||||
// Used for exporting revlogs for algorithm research.
|
||||
rpc ExportDataset(ExportDatasetRequest) returns (generic.Empty);
|
||||
}
|
||||
|
||||
message SchedulingState {
|
||||
|
@ -339,19 +341,19 @@ message RepositionDefaultsResponse {
|
|||
bool shift = 2;
|
||||
}
|
||||
|
||||
message ComputeFsrsWeightsRequest {
|
||||
message ComputeFsrsParamsRequest {
|
||||
/// The search used to gather cards for training
|
||||
string search = 1;
|
||||
repeated float current_weights = 2;
|
||||
repeated float current_params = 2;
|
||||
int64 ignore_revlogs_before_ms = 3;
|
||||
}
|
||||
|
||||
message ComputeFsrsWeightsResponse {
|
||||
repeated float weights = 1;
|
||||
message ComputeFsrsParamsResponse {
|
||||
repeated float params = 1;
|
||||
uint32 fsrs_items = 2;
|
||||
}
|
||||
|
||||
message ComputeFsrsWeightsFromItemsRequest {
|
||||
message ComputeFsrsParamsFromItemsRequest {
|
||||
repeated FsrsItem items = 1;
|
||||
}
|
||||
|
||||
|
@ -360,7 +362,12 @@ message FsrsBenchmarkRequest {
|
|||
}
|
||||
|
||||
message FsrsBenchmarkResponse {
|
||||
repeated float weights = 1;
|
||||
repeated float params = 1;
|
||||
}
|
||||
|
||||
message ExportDatasetRequest {
|
||||
uint32 min_entries = 1;
|
||||
string target_path = 2;
|
||||
}
|
||||
|
||||
message FsrsItem {
|
||||
|
@ -373,7 +380,7 @@ message FsrsReview {
|
|||
}
|
||||
|
||||
message SimulateFsrsReviewRequest {
|
||||
repeated float weights = 1;
|
||||
repeated float params = 1;
|
||||
float desired_retention = 2;
|
||||
uint32 deck_size = 3;
|
||||
uint32 days_to_simulate = 4;
|
||||
|
@ -391,7 +398,7 @@ message SimulateFsrsReviewResponse {
|
|||
}
|
||||
|
||||
message ComputeOptimalRetentionRequest {
|
||||
repeated float weights = 1;
|
||||
repeated float params = 1;
|
||||
uint32 days_to_simulate = 2;
|
||||
uint32 max_interval = 3;
|
||||
string search = 4;
|
||||
|
@ -424,13 +431,13 @@ message GetOptimalRetentionParametersResponse {
|
|||
uint32 review_limit = 15;
|
||||
}
|
||||
|
||||
message EvaluateWeightsRequest {
|
||||
repeated float weights = 1;
|
||||
message EvaluateParamsRequest {
|
||||
repeated float params = 1;
|
||||
string search = 2;
|
||||
int64 ignore_revlogs_before_ms = 3;
|
||||
}
|
||||
|
||||
message EvaluateWeightsResponse {
|
||||
message EvaluateParamsResponse {
|
||||
float log_loss = 1;
|
||||
float rmse_bins = 2;
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ message CardStatsResponse {
|
|||
string custom_data = 20;
|
||||
string preset = 21;
|
||||
optional string original_deck = 22;
|
||||
optional float desired_retention = 23;
|
||||
}
|
||||
|
||||
message GraphsRequest {
|
||||
|
@ -85,11 +86,13 @@ message GraphsResponse {
|
|||
message Retrievability {
|
||||
map<uint32, uint32> retrievability = 1;
|
||||
float average = 2;
|
||||
float sum = 3;
|
||||
float sum_by_card = 3;
|
||||
float sum_by_note = 4;
|
||||
}
|
||||
message FutureDue {
|
||||
map<int32, uint32> future_due = 1;
|
||||
bool have_backlog = 2;
|
||||
uint32 daily_load = 3;
|
||||
}
|
||||
message Today {
|
||||
uint32 answer_count = 1;
|
||||
|
@ -205,6 +208,7 @@ message RevlogEntry {
|
|||
RELEARNING = 2;
|
||||
FILTERED = 3;
|
||||
MANUAL = 4;
|
||||
RESCHEDULED = 5;
|
||||
}
|
||||
int64 id = 1;
|
||||
int64 cid = 2;
|
||||
|
@ -217,7 +221,21 @@ message RevlogEntry {
|
|||
ReviewKind review_kind = 9;
|
||||
}
|
||||
|
||||
message RevlogEntries {
|
||||
repeated RevlogEntry entries = 1;
|
||||
int64 next_day_at = 2;
|
||||
message CardEntry {
|
||||
int64 id = 1;
|
||||
int64 note_id = 2;
|
||||
int64 deck_id = 3;
|
||||
}
|
||||
|
||||
message DeckEntry {
|
||||
int64 id = 1;
|
||||
int64 parent_id = 2;
|
||||
int64 preset_id = 3;
|
||||
}
|
||||
|
||||
message Dataset {
|
||||
repeated RevlogEntry revlogs = 1;
|
||||
repeated CardEntry cards = 2;
|
||||
repeated DeckEntry decks = 3;
|
||||
int64 next_day_at = 4;
|
||||
}
|
||||
|
|
|
@ -149,8 +149,8 @@ class RustBackend(RustBackendGenerated):
|
|||
)
|
||||
return self.format_timespan(seconds=seconds, context=context)
|
||||
|
||||
def compute_weights_from_items(self, items: Iterable[FsrsItem]) -> Sequence[float]:
|
||||
return self.compute_fsrs_weights_from_items(items).weights
|
||||
def compute_params_from_items(self, items: Iterable[FsrsItem]) -> Sequence[float]:
|
||||
return self.compute_fsrs_params_from_items(items).params
|
||||
|
||||
def benchmark(self, train_set: Iterable[FsrsItem]) -> Sequence[float]:
|
||||
return self.fsrs_benchmark(train_set=train_set)
|
||||
|
|
|
@ -420,6 +420,11 @@ class Collection(DeprecatedNamesMixin):
|
|||
def import_json_string(self, json: str) -> ImportLogWithChanges:
|
||||
return self._backend.import_json_string(json)
|
||||
|
||||
def export_dataset_for_research(
|
||||
self, target_path: str, min_entries: int = 0
|
||||
) -> None:
|
||||
self._backend.export_dataset(min_entries=min_entries, target_path=target_path)
|
||||
|
||||
# Image Occlusion
|
||||
##########################################################################
|
||||
|
||||
|
@ -987,6 +992,16 @@ class Collection(DeprecatedNamesMixin):
|
|||
fget=_get_enable_load_balancer, fset=_set_enable_load_balancer
|
||||
)
|
||||
|
||||
def _get_enable_fsrs_short_term_with_steps(self) -> bool:
|
||||
return self.get_config_bool(Config.Bool.FSRS_SHORT_TERM_WITH_STEPS_ENABLED)
|
||||
|
||||
def _set_enable_fsrs_short_term_with_steps(self, value: bool) -> None:
|
||||
self.set_config_bool(Config.Bool.FSRS_SHORT_TERM_WITH_STEPS_ENABLED, value)
|
||||
|
||||
fsrs_short_term_with_steps_enabled = property(
|
||||
fget=_get_enable_fsrs_short_term_with_steps,
|
||||
fset=_set_enable_fsrs_short_term_with_steps,
|
||||
)
|
||||
# Stats
|
||||
##########################################################################
|
||||
|
||||
|
@ -1113,7 +1128,7 @@ class Collection(DeprecatedNamesMixin):
|
|||
self._backend.abort_sync()
|
||||
|
||||
def full_upload_or_download(
|
||||
self, *, auth: SyncAuth, server_usn: int | None, upload: bool
|
||||
self, *, auth: SyncAuth | None, server_usn: int | None, upload: bool
|
||||
) -> None:
|
||||
self._backend.full_upload_or_download(
|
||||
sync_pb2.FullUploadOrDownloadRequest(
|
||||
|
|
|
@ -43,10 +43,6 @@ from anki.models import NotetypeDict
|
|||
from anki.sound import AVTag, SoundOrVideoTag, TTSTag
|
||||
from anki.utils import to_json_bytes
|
||||
|
||||
CARD_BLANK_HELP = (
|
||||
"https://anki.tenderapp.com/kb/card-appearance/the-front-of-this-card-is-blank"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateReplacement:
|
||||
|
|
|
@ -386,13 +386,13 @@ urllib3==2.2.2 \
|
|||
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
|
||||
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
|
||||
# via requests
|
||||
waitress==3.0.0 \
|
||||
--hash=sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1 \
|
||||
--hash=sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669
|
||||
waitress==3.0.1 \
|
||||
--hash=sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac \
|
||||
--hash=sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3
|
||||
# via -r requirements.aqt.in
|
||||
werkzeug==3.0.4 \
|
||||
--hash=sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c \
|
||||
--hash=sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306
|
||||
werkzeug==3.0.6 \
|
||||
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
|
||||
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
|
||||
# via flask
|
||||
wheel==0.44.0 \
|
||||
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
|
||||
|
|
|
@ -608,17 +608,17 @@ urllib3==2.2.2 \
|
|||
# via
|
||||
# requests
|
||||
# types-requests
|
||||
waitress==3.0.0 \
|
||||
--hash=sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1 \
|
||||
--hash=sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669
|
||||
waitress==3.0.1 \
|
||||
--hash=sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac \
|
||||
--hash=sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3
|
||||
# via -r requirements.aqt.in
|
||||
websocket-client==1.8.0 \
|
||||
--hash=sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526 \
|
||||
--hash=sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da
|
||||
# via pychromedevtools
|
||||
werkzeug==3.0.4 \
|
||||
--hash=sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c \
|
||||
--hash=sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306
|
||||
werkzeug==3.0.6 \
|
||||
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
|
||||
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
|
||||
# via flask
|
||||
wheel==0.44.0 \
|
||||
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
|
@ -525,6 +526,7 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None:
|
|||
print(f"Qt {category}: {msg} {context}")
|
||||
|
||||
qInstallMessageHandler(msgHandler)
|
||||
atexit.register(qInstallMessageHandler, None)
|
||||
|
||||
if driver == VideoDriver.OpenGL:
|
||||
# Leaving QT_OPENGL unset appears to sometimes produce different results
|
||||
|
|
|
@ -37,14 +37,21 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
|||
txt = supportText()
|
||||
if mw.addonManager.dirty:
|
||||
txt += "\n" + addon_debug_info()
|
||||
QApplication.clipboard().setText(txt)
|
||||
clipboard = QApplication.clipboard()
|
||||
assert clipboard is not None
|
||||
clipboard.setText(txt)
|
||||
tooltip(tr.about_copied_to_clipboard(), parent=dialog)
|
||||
|
||||
btn = QPushButton(tr.about_copy_debug_info())
|
||||
qconnect(btn.clicked, on_copy)
|
||||
abt.buttonBox.addButton(btn, QDialogButtonBox.ButtonRole.ActionRole)
|
||||
abt.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setFocus()
|
||||
|
||||
ok_button = abt.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
|
||||
assert ok_button is not None
|
||||
ok_button.setFocus()
|
||||
|
||||
btnLayout = abt.buttonBox.layout()
|
||||
assert btnLayout is not None
|
||||
btnLayout.setContentsMargins(12, 12, 12, 12)
|
||||
|
||||
# WebView cleanup
|
||||
|
@ -52,7 +59,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
|||
|
||||
def on_dialog_destroyed() -> None:
|
||||
abt.label.cleanup()
|
||||
abt.label = None
|
||||
abt.label = None # type: ignore
|
||||
|
||||
qconnect(dialog.destroyed, on_dialog_destroyed)
|
||||
|
||||
|
|
|
@ -152,10 +152,13 @@ class AddCards(QMainWindow):
|
|||
def on_deck_changed(self, deck_id: int) -> None:
|
||||
gui_hooks.add_cards_did_change_deck(deck_id)
|
||||
|
||||
def on_notetype_change(self, notetype_id: NotetypeId) -> None:
|
||||
def on_notetype_change(
|
||||
self, notetype_id: NotetypeId, update_deck: bool = True
|
||||
) -> None:
|
||||
# need to adjust current deck?
|
||||
if deck_id := self.col.default_deck_for_notetype(notetype_id):
|
||||
self.deck_chooser.selected_deck_id = deck_id
|
||||
if update_deck:
|
||||
if deck_id := self.col.default_deck_for_notetype(notetype_id):
|
||||
self.deck_chooser.selected_deck_id = deck_id
|
||||
|
||||
# only used for detecting changed sticky fields on close
|
||||
self._last_added_note = None
|
||||
|
@ -224,7 +227,8 @@ class AddCards(QMainWindow):
|
|||
self.col.defaults_for_adding(
|
||||
current_review_card=self.mw.reviewer.card
|
||||
).notetype_id
|
||||
)
|
||||
),
|
||||
update_deck=False,
|
||||
)
|
||||
|
||||
def _new_note(self) -> Note:
|
||||
|
|
|
@ -63,7 +63,7 @@ class ChangeNotetypeDialog(QDialog):
|
|||
|
||||
def reject(self) -> None:
|
||||
self.web.cleanup()
|
||||
self.web = None
|
||||
self.web = None # type: ignore
|
||||
saveGeom(self, self.TITLE)
|
||||
QDialog.reject(self)
|
||||
|
||||
|
|
|
@ -61,7 +61,9 @@ class CardLayout(QDialog):
|
|||
self.ord = ord
|
||||
self.col = self.mw.col.weakref()
|
||||
self.mm = self.mw.col.models
|
||||
self.model = note.note_type()
|
||||
note_type = note.note_type()
|
||||
assert note_type is not None
|
||||
self.model = note_type
|
||||
self.templates = self.model["tmpls"]
|
||||
self.fill_empty_action_toggled = fill_empty
|
||||
self.night_mode_is_enabled = theme_manager.night_mode
|
||||
|
@ -404,6 +406,7 @@ class CardLayout(QDialog):
|
|||
m = QMenu(self)
|
||||
|
||||
a = m.addAction(tr.card_templates_fill_empty())
|
||||
assert a is not None
|
||||
a.setCheckable(True)
|
||||
a.setChecked(self.fill_empty_action_toggled)
|
||||
qconnect(a.triggered, self.on_fill_empty_action_toggled)
|
||||
|
@ -411,11 +414,13 @@ class CardLayout(QDialog):
|
|||
a.setVisible(False)
|
||||
|
||||
a = m.addAction(tr.card_templates_night_mode())
|
||||
assert a is not None
|
||||
a.setCheckable(True)
|
||||
a.setChecked(self.night_mode_is_enabled)
|
||||
qconnect(a.triggered, self.on_night_mode_action_toggled)
|
||||
|
||||
a = m.addAction(tr.card_templates_add_mobile_class())
|
||||
assert a is not None
|
||||
a.setCheckable(True)
|
||||
a.setChecked(self.mobile_emulation_enabled)
|
||||
qconnect(a.toggled, self.on_mobile_class_action_toggled)
|
||||
|
@ -754,6 +759,7 @@ class CardLayout(QDialog):
|
|||
a = m.addAction(
|
||||
tr.actions_with_ellipsis(action=tr.card_templates_restore_to_default())
|
||||
)
|
||||
assert a is not None
|
||||
qconnect(
|
||||
a.triggered,
|
||||
lambda: self.on_restore_to_default(), # pylint: disable=unnecessary-lambda
|
||||
|
@ -761,15 +767,19 @@ class CardLayout(QDialog):
|
|||
|
||||
if not self._isCloze():
|
||||
a = m.addAction(tr.card_templates_add_card_type())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onAddCard)
|
||||
|
||||
a = m.addAction(tr.card_templates_remove_card_type())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onRemove)
|
||||
|
||||
a = m.addAction(tr.card_templates_rename_card_type())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onRename)
|
||||
|
||||
a = m.addAction(tr.card_templates_reposition_card_type())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onReorder)
|
||||
|
||||
m.addSeparator()
|
||||
|
@ -780,9 +790,11 @@ class CardLayout(QDialog):
|
|||
else:
|
||||
s = tr.card_templates_off()
|
||||
a = m.addAction(tr.card_templates_deck_override() + s)
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onTargetDeck)
|
||||
|
||||
a = m.addAction(tr.card_templates_browser_appearance())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onBrowserDisplay)
|
||||
|
||||
m.popup(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0)))
|
||||
|
@ -834,7 +846,9 @@ class CardLayout(QDialog):
|
|||
te.setCol(self.col)
|
||||
l.addWidget(te)
|
||||
if t["did"]:
|
||||
te.setText(self.col.decks.get(t["did"])["name"])
|
||||
deck = self.col.decks.get(t["did"])
|
||||
assert deck is not None
|
||||
te.setText(deck["name"])
|
||||
te.selectAll()
|
||||
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||
qconnect(bb.rejected, d.close)
|
||||
|
@ -927,10 +941,10 @@ class CardLayout(QDialog):
|
|||
saveGeom(self, "CardLayout")
|
||||
saveSplitter(self.mainArea, "CardLayoutMainArea")
|
||||
self.preview_web.cleanup()
|
||||
self.preview_web = None
|
||||
self.model = None
|
||||
self.rendered_card = None
|
||||
self.mw = None
|
||||
self.preview_web = None # type: ignore
|
||||
self.model = None # type: ignore
|
||||
self.rendered_card = None # type: ignore
|
||||
self.mw = None # type: ignore
|
||||
|
||||
def onHelp(self) -> None:
|
||||
openHelp(HelpPage.TEMPLATES)
|
||||
|
|
|
@ -144,7 +144,11 @@ class CustomStudy(QDialog):
|
|||
form.spin.setValue(current_spinner_value)
|
||||
form.preSpin.setText(text_before_spinner)
|
||||
form.postSpin.setText(text_after_spinner)
|
||||
form.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(ok)
|
||||
|
||||
ok_button = form.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
|
||||
assert ok_button is not None
|
||||
ok_button.setText(ok)
|
||||
|
||||
self.radioIdx = idx
|
||||
|
||||
def accept(self) -> None:
|
||||
|
|
|
@ -72,8 +72,10 @@ class DebugConsole(QDialog):
|
|||
qconnect(self._script.currentIndexChanged, self._on_script_change)
|
||||
|
||||
def _setup_text_edits(self):
|
||||
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
|
||||
font.setPointSize(self._text.font().pointSize() + 1)
|
||||
font = QFont("Consolas")
|
||||
if not font.exactMatch():
|
||||
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
|
||||
font.setPointSize(self._text.font().pointSize())
|
||||
self._text.setFont(font)
|
||||
self._log.setFont(font)
|
||||
|
||||
|
@ -196,6 +198,7 @@ class DebugConsole(QDialog):
|
|||
|
||||
def _on_context_menu(self, text_edit: QPlainTextEdit) -> None:
|
||||
menu = text_edit.createStandardContextMenu()
|
||||
assert menu is not None
|
||||
menu.addSeparator()
|
||||
for action in self._actions():
|
||||
entry = menu.addAction(action.name)
|
||||
|
@ -227,7 +230,7 @@ class DebugConsole(QDialog):
|
|||
sys.stderr = self._oldStderr
|
||||
sys.stdout = self._oldStdout
|
||||
|
||||
def _card_repr(self, card: anki.cards.Card) -> None:
|
||||
def _card_repr(self, card: anki.cards.Card | None) -> None:
|
||||
import copy
|
||||
import pprint
|
||||
|
||||
|
@ -316,6 +319,7 @@ class DebugConsole(QDialog):
|
|||
)
|
||||
self._log.appendPlainText(to_append)
|
||||
slider = self._log.verticalScrollBar()
|
||||
assert slider is not None
|
||||
slider.setValue(slider.maximum())
|
||||
self._log.ensureCursorVisible()
|
||||
|
||||
|
|
|
@ -309,12 +309,16 @@ class DeckBrowser:
|
|||
def _showOptions(self, did: str) -> None:
|
||||
m = QMenu(self.mw)
|
||||
a = m.addAction(tr.actions_rename())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, lambda b, did=did: self._rename(DeckId(int(did))))
|
||||
a = m.addAction(tr.actions_options())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, lambda b, did=did: self._options(DeckId(int(did))))
|
||||
a = m.addAction(tr.actions_export())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, lambda b, did=did: self._export(DeckId(int(did))))
|
||||
a = m.addAction(tr.actions_delete())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, lambda b, did=did: self._delete(DeckId(int(did))))
|
||||
gui_hooks.deck_browser_will_show_options_menu(m, int(did))
|
||||
m.popup(QCursor.pos())
|
||||
|
@ -357,9 +361,9 @@ class DeckBrowser:
|
|||
).run_in_background()
|
||||
|
||||
def _delete(self, did: DeckId) -> None:
|
||||
deck_name = self.mw.col.decks.find_deck_in_tree(
|
||||
self._render_data.tree, did
|
||||
).name
|
||||
deck = self.mw.col.decks.find_deck_in_tree(self._render_data.tree, did)
|
||||
assert deck is not None
|
||||
deck_name = deck.name
|
||||
remove_decks(
|
||||
parent=self.mw, deck_ids=[did], deck_name=deck_name
|
||||
).run_in_background()
|
||||
|
|
|
@ -99,7 +99,9 @@ class DeckChooser(QHBoxLayout):
|
|||
def callback(ret: StudyDeck) -> None:
|
||||
if not ret.name:
|
||||
return
|
||||
new_selected_deck_id = self.mw.col.decks.by_name(ret.name)["id"]
|
||||
deck = self.mw.col.decks.by_name(ret.name)
|
||||
assert deck is not None
|
||||
new_selected_deck_id = deck["id"]
|
||||
if self.selected_deck_id != new_selected_deck_id:
|
||||
self.selected_deck_id = new_selected_deck_id
|
||||
if func := self.on_deck_changed:
|
||||
|
|
|
@ -60,6 +60,7 @@ class DeckDescriptionDialog(QDialog):
|
|||
|
||||
button_box = QDialogButtonBox()
|
||||
ok = button_box.addButton(QDialogButtonBox.StandardButton.Ok)
|
||||
assert ok is not None
|
||||
qconnect(ok.clicked, self.save_and_accept)
|
||||
box.addWidget(button_box)
|
||||
|
||||
|
|
|
@ -67,10 +67,10 @@ class DeckOptionsDialog(QDialog):
|
|||
elif cmd == "_close":
|
||||
self._close()
|
||||
|
||||
def closeEvent(self, evt: QCloseEvent) -> None:
|
||||
def closeEvent(self, evt: QCloseEvent | None) -> None:
|
||||
if self._close_event_has_cleaned_up:
|
||||
evt.accept()
|
||||
return
|
||||
return super().closeEvent(evt)
|
||||
assert evt is not None
|
||||
evt.ignore()
|
||||
self.check_pending_changes()
|
||||
|
||||
|
@ -98,7 +98,7 @@ class DeckOptionsDialog(QDialog):
|
|||
def reject(self) -> None:
|
||||
self.mw.col.set_wants_abort()
|
||||
self.web.cleanup()
|
||||
self.web = None
|
||||
self.web = None # type: ignore
|
||||
saveGeom(self, self.TITLE)
|
||||
QDialog.reject(self)
|
||||
|
||||
|
@ -113,10 +113,14 @@ def confirm_deck_then_display_options(active_card: Card | None = None) -> None:
|
|||
decks = [aqt.mw.col.decks.current()]
|
||||
if card := active_card:
|
||||
if card.odid and card.odid != decks[0]["id"]:
|
||||
decks.append(aqt.mw.col.decks.get(card.odid))
|
||||
deck = aqt.mw.col.decks.get(card.odid)
|
||||
assert deck is not None
|
||||
decks.append(deck)
|
||||
|
||||
if not any(d["id"] == card.did for d in decks):
|
||||
decks.append(aqt.mw.col.decks.get(card.did))
|
||||
deck = aqt.mw.col.decks.get(card.did)
|
||||
assert deck is not None
|
||||
decks.append(deck)
|
||||
|
||||
if len(decks) == 1:
|
||||
display_options_for_deck(decks[0])
|
||||
|
@ -143,13 +147,16 @@ def _deck_prompt_dialog(decks: list[DeckDict]) -> None:
|
|||
|
||||
|
||||
def display_options_for_deck_id(deck_id: DeckId) -> None:
|
||||
display_options_for_deck(aqt.mw.col.decks.get(deck_id))
|
||||
deck = aqt.mw.col.decks.get(deck_id)
|
||||
assert deck is not None
|
||||
display_options_for_deck(deck)
|
||||
|
||||
|
||||
def display_options_for_deck(deck: DeckDict) -> None:
|
||||
if not deck["dyn"]:
|
||||
if KeyboardModifiersPressed().shift or not aqt.mw.col.v3_scheduler():
|
||||
deck_legacy = aqt.mw.col.decks.get(DeckId(deck["id"]))
|
||||
assert deck_legacy is not None
|
||||
aqt.deckconf.DeckConf(aqt.mw, deck_legacy)
|
||||
else:
|
||||
DeckOptionsDialog(aqt.mw, deck)
|
||||
|
|
|
@ -28,10 +28,12 @@ class EditCurrent(QMainWindow):
|
|||
self,
|
||||
editor_mode=aqt.editor.EditorMode.EDIT_CURRENT,
|
||||
)
|
||||
assert self.mw.reviewer.card is not None
|
||||
self.editor.card = self.mw.reviewer.card
|
||||
self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0)
|
||||
restoreGeom(self, "editcurrent")
|
||||
close_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close)
|
||||
assert close_button is not None
|
||||
close_button.setShortcut(QKeySequence("Ctrl+Return"))
|
||||
# qt5.14+ doesn't handle numpad enter on Windows
|
||||
self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self)
|
||||
|
@ -46,6 +48,7 @@ class EditCurrent(QMainWindow):
|
|||
# reload note
|
||||
note = self.editor.note
|
||||
try:
|
||||
assert note is not None
|
||||
note.load()
|
||||
except NotFoundError:
|
||||
# note's been deleted
|
||||
|
@ -65,7 +68,7 @@ class EditCurrent(QMainWindow):
|
|||
if card := self.mw.reviewer.card:
|
||||
self.editor.set_note(card.note())
|
||||
|
||||
def closeEvent(self, evt: QCloseEvent) -> None:
|
||||
def closeEvent(self, evt: QCloseEvent | None) -> None:
|
||||
self.editor.call_after_note_saved(self.cleanup)
|
||||
|
||||
def _saveAndClose(self) -> None:
|
||||
|
|
170
qt/aqt/editor.py
170
qt/aqt/editor.py
|
@ -34,7 +34,7 @@ from anki.collection import Config, SearchNode
|
|||
from anki.consts import MODEL_CLOZE
|
||||
from anki.hooks import runFilter
|
||||
from anki.httpclient import HttpClient
|
||||
from anki.models import NotetypeId, StockNotetype
|
||||
from anki.models import NotetypeDict, NotetypeId, StockNotetype
|
||||
from anki.notes import Note, NoteFieldsCheckResult, NoteId
|
||||
from anki.utils import checksum, is_lin, is_mac, is_win, namedtmp
|
||||
from aqt import AnkiQt, colors, gui_hooks
|
||||
|
@ -242,38 +242,35 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
rightside: bool = True,
|
||||
) -> str:
|
||||
"""Assign func to bridge cmd, register shortcut, return button"""
|
||||
if func:
|
||||
|
||||
def wrapped_func(editor: Editor) -> None:
|
||||
self.call_after_note_saved(
|
||||
functools.partial(func, editor), keepFocus=True
|
||||
)
|
||||
def wrapped_func(editor: Editor) -> None:
|
||||
self.call_after_note_saved(functools.partial(func, editor), keepFocus=True)
|
||||
|
||||
self._links[cmd] = wrapped_func
|
||||
self._links[cmd] = wrapped_func
|
||||
|
||||
if keys:
|
||||
if keys:
|
||||
|
||||
def on_activated() -> None:
|
||||
wrapped_func(self)
|
||||
def on_activated() -> None:
|
||||
wrapped_func(self)
|
||||
|
||||
if toggleable:
|
||||
# generate a random id for triggering toggle
|
||||
id = id or str(randrange(1_000_000))
|
||||
if toggleable:
|
||||
# generate a random id for triggering toggle
|
||||
id = id or str(randrange(1_000_000))
|
||||
|
||||
def on_hotkey() -> None:
|
||||
on_activated()
|
||||
self.web.eval(
|
||||
f'toggleEditorButton(document.getElementById("{id}"));'
|
||||
)
|
||||
def on_hotkey() -> None:
|
||||
on_activated()
|
||||
self.web.eval(
|
||||
f'toggleEditorButton(document.getElementById("{id}"));'
|
||||
)
|
||||
|
||||
else:
|
||||
on_hotkey = on_activated
|
||||
else:
|
||||
on_hotkey = on_activated
|
||||
|
||||
QShortcut( # type: ignore
|
||||
QKeySequence(keys),
|
||||
self.widget,
|
||||
activated=on_hotkey,
|
||||
)
|
||||
QShortcut( # type: ignore
|
||||
QKeySequence(keys),
|
||||
self.widget,
|
||||
activated=on_hotkey,
|
||||
)
|
||||
|
||||
btn = self._addButton(
|
||||
icon,
|
||||
|
@ -363,7 +360,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
def _onFields(self) -> None:
|
||||
from aqt.fields import FieldDialog
|
||||
|
||||
FieldDialog(self.mw, self.note.note_type(), parent=self.parentWindow)
|
||||
FieldDialog(self.mw, self.note_type(), parent=self.parentWindow)
|
||||
|
||||
def onCardLayout(self) -> None:
|
||||
self.call_after_note_saved(self._onCardLayout)
|
||||
|
@ -375,6 +372,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
ord = self.card.ord
|
||||
else:
|
||||
ord = 0
|
||||
|
||||
assert self.note is not None
|
||||
CardLayout(
|
||||
self.mw,
|
||||
self.note,
|
||||
|
@ -435,7 +434,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
gui_hooks.editor_did_focus_field(self.note, self.currentField)
|
||||
|
||||
elif cmd.startswith("toggleStickyAll"):
|
||||
model = self.note.note_type()
|
||||
model = self.note_type()
|
||||
flds = model["flds"]
|
||||
|
||||
any_sticky = any([fld["sticky"] for fld in flds])
|
||||
|
@ -456,7 +455,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
(type, num) = cmd.split(":", 1)
|
||||
ord = int(num)
|
||||
|
||||
model = self.note.note_type()
|
||||
model = self.note_type()
|
||||
fld = model["flds"][ord]
|
||||
new_state = not fld["sticky"]
|
||||
fld["sticky"] = new_state
|
||||
|
@ -469,10 +468,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
|
||||
elif cmd.startswith("lastTextColor"):
|
||||
(_, textColor) = cmd.split(":", 1)
|
||||
assert self.mw.pm.profile is not None
|
||||
self.mw.pm.profile["lastTextColor"] = textColor
|
||||
|
||||
elif cmd.startswith("lastHighlightColor"):
|
||||
(_, highlightColor) = cmd.split(":", 1)
|
||||
assert self.mw.pm.profile is not None
|
||||
self.mw.pm.profile["lastHighlightColor"] = highlightColor
|
||||
|
||||
elif cmd.startswith("saveTags"):
|
||||
|
@ -545,11 +546,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
for fld, val in self.note.items()
|
||||
]
|
||||
|
||||
flds = self.note.note_type()["flds"]
|
||||
note_type = self.note_type()
|
||||
flds = note_type["flds"]
|
||||
collapsed = [fld["collapsed"] for fld in flds]
|
||||
plain_texts = [fld.get("plainText", False) for fld in flds]
|
||||
descriptions = [fld.get("description", "") for fld in flds]
|
||||
notetype_meta = {"id": self.note.mid, "modTime": self.note.note_type()["mod"]}
|
||||
notetype_meta = {"id": self.note.mid, "modTime": note_type["mod"]}
|
||||
|
||||
self.widget.show()
|
||||
|
||||
|
@ -566,6 +568,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
self.web.setFocus()
|
||||
gui_hooks.editor_did_load_note(self)
|
||||
|
||||
assert self.mw.pm.profile is not None
|
||||
text_color = self.mw.pm.profile.get("lastTextColor", "#0000ff")
|
||||
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#0000ff")
|
||||
|
||||
|
@ -590,7 +593,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
"""
|
||||
|
||||
if self.addMode:
|
||||
sticky = [field["sticky"] for field in self.note.note_type()["flds"]]
|
||||
sticky = [field["sticky"] for field in self.note_type()["flds"]]
|
||||
js += " setSticky(%s);" % json.dumps(sticky)
|
||||
|
||||
if (
|
||||
|
@ -607,6 +610,9 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
|
||||
def _save_current_note(self) -> None:
|
||||
"Call after note is updated with data from webview."
|
||||
if not self.note:
|
||||
return
|
||||
|
||||
update_note(parent=self.widget, note=self.note).run_in_background(
|
||||
initiator=self
|
||||
)
|
||||
|
@ -614,7 +620,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
def fonts(self) -> list[tuple[str, int, bool]]:
|
||||
return [
|
||||
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
|
||||
for f in self.note.note_type()["flds"]
|
||||
for f in self.note_type()["flds"]
|
||||
]
|
||||
|
||||
def call_after_note_saved(
|
||||
|
@ -648,6 +654,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
checkValid = _check_and_update_duplicate_display_async
|
||||
|
||||
def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -> None:
|
||||
assert self.note is not None
|
||||
cols = [""] * len(self.note.fields)
|
||||
cloze_hint = ""
|
||||
if result == NoteFieldsCheckResult.DUPLICATE:
|
||||
|
@ -665,13 +672,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
)
|
||||
|
||||
def showDupes(self) -> None:
|
||||
assert self.note is not None
|
||||
aqt.dialogs.open(
|
||||
"Browser",
|
||||
self.mw,
|
||||
search=(
|
||||
SearchNode(
|
||||
dupe=SearchNode.Dupe(
|
||||
notetype_id=self.note.note_type()["id"],
|
||||
notetype_id=self.note_type()["id"],
|
||||
first_field=self.note.fields[0],
|
||||
)
|
||||
),
|
||||
|
@ -681,7 +689,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
|
||||
if not self.note:
|
||||
return True
|
||||
m = self.note.note_type()
|
||||
m = self.note_type()
|
||||
for c, f in enumerate(self.note.fields):
|
||||
f = f.replace("<br>", "").strip()
|
||||
notChangedvalues = {"", "<br>"}
|
||||
|
@ -696,7 +704,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
|
||||
if self.web:
|
||||
self.web.cleanup()
|
||||
self.web = None
|
||||
self.web = None # type: ignore
|
||||
|
||||
# legacy
|
||||
|
||||
|
@ -729,9 +737,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
if self.tags.col != self.mw.col:
|
||||
self.tags.setCol(self.mw.col)
|
||||
if not self.tags.text() or not self.addMode:
|
||||
assert self.note is not None
|
||||
self.tags.setText(self.note.string_tags().strip())
|
||||
|
||||
def on_tag_focus_lost(self) -> None:
|
||||
assert self.note is not None
|
||||
self.note.tags = self.mw.col.tags.split(self.tags.text())
|
||||
gui_hooks.editor_did_update_tags(self.note)
|
||||
if not self.addMode:
|
||||
|
@ -826,7 +836,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
# Media downloads
|
||||
######################################################################
|
||||
|
||||
def urlToLink(self, url: str) -> str | None:
|
||||
def urlToLink(self, url: str) -> str:
|
||||
fname = self.urlToFile(url)
|
||||
if not fname:
|
||||
return '<a href="{}">{}</a>'.format(
|
||||
|
@ -1037,8 +1047,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
######################################################################
|
||||
|
||||
def current_notetype_is_image_occlusion(self) -> bool:
|
||||
return bool(self.note) and (
|
||||
self.note.note_type().get("originalStockKind", None)
|
||||
if not self.note:
|
||||
return False
|
||||
|
||||
return (
|
||||
self.note_type().get("originalStockKind", None)
|
||||
== StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION
|
||||
)
|
||||
|
||||
|
@ -1049,6 +1062,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
image_path=image_path, notetype_id=0
|
||||
)
|
||||
else:
|
||||
assert self.note is not None
|
||||
self.setup_mask_editor_for_existing_note(
|
||||
note_id=self.note.id, image_path=image_path
|
||||
)
|
||||
|
@ -1075,8 +1089,10 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
def select_image_from_clipboard_and_occlude(self) -> None:
|
||||
"""Set up the mask editor for the image in the clipboard."""
|
||||
|
||||
clipoard = self.mw.app.clipboard()
|
||||
mime = clipoard.mimeData()
|
||||
clipboard = self.mw.app.clipboard()
|
||||
assert clipboard is not None
|
||||
mime = clipboard.mimeData()
|
||||
assert mime is not None
|
||||
if not mime.hasImage():
|
||||
showWarning(tr.editing_no_image_found_on_clipboard())
|
||||
return
|
||||
|
@ -1160,6 +1176,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
|
||||
@deprecated(info=_js_legacy)
|
||||
def _onHtmlEdit(self, field: int) -> None:
|
||||
assert self.note is not None
|
||||
d = QDialog(self.widget, Qt.WindowType.Window)
|
||||
form = aqt.forms.edithtml.Ui_Dialog()
|
||||
form.setupUi(d)
|
||||
|
@ -1223,7 +1240,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
@deprecated(info=_js_legacy)
|
||||
def _onCloze(self) -> None:
|
||||
# check that the model is set up for cloze deletion
|
||||
if self.note.note_type()["type"] != MODEL_CLOZE:
|
||||
if self.note_type()["type"] != MODEL_CLOZE:
|
||||
if self.addMode:
|
||||
tooltip(tr.editing_warning_cloze_deletions_will_not_work())
|
||||
else:
|
||||
|
@ -1231,7 +1248,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
return
|
||||
# find the highest existing cloze
|
||||
highest = 0
|
||||
for name, val in list(self.note.items()):
|
||||
assert self.note is not None
|
||||
for _, val in list(self.note.items()):
|
||||
m = re.findall(r"\{\{c(\d+)::", val)
|
||||
if m:
|
||||
highest = max(highest, sorted(int(x) for x in m)[-1])
|
||||
|
@ -1243,6 +1261,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
self.web.eval("wrap('{{c%d::', '}}');" % highest)
|
||||
|
||||
def setupForegroundButton(self) -> None:
|
||||
assert self.mw.pm.profile is not None
|
||||
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
|
||||
|
||||
# use last colour
|
||||
|
@ -1276,6 +1295,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
@deprecated(info=_js_legacy)
|
||||
def onColourChanged(self) -> None:
|
||||
self._updateForegroundButton()
|
||||
assert self.mw.pm.profile is not None
|
||||
self.mw.pm.profile["lastColour"] = self.fcolour
|
||||
|
||||
@deprecated(info=_js_legacy)
|
||||
|
@ -1300,6 +1320,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
(tr.editing_edit_html(), self.onHtmlEdit, "Ctrl+Shift+X"),
|
||||
):
|
||||
a = m.addAction(text)
|
||||
assert a is not None
|
||||
qconnect(a.triggered, handler)
|
||||
a.setShortcut(QKeySequence(shortcut))
|
||||
|
||||
|
@ -1387,6 +1408,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
addImageForOcclusionFromClipboard=Editor.select_image_from_clipboard_and_occlude,
|
||||
)
|
||||
|
||||
def note_type(self) -> NotetypeDict:
|
||||
assert self.note is not None
|
||||
note_type = self.note.note_type()
|
||||
assert note_type is not None
|
||||
return note_type
|
||||
|
||||
|
||||
# Pasting, drag & drop, and keyboard layouts
|
||||
######################################################################
|
||||
|
@ -1403,6 +1430,7 @@ class EditorWebView(AnkiWebView):
|
|||
self._internal_field_text_for_paste: str | None = None
|
||||
self._last_known_clipboard_mime: QMimeData | None = None
|
||||
clip = self.editor.mw.app.clipboard()
|
||||
assert clip is not None
|
||||
clip.dataChanged.connect(self._on_clipboard_change)
|
||||
gui_hooks.editor_web_view_did_init(self)
|
||||
|
||||
|
@ -1410,23 +1438,28 @@ class EditorWebView(AnkiWebView):
|
|||
self._store_field_content_on_next_clipboard_change = True
|
||||
self._internal_field_text_for_paste = None
|
||||
|
||||
def _on_clipboard_change(self) -> None:
|
||||
self._last_known_clipboard_mime = self.editor.mw.app.clipboard().mimeData()
|
||||
def _on_clipboard_change(
|
||||
self, mode: QClipboard.Mode = QClipboard.Mode.Clipboard
|
||||
) -> None:
|
||||
self._last_known_clipboard_mime = self._clipboard().mimeData(mode)
|
||||
if self._store_field_content_on_next_clipboard_change:
|
||||
# if the flag was set, save the field data
|
||||
self._internal_field_text_for_paste = self._get_clipboard_html_for_field()
|
||||
self._internal_field_text_for_paste = self._get_clipboard_html_for_field(
|
||||
mode
|
||||
)
|
||||
self._store_field_content_on_next_clipboard_change = False
|
||||
elif (
|
||||
self._internal_field_text_for_paste != self._get_clipboard_html_for_field()
|
||||
elif self._internal_field_text_for_paste != self._get_clipboard_html_for_field(
|
||||
mode
|
||||
):
|
||||
# if we've previously saved the field, blank it out if the clipboard state has changed
|
||||
self._internal_field_text_for_paste = None
|
||||
|
||||
def _get_clipboard_html_for_field(self):
|
||||
clip = self.editor.mw.app.clipboard()
|
||||
mime = clip.mimeData()
|
||||
def _get_clipboard_html_for_field(self, mode: QClipboard.Mode) -> str | None:
|
||||
clip = self._clipboard()
|
||||
mime = clip.mimeData(mode)
|
||||
assert mime is not None
|
||||
if not mime.hasHtml():
|
||||
return
|
||||
return None
|
||||
return mime.html()
|
||||
|
||||
def onCut(self) -> None:
|
||||
|
@ -1440,6 +1473,7 @@ class EditorWebView(AnkiWebView):
|
|||
|
||||
def _opened_context_menu_on_image(self) -> bool:
|
||||
context_menu_request = self.lastContextMenuRequest()
|
||||
assert context_menu_request is not None
|
||||
return (
|
||||
context_menu_request.mediaType()
|
||||
== context_menu_request.MediaType.MediaTypeImage
|
||||
|
@ -1455,15 +1489,17 @@ class EditorWebView(AnkiWebView):
|
|||
|
||||
def _onPaste(self, mode: QClipboard.Mode) -> None:
|
||||
# Since _on_clipboard_change doesn't always trigger properly on macOS, we do a double check if any changes were made before pasting
|
||||
if self._last_known_clipboard_mime != self.editor.mw.app.clipboard().mimeData():
|
||||
self._on_clipboard_change()
|
||||
clipboard = self._clipboard()
|
||||
if self._last_known_clipboard_mime != clipboard.mimeData(mode):
|
||||
self._on_clipboard_change(mode)
|
||||
extended = self._wantsExtendedPaste()
|
||||
if html := self._internal_field_text_for_paste:
|
||||
print("reuse internal")
|
||||
self.editor.doPaste(html, True, extended)
|
||||
else:
|
||||
print("use clipboard")
|
||||
mime = self.editor.mw.app.clipboard().mimeData(mode=mode)
|
||||
mime = clipboard.mimeData(mode=mode)
|
||||
assert mime is not None
|
||||
html, internal = self._processMime(mime, extended)
|
||||
if html:
|
||||
self.editor.doPaste(html, internal, extended)
|
||||
|
@ -1474,12 +1510,15 @@ class EditorWebView(AnkiWebView):
|
|||
def onMiddleClickPaste(self) -> None:
|
||||
self._onPaste(QClipboard.Mode.Selection)
|
||||
|
||||
def dragEnterEvent(self, evt: QDragEnterEvent) -> None:
|
||||
def dragEnterEvent(self, evt: QDragEnterEvent | None) -> None:
|
||||
assert evt is not None
|
||||
evt.accept()
|
||||
|
||||
def dropEvent(self, evt: QDropEvent) -> None:
|
||||
def dropEvent(self, evt: QDropEvent | None) -> None:
|
||||
assert evt is not None
|
||||
extended = self._wantsExtendedPaste()
|
||||
mime = evt.mimeData()
|
||||
assert mime is not None
|
||||
cursor_pos = self.mapFromGlobal(QCursor.pos())
|
||||
|
||||
if evt.source() and mime.hasHtml():
|
||||
|
@ -1585,12 +1624,13 @@ class EditorWebView(AnkiWebView):
|
|||
|
||||
return fname
|
||||
|
||||
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
|
||||
def contextMenuEvent(self, evt: QContextMenuEvent | None) -> None:
|
||||
m = QMenu(self)
|
||||
if self.hasSelection():
|
||||
self._add_cut_action(m)
|
||||
self._add_copy_action(m)
|
||||
a = m.addAction(tr.editing_paste())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onPaste)
|
||||
if self._opened_context_menu_on_image():
|
||||
self._add_image_menu(m)
|
||||
|
@ -1599,26 +1639,38 @@ class EditorWebView(AnkiWebView):
|
|||
|
||||
def _add_cut_action(self, menu: QMenu) -> None:
|
||||
a = menu.addAction(tr.editing_cut())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onCut)
|
||||
|
||||
def _add_copy_action(self, menu: QMenu) -> None:
|
||||
a = menu.addAction(tr.actions_copy())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onCopy)
|
||||
|
||||
def _add_image_menu(self, menu: QMenu) -> None:
|
||||
a = menu.addAction(tr.editing_copy_image())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.on_copy_image)
|
||||
|
||||
url = self.lastContextMenuRequest().mediaUrl()
|
||||
context_menu_request = self.lastContextMenuRequest()
|
||||
assert context_menu_request is not None
|
||||
url = context_menu_request.mediaUrl()
|
||||
file_name = url.fileName()
|
||||
path = os.path.join(self.editor.mw.col.media.dir(), file_name)
|
||||
a = menu.addAction(tr.editing_open_image())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, lambda: openFolder(path))
|
||||
|
||||
if is_win or is_mac:
|
||||
a = menu.addAction(tr.editing_show_in_folder())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, lambda: show_in_folder(path))
|
||||
|
||||
def _clipboard(self) -> QClipboard:
|
||||
clipboard = self.editor.mw.app.clipboard()
|
||||
assert clipboard is not None
|
||||
return clipboard
|
||||
|
||||
|
||||
# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light"
|
||||
# - there may be other cases like a trailing 'Bold' that need fixing, but will
|
||||
|
@ -1648,7 +1700,7 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|
|||
|
||||
|
||||
def set_cloze_button(editor: Editor) -> None:
|
||||
action = "show" if editor.note.note_type()["type"] == MODEL_CLOZE else "hide"
|
||||
action = "show" if editor.note_type()["type"] == MODEL_CLOZE else "hide"
|
||||
editor.web.eval(
|
||||
'require("anki/ui").loaded.then(() =>'
|
||||
f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("cloze")'
|
||||
|
|
|
@ -38,7 +38,7 @@ class EmptyCardsDialog(QDialog):
|
|||
|
||||
def __init__(self, mw: aqt.main.AnkiQt, report: EmptyCardsReport) -> None:
|
||||
super().__init__(mw)
|
||||
self.mw = mw.weakref()
|
||||
self.mw = mw
|
||||
self.mw.garbage_collect_on_dialog_finish(self)
|
||||
self.report = report
|
||||
self.form = aqt.forms.emptycards.Ui_Dialog()
|
||||
|
@ -63,7 +63,7 @@ class EmptyCardsDialog(QDialog):
|
|||
|
||||
def on_finished(code: Any) -> None:
|
||||
self.form.webview.cleanup()
|
||||
self.form.webview = None
|
||||
self.form.webview = None # type: ignore
|
||||
saveGeom(self, "emptycards")
|
||||
|
||||
qconnect(self.finished, on_finished)
|
||||
|
@ -71,6 +71,7 @@ class EmptyCardsDialog(QDialog):
|
|||
self._delete_button = self.form.buttonBox.addButton(
|
||||
tr.empty_cards_delete_button(), QDialogButtonBox.ButtonRole.ActionRole
|
||||
)
|
||||
assert self._delete_button is not None
|
||||
self._delete_button.setAutoDefault(False)
|
||||
qconnect(self._delete_button.clicked, self._on_delete)
|
||||
|
||||
|
|
|
@ -41,25 +41,30 @@ class FieldDialog(QDialog):
|
|||
self.model = nt
|
||||
self.mm._remove_from_cache(self.model["id"])
|
||||
self.change_tracker = ChangeTracker(self.mw)
|
||||
self.webview = None
|
||||
|
||||
self.form = aqt.forms.fields.Ui_Dialog()
|
||||
self.form.setupUi(self)
|
||||
|
||||
self.setWindowTitle(
|
||||
without_unicode_isolation(tr.fields_fields_for(val=self.model["name"]))
|
||||
)
|
||||
|
||||
self.form = aqt.forms.fields.Ui_Dialog()
|
||||
self.form.setupUi(self)
|
||||
self.webview = None
|
||||
|
||||
disable_help_button(self)
|
||||
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help).setAutoDefault(
|
||||
False
|
||||
)
|
||||
self.form.buttonBox.button(
|
||||
help_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help)
|
||||
assert help_button is not None
|
||||
help_button.setAutoDefault(False)
|
||||
|
||||
cancel_button = self.form.buttonBox.button(
|
||||
QDialogButtonBox.StandardButton.Cancel
|
||||
).setAutoDefault(False)
|
||||
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save).setAutoDefault(
|
||||
False
|
||||
)
|
||||
assert cancel_button is not None
|
||||
cancel_button.setAutoDefault(False)
|
||||
|
||||
save_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save)
|
||||
assert save_button is not None
|
||||
save_button.setAutoDefault(False)
|
||||
|
||||
self.currentIdx: int | None = None
|
||||
self.fillFields()
|
||||
self.setupSignals()
|
||||
|
@ -112,6 +117,7 @@ class FieldDialog(QDialog):
|
|||
# for pylint
|
||||
return
|
||||
# the item in idx is removed thus subtract 1.
|
||||
assert idx is not None
|
||||
if idx < dropPos:
|
||||
movePos -= 1
|
||||
self.moveField(movePos + 1) # convert to 1 based.
|
||||
|
@ -144,6 +150,9 @@ class FieldDialog(QDialog):
|
|||
return txt
|
||||
|
||||
def onRename(self) -> None:
|
||||
if self.currentIdx is None:
|
||||
return
|
||||
|
||||
idx = self.currentIdx
|
||||
f = self.model["flds"][idx]
|
||||
name = self._uniqueName(tr.actions_new_name(), self.currentIdx, f["name"])
|
||||
|
@ -195,6 +204,7 @@ class FieldDialog(QDialog):
|
|||
|
||||
def onPosition(self, delta: int = -1) -> None:
|
||||
idx = self.currentIdx
|
||||
assert idx is not None
|
||||
l = len(self.model["flds"])
|
||||
txt = getOnlyText(tr.fields_new_position_1(val=l), default=str(idx + 1))
|
||||
if not txt:
|
||||
|
|
|
@ -130,9 +130,10 @@ class FilteredDeckConfigDialog(QDialog):
|
|||
build_label = tr.actions_rebuild()
|
||||
else:
|
||||
build_label = tr.decks_build()
|
||||
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(
|
||||
build_label
|
||||
)
|
||||
|
||||
ok_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
|
||||
assert ok_button is not None
|
||||
ok_button.setText(build_label)
|
||||
|
||||
form.resched.setChecked(config.reschedule)
|
||||
self._onReschedToggled(0)
|
||||
|
|
|
@ -34,11 +34,11 @@ class Flag:
|
|||
class FlagManager:
|
||||
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||
self.mw = mw
|
||||
self._flags: list[Flag] | None = None
|
||||
self._flags: list[Flag] = []
|
||||
|
||||
def all(self) -> list[Flag]:
|
||||
"""Return a list of all flags."""
|
||||
if self._flags is None:
|
||||
if not self._flags:
|
||||
self._load_flags()
|
||||
return self._flags
|
||||
|
||||
|
@ -57,7 +57,7 @@ class FlagManager:
|
|||
|
||||
def require_refresh(self) -> None:
|
||||
"Discard cached labels."
|
||||
self._flags = None
|
||||
self._flags = []
|
||||
|
||||
def _load_flags(self) -> None:
|
||||
labels = cast(dict[str, str], self.mw.col.get_config("flagLabels", {}))
|
||||
|
|
|
@ -611,28 +611,6 @@
|
|||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_4">
|
||||
<property name="title">
|
||||
<string>preferences_import_export</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_11">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="legacy_import_export">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">Legacy import/export handling</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
|
@ -1293,7 +1271,6 @@
|
|||
<tabstop>useCurrent</tabstop>
|
||||
<tabstop>default_search_text</tabstop>
|
||||
<tabstop>ignore_accents_in_search</tabstop>
|
||||
<tabstop>legacy_import_export</tabstop>
|
||||
<tabstop>syncMedia</tabstop>
|
||||
<tabstop>syncOnProgramOpen</tabstop>
|
||||
<tabstop>autoSyncMedia</tabstop>
|
||||
|
|
|
@ -60,7 +60,7 @@ class ChangeMap(QDialog):
|
|||
self.frm.fields.setCurrentRow(n + 1)
|
||||
self.field: str | None = None
|
||||
|
||||
def getField(self) -> str:
|
||||
def getField(self) -> str | None:
|
||||
self.exec()
|
||||
return self.field
|
||||
|
||||
|
@ -91,8 +91,10 @@ class ImportDialog(QDialog):
|
|||
self.importer = importer
|
||||
self.frm = aqt.forms.importing.Ui_ImportDialog()
|
||||
self.frm.setupUi(self)
|
||||
help_button = self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help)
|
||||
assert help_button is not None
|
||||
qconnect(
|
||||
self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help).clicked,
|
||||
help_button.clicked,
|
||||
self.helpRequested,
|
||||
)
|
||||
disable_help_button(self)
|
||||
|
@ -103,6 +105,7 @@ class ImportDialog(QDialog):
|
|||
gui_hooks.current_note_type_did_change.append(self.modelChanged)
|
||||
qconnect(self.frm.autoDetect.clicked, self.onDelimiter)
|
||||
self.updateDelimiterButtonText()
|
||||
assert self.mw.pm.profile is not None
|
||||
self.frm.allowHTML.setChecked(self.mw.pm.profile.get("allowHTML", True))
|
||||
qconnect(self.frm.importMode.currentIndexChanged, self.importModeChanged)
|
||||
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get("importMode", 1))
|
||||
|
@ -187,6 +190,7 @@ class ImportDialog(QDialog):
|
|||
showWarning(tr.importing_the_first_field_of_the_note())
|
||||
return
|
||||
self.importer.importMode = self.frm.importMode.currentIndex()
|
||||
assert self.mw.pm.profile is not None
|
||||
self.mw.pm.profile["importMode"] = self.importer.importMode
|
||||
self.importer.allowHTML = self.frm.allowHTML.isChecked()
|
||||
self.mw.pm.profile["allowHTML"] = self.importer.allowHTML
|
||||
|
@ -390,7 +394,7 @@ def importFile(mw: AnkiQt, file: str) -> None:
|
|||
showWarning(invalidZipMsg())
|
||||
except MediaMapInvalid:
|
||||
showWarning(
|
||||
"Unable to read file. It probably requires a newer version of Anki to import. Try unchecking 'Legacy import/export Handling' under Preferences > Editing > Import/Export and see if the problem persists."
|
||||
"Unable to read file. It probably requires a newer version of Anki to import."
|
||||
)
|
||||
except V2ImportIntoV1:
|
||||
showWarning(
|
||||
|
|
|
@ -112,7 +112,6 @@ class MainWebView(AnkiWebView):
|
|||
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
|
||||
self.setMinimumWidth(400)
|
||||
self.setAcceptDrops(True)
|
||||
print("todo: windows paths in import screen")
|
||||
|
||||
# Importing files via drag & drop
|
||||
##########################################################################
|
||||
|
|
|
@ -80,6 +80,7 @@ class MediaChecker:
|
|||
label = progress.media_check
|
||||
|
||||
try:
|
||||
assert self.progress_dialog is not None
|
||||
if self.progress_dialog.wantCancel:
|
||||
self.mw.col.set_wants_abort()
|
||||
except AttributeError:
|
||||
|
@ -165,6 +166,7 @@ class MediaChecker:
|
|||
|
||||
def _on_render_latex(self) -> None:
|
||||
self.progress_dialog = self.mw.progress.start()
|
||||
assert self.progress_dialog is not None
|
||||
try:
|
||||
out = self.mw.col.media.render_all_latex(self._on_render_latex_progress)
|
||||
if self.progress_dialog.wantCancel:
|
||||
|
@ -181,6 +183,7 @@ class MediaChecker:
|
|||
tooltip(tr.media_check_all_latex_rendered())
|
||||
|
||||
def _on_render_latex_progress(self, count: int) -> bool:
|
||||
assert self.progress_dialog is not None
|
||||
if self.progress_dialog.wantCancel:
|
||||
return False
|
||||
|
||||
|
|
|
@ -457,8 +457,8 @@ def update_deck_configs() -> bytes:
|
|||
update.max = val.total_cards
|
||||
update.value = val.current_cards
|
||||
update.label = val.label
|
||||
elif progress.HasField("compute_weights"):
|
||||
val2 = progress.compute_weights
|
||||
elif progress.HasField("compute_params"):
|
||||
val2 = progress.compute_params
|
||||
# prevent an indeterminate progress bar from appearing at the start of each preset
|
||||
update.max = max(val2.total, 1)
|
||||
update.value = val2.current
|
||||
|
@ -621,10 +621,10 @@ exposed_backend_list = [
|
|||
"update_image_occlusion_note",
|
||||
"get_image_occlusion_fields",
|
||||
# SchedulerService
|
||||
"compute_fsrs_weights",
|
||||
"compute_fsrs_params",
|
||||
"compute_optimal_retention",
|
||||
"set_wants_abort",
|
||||
"evaluate_weights",
|
||||
"evaluate_params",
|
||||
"get_optimal_retention_parameters",
|
||||
"simulate_fsrs_review",
|
||||
]
|
||||
|
@ -735,8 +735,12 @@ def _extract_page_context() -> PageContext:
|
|||
return PageContext.NON_LEGACY_PAGE
|
||||
elif referer.path == "/_anki/legacyPageData":
|
||||
query_params = parse_qs(referer.query)
|
||||
id = int(query_params.get("id", [None])[0])
|
||||
return aqt.mw.mediaServer.get_page_context(id)
|
||||
query_id = query_params.get("id")
|
||||
if not query_id:
|
||||
return PageContext.UNKNOWN
|
||||
id = int(query_id[0])
|
||||
page_context = aqt.mw.mediaServer.get_page_context(id)
|
||||
return page_context if page_context else PageContext.UNKNOWN
|
||||
else:
|
||||
return PageContext.UNKNOWN
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ class MediaSyncer:
|
|||
diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True)
|
||||
diag.show()
|
||||
|
||||
timer: QTimer | None = None
|
||||
timer: QTimer
|
||||
|
||||
def check_finished() -> None:
|
||||
if not self.is_syncing():
|
||||
|
|
|
@ -84,6 +84,7 @@ class ModelChooser(QHBoxLayout):
|
|||
if not ret.name:
|
||||
return
|
||||
m = self.deck.models.by_name(ret.name)
|
||||
assert m is not None
|
||||
self.deck.conf["curModel"] = m["id"]
|
||||
cdeck = self.deck.decks.current()
|
||||
cdeck["mid"] = m["id"]
|
||||
|
|
|
@ -111,6 +111,7 @@ class NotetypeChooser(QHBoxLayout):
|
|||
if not ret.name:
|
||||
return
|
||||
notetype = self.mw.col.models.by_name(ret.name)
|
||||
assert notetype is not None
|
||||
if (id := notetype["id"]) != self._selected_notetype_id:
|
||||
self.selected_notetype_id = id
|
||||
|
||||
|
@ -146,7 +147,9 @@ class NotetypeChooser(QHBoxLayout):
|
|||
func(self._selected_notetype_id)
|
||||
|
||||
def selected_notetype_name(self) -> str:
|
||||
return self.mw.col.models.get(self.selected_notetype_id)["name"]
|
||||
selected_notetype = self.mw.col.models.get(self.selected_notetype_id)
|
||||
assert selected_notetype is not None
|
||||
return selected_notetype["name"]
|
||||
|
||||
def _ensure_selected_notetype_valid(self) -> None:
|
||||
if not self.mw.col.models.get(self._selected_notetype_id):
|
||||
|
|
|
@ -224,13 +224,14 @@ class Overview:
|
|||
dyn = ""
|
||||
return f'<div class="descfont descmid description {dyn}">{desc}</div>'
|
||||
|
||||
def _table(self) -> str | None:
|
||||
def _table(self) -> str:
|
||||
counts = list(self.mw.col.sched.counts())
|
||||
current_did = self.mw.col.decks.get_current_id()
|
||||
deck_node = self.mw.col.sched.deck_due_tree(current_did)
|
||||
|
||||
but = self.mw.button
|
||||
if self.mw.col.v3_scheduler():
|
||||
assert deck_node is not None
|
||||
buried_new = deck_node.new_count - counts[0]
|
||||
buried_learning = deck_node.learn_count - counts[1]
|
||||
buried_review = deck_node.review_count - counts[2]
|
||||
|
|
|
@ -41,7 +41,7 @@ def _patch_pkgutil() -> None:
|
|||
def get_data_custom(package: str, resource: str) -> bytes | None:
|
||||
try:
|
||||
module = importlib.import_module(package)
|
||||
reader = module.__loader__.get_resource_reader(package) # type: ignore[attr-defined]
|
||||
reader = module.__loader__.get_resource_reader(package) # type: ignore
|
||||
with reader.open_resource(resource) as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
|
|
|
@ -46,13 +46,16 @@ class Preferences(QDialog):
|
|||
self.form.network_timeout,
|
||||
):
|
||||
spinbox.setSuffix(f" {spinbox.suffix()}")
|
||||
|
||||
disable_help_button(self)
|
||||
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help).setAutoDefault(
|
||||
False
|
||||
)
|
||||
self.form.buttonBox.button(
|
||||
QDialogButtonBox.StandardButton.Close
|
||||
).setAutoDefault(False)
|
||||
help_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help)
|
||||
assert help_button is not None
|
||||
help_button.setAutoDefault(False)
|
||||
|
||||
close_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close)
|
||||
assert close_button is not None
|
||||
close_button.setAutoDefault(False)
|
||||
|
||||
qconnect(
|
||||
self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES)
|
||||
)
|
||||
|
@ -218,6 +221,7 @@ class Preferences(QDialog):
|
|||
qconnect(self.form.syncAnkiHubLogin.clicked, self.ankihub_sync_login)
|
||||
|
||||
def update_login_status(self) -> None:
|
||||
assert self.prof is not None
|
||||
if not self.prof.get("syncKey"):
|
||||
self.form.syncUser.setText(tr.preferences_ankiweb_intro())
|
||||
self.form.syncLogin.setVisible(True)
|
||||
|
@ -241,6 +245,7 @@ class Preferences(QDialog):
|
|||
|
||||
def sync_login(self) -> None:
|
||||
def on_success():
|
||||
assert self.prof is not None
|
||||
if self.prof.get("syncKey"):
|
||||
self.update_login_status()
|
||||
self.confirm_sync_after_login()
|
||||
|
@ -251,6 +256,7 @@ class Preferences(QDialog):
|
|||
if self.mw.media_syncer.is_syncing():
|
||||
showWarning("Can't log out while sync in progress.")
|
||||
return
|
||||
assert self.prof is not None
|
||||
self.prof["syncKey"] = None
|
||||
self.mw.col.media.force_resync()
|
||||
self.update_login_status()
|
||||
|
@ -263,7 +269,10 @@ class Preferences(QDialog):
|
|||
ankihub_login(self.mw, on_success)
|
||||
|
||||
def ankihub_sync_logout(self) -> None:
|
||||
ankihub_logout(self.mw, self.update_login_status, self.mw.pm.ankihub_token())
|
||||
ankihub_token = self.mw.pm.ankihub_token()
|
||||
if ankihub_token is None:
|
||||
return
|
||||
ankihub_logout(self.mw, self.update_login_status, ankihub_token)
|
||||
|
||||
def confirm_sync_after_login(self) -> None:
|
||||
from aqt import mw
|
||||
|
@ -272,6 +281,7 @@ class Preferences(QDialog):
|
|||
self.accept_with_callback(self.mw.on_sync_button_clicked)
|
||||
|
||||
def update_network(self) -> None:
|
||||
assert self.prof is not None
|
||||
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
|
||||
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
|
||||
self.mw.pm.set_periodic_sync_media_minutes(
|
||||
|
@ -349,7 +359,6 @@ class Preferences(QDialog):
|
|||
)
|
||||
self.form.styleLabel.setVisible(not is_win)
|
||||
self.form.styleComboBox.setVisible(not is_win)
|
||||
self.form.legacy_import_export.setChecked(self.mw.pm.legacy_import_export())
|
||||
qconnect(self.form.resetWindowSizes.clicked, self.on_reset_window_sizes)
|
||||
|
||||
self.setup_language()
|
||||
|
@ -367,8 +376,6 @@ class Preferences(QDialog):
|
|||
self.mw.pm.setUiScale(newScale)
|
||||
restart_required = True
|
||||
|
||||
self.mw.pm.set_legacy_import_export(self.form.legacy_import_export.isChecked())
|
||||
|
||||
if restart_required:
|
||||
showInfo(tr.preferences_changes_will_take_effect_when_you())
|
||||
|
||||
|
@ -378,6 +385,7 @@ class Preferences(QDialog):
|
|||
self.mw.set_theme(Theme(index))
|
||||
|
||||
def on_reset_window_sizes(self) -> None:
|
||||
assert self.prof is not None
|
||||
regexp = re.compile(r"(Geom(etry)?|State|Splitter|Header)(\d+.\d+)?$")
|
||||
for key in list(self.prof.keys()):
|
||||
if regexp.search(key):
|
||||
|
|
|
@ -636,7 +636,8 @@ create table if not exists profiles
|
|||
self.meta[f"{self.editor_key(mode)}TagsCollapsed"] = collapsed
|
||||
|
||||
def legacy_import_export(self) -> bool:
|
||||
return self.meta.get("legacy_import", False)
|
||||
"Always returns False so users with this option enabled are not stuck on the legacy importer after the UI option is removed."
|
||||
return False
|
||||
|
||||
def set_legacy_import_export(self, enabled: bool) -> None:
|
||||
self.meta["legacy_import"] = enabled
|
||||
|
|
|
@ -216,6 +216,8 @@ class ProgressManager:
|
|||
self._maybeShow()
|
||||
if not self._shown:
|
||||
return
|
||||
|
||||
assert self._win is not None
|
||||
if label:
|
||||
self._win.form.label.setText(label)
|
||||
|
||||
|
@ -290,6 +292,7 @@ class ProgressManager:
|
|||
self._showWin()
|
||||
|
||||
def _showWin(self) -> None:
|
||||
assert self._win is not None
|
||||
self._shown = time.monotonic()
|
||||
self._win.show()
|
||||
|
||||
|
@ -297,6 +300,7 @@ class ProgressManager:
|
|||
# if the parent window has been deleted, the progress dialog may have
|
||||
# already been dropped; delete it if it hasn't been
|
||||
if not sip.isdeleted(self._win):
|
||||
assert self._win is not None
|
||||
self._win.cancel()
|
||||
self._win = None
|
||||
self._shown = 0
|
||||
|
@ -314,6 +318,7 @@ class ProgressManager:
|
|||
def _on_show_timer(self) -> None:
|
||||
if self.mw.app.focusWindow() is None:
|
||||
# if no window is focused (eg app is minimized), defer display
|
||||
assert self._show_timer is not None
|
||||
self._show_timer.start(10)
|
||||
return
|
||||
|
||||
|
@ -334,7 +339,7 @@ class ProgressManager:
|
|||
|
||||
|
||||
class ProgressDialog(QDialog):
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
def __init__(self, parent: QWidget | None) -> None:
|
||||
QDialog.__init__(self, parent)
|
||||
disable_help_button(self)
|
||||
self.form = aqt.forms.progress.Ui_Dialog()
|
||||
|
@ -349,14 +354,16 @@ class ProgressDialog(QDialog):
|
|||
self.hide()
|
||||
self.deleteLater()
|
||||
|
||||
def closeEvent(self, evt: QCloseEvent) -> None:
|
||||
def closeEvent(self, evt: QCloseEvent | None) -> None:
|
||||
assert evt is not None
|
||||
if self._closingDown:
|
||||
evt.accept()
|
||||
else:
|
||||
self.wantCancel = True
|
||||
evt.ignore()
|
||||
|
||||
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||
def keyPressEvent(self, evt: QKeyEvent | None) -> None:
|
||||
assert evt is not None
|
||||
if evt.key() == Qt.Key.Key_Escape:
|
||||
evt.ignore()
|
||||
self.wantCancel = True
|
||||
|
|
|
@ -245,7 +245,8 @@ av_player = AVPlayer()
|
|||
def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
|
||||
cmd = cmd[:]
|
||||
env = os.environ.copy()
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
# keep LD_LIBRARY_PATH when in snap environment
|
||||
if "LD_LIBRARY_PATH" in env and "SNAP" not in env:
|
||||
del env["LD_LIBRARY_PATH"]
|
||||
|
||||
if is_win:
|
||||
|
|
|
@ -61,9 +61,11 @@ class NewDeckStats(QDialog):
|
|||
b = f.buttonBox.addButton(
|
||||
tr.statistics_save_pdf(), QDialogButtonBox.ButtonRole.ActionRole
|
||||
)
|
||||
assert b is not None
|
||||
qconnect(b.clicked, self.saveImage)
|
||||
b.setAutoDefault(False)
|
||||
b = f.buttonBox.button(QDialogButtonBox.StandardButton.Close)
|
||||
assert b is not None
|
||||
b.setAutoDefault(False)
|
||||
maybeHideClose(self.form.buttonBox)
|
||||
addCloseShortcut(self)
|
||||
|
@ -78,7 +80,7 @@ class NewDeckStats(QDialog):
|
|||
def reject(self) -> None:
|
||||
self.deck_chooser.cleanup()
|
||||
self.form.web.cleanup()
|
||||
self.form.web = None
|
||||
self.form.web = None # type: ignore
|
||||
saveGeom(self, self.name)
|
||||
aqt.dialogs.markClosed("NewDeckStats")
|
||||
QDialog.reject(self)
|
||||
|
@ -92,7 +94,7 @@ class NewDeckStats(QDialog):
|
|||
lambda _: self.refresh()
|
||||
).run_in_background()
|
||||
|
||||
def _imagePath(self) -> str:
|
||||
def _imagePath(self) -> str | None:
|
||||
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
|
||||
name = f"anki-{tr.statistics_stats()}{name}"
|
||||
file = getSaveFile(
|
||||
|
@ -115,7 +117,9 @@ class NewDeckStats(QDialog):
|
|||
# unreadable. A simple fix for now is to scroll to the top of the
|
||||
# page first.
|
||||
def after_scroll(arg: Any) -> None:
|
||||
self.form.web.page().printToPdf(path)
|
||||
form_web_page = self.form.web.page()
|
||||
assert form_web_page is not None
|
||||
form_web_page.printToPdf(path)
|
||||
tooltip(tr.statistics_saved())
|
||||
|
||||
self.form.web.evalWithCallback("window.scrollTo(0, 0);", after_scroll)
|
||||
|
@ -165,6 +169,7 @@ class DeckStats(QDialog):
|
|||
b = f.buttonBox.addButton(
|
||||
tr.statistics_save_pdf(), QDialogButtonBox.ButtonRole.ActionRole
|
||||
)
|
||||
assert b is not None
|
||||
qconnect(b.clicked, self.saveImage)
|
||||
b.setAutoDefault(False)
|
||||
qconnect(f.groups.clicked, lambda: self.changeScope("deck"))
|
||||
|
@ -182,7 +187,7 @@ class DeckStats(QDialog):
|
|||
|
||||
def reject(self) -> None:
|
||||
self.form.web.cleanup()
|
||||
self.form.web = None
|
||||
self.form.web = None # type: ignore
|
||||
saveGeom(self, self.name)
|
||||
aqt.dialogs.markClosed("DeckStats")
|
||||
QDialog.reject(self)
|
||||
|
@ -191,7 +196,7 @@ class DeckStats(QDialog):
|
|||
self.reject()
|
||||
callback()
|
||||
|
||||
def _imagePath(self) -> str:
|
||||
def _imagePath(self) -> str | None:
|
||||
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
|
||||
name = f"anki-{tr.statistics_stats()}{name}"
|
||||
file = getSaveFile(
|
||||
|
@ -208,7 +213,9 @@ class DeckStats(QDialog):
|
|||
path = self._imagePath()
|
||||
if not path:
|
||||
return
|
||||
self.form.web.page().printToPdf(path)
|
||||
form_web_page = self.form.web.page()
|
||||
assert form_web_page is not None
|
||||
form_web_page.printToPdf(path)
|
||||
tooltip(tr.statistics_saved())
|
||||
|
||||
def changePeriod(self, n: int) -> None:
|
||||
|
|
|
@ -101,7 +101,7 @@ class StudyDeck(QDialog):
|
|||
else:
|
||||
self.exec()
|
||||
|
||||
def eventFilter(self, obj: QObject, evt: QEvent) -> bool:
|
||||
def eventFilter(self, obj: QObject | None, evt: QEvent | None) -> bool:
|
||||
if isinstance(evt, QKeyEvent) and evt.type() == QEvent.Type.KeyPress:
|
||||
new_row = current_row = self.form.list.currentRow()
|
||||
rows_count = self.form.list.count()
|
||||
|
@ -178,6 +178,7 @@ class StudyDeck(QDialog):
|
|||
|
||||
def success(out: OpChangesWithId) -> None:
|
||||
deck = self.mw.col.decks.get(DeckId(out.id))
|
||||
assert deck is not None
|
||||
self.name = deck["name"]
|
||||
self.accept_with_callback()
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class Switch(QAbstractButton):
|
|||
self._position = self.end_position
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, _event: QPaintEvent) -> None:
|
||||
def paintEvent(self, _event: QPaintEvent | None) -> None:
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
|
@ -162,12 +162,13 @@ class Switch(QAbstractButton):
|
|||
self._current_label_rectangle(), Qt.AlignmentFlag.AlignCenter, self.label
|
||||
)
|
||||
|
||||
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
||||
def mouseReleaseEvent(self, event: QMouseEvent | None) -> None:
|
||||
super().mouseReleaseEvent(event)
|
||||
assert event is not None
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._animate_toggle()
|
||||
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
def enterEvent(self, event: QEnterEvent | None) -> None:
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
super().enterEvent(event)
|
||||
|
||||
|
|
|
@ -168,7 +168,7 @@ def full_sync(
|
|||
|
||||
|
||||
def confirm_full_download(
|
||||
mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None]
|
||||
mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None]
|
||||
) -> None:
|
||||
# confirmation step required, as some users customize their notetypes
|
||||
# in an empty collection, then want to upload them
|
||||
|
@ -184,7 +184,7 @@ def confirm_full_download(
|
|||
|
||||
|
||||
def confirm_full_upload(
|
||||
mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None]
|
||||
mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None]
|
||||
) -> None:
|
||||
# confirmation step required, as some users have reported an upload
|
||||
# happening despite having their AnkiWeb collection not being empty
|
||||
|
@ -220,7 +220,7 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt, label: str) -> None:
|
|||
|
||||
|
||||
def full_download(
|
||||
mw: aqt.main.AnkiQt, server_usn: int, on_done: Callable[[], None]
|
||||
mw: aqt.main.AnkiQt, server_usn: int | None, on_done: Callable[[], None]
|
||||
) -> None:
|
||||
label = tr.sync_downloading_from_ankiweb()
|
||||
|
||||
|
@ -372,7 +372,9 @@ def get_id_and_pass_from_user(
|
|||
l2.setBuddy(passwd)
|
||||
vbox.addLayout(g)
|
||||
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # type: ignore
|
||||
bb.button(QDialogButtonBox.StandardButton.Ok).setAutoDefault(True)
|
||||
ok_button = bb.button(QDialogButtonBox.StandardButton.Ok)
|
||||
assert ok_button is not None
|
||||
ok_button.setAutoDefault(True)
|
||||
qconnect(bb.accepted, diag.accept)
|
||||
qconnect(bb.rejected, diag.reject)
|
||||
vbox.addWidget(bb)
|
||||
|
|
|
@ -42,13 +42,17 @@ class TagEdit(QLineEdit):
|
|||
l = (d.name for d in self.col.decks.all_names_and_ids())
|
||||
self.model.setStringList(l)
|
||||
|
||||
def focusInEvent(self, evt: QFocusEvent) -> None:
|
||||
def focusInEvent(self, evt: QFocusEvent | None) -> None:
|
||||
QLineEdit.focusInEvent(self, evt)
|
||||
|
||||
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||
def keyPressEvent(self, evt: QKeyEvent | None) -> None:
|
||||
assert evt is not None
|
||||
popup = self._completer.popup()
|
||||
assert popup is not None
|
||||
|
||||
if evt.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down):
|
||||
# show completer on arrow key up/down
|
||||
if not self._completer.popup().isVisible():
|
||||
if not popup.isVisible():
|
||||
self.showCompleter()
|
||||
return
|
||||
if (
|
||||
|
@ -56,24 +60,21 @@ class TagEdit(QLineEdit):
|
|||
and evt.modifiers() & Qt.KeyboardModifier.ControlModifier
|
||||
):
|
||||
# select next completion
|
||||
if not self._completer.popup().isVisible():
|
||||
if not popup.isVisible():
|
||||
self.showCompleter()
|
||||
index = self._completer.currentIndex()
|
||||
self._completer.popup().setCurrentIndex(index)
|
||||
popup.setCurrentIndex(index)
|
||||
cur_row = index.row()
|
||||
if not self._completer.setCurrentRow(cur_row + 1):
|
||||
self._completer.setCurrentRow(0)
|
||||
return
|
||||
if (
|
||||
evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return)
|
||||
and self._completer.popup().isVisible()
|
||||
):
|
||||
if evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) and popup.isVisible():
|
||||
# apply first completion if no suggestion selected
|
||||
selected_row = self._completer.popup().currentIndex().row()
|
||||
selected_row = popup.currentIndex().row()
|
||||
if selected_row == -1:
|
||||
self._completer.setCurrentRow(0)
|
||||
index = self._completer.currentIndex()
|
||||
self._completer.popup().setCurrentIndex(index)
|
||||
popup.setCurrentIndex(index)
|
||||
self.hideCompleter()
|
||||
QWidget.keyPressEvent(self, evt)
|
||||
return
|
||||
|
@ -97,15 +98,19 @@ class TagEdit(QLineEdit):
|
|||
self._completer.setCompletionPrefix(self.text())
|
||||
self._completer.complete()
|
||||
|
||||
def focusOutEvent(self, evt: QFocusEvent) -> None:
|
||||
def focusOutEvent(self, evt: QFocusEvent | None) -> None:
|
||||
QLineEdit.focusOutEvent(self, evt)
|
||||
self.lostFocus.emit() # type: ignore
|
||||
self._completer.popup().hide()
|
||||
popup = self._completer.popup()
|
||||
assert popup is not None
|
||||
popup.hide()
|
||||
|
||||
def hideCompleter(self) -> None:
|
||||
if sip.isdeleted(self._completer): # type: ignore
|
||||
return
|
||||
self._completer.popup().hide()
|
||||
popup = self._completer.popup()
|
||||
assert popup is not None
|
||||
popup.hide()
|
||||
|
||||
|
||||
class TagCompleter(QCompleter):
|
||||
|
@ -120,7 +125,9 @@ class TagCompleter(QCompleter):
|
|||
self.edit = edit
|
||||
self.cursor: int | None = None
|
||||
|
||||
def splitPath(self, tags: str) -> list[str]:
|
||||
def splitPath(self, tags: str | None) -> list[str]:
|
||||
assert tags is not None
|
||||
assert self.edit.col is not None
|
||||
stripped_tags = tags.strip()
|
||||
stripped_tags = re.sub(" +", " ", stripped_tags)
|
||||
self.tags = self.edit.col.tags.split(stripped_tags)
|
||||
|
|
|
@ -50,7 +50,9 @@ class TagLimit(QDialog):
|
|||
list.addItem(item)
|
||||
if select:
|
||||
idx = list.indexFromItem(item)
|
||||
list.selectionModel().select(
|
||||
list_selection_model = list.selectionModel()
|
||||
assert list_selection_model is not None
|
||||
list_selection_model.select(
|
||||
idx, QItemSelectionModel.SelectionFlag.Select
|
||||
)
|
||||
|
||||
|
@ -77,12 +79,16 @@ class TagLimit(QDialog):
|
|||
if want_active:
|
||||
item = self.form.activeList.item(c)
|
||||
idx = self.form.activeList.indexFromItem(item)
|
||||
if self.form.activeList.selectionModel().isSelected(idx):
|
||||
active_list_selection_model = self.form.activeList.selectionModel()
|
||||
assert active_list_selection_model is not None
|
||||
if active_list_selection_model.isSelected(idx):
|
||||
include_tags.append(tag.name)
|
||||
# inactive
|
||||
item = self.form.inactiveList.item(c)
|
||||
idx = self.form.inactiveList.indexFromItem(item)
|
||||
if self.form.inactiveList.selectionModel().isSelected(idx):
|
||||
inactive_list_selection_model = self.form.inactiveList.selectionModel()
|
||||
assert inactive_list_selection_model is not None
|
||||
if inactive_list_selection_model.isSelected(idx):
|
||||
exclude_tags.append(tag.name)
|
||||
|
||||
if (len(include_tags) + len(exclude_tags)) > 100:
|
||||
|
|
|
@ -231,7 +231,9 @@ class ThemeManager:
|
|||
self._current_widget_style = new_widget_style
|
||||
app = aqt.mw.app
|
||||
if not self._default_style:
|
||||
self._default_style = app.style().objectName()
|
||||
style = app.style()
|
||||
assert style is not None
|
||||
self._default_style = style.objectName()
|
||||
self._apply_palette(app)
|
||||
self._apply_style(app)
|
||||
gui_hooks.theme_did_change()
|
||||
|
|
|
@ -37,7 +37,9 @@ class BottomToolbar:
|
|||
class ToolbarWebView(AnkiWebView):
|
||||
hide_condition: Callable[..., bool]
|
||||
|
||||
def __init__(self, mw: aqt.AnkiQt, kind: AnkiWebViewKind | None = None) -> None:
|
||||
def __init__(
|
||||
self, mw: aqt.AnkiQt, kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT
|
||||
) -> None:
|
||||
AnkiWebView.__init__(self, mw, kind=kind)
|
||||
self.mw = mw
|
||||
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
|
||||
|
@ -172,7 +174,7 @@ class TopWebView(ToolbarWebView):
|
|||
self.eval("""document.body.style.setProperty("min-height", "0px"); """)
|
||||
self.evalWithCallback("document.documentElement.offsetHeight", self._onHeight)
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
def resizeEvent(self, event: QResizeEvent | None) -> None:
|
||||
super().resizeEvent(event)
|
||||
|
||||
self.mw.web.evalWithCallback(
|
||||
|
|
|
@ -189,6 +189,7 @@ class MacTTSPlayer(TTSProcessPlayer):
|
|||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
# write the input text to stdin
|
||||
assert self._process.stdin is not None
|
||||
self._process.stdin.write(tag.field_text.encode("utf8"))
|
||||
self._process.stdin.close()
|
||||
self._wait_for_termination(tag)
|
||||
|
@ -247,6 +248,7 @@ class MacTTSFilePlayer(MacTTSPlayer):
|
|||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
# write the input text to stdin
|
||||
assert self._process.stdin is not None
|
||||
self._process.stdin.write(tag.field_text.encode("utf8"))
|
||||
self._process.stdin.close()
|
||||
self._wait_for_termination(tag)
|
||||
|
|
|
@ -52,7 +52,7 @@ def check_for_update() -> None:
|
|||
|
||||
QueryOp(parent=mw, op=do_check, success=on_done).failure(
|
||||
on_fail
|
||||
).run_in_background()
|
||||
).without_collection().run_in_background()
|
||||
|
||||
|
||||
def prompt_to_update(mw: aqt.AnkiQt, ver: str) -> None:
|
||||
|
|
101
qt/aqt/utils.py
101
qt/aqt/utils.py
|
@ -118,10 +118,13 @@ HelpPageArgument = Union["HelpPage.V", str]
|
|||
|
||||
|
||||
def openHelp(section: HelpPageArgument) -> None:
|
||||
assert tr.backend is not None
|
||||
backend = tr.backend()
|
||||
assert backend is not None
|
||||
if isinstance(section, str):
|
||||
link = tr.backend().help_page_link(page=HelpPage.INDEX) + section
|
||||
link = backend.help_page_link(page=HelpPage.INDEX) + section
|
||||
else:
|
||||
link = tr.backend().help_page_link(page=section)
|
||||
link = backend.help_page_link(page=section)
|
||||
openLink(link)
|
||||
|
||||
|
||||
|
@ -170,17 +173,20 @@ class MessageBox(QMessageBox):
|
|||
b = self.addButton(button)
|
||||
# a translator has complained the default Qt translation is inappropriate, so we override it
|
||||
if button == QMessageBox.StandardButton.Discard:
|
||||
assert b is not None
|
||||
b.setText(tr.actions_discard())
|
||||
elif isinstance(button, tuple):
|
||||
b = self.addButton(button[0], button[1])
|
||||
else:
|
||||
continue
|
||||
if callback is not None:
|
||||
assert b is not None
|
||||
qconnect(b.clicked, partial(callback, i))
|
||||
if i == default_button:
|
||||
self.setDefaultButton(b)
|
||||
if help is not None:
|
||||
b = self.addButton(QMessageBox.StandardButton.Help)
|
||||
assert b is not None
|
||||
qconnect(b.clicked, lambda: openHelp(help))
|
||||
self.open()
|
||||
|
||||
|
@ -316,9 +322,11 @@ def showInfo(
|
|||
mb.setDefaultButton(default)
|
||||
else:
|
||||
b = mb.addButton(QMessageBox.StandardButton.Ok)
|
||||
assert b is not None
|
||||
b.setDefault(True)
|
||||
if help is not None:
|
||||
b = mb.addButton(QMessageBox.StandardButton.Help)
|
||||
assert b is not None
|
||||
qconnect(b.clicked, lambda: openHelp(help))
|
||||
b.setAutoDefault(False)
|
||||
return mb.exec()
|
||||
|
@ -363,7 +371,9 @@ def showText(
|
|||
if copyBtn:
|
||||
|
||||
def onCopy() -> None:
|
||||
QApplication.clipboard().setText(text.toPlainText())
|
||||
clipboard = QApplication.clipboard()
|
||||
assert clipboard is not None
|
||||
clipboard.setText(text.toPlainText())
|
||||
|
||||
btn = QPushButton(tr.qt_misc_copy_to_clipboard())
|
||||
qconnect(btn.clicked, onCopy)
|
||||
|
@ -415,6 +425,7 @@ def askUser(
|
|||
default = QMessageBox.StandardButton.Yes
|
||||
r = msgfunc(parent, title, text, sb, default)
|
||||
if r == QMessageBox.StandardButton.Help:
|
||||
assert help is not None
|
||||
openHelp(help)
|
||||
else:
|
||||
break
|
||||
|
@ -431,7 +442,7 @@ class ButtonedDialog(QMessageBox):
|
|||
title: str = "Anki",
|
||||
):
|
||||
QMessageBox.__init__(self, parent)
|
||||
self._buttons: list[QPushButton] = []
|
||||
self._buttons: list[QPushButton | None] = []
|
||||
self.setWindowTitle(title)
|
||||
self.help = help
|
||||
self.setIcon(QMessageBox.Icon.Warning)
|
||||
|
@ -444,11 +455,13 @@ class ButtonedDialog(QMessageBox):
|
|||
|
||||
def run(self) -> str:
|
||||
self.exec()
|
||||
but = self.clickedButton().text()
|
||||
if but == "Help":
|
||||
clicked_button = self.clickedButton()
|
||||
assert clicked_button is not None
|
||||
txt = clicked_button.text()
|
||||
if txt == "Help":
|
||||
# FIXME stop dialog closing?
|
||||
assert self.help is not None
|
||||
openHelp(self.help)
|
||||
txt = self.clickedButton().text()
|
||||
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
|
||||
return txt.replace("&", "")
|
||||
|
||||
|
@ -504,13 +517,18 @@ class GetTextDialog(QDialog):
|
|||
b = QDialogButtonBox(buts) # type: ignore
|
||||
v.addWidget(b)
|
||||
self.setLayout(v)
|
||||
qconnect(b.button(QDialogButtonBox.StandardButton.Ok).clicked, self.accept)
|
||||
qconnect(b.button(QDialogButtonBox.StandardButton.Cancel).clicked, self.reject)
|
||||
ok_button = b.button(QDialogButtonBox.StandardButton.Ok)
|
||||
assert ok_button is not None
|
||||
qconnect(ok_button.clicked, self.accept)
|
||||
|
||||
cancel_button = b.button(QDialogButtonBox.StandardButton.Cancel)
|
||||
assert cancel_button is not None
|
||||
qconnect(cancel_button.clicked, self.reject)
|
||||
|
||||
if help:
|
||||
qconnect(
|
||||
b.button(QDialogButtonBox.StandardButton.Help).clicked,
|
||||
self.helpRequested,
|
||||
)
|
||||
help_button = b.button(QDialogButtonBox.StandardButton.Help)
|
||||
assert help_button is not None
|
||||
qconnect(help_button.clicked, self.helpRequested)
|
||||
self.l.setFocus()
|
||||
|
||||
def accept(self) -> None:
|
||||
|
@ -520,7 +538,8 @@ class GetTextDialog(QDialog):
|
|||
return QDialog.reject(self)
|
||||
|
||||
def helpRequested(self) -> None:
|
||||
openHelp(self.help)
|
||||
if self.help is not None:
|
||||
openHelp(self.help)
|
||||
|
||||
|
||||
def getText(
|
||||
|
@ -624,6 +643,7 @@ def getFile(
|
|||
if dir and key:
|
||||
raise Exception("expected dir or key")
|
||||
if not dir:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
dirkey = f"{key}Directory"
|
||||
dir = aqt.mw.pm.profile.get(dirkey, "")
|
||||
else:
|
||||
|
@ -635,6 +655,7 @@ def getFile(
|
|||
else QFileDialog.FileMode.ExistingFile
|
||||
)
|
||||
d.setFileMode(mode)
|
||||
assert dir is not None
|
||||
if os.path.exists(dir):
|
||||
d.setDirectory(dir)
|
||||
d.setWindowTitle(title)
|
||||
|
@ -644,6 +665,7 @@ def getFile(
|
|||
def accept() -> None:
|
||||
files = list(d.selectedFiles())
|
||||
if dirkey:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
dir = os.path.dirname(files[0])
|
||||
aqt.mw.pm.profile[dirkey] = dir
|
||||
result = files if multi else files[0]
|
||||
|
@ -683,10 +705,11 @@ def getSaveFile(
|
|||
dir_description: str,
|
||||
key: str,
|
||||
ext: str,
|
||||
fname: str | None = None,
|
||||
) -> str:
|
||||
fname: str = "",
|
||||
) -> str | None:
|
||||
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
||||
variable. The file dialog will default to open with FNAME."""
|
||||
assert aqt.mw.pm.profile is not None
|
||||
config_key = f"{dir_description}Directory"
|
||||
|
||||
defaultPath = QStandardPaths.writableLocation(
|
||||
|
@ -709,9 +732,10 @@ def getSaveFile(
|
|||
dir = os.path.dirname(file)
|
||||
aqt.mw.pm.profile[config_key] = dir
|
||||
# check if it exists
|
||||
if os.path.exists(file):
|
||||
if not askUser(tr.qt_misc_this_file_exists_are_you_sure(), parent):
|
||||
return None
|
||||
if os.path.exists(file) and not askUser(
|
||||
tr.qt_misc_this_file_exists_are_you_sure(), parent
|
||||
):
|
||||
return None
|
||||
return file
|
||||
|
||||
|
||||
|
@ -735,6 +759,7 @@ def _qt_state_key(kind: _QtStateKeyKind, key: str) -> str:
|
|||
def saveGeom(widget: QWidget, key: str) -> None:
|
||||
# restoring a fullscreen window breaks the tab functionality of 5.15
|
||||
if not widget.isFullScreen() or qtmajor == 6:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key = _qt_state_key(_QtStateKeyKind.GEOMETRY, key)
|
||||
aqt.mw.pm.profile[key] = widget.saveGeometry()
|
||||
|
||||
|
@ -745,6 +770,7 @@ def restoreGeom(
|
|||
adjustSize: bool = False,
|
||||
default_size: tuple[int, int] | None = None,
|
||||
) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key = _qt_state_key(_QtStateKeyKind.GEOMETRY, key)
|
||||
if existing_geom := aqt.mw.pm.profile.get(key):
|
||||
widget.restoreGeometry(existing_geom)
|
||||
|
@ -756,7 +782,9 @@ def restoreGeom(
|
|||
|
||||
|
||||
def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
||||
handle = widget.window().windowHandle()
|
||||
window = widget.window()
|
||||
assert window is not None
|
||||
handle = window.windowHandle()
|
||||
if not handle:
|
||||
# window has not yet been shown, retry later
|
||||
aqt.mw.progress.timer(
|
||||
|
@ -765,7 +793,9 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
|||
return
|
||||
|
||||
# ensure widget is smaller than screen bounds
|
||||
geom = handle.screen().availableGeometry()
|
||||
screen = handle.screen()
|
||||
assert screen is not None
|
||||
geom = screen.availableGeometry()
|
||||
wsize = widget.size()
|
||||
cappedWidth = min(geom.width(), wsize.width())
|
||||
cappedHeight = min(geom.height(), wsize.height())
|
||||
|
@ -784,44 +814,52 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
|||
|
||||
|
||||
def saveState(widget: QFileDialog | QMainWindow, key: str) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key = _qt_state_key(_QtStateKeyKind.STATE, key)
|
||||
aqt.mw.pm.profile[key] = widget.saveState()
|
||||
|
||||
|
||||
def restoreState(widget: QFileDialog | QMainWindow, key: str) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key = _qt_state_key(_QtStateKeyKind.STATE, key)
|
||||
if data := aqt.mw.pm.profile.get(key):
|
||||
widget.restoreState(data)
|
||||
|
||||
|
||||
def saveSplitter(widget: QSplitter, key: str) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key = _qt_state_key(_QtStateKeyKind.SPLITTER, key)
|
||||
aqt.mw.pm.profile[key] = widget.saveState()
|
||||
|
||||
|
||||
def restoreSplitter(widget: QSplitter, key: str) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key = _qt_state_key(_QtStateKeyKind.SPLITTER, key)
|
||||
if data := aqt.mw.pm.profile.get(key):
|
||||
widget.restoreState(data)
|
||||
|
||||
|
||||
def saveHeader(widget: QHeaderView, key: str) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key = _qt_state_key(_QtStateKeyKind.HEADER, key)
|
||||
aqt.mw.pm.profile[key] = widget.saveState()
|
||||
|
||||
|
||||
def restoreHeader(widget: QHeaderView, key: str) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key = _qt_state_key(_QtStateKeyKind.HEADER, key)
|
||||
if state := aqt.mw.pm.profile.get(key):
|
||||
widget.restoreState(state)
|
||||
|
||||
|
||||
def save_is_checked(widget: QCheckBox, key: str) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key += "IsChecked"
|
||||
aqt.mw.pm.profile[key] = widget.isChecked()
|
||||
|
||||
|
||||
def restore_is_checked(widget: QCheckBox, key: str) -> None:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
key += "IsChecked"
|
||||
if aqt.mw.pm.profile.get(key) is not None:
|
||||
widget.setChecked(aqt.mw.pm.profile[key])
|
||||
|
@ -847,8 +885,11 @@ def restore_combo_index_for_session(
|
|||
|
||||
|
||||
def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> str:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
name += "BoxHistory"
|
||||
text_input = comboBox.lineEdit().text()
|
||||
line_edit = comboBox.lineEdit()
|
||||
assert line_edit is not None
|
||||
text_input = line_edit.text()
|
||||
if text_input in history:
|
||||
history.remove(text_input)
|
||||
history.insert(0, text_input)
|
||||
|
@ -861,14 +902,17 @@ def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> st
|
|||
|
||||
|
||||
def restore_combo_history(comboBox: QComboBox, name: str) -> list[str]:
|
||||
assert aqt.mw.pm.profile is not None
|
||||
name += "BoxHistory"
|
||||
history = aqt.mw.pm.profile.get(name, [])
|
||||
comboBox.addItems([""] + history)
|
||||
if history:
|
||||
session_input = aqt.mw.pm.session.get(name)
|
||||
if session_input and session_input == history[0]:
|
||||
comboBox.lineEdit().setText(session_input)
|
||||
comboBox.lineEdit().selectAll()
|
||||
line_edit = comboBox.lineEdit()
|
||||
assert line_edit is not None
|
||||
line_edit.setText(session_input)
|
||||
line_edit.selectAll()
|
||||
return history
|
||||
|
||||
|
||||
|
@ -980,7 +1024,7 @@ def send_to_trash(path: Path) -> None:
|
|||
except Exception as exc:
|
||||
# Linux users may not have a trash folder set up
|
||||
print("trash failure:", path, exc)
|
||||
if path.is_dir:
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
path.unlink()
|
||||
|
@ -1005,7 +1049,8 @@ def tooltip(
|
|||
class CustomLabel(QLabel):
|
||||
silentlyClose = True
|
||||
|
||||
def mousePressEvent(self, evt: QMouseEvent) -> None:
|
||||
def mousePressEvent(self, evt: QMouseEvent | None) -> None:
|
||||
assert evt is not None
|
||||
evt.accept()
|
||||
self.hide()
|
||||
|
||||
|
@ -1074,7 +1119,7 @@ class MenuList:
|
|||
print(
|
||||
"MenuList will be removed; please copy it into your add-on's code if you need it."
|
||||
)
|
||||
self.children: list[MenuListChild] = []
|
||||
self.children: list[MenuListChild | None] = []
|
||||
|
||||
def addItem(self, title: str, func: Callable) -> MenuItem:
|
||||
item = MenuItem(title, func)
|
||||
|
@ -1114,6 +1159,7 @@ class SubMenu(MenuList):
|
|||
|
||||
def renderTo(self, menu: QMenu) -> None:
|
||||
submenu = menu.addMenu(self.title)
|
||||
assert submenu is not None
|
||||
super().renderTo(submenu)
|
||||
|
||||
|
||||
|
@ -1124,6 +1170,7 @@ class MenuItem:
|
|||
|
||||
def renderTo(self, qmenu: QMenu) -> None:
|
||||
a = qmenu.addAction(self.title)
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.func)
|
||||
|
||||
|
||||
|
|
|
@ -8,9 +8,9 @@ import json
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Sequence
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Optional, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import anki
|
||||
import anki.lang
|
||||
|
@ -89,17 +89,23 @@ class AnkiWebPage(QWebEnginePage):
|
|||
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
|
||||
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
|
||||
script.setRunsOnSubFrames(False)
|
||||
self.profile().scripts().insert(script)
|
||||
|
||||
profile = self.profile()
|
||||
assert profile is not None
|
||||
scripts = profile.scripts()
|
||||
assert scripts is not None
|
||||
scripts.insert(script)
|
||||
|
||||
def javaScriptConsoleMessage(
|
||||
self,
|
||||
level: QWebEnginePage.JavaScriptConsoleMessageLevel,
|
||||
msg: str,
|
||||
msg: str | None,
|
||||
line: int,
|
||||
srcID: str,
|
||||
srcID: str | None,
|
||||
) -> None:
|
||||
# not translated because console usually not visible,
|
||||
# and may only accept ascii text
|
||||
assert srcID is not None
|
||||
if srcID.startswith("data"):
|
||||
srcID = ""
|
||||
else:
|
||||
|
@ -162,10 +168,16 @@ class AnkiWebPage(QWebEnginePage):
|
|||
def _onCmd(self, str: str) -> Any:
|
||||
return self._onBridgeCmd(str)
|
||||
|
||||
def javaScriptAlert(self, frame: Any, text: str) -> None:
|
||||
def javaScriptAlert(self, frame: Any, text: str | None) -> None:
|
||||
if text is None:
|
||||
return
|
||||
|
||||
showInfo(text)
|
||||
|
||||
def javaScriptConfirm(self, frame: Any, text: str) -> bool:
|
||||
def javaScriptConfirm(self, frame: Any, text: str | None) -> bool:
|
||||
if text is None:
|
||||
return False
|
||||
|
||||
return askUser(text)
|
||||
|
||||
|
||||
|
@ -285,7 +297,7 @@ class AnkiWebView(QWebEngineView):
|
|||
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
||||
|
||||
self._domDone = True
|
||||
self._pendingActions: list[Callable[[], None]] = []
|
||||
self._pendingActions: list[tuple[str, Sequence[Any]]] = []
|
||||
self.requiresCol = True
|
||||
self.setPage(self._page)
|
||||
self._disable_zoom = False
|
||||
|
@ -328,7 +340,9 @@ class AnkiWebView(QWebEngineView):
|
|||
# with target="_blank") and return view
|
||||
return AnkiWebView()
|
||||
|
||||
def eventFilter(self, obj: QObject, evt: QEvent) -> bool:
|
||||
def eventFilter(self, obj: QObject | None, evt: QEvent | None) -> bool:
|
||||
if evt is None:
|
||||
return False
|
||||
if self._disable_zoom and is_gesture_or_zoom_event(evt):
|
||||
return True
|
||||
|
||||
|
@ -377,31 +391,34 @@ class AnkiWebView(QWebEngineView):
|
|||
def onSelectAll(self) -> None:
|
||||
self.triggerPageAction(QWebEnginePage.WebAction.SelectAll)
|
||||
|
||||
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
|
||||
def contextMenuEvent(self, evt: QContextMenuEvent | None) -> None:
|
||||
m = QMenu(self)
|
||||
self._maybe_add_copy_action(m)
|
||||
gui_hooks.webview_will_show_context_menu(self, m)
|
||||
m.popup(QCursor.pos())
|
||||
if m.actions():
|
||||
m.popup(QCursor.pos())
|
||||
|
||||
def _maybe_add_copy_action(self, menu: QMenu) -> None:
|
||||
if self.hasSelection():
|
||||
a = menu.addAction(tr.actions_copy())
|
||||
assert a is not None
|
||||
qconnect(a.triggered, self.onCopy)
|
||||
|
||||
def dropEvent(self, evt: QDropEvent) -> None:
|
||||
def dropEvent(self, evt: QDropEvent | None) -> None:
|
||||
if self.allow_drops:
|
||||
super().dropEvent(evt)
|
||||
|
||||
def setHtml( # type: ignore[override]
|
||||
self, html: str, context: PageContext | None = None
|
||||
) -> None:
|
||||
from aqt.mediasrv import PageContext
|
||||
|
||||
# discard any previous pending actions
|
||||
self._pendingActions = []
|
||||
self._domDone = True
|
||||
if context is None:
|
||||
context = PageContext.UNKNOWN
|
||||
self._queueAction(lambda: self._setHtml(html, context))
|
||||
self._queueAction("setHtml", html, context)
|
||||
self.set_open_links_externally(True)
|
||||
self.allow_drops = False
|
||||
self.show()
|
||||
|
@ -453,7 +470,9 @@ class AnkiWebView(QWebEngineView):
|
|||
return 1
|
||||
|
||||
def setPlaybackRequiresGesture(self, value: bool) -> None:
|
||||
self.settings().setAttribute(
|
||||
settings = self.settings()
|
||||
assert settings is not None
|
||||
settings.setAttribute(
|
||||
QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture, value
|
||||
)
|
||||
|
||||
|
@ -630,10 +649,13 @@ html {{ {font} }}
|
|||
def eval(self, js: str) -> None:
|
||||
self.evalWithCallback(js, None)
|
||||
|
||||
def evalWithCallback(self, js: str, cb: Optional[Callable]) -> None:
|
||||
self._queueAction(lambda: self._evalWithCallback(js, cb))
|
||||
def evalWithCallback(self, js: str, cb: Callable | None) -> None:
|
||||
self._queueAction("eval", js, cb)
|
||||
|
||||
def _evalWithCallback(self, js: str, cb: Callable[[Any], Any] | None) -> None:
|
||||
page = self.page()
|
||||
assert page is not None
|
||||
|
||||
def _evalWithCallback(self, js: str, cb: Optional[Callable[[Any], Any]]) -> None:
|
||||
if cb:
|
||||
|
||||
def handler(val: Any) -> None:
|
||||
|
@ -642,20 +664,26 @@ html {{ {font} }}
|
|||
return
|
||||
cb(val)
|
||||
|
||||
self.page().runJavaScript(js, handler)
|
||||
page.runJavaScript(js, handler)
|
||||
else:
|
||||
self.page().runJavaScript(js)
|
||||
page.runJavaScript(js)
|
||||
|
||||
def _queueAction(self, action: Callable[[], None]) -> None:
|
||||
self._pendingActions.append(action)
|
||||
def _queueAction(self, name: str, *args: Any) -> None:
|
||||
self._pendingActions.append((name, args))
|
||||
self._maybeRunActions()
|
||||
|
||||
def _maybeRunActions(self) -> None:
|
||||
if sip.isdeleted(self):
|
||||
return
|
||||
while self._pendingActions and self._domDone:
|
||||
action = self._pendingActions.pop(0)
|
||||
action()
|
||||
name, args = self._pendingActions.pop(0)
|
||||
|
||||
if name == "eval":
|
||||
self._evalWithCallback(*args)
|
||||
elif name == "setHtml":
|
||||
self._setHtml(*args)
|
||||
else:
|
||||
raise Exception(f"unknown action: {name}")
|
||||
|
||||
def _openLinksExternally(self, url: str) -> None:
|
||||
openLink(url)
|
||||
|
@ -677,7 +705,9 @@ html {{ {font} }}
|
|||
return
|
||||
|
||||
if not self._filterSet:
|
||||
self.focusProxy().installEventFilter(self)
|
||||
focus_proxy = self.focusProxy()
|
||||
assert focus_proxy is not None
|
||||
focus_proxy.installEventFilter(self)
|
||||
self._filterSet = True
|
||||
|
||||
if cmd == "domDone":
|
||||
|
|
9
qt/bundle/Cargo.lock
generated
9
qt/bundle/Cargo.lock
generated
|
@ -202,12 +202,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.22"
|
||||
version = "0.1.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1b8479c593dba88c2741fc50b92e13dbabbbe0bd504d979f244ccc1a5b1c01"
|
||||
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cty",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -267,9 +268,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.26"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb74897ce508e6c49156fd1476fc5922cbc6e75183c65e399c765a09122e5130"
|
||||
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
|
|
|
@ -103,7 +103,6 @@ tracing-subscriber.workspace = true
|
|||
unic-ucd-category.workspace = true
|
||||
unicase.workspace = true
|
||||
unicode-normalization.workspace = true
|
||||
utime.workspace = true
|
||||
zip.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ pub enum FileOp {
|
|||
Sync,
|
||||
Metadata,
|
||||
DecodeUtf8Filename,
|
||||
SetFileTimes,
|
||||
/// For legacy errors without any context.
|
||||
Unknown,
|
||||
}
|
||||
|
@ -61,6 +62,7 @@ impl FileIoError {
|
|||
FileOp::Sync => "sync".into(),
|
||||
FileOp::Metadata => "get metadata".into(),
|
||||
FileOp::DecodeUtf8Filename => "decode utf8 filename".into(),
|
||||
FileOp::SetFileTimes => "set file times".into(),
|
||||
},
|
||||
self.path.to_string_lossy(),
|
||||
self.source
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
mod error;
|
||||
|
||||
use std::fs::File;
|
||||
use std::fs::FileTimes;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::io::Seek;
|
||||
use std::io::Write;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
@ -37,6 +40,13 @@ pub fn open_file(path: impl AsRef<Path>) -> Result<File> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn open_file_ext(path: impl AsRef<Path>, options: OpenOptions) -> Result<File> {
|
||||
options.open(&path).context(FileIoSnafu {
|
||||
path: path.as_ref(),
|
||||
op: FileOp::Open,
|
||||
})
|
||||
}
|
||||
|
||||
/// See [std::fs::write].
|
||||
pub fn write_file(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
|
||||
std::fs::write(&path, contents).context(FileIoSnafu {
|
||||
|
@ -45,6 +55,45 @@ pub fn write_file(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<
|
|||
})
|
||||
}
|
||||
|
||||
pub fn write_file_and_flush(
|
||||
path: impl AsRef<Path> + Clone,
|
||||
contents: impl AsRef<[u8]>,
|
||||
) -> Result<()> {
|
||||
let mut file = create_file(path.clone())?;
|
||||
file.write_all(contents.as_ref()).context(FileIoSnafu {
|
||||
path: path.clone().as_ref(),
|
||||
op: FileOp::Write,
|
||||
})?;
|
||||
file.sync_all().context(FileIoSnafu {
|
||||
path: path.as_ref(),
|
||||
op: FileOp::Sync,
|
||||
})
|
||||
}
|
||||
|
||||
/// See [File::set_times].
|
||||
pub fn set_file_times(path: impl AsRef<Path>, times: FileTimes) -> Result<()> {
|
||||
#[cfg(not(windows))]
|
||||
let file = open_file(&path)?;
|
||||
|
||||
#[cfg(windows)]
|
||||
let file = {
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
open_file_ext(
|
||||
&path,
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
// It's required to modify the time attributes of a directory in windows system.
|
||||
.custom_flags(0x02000000) // FILE_FLAG_BACKUP_SEMANTICS
|
||||
.to_owned(),
|
||||
)?
|
||||
};
|
||||
|
||||
file.set_times(times).context(FileIoSnafu {
|
||||
path: path.as_ref(),
|
||||
op: FileOp::SetFileTimes,
|
||||
})
|
||||
}
|
||||
|
||||
/// See [std::fs::remove_file].
|
||||
#[allow(dead_code)]
|
||||
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
|
||||
|
|
|
@ -29,13 +29,13 @@ pub fn write_rust_protos(descriptors_path: PathBuf) -> Result<DescriptorPool> {
|
|||
)
|
||||
.type_attribute(
|
||||
"Deck.Normal.DayLimit",
|
||||
"#[derive(Copy, Eq, serde::Deserialize, serde::Serialize)]",
|
||||
"#[derive(Eq, serde::Deserialize, serde::Serialize)]",
|
||||
)
|
||||
.type_attribute("HelpPageLinkRequest.HelpPage", "#[derive(strum::EnumIter)]")
|
||||
.type_attribute("CsvMetadata.Delimiter", "#[derive(strum::EnumIter)]")
|
||||
.type_attribute(
|
||||
"Preferences.BackupLimits",
|
||||
"#[derive(Copy, serde::Deserialize, serde::Serialize)]",
|
||||
"#[derive(serde::Deserialize, serde::Serialize)]",
|
||||
)
|
||||
.type_attribute(
|
||||
"CsvMetadata.DupeResolution",
|
||||
|
|
|
@ -11,7 +11,7 @@ use serde::Serialize;
|
|||
|
||||
use crate::ankihub::login::LoginRequest;
|
||||
|
||||
static API_VERSION: &str = "18.0";
|
||||
static API_VERSION: &str = "19.0";
|
||||
static DEFAULT_API_URL: &str = "https://app.ankihub.net/api/";
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -38,6 +38,7 @@ impl From<BoolKeyProto> for BoolKey {
|
|||
BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards,
|
||||
BoolKeyProto::RenderLatex => BoolKey::RenderLatex,
|
||||
BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled,
|
||||
BoolKeyProto::FsrsShortTermWithStepsEnabled => BoolKey::FsrsShortTermWithStepsEnabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ impl AnkiError {
|
|||
AnkiError::InvalidId => Kind::InvalidInput,
|
||||
AnkiError::InvalidMethodIndex
|
||||
| AnkiError::InvalidServiceIndex
|
||||
| AnkiError::FsrsWeightsInvalid
|
||||
| AnkiError::FsrsParamsInvalid
|
||||
| AnkiError::FsrsUnableToDetermineDesiredRetention
|
||||
| AnkiError::FsrsInsufficientData => Kind::InvalidInput,
|
||||
#[cfg(windows)]
|
||||
|
@ -66,6 +66,7 @@ impl From<SyncErrorKind> for Kind {
|
|||
fn from(err: SyncErrorKind) -> Self {
|
||||
match err {
|
||||
SyncErrorKind::AuthFailed => Kind::SyncAuthError,
|
||||
SyncErrorKind::ServerMessage => Kind::SyncServerMessage,
|
||||
_ => Kind::SyncOtherError,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,12 +111,12 @@ impl Card {
|
|||
/// Returns the card's due date as a timestamp if it has one.
|
||||
fn due_time(&self, timing: &SchedTimingToday) -> Option<TimestampSecs> {
|
||||
if self.queue == CardQueue::Learn {
|
||||
Some(TimestampSecs(self.due as i64))
|
||||
Some(TimestampSecs(self.original_or_current_due() as i64))
|
||||
} else if self.is_due_in_days() {
|
||||
Some(
|
||||
TimestampSecs::now().adding_secs(
|
||||
((self.original_or_current_due() - timing.days_elapsed as i32)
|
||||
.saturating_mul(86400)) as i64,
|
||||
(self.original_or_current_due() as i64 - timing.days_elapsed as i64)
|
||||
.saturating_mul(86400),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
@ -128,12 +128,15 @@ impl Card {
|
|||
/// date' or an add-on has changed the due date, this won't be accurate.
|
||||
pub(crate) fn days_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> {
|
||||
if !self.is_due_in_days() {
|
||||
Some((timing.next_day_at.0 as u32).saturating_sub(self.due.max(0) as u32) / 86_400)
|
||||
Some(
|
||||
(timing.next_day_at.0 as u32).saturating_sub(self.original_or_current_due() as u32)
|
||||
/ 86_400,
|
||||
)
|
||||
} else {
|
||||
self.due_time(timing).map(|due| {
|
||||
due.adding_secs(-86_400 * self.interval as i64)
|
||||
.elapsed_secs() as u32
|
||||
/ 86_400
|
||||
(due.adding_secs(-86_400 * self.interval as i64)
|
||||
.elapsed_secs()
|
||||
/ 86_400) as u32
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ pub enum BoolKey {
|
|||
WithDeckConfigs,
|
||||
Fsrs,
|
||||
LoadBalancerEnabled,
|
||||
FsrsShortTermWithStepsEnabled,
|
||||
#[strum(to_string = "normalize_note_text")]
|
||||
NormalizeNoteText,
|
||||
#[strum(to_string = "dayLearnFirst")]
|
||||
|
|
|
@ -74,11 +74,12 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
|
|||
bury_new: false,
|
||||
bury_reviews: false,
|
||||
bury_interday_learning: false,
|
||||
fsrs_weights: vec![],
|
||||
fsrs_params_4: vec![],
|
||||
fsrs_params_5: vec![],
|
||||
desired_retention: 0.9,
|
||||
other: Vec::new(),
|
||||
historical_retention: 0.9,
|
||||
weight_search: String::new(),
|
||||
param_search: String::new(),
|
||||
ignore_revlogs_before_date: String::new(),
|
||||
easy_days_percentages: Vec::new(),
|
||||
};
|
||||
|
@ -105,6 +106,15 @@ impl DeckConfig {
|
|||
self.mtime_secs = TimestampSecs::now();
|
||||
self.usn = usn;
|
||||
}
|
||||
|
||||
/// Retrieve the FSRS 5.0 params, falling back on 4.x ones.
|
||||
pub fn fsrs_params(&self) -> &Vec<f32> {
|
||||
if self.inner.fsrs_params_5.len() == 19 {
|
||||
&self.inner.fsrs_params_5
|
||||
} else {
|
||||
&self.inner.fsrs_params_4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
|
|
|
@ -69,8 +69,10 @@ pub struct DeckConfSchema11 {
|
|||
#[serde(default)]
|
||||
bury_interday_learning: bool,
|
||||
|
||||
#[serde(default, rename = "fsrsWeights")]
|
||||
fsrs_params_4: Vec<f32>,
|
||||
#[serde(default)]
|
||||
fsrs_weights: Vec<f32>,
|
||||
fsrs_params_5: Vec<f32>,
|
||||
#[serde(default)]
|
||||
desired_retention: f32,
|
||||
#[serde(default)]
|
||||
|
@ -92,8 +94,8 @@ pub struct DeckConfSchema11 {
|
|||
#[serde(default)]
|
||||
/// historical retention
|
||||
sm2_retention: f32,
|
||||
#[serde(default)]
|
||||
weight_search: String,
|
||||
#[serde(default, rename = "weightSearch")]
|
||||
param_search: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
other: HashMap<String, Value>,
|
||||
|
@ -306,10 +308,11 @@ impl Default for DeckConfSchema11 {
|
|||
new_sort_order: 0,
|
||||
new_gather_priority: 0,
|
||||
bury_interday_learning: false,
|
||||
fsrs_weights: vec![],
|
||||
fsrs_params_4: vec![],
|
||||
fsrs_params_5: vec![],
|
||||
desired_retention: 0.9,
|
||||
sm2_retention: 0.9,
|
||||
weight_search: "".to_string(),
|
||||
param_search: "".to_string(),
|
||||
ignore_revlogs_before_date: "".to_string(),
|
||||
easy_days_percentages: vec![1.0; 7],
|
||||
}
|
||||
|
@ -386,12 +389,13 @@ impl From<DeckConfSchema11> for DeckConfig {
|
|||
bury_new: c.new.bury,
|
||||
bury_reviews: c.rev.bury,
|
||||
bury_interday_learning: c.bury_interday_learning,
|
||||
fsrs_weights: c.fsrs_weights,
|
||||
fsrs_params_4: c.fsrs_params_4,
|
||||
fsrs_params_5: c.fsrs_params_5,
|
||||
ignore_revlogs_before_date: c.ignore_revlogs_before_date,
|
||||
easy_days_percentages: c.easy_days_percentages,
|
||||
desired_retention: c.desired_retention,
|
||||
historical_retention: c.sm2_retention,
|
||||
weight_search: c.weight_search,
|
||||
param_search: c.param_search,
|
||||
other: other_bytes,
|
||||
},
|
||||
}
|
||||
|
@ -498,10 +502,11 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
|||
new_sort_order: i.new_card_sort_order,
|
||||
new_gather_priority: i.new_card_gather_priority,
|
||||
bury_interday_learning: i.bury_interday_learning,
|
||||
fsrs_weights: i.fsrs_weights,
|
||||
fsrs_params_4: i.fsrs_params_4,
|
||||
fsrs_params_5: i.fsrs_params_5,
|
||||
desired_retention: i.desired_retention,
|
||||
sm2_retention: i.historical_retention,
|
||||
weight_search: i.weight_search,
|
||||
param_search: i.param_search,
|
||||
ignore_revlogs_before_date: i.ignore_revlogs_before_date,
|
||||
easy_days_percentages: i.easy_days_percentages,
|
||||
}
|
||||
|
@ -526,6 +531,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
|
|||
"interdayLearningMix",
|
||||
"newGatherPriority",
|
||||
"fsrsWeights",
|
||||
"fsrsParams5",
|
||||
"desiredRetention",
|
||||
"stopTimerOnAnswer",
|
||||
"secondsToShowQuestion",
|
||||
|
|
|
@ -21,7 +21,7 @@ use crate::decks::NormalDeck;
|
|||
use crate::prelude::*;
|
||||
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry;
|
||||
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
|
||||
use crate::scheduler::fsrs::weights::ignore_revlogs_before_ms_from_config;
|
||||
use crate::scheduler::fsrs::params::ignore_revlogs_before_ms_from_config;
|
||||
use crate::search::JoinSearches;
|
||||
use crate::search::Negated;
|
||||
use crate::search::SearchNode;
|
||||
|
@ -50,7 +50,7 @@ impl Collection {
|
|||
deck: DeckId,
|
||||
) -> Result<anki_proto::deck_config::DeckConfigsForUpdate> {
|
||||
let mut defaults = DeckConfig::default();
|
||||
defaults.inner.fsrs_weights = DEFAULT_PARAMETERS.into();
|
||||
defaults.inner.fsrs_params_5 = DEFAULT_PARAMETERS.into();
|
||||
let last_optimize = self.get_config_i32(I32ConfigKey::LastFsrsOptimize) as u32;
|
||||
let days_since_last_fsrs_optimize = if last_optimize > 0 {
|
||||
self.timing_today()?
|
||||
|
@ -88,6 +88,12 @@ impl Collection {
|
|||
// grab the config and sort it
|
||||
let mut config = self.storage.all_deck_config()?;
|
||||
config.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
||||
// pre-fill empty fsrs 5 params with 4 params
|
||||
config.iter_mut().for_each(|c| {
|
||||
if c.inner.fsrs_params_5.is_empty() {
|
||||
c.inner.fsrs_params_5 = c.inner.fsrs_params_4.clone();
|
||||
}
|
||||
});
|
||||
|
||||
// combine with use counts
|
||||
let counts = self.get_deck_config_use_counts()?;
|
||||
|
@ -153,14 +159,20 @@ impl Collection {
|
|||
configs_after_update.remove(dcid);
|
||||
}
|
||||
|
||||
if req.mode == UpdateDeckConfigsMode::ComputeAllWeights {
|
||||
self.compute_all_weights(&mut req)?;
|
||||
if req.mode == UpdateDeckConfigsMode::ComputeAllParams {
|
||||
self.compute_all_params(&mut req)?;
|
||||
}
|
||||
|
||||
// add/update provided configs
|
||||
for conf in &mut req.configs {
|
||||
// If the user has provided empty FSRS5 params, zero out any
|
||||
// old params as well, so we don't fall back on them, which would
|
||||
// be surprising as they're not shown in the GUI.
|
||||
if conf.inner.fsrs_params_5.is_empty() {
|
||||
conf.inner.fsrs_params_4.clear();
|
||||
}
|
||||
// check the provided parameters are valid before we save them
|
||||
FSRS::new(Some(&conf.inner.fsrs_weights))?;
|
||||
FSRS::new(Some(conf.fsrs_params()))?;
|
||||
self.add_or_update_deck_config(conf)?;
|
||||
configs_after_update.insert(conf.id, conf.clone());
|
||||
}
|
||||
|
@ -195,13 +207,13 @@ impl Collection {
|
|||
if let Ok(normal) = deck.normal() {
|
||||
let deck_id = deck.id;
|
||||
|
||||
// previous order & weights
|
||||
// previous order & params
|
||||
let previous_config_id = DeckConfigId(normal.config_id);
|
||||
let previous_config = configs_before_update.get(&previous_config_id);
|
||||
let previous_order = previous_config
|
||||
.map(|c| c.inner.new_card_insert_order())
|
||||
.unwrap_or_default();
|
||||
let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights);
|
||||
let previous_params = previous_config.map(|c| c.fsrs_params());
|
||||
let previous_retention = previous_config.map(|c| c.inner.desired_retention);
|
||||
|
||||
// if a selected (sub)deck, or its old config was removed, update deck to point
|
||||
|
@ -227,11 +239,11 @@ impl Collection {
|
|||
self.sort_deck(deck_id, current_order, usn)?;
|
||||
}
|
||||
|
||||
// if weights differ, memory state needs to be recomputed
|
||||
let current_weights = current_config.map(|c| &c.inner.fsrs_weights);
|
||||
// if params differ, memory state needs to be recomputed
|
||||
let current_params = current_config.map(|c| c.fsrs_params());
|
||||
let current_retention = current_config.map(|c| c.inner.desired_retention);
|
||||
if fsrs_toggled
|
||||
|| previous_weights != current_weights
|
||||
|| previous_params != current_params
|
||||
|| previous_retention != current_retention
|
||||
{
|
||||
decks_needing_memory_recompute
|
||||
|
@ -249,10 +261,10 @@ impl Collection {
|
|||
.into_iter()
|
||||
.map(|(conf_id, search)| {
|
||||
let config = configs_after_update.get(&conf_id);
|
||||
let weights = config.and_then(|c| {
|
||||
let params = config.and_then(|c| {
|
||||
if req.fsrs {
|
||||
Some(UpdateMemoryStateRequest {
|
||||
weights: c.inner.fsrs_weights.clone(),
|
||||
params: c.fsrs_params().clone(),
|
||||
desired_retention: c.inner.desired_retention,
|
||||
max_interval: c.inner.maximum_review_interval,
|
||||
reschedule: req.fsrs_reschedule,
|
||||
|
@ -262,12 +274,9 @@ impl Collection {
|
|||
None
|
||||
}
|
||||
});
|
||||
let search = SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search))
|
||||
.and(SearchNode::State(StateKind::Suspended).negated())
|
||||
.try_into_search()?;
|
||||
Ok(UpdateMemoryStateEntry {
|
||||
req: weights,
|
||||
search,
|
||||
req: params,
|
||||
search: SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)),
|
||||
ignore_before: config
|
||||
.map(ignore_revlogs_before_ms_from_config)
|
||||
.unwrap_or(Ok(0.into()))?,
|
||||
|
@ -320,7 +329,7 @@ impl Collection {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compute_all_weights(&mut self, req: &mut UpdateDeckConfigsRequest) -> Result<()> {
|
||||
fn compute_all_params(&mut self, req: &mut UpdateDeckConfigsRequest) -> Result<()> {
|
||||
require!(req.fsrs, "FSRS must be enabled");
|
||||
|
||||
// frontend didn't include any unmodified deck configs, so we need to fill them
|
||||
|
@ -335,28 +344,28 @@ impl Collection {
|
|||
// other parts of the code expect the currently-selected preset to come last
|
||||
req.configs.push(previous_last);
|
||||
|
||||
// calculate and apply weights to each preset
|
||||
// calculate and apply params to each preset
|
||||
let config_len = req.configs.len() as u32;
|
||||
for (idx, config) in req.configs.iter_mut().enumerate() {
|
||||
let search = if config.inner.weight_search.trim().is_empty() {
|
||||
let search = if config.inner.param_search.trim().is_empty() {
|
||||
SearchNode::Preset(config.name.clone())
|
||||
.and(SearchNode::State(StateKind::Suspended).negated())
|
||||
.try_into_search()?
|
||||
.to_string()
|
||||
} else {
|
||||
config.inner.weight_search.clone()
|
||||
config.inner.param_search.clone()
|
||||
};
|
||||
let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?;
|
||||
match self.compute_weights(
|
||||
match self.compute_params(
|
||||
&search,
|
||||
ignore_revlogs_before_ms,
|
||||
idx as u32 + 1,
|
||||
config_len,
|
||||
&config.inner.fsrs_weights,
|
||||
config.fsrs_params(),
|
||||
) {
|
||||
Ok(weights) => {
|
||||
println!("{}: {:?}", config.name, weights.weights);
|
||||
config.inner.fsrs_weights = weights.weights;
|
||||
Ok(params) => {
|
||||
println!("{}: {:?}", config.name, params.params);
|
||||
config.inner.fsrs_params_5 = params.params;
|
||||
}
|
||||
Err(AnkiError::Interrupted) => return Err(AnkiError::Interrupted),
|
||||
Err(err) => {
|
||||
|
|
|
@ -60,7 +60,12 @@ fn search_order_label(order: FilteredSearchOrder, tr: &I18n) -> String {
|
|||
FilteredSearchOrder::Added => tr.decks_order_added(),
|
||||
FilteredSearchOrder::Due => tr.decks_order_due(),
|
||||
FilteredSearchOrder::ReverseAdded => tr.decks_latest_added_first(),
|
||||
FilteredSearchOrder::DuePriority => tr.decks_relative_overdueness(),
|
||||
FilteredSearchOrder::RetrievabilityAscending => {
|
||||
tr.deck_config_sort_order_retrievability_ascending()
|
||||
}
|
||||
FilteredSearchOrder::RetrievabilityDescending => {
|
||||
tr.deck_config_sort_order_retrievability_descending()
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ pub enum AnkiError {
|
|||
},
|
||||
InvalidMethodIndex,
|
||||
InvalidServiceIndex,
|
||||
FsrsWeightsInvalid,
|
||||
FsrsParamsInvalid,
|
||||
/// Returned by fsrs-rs; may happen even if 400+ reviews
|
||||
FsrsInsufficientData,
|
||||
/// Generated by our backend if count < 400
|
||||
|
@ -148,7 +148,6 @@ impl AnkiError {
|
|||
tr.card_templates_identical_front(index + 1)
|
||||
}
|
||||
CardTypeErrorDetails::MissingCloze => tr.card_templates_missing_cloze(),
|
||||
CardTypeErrorDetails::ExtraneousCloze => tr.card_templates_extraneous_cloze(),
|
||||
};
|
||||
format!("{}<br>{}", header, details)
|
||||
}
|
||||
|
@ -181,7 +180,7 @@ impl AnkiError {
|
|||
AnkiError::FsrsInsufficientReviews { count } => {
|
||||
tr.deck_config_must_have_400_reviews(*count).into()
|
||||
}
|
||||
AnkiError::FsrsWeightsInvalid => tr.deck_config_invalid_parameters().into(),
|
||||
AnkiError::FsrsParamsInvalid => tr.deck_config_invalid_parameters().into(),
|
||||
AnkiError::SchedulerUpgradeRequired => {
|
||||
tr.scheduling_update_required().replace("V2", "v3")
|
||||
}
|
||||
|
@ -203,7 +202,6 @@ impl AnkiError {
|
|||
CardTypeErrorDetails::Duplicate { .. } => HelpPage::CardTypeDuplicate,
|
||||
CardTypeErrorDetails::NoFrontField => HelpPage::CardTypeNoFrontField,
|
||||
CardTypeErrorDetails::MissingCloze => HelpPage::CardTypeMissingCloze,
|
||||
CardTypeErrorDetails::ExtraneousCloze => HelpPage::CardTypeExtraneousCloze,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
@ -323,5 +321,4 @@ pub enum CardTypeErrorDetails {
|
|||
NoFrontField,
|
||||
NoSuchField { field: String },
|
||||
MissingCloze,
|
||||
ExtraneousCloze,
|
||||
}
|
||||
|
|
|
@ -497,9 +497,9 @@ fn maybe_set_tags_column(metadata: &mut CsvMetadata, meta_columns: &HashSet<usiz
|
|||
if metadata.tags_column == 0 {
|
||||
if let Some(CsvNotetype::GlobalNotetype(ref global)) = metadata.notetype {
|
||||
let max_field = global.field_columns.iter().max().copied().unwrap_or(0);
|
||||
for idx in (max_field + 1) as usize..metadata.column_labels.len() {
|
||||
for idx in (max_field + 1) as usize..=metadata.column_labels.len() {
|
||||
if !meta_columns.contains(&idx) {
|
||||
metadata.tags_column = max_field + 1;
|
||||
metadata.tags_column = idx as u32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -843,4 +843,23 @@ pub(in crate::import_export) mod test {
|
|||
);
|
||||
assert_eq!(metadata!(col, "\u{feff}tags:foo\n").global_tags, ["foo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_set_tags_column_if_all_are_field_columns() {
|
||||
let meta_columns = Default::default();
|
||||
let mut metadata = CsvMetadata::defaults_for_testing();
|
||||
maybe_set_tags_column(&mut metadata, &meta_columns);
|
||||
assert_eq!(metadata.tags_column, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_tags_column_to_next_unused_column() {
|
||||
let mut meta_columns = HashSet::default();
|
||||
meta_columns.insert(3);
|
||||
let mut metadata = CsvMetadata::defaults_for_testing();
|
||||
metadata.column_labels.push(String::new());
|
||||
metadata.column_labels.push(String::new());
|
||||
maybe_set_tags_column(&mut metadata, &meta_columns);
|
||||
assert_eq!(metadata.tags_column, 4);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,9 +38,6 @@ pub fn help_page_link_suffix(page: HelpPage) -> &'static str {
|
|||
"templates/errors.html#no-field-replacement-on-front-side"
|
||||
}
|
||||
HelpPage::CardTypeMissingCloze => "templates/errors.html#no-cloze-filter-on-cloze-notetype",
|
||||
HelpPage::CardTypeExtraneousCloze => {
|
||||
"templates/errors.html#cloze-filter-outside-cloze-notetype"
|
||||
}
|
||||
HelpPage::Troubleshooting => "troubleshooting.html",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -546,6 +546,7 @@ pub(crate) mod test {
|
|||
use anki_io::create_dir;
|
||||
use anki_io::read_to_string;
|
||||
use anki_io::write_file;
|
||||
use anki_io::write_file_and_flush;
|
||||
use tempfile::tempdir;
|
||||
use tempfile::TempDir;
|
||||
|
||||
|
@ -696,7 +697,7 @@ Unused: unused.jpg
|
|||
fn unicode_normalization() -> Result<()> {
|
||||
let (_dir, mgr, mut col) = common_setup()?;
|
||||
|
||||
write_file(mgr.media_folder.join("ぱぱ.jpg"), "nfd encoding")?;
|
||||
write_file_and_flush(mgr.media_folder.join("ぱぱ.jpg"), "nfd encoding")?;
|
||||
|
||||
let mut output = {
|
||||
let mut checker = col.media_checker()?;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue