Merge remote-tracking branch 'upstream/main' into cmmr-uses-simulate-config

This commit is contained in:
Luc Mcgrady 2025-04-25 11:41:44 +01:00
commit f5e0935ade
95 changed files with 674 additions and 339 deletions

View file

@ -1 +1 @@
25.02
25.05

View file

@ -222,6 +222,7 @@ ikkz <ylei.mk@gmail.com>
derivativeoflog7 <https://github.com/derivativeoflog7>
rreemmii-dev <https://github.com/rreemmii-dev>
babofitos <https://github.com/babofitos>
Jonathan Schoreels <https://github.com/JSchoreels>
********************

157
Cargo.lock generated
View file

@ -142,7 +142,7 @@ dependencies = [
"serde_tuple",
"sha1",
"snafu",
"strum",
"strum 0.26.3",
"syn 2.0.96",
"tempfile",
"tokio",
@ -218,7 +218,7 @@ dependencies = [
"prost-types",
"serde",
"snafu",
"strum",
"strum 0.26.3",
]
[[package]]
@ -574,11 +574,12 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bincode"
version = "2.0.0-rc.3"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"serde",
"unty",
]
[[package]]
@ -664,9 +665,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "burn"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55af4c56b540bcf00cf1c7e13b1c60644734906495048afbd4a79aabd0a6efbe"
checksum = "22149f3b5ab6628e9e9c0b29156b906d32d36bbf76f2c34ad5ce1801f5b4486e"
dependencies = [
"burn-core",
"burn-train",
@ -674,9 +675,9 @@ dependencies = [
[[package]]
name = "burn-autodiff"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa53181463ef16220438e240f10e1e8cb2fcf1824dbc33b8f259a454ff5f46f"
checksum = "f2167ab07f9be5f2a027accba92d8dde02ea905f35844f8529bb2533b4fc8646"
dependencies = [
"burn-common",
"burn-tensor",
@ -687,9 +688,9 @@ dependencies = [
[[package]]
name = "burn-candle"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b49a6da72c10ac552b3c023d74dade9714c10aac0fc5f33cfc4ca389463b99e"
checksum = "aeef1204c4d33dd71a9628a311178eb149131c65234eb64e8201e27cf1ee1ba0"
dependencies = [
"burn-tensor",
"candle-core",
@ -699,9 +700,9 @@ dependencies = [
[[package]]
name = "burn-common"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a1471949b06002c984df9d753a084a79149841dd7935911d9e432b8478f9fd5"
checksum = "fb516d1faa50628828b3c2b79db3e483f20d62966f7dae68c6f21743f5f7e8ef"
dependencies = [
"cubecl-common",
"getrandom 0.2.15",
@ -712,9 +713,9 @@ dependencies = [
[[package]]
name = "burn-core"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f8ebbf7d5c8bdc269260bd8e7ce08e488e6625da19b3d80ca34a729d78a77ab"
checksum = "594c44ac9f2996c2c0b92f5a44a1287d41fca3954182601a4a29b628a5973357"
dependencies = [
"ahash",
"bincode",
@ -747,9 +748,9 @@ dependencies = [
[[package]]
name = "burn-cuda"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90534d6c7f909a8cad49470921dc3eb2b118f7a3c8bde313defba3f4cae3ac3"
checksum = "08fe1e5f285214d16cfd298453b807675c9a4ed742a35c8807be42af69e8ee97"
dependencies = [
"burn-jit",
"burn-tensor",
@ -762,9 +763,9 @@ dependencies = [
[[package]]
name = "burn-dataset"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b851cb5165da57871bed2c48a29673dde0ddbd198a39b2a37411b6adf6df6ad"
checksum = "92a5cde6c09c751fb6aafca10d8e18faa42fe18eef44b4768851575c20db6904"
dependencies = [
"csv",
"derive-new 0.7.0",
@ -774,17 +775,17 @@ dependencies = [
"sanitize-filename 0.6.0",
"serde",
"serde_json",
"strum",
"strum_macros",
"strum 0.26.3",
"strum_macros 0.26.4",
"tempfile",
"thiserror 2.0.11",
]
[[package]]
name = "burn-derive"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f784ffe0df57848ba232e5f40a1c1f5df3571df59bec99ba32bc7610fd9e811"
checksum = "2bb8f828a681946b07a87750ed0593d885e7b101653bd6a3bb1942976156bb48"
dependencies = [
"derive-new 0.7.0",
"proc-macro2",
@ -794,9 +795,9 @@ dependencies = [
[[package]]
name = "burn-hip"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd9fbfee77b3d2b67bf434b883ec6ed73f4f9bf1fd8d59f9dde217c7a4b5285d"
checksum = "84191ed69af8c48a133c05e0ee4dfe73d7a3b8f96e3ceec899b4f85b19072232"
dependencies = [
"burn-jit",
"burn-tensor",
@ -809,9 +810,9 @@ dependencies = [
[[package]]
name = "burn-jit"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6b06689c4e8d6cfdcaf0b0e168e58a931c3935414e48f4e3e3e85e8d7a77a0"
checksum = "6b723ddb46032953c4fb908feca57b470b0c9839f808fcd52de04a8510b88a23"
dependencies = [
"burn-common",
"burn-tensor",
@ -831,9 +832,9 @@ dependencies = [
[[package]]
name = "burn-ndarray"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419fa3eda8cf9fddce0d156946b3d46642c10a41569b23e7855f775f862d310a"
checksum = "1b8ce3bd0f1e792b53610d291eb463d9790449688c4455a496c46266e46a179f"
dependencies = [
"atomic_float",
"burn-autodiff",
@ -842,7 +843,7 @@ dependencies = [
"derive-new 0.7.0",
"libm",
"matrixmultiply",
"ndarray 0.16.1",
"ndarray",
"num-traits",
"portable-atomic-util",
"rand",
@ -851,9 +852,9 @@ dependencies = [
[[package]]
name = "burn-router"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39bdb6d5c749221741a362da9b3ea3157304f831ab4b4a6902725a1efaea159"
checksum = "b6a1a6cb08eccb65b112bc5853ac35c88faa8b04b03270bcfb1ac8a253a066bb"
dependencies = [
"burn-common",
"burn-tensor",
@ -864,9 +865,9 @@ dependencies = [
[[package]]
name = "burn-tensor"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24db20273a636d5340e5a29af142722e0a657491e6b3cfcceb1e62eb862b3b37"
checksum = "ab959e7da2e7514b959d841c93e8e026233aa77284f5d976099a8f5251e3ba99"
dependencies = [
"burn-common",
"bytemuck",
@ -885,9 +886,9 @@ dependencies = [
[[package]]
name = "burn-train"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714298cbc0c41f48d53cb1e6aeb6203b49b6110620517f69fbcc37a9b41cb6c8"
checksum = "c004e8c761ad50c568739581a2dab38aa2f4db723183fb189f130e6d2b4e0d1c"
dependencies = [
"async-channel",
"burn-core",
@ -906,9 +907,9 @@ dependencies = [
[[package]]
name = "burn-wgpu"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ef5b6c56da563a708b2da16f0559a061e7b93f3acae63903734ee978c9b9f93"
checksum = "5f80a3413527087e73042c807d41d6fd3a1e8a28eb519e3ad5e91f6354cbfb9d"
dependencies = [
"burn-jit",
"burn-tensor",
@ -2099,19 +2100,20 @@ dependencies = [
[[package]]
name = "fsrs"
version = "3.0.0"
source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=08d90d1363b0c4722422bf0ef71ed8fd7d053f8a#08d90d1363b0c4722422bf0ef71ed8fd7d053f8a"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e39017be91629761c3a2802032eb38d226227e1a21433381da4310612199ee"
dependencies = [
"burn",
"itertools 0.12.1",
"itertools 0.14.0",
"log",
"ndarray 0.15.6",
"ndarray",
"ndarray-rand",
"priority-queue",
"rand",
"rayon",
"serde",
"snafu",
"strum",
"strum 0.27.1",
]
[[package]]
@ -3254,18 +3256,18 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.12.1"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
@ -3376,7 +3378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@ -3441,7 +3443,7 @@ dependencies = [
"linkcheck",
"regex",
"reqwest 0.12.8",
"strum",
"strum 0.26.3",
"tokio",
]
@ -3831,19 +3833,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "ndarray"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
dependencies = [
"matrixmultiply",
"num-complex",
"num-integer",
"num-traits",
"rawpointer",
]
[[package]]
name = "ndarray"
version = "0.16.1"
@ -3862,11 +3851,11 @@ dependencies = [
[[package]]
name = "ndarray-rand"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65608f937acc725f5b164dcf40f4f0bc5d67dc268ab8a649d3002606718c4588"
checksum = "f093b3db6fd194718dcdeea6bd8c829417deae904e3fcc7732dabcd4416d25d8"
dependencies = [
"ndarray 0.15.6",
"ndarray",
"rand",
"rand_distr",
]
@ -4608,9 +4597,9 @@ dependencies = [
[[package]]
name = "priority-queue"
version = "2.1.1"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d"
checksum = "ef08705fa1589a1a59aa924ad77d14722cb0cd97b67dd5004ed5f4a4873fce8d"
dependencies = [
"autocfg",
"equivalent",
@ -5561,9 +5550,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4"
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
@ -5590,9 +5579,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
@ -5869,7 +5858,16 @@ version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
"strum_macros 0.26.4",
]
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros 0.27.1",
]
[[package]]
@ -5885,6 +5883,19 @@ dependencies = [
"syn 2.0.96",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.96",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -6702,6 +6713,12 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.4"

View file

@ -35,9 +35,9 @@ git = "https://github.com/ankitects/linkcheck.git"
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs]
# version = "=2.0.3"
git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
rev = "08d90d1363b0c4722422bf0ef71ed8fd7d053f8a"
version = "3.0.0"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# rev = "c7717682997a8a6d53d97c7196281e745c5b3c8e"
# path = "../open-spaced-repetition/fsrs-rs"
[workspace.dependencies]

View file

@ -334,7 +334,7 @@
},
{
"name": "bincode",
"version": "2.0.0-rc.3",
"version": "2.0.1",
"authors": "Ty Overby <ty@pre-alpha.com>|Zoey Riordan <zoey@dos.cafe>|Victor Koenders <bincode@trangar.com>",
"repository": "https://github.com/bincode-org/bincode",
"license": "MIT",
@ -415,7 +415,7 @@
},
{
"name": "burn",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn",
"license": "Apache-2.0 OR MIT",
@ -424,7 +424,7 @@
},
{
"name": "burn-autodiff",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-autodiff",
"license": "Apache-2.0 OR MIT",
@ -433,7 +433,7 @@
},
{
"name": "burn-candle",
"version": "0.16.0",
"version": "0.16.1",
"authors": "louisfd <louisfd94@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-candle",
"license": "Apache-2.0 OR MIT",
@ -442,7 +442,7 @@
},
{
"name": "burn-common",
"version": "0.16.0",
"version": "0.16.1",
"authors": "Dilshod Tadjibaev (@antimora)",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-common",
"license": "Apache-2.0 OR MIT",
@ -451,7 +451,7 @@
},
{
"name": "burn-core",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-core",
"license": "Apache-2.0 OR MIT",
@ -460,7 +460,7 @@
},
{
"name": "burn-cuda",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-cuda",
"license": "Apache-2.0 OR MIT",
@ -469,7 +469,7 @@
},
{
"name": "burn-dataset",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-dataset",
"license": "Apache-2.0 OR MIT",
@ -478,7 +478,7 @@
},
{
"name": "burn-derive",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-derive",
"license": "Apache-2.0 OR MIT",
@ -487,7 +487,7 @@
},
{
"name": "burn-hip",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-hip",
"license": "Apache-2.0 OR MIT",
@ -496,7 +496,7 @@
},
{
"name": "burn-jit",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-jit",
"license": "Apache-2.0 OR MIT",
@ -505,7 +505,7 @@
},
{
"name": "burn-ndarray",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-ndarray",
"license": "Apache-2.0 OR MIT",
@ -514,7 +514,7 @@
},
{
"name": "burn-router",
"version": "0.16.0",
"version": "0.16.1",
"authors": "guillaumelagrange <lagrange.guillaume.1@gmail.com>|nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-router",
"license": "Apache-2.0 OR MIT",
@ -523,7 +523,7 @@
},
{
"name": "burn-tensor",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-tensor",
"license": "Apache-2.0 OR MIT",
@ -532,7 +532,7 @@
},
{
"name": "burn-train",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-train",
"license": "Apache-2.0 OR MIT",
@ -541,7 +541,7 @@
},
{
"name": "burn-wgpu",
"version": "0.16.0",
"version": "0.16.1",
"authors": "nathanielsimard <nathaniel.simard.42@gmail.com>",
"repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-wgpu",
"license": "Apache-2.0 OR MIT",
@ -2071,7 +2071,7 @@
},
{
"name": "itertools",
"version": "0.12.1",
"version": "0.13.0",
"authors": "bluss",
"repository": "https://github.com/rust-itertools/itertools",
"license": "Apache-2.0 OR MIT",
@ -2080,7 +2080,7 @@
},
{
"name": "itertools",
"version": "0.13.0",
"version": "0.14.0",
"authors": "bluss",
"repository": "https://github.com/rust-itertools/itertools",
"license": "Apache-2.0 OR MIT",
@ -2429,15 +2429,6 @@
"license_file": null,
"description": "A wrapper over a platform's native TLS implementation"
},
{
"name": "ndarray",
"version": "0.15.6",
"authors": "Ulrik Sverdrup \"bluss\"|Jim Turner",
"repository": "https://github.com/rust-ndarray/ndarray",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "An n-dimensional array for general elements and for numerics. Lightweight array views and slicing; views support chunking and splitting."
},
{
"name": "ndarray",
"version": "0.16.1",
@ -2449,7 +2440,7 @@
},
{
"name": "ndarray-rand",
"version": "0.14.0",
"version": "0.15.0",
"authors": "bluss",
"repository": "https://github.com/rust-ndarray/ndarray",
"license": "Apache-2.0 OR MIT",
@ -2971,7 +2962,7 @@
},
{
"name": "priority-queue",
"version": "2.1.1",
"version": "2.3.1",
"authors": "Gianmarco Garrisi <gianmarcogarrisi@tutanota.com>",
"repository": "https://github.com/garro95/priority-queue",
"license": "LGPL-3.0-or-later OR MPL-2.0",
@ -3574,7 +3565,7 @@
},
{
"name": "serde",
"version": "1.0.217",
"version": "1.0.219",
"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",
@ -3601,7 +3592,7 @@
},
{
"name": "serde_derive",
"version": "1.0.217",
"version": "1.0.219",
"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",
@ -3851,6 +3842,15 @@
"license_file": null,
"description": "Helpful macros for working with enums and strings"
},
{
"name": "strum",
"version": "0.27.1",
"authors": "Peter Glotfelty <peter.glotfelty@microsoft.com>",
"repository": "https://github.com/Peternator7/strum",
"license": "MIT",
"license_file": null,
"description": "Helpful macros for working with enums and strings"
},
{
"name": "strum_macros",
"version": "0.26.4",
@ -3860,6 +3860,15 @@
"license_file": null,
"description": "Helpful macros for working with enums and strings"
},
{
"name": "strum_macros",
"version": "0.27.1",
"authors": "Peter Glotfelty <peter.glotfelty@microsoft.com>",
"repository": "https://github.com/Peternator7/strum",
"license": "MIT",
"license_file": null,
"description": "Helpful macros for working with enums and strings"
},
{
"name": "subtle",
"version": "2.6.1",
@ -4427,6 +4436,15 @@
"license_file": null,
"description": "Safe, fast, zero-panic, zero-crashing, zero-allocation parsing of untrusted inputs in Rust."
},
{
"name": "unty",
"version": "0.0.4",
"authors": "Victor Koenders <bincode@trang.ar>",
"repository": "https://github.com/bincode-org/unty",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "Explicitly types your generics"
},
{
"name": "url",
"version": "2.5.4",

@ -1 +1 @@
Subproject commit b295a4c06eb07c9d588581ca1cbd9f7ec7ba27c8
Subproject commit 647e3cb0e697c51248201c4fb0df36514e765aa5

View file

@ -499,8 +499,12 @@ deck-config-desired-retention-below-optimal = Your desired retention is below op
deck-config-fsrs-simulator-experimental = FSRS simulator (experimental)
deck-config-additional-new-cards-to-simulate = Additional new cards to simulate
deck-config-simulate = Simulate
deck-config-clear-last-simulate = Clear last simulation
deck-config-clear-last-simulate = Clear Last Simulation
deck-config-fsrs-simulator-radio-count = Reviews
deck-config-advanced-settings = Advanced Settings
deck-config-smooth-graph = Smooth graph
deck-config-suspend-leeches = Suspend leeches
deck-config-save-options-to-preset = Save Changes to Preset
# Radio button in the FSRS simulation diagram (Deck options -> FSRS) selecting
# to show the total number of cards that can be recalled or retrieved on a
# specific date.

View file

@ -114,6 +114,7 @@ statistics-counts-separate-suspended-buried-cards = Separate suspended/buried ca
statistics-true-retention-title = True Retention
statistics-true-retention-subtitle = Pass rate of cards with an interval ≥ 1 day.
statistics-true-retention-tooltip = If you are using FSRS, your true retention is expected to be close to your desired retention. Please keep in mind that data for a single day is noisy, so it's better to look at monthly data.
statistics-true-retention-range = Range
statistics-true-retention-pass = Pass
statistics-true-retention-fail = Fail

@ -1 +1 @@
Subproject commit 620cb519a5b2ef623fef8fc7a0132fb1394fe3fa
Subproject commit b3562ed3594d2afa0973edb877cc2f701ff162c3

View file

@ -69,7 +69,6 @@
"bootstrap-icons": "^1.10.5",
"codemirror": "^5.63.1",
"d3": "^7.0.0",
"dompurify": "^3.2.5",
"fabric": "^5.3.0",
"hammerjs": "^2.0.8",
"intl-pluralrules": "^2.0.0",

View file

@ -50,6 +50,7 @@ message Card {
optional uint32 original_position = 18;
optional FsrsMemoryState memory_state = 20;
optional float desired_retention = 21;
optional float decay = 22;
string custom_data = 19;
}

View file

@ -122,9 +122,10 @@ message DeckConfig {
repeated float fsrs_params_4 = 3;
repeated float fsrs_params_5 = 5;
repeated float fsrs_params_6 = 6;
// consider saving remaining ones for fsrs param changes
reserved 6 to 8;
reserved 7 to 8;
uint32 new_per_day = 9;
uint32 reviews_per_day = 10;

View file

@ -33,6 +33,7 @@ service NotetypesService {
rpc GetFieldNames(NotetypeId) returns (generic.StringList);
rpc RestoreNotetypeToStock(RestoreNotetypeToStockRequest)
returns (collection.OpChanges);
rpc GetClozeFieldOrds(NotetypeId) returns (GetClozeFieldOrdsResponse);
}
// Implicitly includes any of the above methods that are not listed in the
@ -242,3 +243,7 @@ enum ClozeField {
CLOZE_FIELD_TEXT = 0;
CLOZE_FIELD_BACK_EXTRA = 1;
}
message GetClozeFieldOrdsResponse {
repeated uint32 ords = 1;
}

View file

@ -422,17 +422,16 @@ message GetOptimalRetentionParametersResponse {
uint32 learn_span = 2;
float max_cost_perday = 3;
float max_ivl = 4;
repeated float learn_costs = 5;
repeated float review_costs = 6;
repeated float first_rating_prob = 7;
repeated float review_rating_prob = 8;
repeated float first_rating_offsets = 9;
repeated float first_session_lens = 10;
float forget_rating_offset = 11;
float forget_session_len = 12;
float loss_aversion = 13;
uint32 learn_limit = 14;
uint32 review_limit = 15;
repeated float first_rating_prob = 5;
repeated float review_rating_prob = 6;
float loss_aversion = 7;
uint32 learn_limit = 8;
uint32 review_limit = 9;
repeated float learning_step_transitions = 10;
repeated float relearning_step_transitions = 11;
repeated float state_rating_costs = 12;
uint32 learning_step_count = 13;
uint32 relearning_step_count = 14;
}
message EvaluateParamsRequest {

View file

@ -65,6 +65,7 @@ message CardStatsResponse {
string preset = 21;
optional string original_deck = 22;
optional float desired_retention = 23;
repeated float fsrs_params = 24;
}
message GraphsRequest {

View file

@ -281,6 +281,10 @@ class ModelManager(DeprecatedNamesMixin):
def sort_idx(self, notetype: NotetypeDict) -> int:
return notetype["sortf"]
def cloze_fields(self, mid: NotetypeId) -> Sequence[int]:
"""The list of index of fields that are used by cloze deletion in the note type with id `mid`."""
return self.col._backend.get_cloze_field_ords(mid)
# Adding & changing fields
##################################################

View file

@ -6,3 +6,4 @@ protobuf>=4.21
requests[socks]
distro; sys_platform != "darwin" and sys_platform != "win32"
psutil; sys_platform == "win32"
typing_extensions

View file

@ -382,6 +382,10 @@ tomli==2.0.1 \
# via
# build
# pip-tools
typing-extensions==4.13.2 \
--hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
--hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
# via -r requirements.anki.in
urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168

View file

@ -597,6 +597,7 @@ typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# -r requirements.anki.in
# astroid
# black
# fluent-syntax

View file

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

View file

@ -40,7 +40,7 @@ from aqt import gui_hooks
from aqt.log import ADDON_LOGGER_PREFIX, find_addon_logger, get_addon_logs_folder
from aqt.qt import *
from aqt.utils import (
addCloseShortcut,
add_close_shortcut,
askUser,
disable_help_button,
getFile,
@ -829,7 +829,7 @@ class AddonsDialog(QDialog):
self.setAcceptDrops(True)
self.redrawAddons()
restoreGeom(self, "addons")
addCloseShortcut(self)
add_close_shortcut(self)
gui_hooks.addons_dialog_will_show(self)
self._onAddonSelectionChanged()
self.show()

View file

@ -57,6 +57,7 @@ from aqt.undo import UndoActionsInfo
from aqt.utils import (
HelpPage,
KeyboardModifiersPressed,
add_close_shortcut,
add_ellipsis_to_action_label,
current_window,
ensure_editor_saved,
@ -1092,6 +1093,7 @@ class Browser(QMainWindow):
dialog.setWindowTitle(tr.actions_grade_now())
layout = QHBoxLayout()
dialog.setLayout(layout)
add_close_shortcut(dialog)
# Add grade buttons
for ease, label in [

View file

@ -14,7 +14,7 @@ from anki.errors import NotFoundError
from anki.lang import without_unicode_isolation
from aqt.qt import *
from aqt.utils import (
addCloseShortcut,
add_close_shortcut,
disable_help_button,
qconnect,
restoreGeom,
@ -53,7 +53,7 @@ class CardInfoDialog(QDialog):
self.mw.garbage_collect_on_dialog_finish(self)
disable_help_button(self)
restoreGeom(self, self.GEOMETRY_KEY, default_size=(800, 800))
addCloseShortcut(self)
add_close_shortcut(self)
setWindowIcon(self)
self.web: AnkiWebView | None = AnkiWebView(

View file

@ -14,7 +14,6 @@ from anki.collection import SearchNode
from anki.notes import NoteId
from aqt.qt import *
from aqt.qt import sip
from aqt.webview import AnkiWebViewKind
from ..operations import QueryOp
from ..operations.tag import add_tags_to_notes
@ -52,7 +51,6 @@ class FindDuplicatesDialog(QDialog):
self._dupes: list[tuple[str, list[NoteId]]] = []
# links
form.webView.set_kind(AnkiWebViewKind.FIND_DUPLICATES)
form.webView.set_bridge_command(self._on_duplicate_clicked, context=self)
form.webView.stdHtml("", context=self)

View file

@ -29,6 +29,7 @@ class SearchContext:
browser: aqt.browser.Browser
order: bool | str | Column = True
reverse: bool = False
addon_metadata: dict | None = None
# if set, provided ids will be used instead of the regular search
ids: Sequence[ItemId] | None = None

View file

@ -269,6 +269,7 @@ class DataModel(QAbstractTableModel):
# invalid sort column in config
context.order = self.columns["noteCrt"]
context.reverse = self._state.sort_backwards
context.addon_metadata = {}
gui_hooks.browser_will_search(context)
if context.ids is None:
context.ids = self._state.find_items(

View file

@ -15,7 +15,7 @@ from anki.notes import NoteId
from aqt.operations.notetype import change_notetype_of_notes
from aqt.qt import *
from aqt.utils import (
addCloseShortcut,
add_close_shortcut,
disable_help_button,
restoreGeom,
saveGeom,
@ -49,7 +49,7 @@ class ChangeNotetypeDialog(QDialog):
self.setMinimumSize(400, 300)
disable_help_button(self)
restoreGeom(self, self.TITLE, default_size=(800, 800))
addCloseShortcut(self)
add_close_shortcut(self)
self.web = AnkiWebView(kind=AnkiWebViewKind.CHANGE_NOTETYPE)
self.web.setVisible(False)

View file

@ -27,6 +27,7 @@ from aqt.sound import av_player, play_clicked_audio
from aqt.theme import theme_manager
from aqt.utils import (
HelpPage,
add_close_shortcut,
ask_user_dialog,
askUser,
disable_help_button,
@ -90,6 +91,7 @@ class CardLayout(QDialog):
gui_hooks.card_layout_will_show(self)
self.redraw_everything()
restoreGeom(self, "CardLayout")
add_close_shortcut(self)
restoreSplitter(self.mainArea, "CardLayoutMainArea")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.show()

View file

@ -15,7 +15,7 @@ from aqt.operations import QueryOp
from aqt.operations.scheduling import custom_study
from aqt.qt import *
from aqt.taglimit import TagLimit
from aqt.utils import disable_help_button, tr
from aqt.utils import add_close_shortcut, disable_help_button, tr
RADIO_NEW = 1
RADIO_REV = 2
@ -63,6 +63,7 @@ class CustomStudy(QDialog):
self.form.setupUi(self)
disable_help_button(self)
self.setupSignals()
add_close_shortcut(self)
self.form.radioNew.click()
self.open()

View file

@ -10,7 +10,7 @@ from anki.decks import DeckDict
from aqt.operations import QueryOp
from aqt.operations.deck import update_deck_dict
from aqt.qt import *
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr
from aqt.utils import add_close_shortcut, disable_help_button, restoreGeom, saveGeom, tr
class DeckDescriptionDialog(QDialog):
@ -45,7 +45,7 @@ class DeckDescriptionDialog(QDialog):
self.setMinimumWidth(400)
disable_help_button(self)
restoreGeom(self, self.TITLE)
addCloseShortcut(self)
add_close_shortcut(self)
box = QVBoxLayout()

View file

@ -13,7 +13,7 @@ from aqt import gui_hooks
from aqt.qt import *
from aqt.utils import (
KeyboardModifiersPressed,
addCloseShortcut,
add_close_shortcut,
disable_help_button,
restoreGeom,
saveGeom,
@ -42,7 +42,7 @@ class DeckOptionsDialog(QDialog):
self.setMinimumWidth(400)
disable_help_button(self)
restoreGeom(self, self.TITLE, default_size=(800, 800))
addCloseShortcut(self)
add_close_shortcut(self)
self.web = AnkiWebView(kind=AnkiWebViewKind.DECK_OPTIONS)
self.web.load_sveltekit_page(f"deck-options/{self._deck['id']}")

View file

@ -9,7 +9,7 @@ from anki.collection import OpChanges
from anki.errors import NotFoundError
from aqt import gui_hooks
from aqt.qt import *
from aqt.utils import restoreGeom, saveGeom, tr
from aqt.utils import add_close_shortcut, restoreGeom, saveGeom, tr
class EditCurrent(QMainWindow):
@ -36,6 +36,7 @@ class EditCurrent(QMainWindow):
close_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close)
assert close_button is not None
close_button.setShortcut(QKeySequence("Ctrl+Return"))
add_close_shortcut(self)
# qt5.14+ doesn't handle numpad enter on Windows
self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self)
qconnect(self.compat_add_shorcut.activated, close_button.click)

View file

@ -320,7 +320,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
label_element = cmd
title_attribute = shortcut(title_attribute)
cmd_to_toggle_button = "toggleEditorButton(this);" if toggleable else ""
id_attribute_assignment = f"id={id}" if id else ""
class_attribute = "linkb" if rightside else "rounded"
if not disables:
@ -328,11 +327,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
return f"""<button tabindex=-1
{id_attribute_assignment}
class="{class_attribute}"
class="anki-addon-button {class_attribute}"
type="button"
title="{title_attribute}"
onclick="pycmd('{cmd}');{cmd_to_toggle_button}return false;"
onmousedown="window.event.preventDefault();"
data-cantoggle="{int(toggleable)}"
data-command="{cmd}"
>
{image_element}
{label_element}
@ -556,6 +555,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
note_type = self.note_type()
flds = note_type["flds"]
collapsed = [fld["collapsed"] for fld in flds]
cloze_fields_ords = self.mw.col.models.cloze_fields(self.note.mid)
cloze_fields = [ord in cloze_fields_ords for ord in range(len(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": note_type["mod"]}
@ -585,6 +586,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
setIsImageOcclusion({json.dumps(self.current_notetype_is_image_occlusion())});
setNotetypeMeta({json.dumps(notetype_meta)});
setCollapsed({json.dumps(collapsed)});
setClozeFields({json.dumps(cloze_fields)});
setPlainTexts({json.dumps(plain_texts)});
setDescriptions({json.dumps(descriptions)});
setFonts({json.dumps(self.fonts())});

View file

@ -15,7 +15,6 @@ from anki.collection import EmptyCardsReport
from aqt import gui_hooks
from aqt.qt import QDialog, QDialogButtonBox, qconnect
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip, tr
from aqt.webview import AnkiWebViewKind
def show_empty_cards(mw: aqt.main.AnkiQt) -> None:
@ -47,7 +46,6 @@ class EmptyCardsDialog(QDialog):
self.setWindowTitle(tr.empty_cards_window_title())
disable_help_button(self)
self.form.keep_notes.setText(tr.empty_cards_preserve_notes_checkbox())
self.form.webview.set_kind(AnkiWebViewKind.EMPTY_CARDS)
self.form.webview.set_bridge_command(self._on_note_link_clicked, self)
gui_hooks.empty_cards_will_show(self)

View file

@ -19,6 +19,7 @@ from aqt import gui_hooks
from aqt.errors import show_exception
from aqt.qt import *
from aqt.utils import (
add_close_shortcut,
checkInvalidFilename,
disable_help_button,
getSaveFile,
@ -46,6 +47,7 @@ class ExportDialog(QDialog):
self.cids = cids
disable_help_button(self)
self.setup(did)
add_close_shortcut(self)
self.exec()
def setup(self, did: DeckId | None) -> None:

View file

@ -15,6 +15,7 @@ from aqt.qt import *
from aqt.schema_change_tracker import ChangeTracker
from aqt.utils import (
HelpPage,
add_close_shortcut,
askUser,
disable_help_button,
getOnlyText,
@ -50,6 +51,7 @@ class FieldDialog(QDialog):
without_unicode_isolation(tr.fields_fields_for(val=self.model["name"]))
)
add_close_shortcut(self)
disable_help_button(self)
help_button = self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help)
assert help_button is not None

View file

@ -30,7 +30,7 @@
<number>0</number>
</property>
<item>
<widget class="AnkiWebView" name="webview" native="true">
<widget class="EmptyCardsWebView" name="webview" native="true">
<property name="url" stdset="0">
<url>
<string notr="true">about:blank</string>
@ -81,7 +81,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AnkiWebView</class>
<class>EmptyCardsWebView</class>
<extends>QWidget</extends>
<header location="global">aqt/webview</header>
<container>1</container>

View file

@ -73,7 +73,7 @@
<number>0</number>
</property>
<item>
<widget class="AnkiWebView" name="webView" native="true">
<widget class="FindDupesWebView" name="webView" native="true">
<property name="url" stdset="0">
<url>
<string notr="true">about:blank</string>
@ -98,7 +98,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AnkiWebView</class>
<class>FindDupesWebView</class>
<extends>QWidget</extends>
<header location="global">aqt/webview</header>
<container>1</container>

View file

@ -30,7 +30,7 @@
<number>0</number>
</property>
<item>
<widget class="AnkiWebView" name="web" native="true">
<widget class="StatsWebView" name="web" native="true">
<property name="url" stdset="0">
<url>
<string notr="true">about:blank</string>
@ -146,7 +146,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AnkiWebView</class>
<class>StatsWebView</class>
<extends>QWidget</extends>
<header location="global">aqt/webview</header>
<container>1</container>

View file

@ -27,6 +27,7 @@ from aqt.operations import QueryOp
from aqt.progress import ProgressUpdate
from aqt.qt import *
from aqt.utils import (
add_close_shortcut,
checkInvalidFilename,
disable_help_button,
getSaveFile,
@ -53,6 +54,7 @@ class ExportDialog(QDialog):
self.nids = nids
disable_help_button(self)
self.setup(did)
add_close_shortcut(self)
self.open()
def setup(self, did: DeckId | None) -> None:

View file

@ -12,7 +12,7 @@ import aqt.deckconf
import aqt.main
import aqt.operations
from aqt.qt import *
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr
from aqt.utils import add_close_shortcut, disable_help_button, restoreGeom, saveGeom, tr
from aqt.webview import AnkiWebView, AnkiWebViewKind
@ -62,7 +62,7 @@ class ImportDialog(QDialog):
self.setMinimumSize(*self.MIN_SIZE)
disable_help_button(self)
restoreGeom(self, self.args.title, default_size=self.DEFAULT_SIZE)
addCloseShortcut(self)
add_close_shortcut(self)
self.web: AnkiWebView | None = AnkiWebView(kind=self.args.kind)
self.web.setVisible(False)

View file

@ -141,14 +141,17 @@ class MediaServer(threading.Thread):
) -> None:
self._legacy_pages[id] = LegacyPage(html, context)
def get_page(self, id: int) -> LegacyPage | None:
return self._legacy_pages.get(id)
def get_page_html(self, id: int) -> str | None:
if page := self._legacy_pages.get(id):
if page := self.get_page(id):
return page.html
else:
return None
def get_page_context(self, id: int) -> PageContext | None:
if page := self._legacy_pages.get(id):
if page := self.get_page(id):
return page.context
else:
return None
@ -742,8 +745,21 @@ def _handle_dynamic_request(req: DynamicRequest) -> Response:
def legacy_page_data() -> Response:
id = int(request.args["id"])
if html := aqt.mw.mediaServer.get_page_html(id):
return Response(html, mimetype="text/html")
page = aqt.mw.mediaServer.get_page(id)
if page:
response = Response(page.html, mimetype="text/html")
# Prevent JS in field content from being executed in the editor, as it would
# have access to our internal API, and is a security risk.
if page.context == PageContext.EDITOR:
port = aqt.mw.mediaServer.getPort()
csp_paths = (
f"http://127.0.0.1:{port}/_anki/",
f"http://127.0.0.1:{port}/_addons/",
)
response.headers["Content-Security-Policy"] = (
f"script-src {' '.join(csp_paths)}"
)
return response
else:
return _text_response(HTTPStatus.NOT_FOUND, "page not found")
@ -752,7 +768,10 @@ _APIKEY = "".join(random.choices(string.ascii_letters + string.digits, k=32))
def _have_api_access() -> bool:
return request.headers.get("Authorization") == f"Bearer {_APIKEY}"
return (
request.headers.get("Authorization") == f"Bearer {_APIKEY}"
or os.environ.get("ANKI_API_HOST") == "0.0.0.0"
)
# this currently only handles a single method; in the future, idempotent

View file

@ -24,7 +24,7 @@ from anki.scheduler.v3 import CardAnswer
from anki.scheduler.v3 import Scheduler as V3Scheduler
from aqt.operations import CollectionOp
from aqt.qt import *
from aqt.utils import disable_help_button, getText, tooltip, tr
from aqt.utils import add_close_shortcut, disable_help_button, getText, tooltip, tr
def set_due_date_dialog(
@ -104,6 +104,7 @@ def forget_cards(
dialog = QDialog(parent)
disable_help_button(dialog)
add_close_shortcut(dialog)
form = aqt.forms.forget.Ui_Dialog()
form.setupUi(dialog)
@ -153,6 +154,7 @@ def reposition_new_cards_dialog(
dialog = QDialog(parent)
disable_help_button(dialog)
add_close_shortcut(dialog)
dialog.setWindowModality(Qt.WindowModality.WindowModal)
form = aqt.forms.reposition.Ui_Dialog()
form.setupUi(dialog)

View file

@ -22,6 +22,7 @@ from aqt.sync import sync_login
from aqt.theme import Theme
from aqt.utils import (
HelpPage,
add_close_shortcut,
askUser,
disable_help_button,
is_win,
@ -64,6 +65,7 @@ class Preferences(QDialog):
self.setup_profile()
self.setup_global()
self.setup_configurable_answer_keys()
add_close_shortcut(self)
self.show()
def setup_configurable_answer_keys(self):

View file

@ -16,7 +16,7 @@ from aqt.operations.deck import set_current_deck
from aqt.qt import *
from aqt.theme import theme_manager
from aqt.utils import (
addCloseShortcut,
add_close_shortcut,
disable_help_button,
getSaveFile,
maybeHideClose,
@ -25,7 +25,7 @@ from aqt.utils import (
tooltip,
tr,
)
from aqt.webview import AnkiWebViewKind
from aqt.webview import LegacyStatsWebView
class NewDeckStats(QDialog):
@ -69,9 +69,8 @@ class NewDeckStats(QDialog):
assert b is not None
b.setAutoDefault(False)
maybeHideClose(self.form.buttonBox)
addCloseShortcut(self)
add_close_shortcut(self)
gui_hooks.stats_dialog_will_show(self)
self.form.web.set_kind(AnkiWebViewKind.DECK_STATS)
self.form.web.hide_while_preserving_layout()
self.show()
self.refresh()
@ -154,6 +153,9 @@ class DeckStats(QDialog):
self.name = "deckStats"
self.period = 0
self.form = aqt.forms.stats.Ui_Dialog()
# Hack: Switch out web views dynamically to avoid maintaining multiple
# Qt forms for different versions of the stats dialog.
self.form.web = LegacyStatsWebView(self.mw)
self.oldPos = None
self.wholeCollection = False
self.setMinimumWidth(700)
@ -180,7 +182,7 @@ class DeckStats(QDialog):
qconnect(f.year.clicked, lambda: self.changePeriod(1))
qconnect(f.life.clicked, lambda: self.changePeriod(2))
maybeHideClose(self.form.buttonBox)
addCloseShortcut(self)
add_close_shortcut(self)
gui_hooks.stats_dialog_old_will_show(self)
self.show()
self.refresh()
@ -232,7 +234,6 @@ class DeckStats(QDialog):
stats = self.mw.col.stats()
stats.wholeCollection = self.wholeCollection
self.report = stats.report(type=self.period)
self.form.web.set_kind(AnkiWebViewKind.LEGACY_DECK_STATS)
self.form.web.stdHtml(
f"<html><body>{self.report}</body></html>",
js=["js/vendor/jquery.min.js", "js/vendor/plot.js"],

View file

@ -16,6 +16,7 @@ from aqt.qt import *
from aqt.utils import (
HelpPage,
HelpPageArgument,
add_close_shortcut,
disable_help_button,
openHelp,
restoreGeom,
@ -52,6 +53,7 @@ class StudyDeck(QDialog):
gui_hooks.state_did_reset.append(self.onReset)
self.geomKey = f"studyDeck-{geomKey}"
restoreGeom(self, self.geomKey)
add_close_shortcut(self)
disable_help_button(self)
if not cancel:
self.form.buttonBox.removeButton(

View file

@ -28,6 +28,7 @@ from aqt.qt import (
qconnect,
)
from aqt.utils import (
add_close_shortcut,
ask_user_dialog,
disable_help_button,
show_warning,
@ -380,6 +381,7 @@ def get_id_and_pass_from_user(
qconnect(bb.accepted, diag.accept)
qconnect(bb.rejected, diag.reject)
vbox.addWidget(bb)
add_close_shortcut(diag)
diag.setLayout(vbox)
diag.adjustSize()
diag.show()

View file

@ -560,6 +560,7 @@ def getText(
d = GetTextDialog(
parent, prompt, help=help, edit=edit, default=default, title=title, **kwargs
)
add_close_shortcut(d)
d.setWindowModality(Qt.WindowModality.WindowModal)
if geomKey:
restoreGeom(d, geomKey)
@ -988,14 +989,6 @@ def maybeHideClose(bbox: QDialogButtonBox) -> None:
bbox.removeButton(b)
def addCloseShortcut(widg: QDialog) -> None:
if not is_mac:
return
shortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
qconnect(shortcut.activated, widg.reject)
setattr(widg, "_closeShortcut", shortcut)
def add_close_shortcut(widg: QWidget) -> None:
if not is_mac:
return

View file

@ -10,7 +10,9 @@ import re
import sys
from collections.abc import Callable, Sequence
from enum import Enum
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, Type, cast
from typing_extensions import TypedDict, Unpack
import anki
import anki.lang
@ -360,7 +362,9 @@ class AnkiWebView(QWebEngineView):
kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT,
) -> None:
QWebEngineView.__init__(self, parent=parent)
self.set_kind(kind)
self._kind = kind
self.set_title(kind.value)
self.setPage(AnkiWebPage(self._onBridgeCmd, kind, self))
# reduce flicker
self.page().setBackgroundColor(theme_manager.qcolor(colors.CANVAS))
@ -390,17 +394,6 @@ class AnkiWebView(QWebEngineView):
"""
)
def set_kind(self, kind: AnkiWebViewKind) -> None:
self._kind = kind
self.set_title(kind.value)
# this is an ugly hack to avoid breakages caused by
# creating a default webview then immediately calling set_kind, which results
# in the creation of two pages, and the second fails as the domDone
# signal from the first one is received
if kind != AnkiWebViewKind.DEFAULT:
self.setPage(AnkiWebPage(self._onBridgeCmd, kind, self))
self.page().setBackgroundColor(theme_manager.qcolor(colors.CANVAS))
def page(self) -> AnkiWebPage:
return cast(AnkiWebPage, super().page())
@ -968,3 +961,53 @@ html {{ {font} }}
@deprecated(info="use theme_manager.qcolor() instead")
def get_window_bg_color(self, night_mode: bool | None = None) -> QColor:
return theme_manager.qcolor(colors.CANVAS)
# Pre-configured classes for use in Qt Designer
##########################################################################
class _AnkiWebViewKwargs(TypedDict, total=False):
parent: QWidget | None
title: str
kind: AnkiWebViewKind
def _create_ankiwebview_subclass(
name: str,
/,
**fixed_kwargs: Unpack[_AnkiWebViewKwargs],
) -> Type[AnkiWebView]:
def __init__(self, *args: Any, **kwargs: _AnkiWebViewKwargs) -> None:
# usersupplied kwargs override fixed kwargs
merged = cast(_AnkiWebViewKwargs, {**fixed_kwargs, **kwargs})
AnkiWebView.__init__(self, *args, **merged)
__init__.__qualname__ = f"{name}.__init__"
if fixed_kwargs:
__init__.__doc__ = (
f"Autogenerated wrapper that presets "
f"{', '.join(f'{k}={v!r}' for k, v in fixed_kwargs.items())}."
)
cls: Type[AnkiWebView] = type(name, (AnkiWebView,), {"__init__": __init__})
return cls
# These subclasses are used in Qt Designer UI files to allow for configuring
# web views at initialization time (custom widgets can otherwise only be
# initialized with the default constructor)
StatsWebView = _create_ankiwebview_subclass(
"StatsWebView", kind=AnkiWebViewKind.DECK_STATS
)
LegacyStatsWebView = _create_ankiwebview_subclass(
"LegacyStatsWebView", kind=AnkiWebViewKind.LEGACY_DECK_STATS
)
EmptyCardsWebView = _create_ankiwebview_subclass(
"EmptyCardsWebView", kind=AnkiWebViewKind.EMPTY_CARDS
)
FindDupesWebView = _create_ankiwebview_subclass(
"FindDupesWebView", kind=AnkiWebViewKind.FIND_DUPLICATES
)

View file

@ -549,6 +549,10 @@ hooks = [
You can modify context.search to change the text that is sent to the
searching backend.
If you need to pass metadata to the browser_did_search hook, you can
do it with context.addon_metadata. For example, to trigger filtering
based on a new custom filter.
If you set context.ids to a list of ids, the regular search will
not be performed, and the provided ids will be used instead.
@ -578,6 +582,9 @@ hooks = [
backend did not recognize will be returned as an empty string, and can be
replaced with custom content.
You can retrieve metadata passed from browser_will_search with
context.addon_metadata (for example to trigger post-processing filtering).
Columns is a list of string values identifying what each column in the row
represents.
""",

View file

@ -4,6 +4,7 @@
use std::sync::Arc;
use fsrs::FSRS;
use fsrs::FSRS5_DEFAULT_DECAY;
use itertools::Itertools;
use strum::Display;
use strum::EnumIter;
@ -541,10 +542,13 @@ impl RowContext {
.memory_state
.as_ref()
.zip(self.cards[0].days_since_last_review(&self.timing))
.map(|(state, days_elapsed)| {
let r = FSRS::new(None)
.unwrap()
.current_retrievability((*state).into(), days_elapsed);
.zip(Some(self.cards[0].decay.unwrap_or(FSRS5_DEFAULT_DECAY)))
.map(|((state, days_elapsed), decay)| {
let r = FSRS::new(None).unwrap().current_retrievability(
(*state).into(),
days_elapsed,
decay,
);
format!("{:.0}%", r * 100.)
})
.unwrap_or_default()

View file

@ -95,6 +95,7 @@ pub struct Card {
pub(crate) original_position: Option<u32>,
pub(crate) memory_state: Option<FsrsMemoryState>,
pub(crate) desired_retention: Option<f32>,
pub(crate) decay: Option<f32>,
/// JSON object or empty; exposed through the reviewer for persisting custom
/// state
pub(crate) custom_data: String,
@ -145,6 +146,7 @@ impl Default for Card {
original_position: None,
memory_state: None,
desired_retention: None,
decay: None,
custom_data: String::new(),
}
}

View file

@ -106,6 +106,7 @@ impl TryFrom<anki_proto::cards::Card> for Card {
original_position: c.original_position,
memory_state: c.memory_state.map(Into::into),
desired_retention: c.desired_retention,
decay: c.decay,
custom_data: c.custom_data,
})
}
@ -134,6 +135,7 @@ impl From<Card> for anki_proto::cards::Card {
original_position: c.original_position,
memory_state: c.memory_state.map(Into::into),
desired_retention: c.desired_retention,
decay: c.decay,
custom_data: c.custom_data,
}
}

View file

@ -43,6 +43,7 @@ mod mathjax_caps {
#[derive(Debug)]
enum Token<'a> {
// The parameter is the cloze number as is appears in the field content.
OpenCloze(u16),
Text(&'a str),
CloseCloze,
@ -114,6 +115,7 @@ enum TextOrCloze<'a> {
#[derive(Debug)]
struct ExtractedCloze<'a> {
// `ordinal` is the cloze number as is appears in the field content.
ordinal: u16,
nodes: Vec<TextOrCloze<'a>>,
hint: Option<&'a str>,
@ -409,12 +411,14 @@ pub fn expand_clozes_to_reveal_latex(text: &str) -> String {
buf
}
// Whether `text` contains any cloze number above 0
pub(crate) fn contains_cloze(text: &str) -> bool {
parse_text_with_clozes(text)
.iter()
.any(|node| matches!(node, TextOrCloze::Cloze(e) if e.ordinal != 0))
}
/// Returns the set of cloze number as they appear in the fields's content.
pub fn cloze_numbers_in_string(html: &str) -> HashSet<u16> {
let mut set = HashSet::with_capacity(4);
add_cloze_numbers_in_string(html, &mut set);
@ -432,11 +436,21 @@ fn add_cloze_numbers_in_text_with_clozes(nodes: &[TextOrCloze], set: &mut HashSe
}
}
/// Add to `set` the cloze numbers as they appear in `field`.
#[allow(clippy::implicit_hasher)]
pub fn add_cloze_numbers_in_string(field: &str, set: &mut HashSet<u16>) {
add_cloze_numbers_in_text_with_clozes(&parse_text_with_clozes(field), set)
}
/// The set of cloze numbers as they appear in any of the fields from `fields`.
pub fn cloze_number_in_fields(fields: impl IntoIterator<Item: AsRef<str>>) -> HashSet<u16> {
let mut set = HashSet::with_capacity(4);
for field in fields {
add_cloze_numbers_in_string(field.as_ref(), &mut set);
}
set
}
fn strip_html_inside_mathjax(text: &str) -> Cow<str> {
MATHJAX.replace_all(text, |caps: &Captures| -> String {
format!(

View file

@ -76,6 +76,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
bury_interday_learning: false,
fsrs_params_4: vec![],
fsrs_params_5: vec![],
fsrs_params_6: vec![],
desired_retention: 0.9,
other: Vec::new(),
historical_retention: 0.9,
@ -107,9 +108,11 @@ impl DeckConfig {
self.usn = usn;
}
/// Retrieve the FSRS 5.0 params, falling back on 4.x ones.
/// Retrieve the FSRS 6.0 params, falling back on 5.0 or 4.x ones.
pub fn fsrs_params(&self) -> &Vec<f32> {
if self.inner.fsrs_params_5.len() == 19 {
if self.inner.fsrs_params_6.len() == 21 {
&self.inner.fsrs_params_6
} else if self.inner.fsrs_params_5.len() == 19 {
&self.inner.fsrs_params_5
} else {
&self.inner.fsrs_params_4

View file

@ -74,6 +74,8 @@ pub struct DeckConfSchema11 {
#[serde(default)]
fsrs_params_5: Vec<f32>,
#[serde(default)]
fsrs_params_6: Vec<f32>,
#[serde(default)]
desired_retention: f32,
#[serde(default)]
ignore_revlogs_before_date: String,
@ -310,6 +312,7 @@ impl Default for DeckConfSchema11 {
bury_interday_learning: false,
fsrs_params_4: vec![],
fsrs_params_5: vec![],
fsrs_params_6: vec![],
desired_retention: 0.9,
sm2_retention: 0.9,
param_search: "".to_string(),
@ -391,6 +394,7 @@ impl From<DeckConfSchema11> for DeckConfig {
bury_interday_learning: c.bury_interday_learning,
fsrs_params_4: c.fsrs_params_4,
fsrs_params_5: c.fsrs_params_5,
fsrs_params_6: c.fsrs_params_6,
ignore_revlogs_before_date: c.ignore_revlogs_before_date,
easy_days_percentages: c.easy_days_percentages,
desired_retention: c.desired_retention,
@ -504,6 +508,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
bury_interday_learning: i.bury_interday_learning,
fsrs_params_4: i.fsrs_params_4,
fsrs_params_5: i.fsrs_params_5,
fsrs_params_6: i.fsrs_params_6,
desired_retention: i.desired_retention,
sm2_retention: i.historical_retention,
param_search: i.param_search,
@ -532,6 +537,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
"newGatherPriority",
"fsrsWeights",
"fsrsParams5",
"fsrsParams6",
"desiredRetention",
"stopTimerOnAnswer",
"secondsToShowQuestion",

View file

@ -50,7 +50,7 @@ impl Collection {
deck: DeckId,
) -> Result<anki_proto::deck_config::DeckConfigsForUpdate> {
let mut defaults = DeckConfig::default();
defaults.inner.fsrs_params_5 = DEFAULT_PARAMETERS.into();
defaults.inner.fsrs_params_6 = 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,10 +88,14 @@ 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
// pre-fill empty fsrs params with older 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();
if c.inner.fsrs_params_6.is_empty() {
c.inner.fsrs_params_6 = if c.inner.fsrs_params_5.is_empty() {
c.inner.fsrs_params_4.clone()
} else {
c.inner.fsrs_params_5.clone()
};
}
});
@ -165,10 +169,11 @@ impl Collection {
// add/update provided configs
for conf in &mut req.configs {
// If the user has provided empty FSRS5 params, zero out any
// If the user has provided empty FSRS6 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() {
if conf.inner.fsrs_params_6.is_empty() {
conf.inner.fsrs_params_5.clear();
conf.inner.fsrs_params_4.clear();
}
// check the provided parameters are valid before we save them
@ -370,7 +375,7 @@ impl Collection {
) {
Ok(params) => {
println!("{}: {:?}", config.name, params.params);
config.inner.fsrs_params_5 = params.params;
config.inner.fsrs_params_6 = params.params;
}
Err(AnkiError::Interrupted) => return Err(AnkiError::Interrupted),
Err(err) => {

View file

@ -1,8 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::collections::HashSet;
use crate::cloze::add_cloze_numbers_in_string;
use crate::cloze::cloze_number_in_fields;
use crate::collection::Collection;
use crate::decks::DeckId;
use crate::error;
@ -128,10 +126,7 @@ impl crate::services::NotesService for Collection {
&mut self,
note: anki_proto::notes::Note,
) -> error::Result<anki_proto::notes::ClozeNumbersInNoteResponse> {
let mut set = HashSet::with_capacity(4);
for field in &note.fields {
add_cloze_numbers_in_string(field, &mut set);
}
let set = cloze_number_in_fields(note.fields);
Ok(anki_proto::notes::ClozeNumbersInNoteResponse {
numbers: set.into_iter().map(|n| n as u32).collect(),
})

View file

@ -11,7 +11,7 @@ use rand::Rng;
use rand::SeedableRng;
use super::Notetype;
use crate::cloze::add_cloze_numbers_in_string;
use crate::cloze::cloze_number_in_fields;
use crate::notetype::NotetypeKind;
use crate::prelude::*;
use crate::template::ParsedTemplate;
@ -148,10 +148,7 @@ impl<N: Deref<Target = Notetype>> CardGenContext<N> {
extracted: &ExtractedCardInfo,
) -> Vec<CardToGenerate> {
// gather all cloze numbers
let mut set = HashSet::with_capacity(4);
for field in note.fields() {
add_cloze_numbers_in_string(field, &mut set);
}
let set = cloze_number_in_fields(note.fields());
set.into_iter()
.filter_map(|cloze_ord| {
let card_ord = cloze_ord.saturating_sub(1).min(499);

View file

@ -212,6 +212,21 @@ impl crate::services::NotetypesService for Collection {
)
.map(Into::into)
}
fn get_cloze_field_ords(
&mut self,
input: anki_proto::notetypes::NotetypeId,
) -> error::Result<anki_proto::notetypes::GetClozeFieldOrdsResponse> {
Ok(anki_proto::notetypes::GetClozeFieldOrdsResponse {
ords: self
.get_notetype(input.into())?
.unwrap()
.cloze_fields()
.iter()
.map(|ord| (*ord) as u32)
.collect(),
})
}
}
impl From<anki_proto::notetypes::Notetype> for Notetype {

View file

@ -7,6 +7,8 @@ use anki_proto::scheduler::ComputeMemoryStateResponse;
use fsrs::FSRSItem;
use fsrs::MemoryState;
use fsrs::FSRS;
use fsrs::FSRS5_DEFAULT_DECAY;
use fsrs::FSRS6_DEFAULT_DECAY;
use itertools::Itertools;
use super::params::ignore_revlogs_before_ms_from_config;
@ -76,6 +78,15 @@ impl Collection {
.then(|| Rescheduler::new(self))
.transpose()?;
let fsrs = FSRS::new(req.as_ref().map(|w| &w.params[..]).or(Some([].as_slice())))?;
let decay = req.as_ref().map(|w| {
if w.params.is_empty() {
FSRS6_DEFAULT_DECAY // default decay for FSRS-6
} else if w.params.len() < 21 {
FSRS5_DEFAULT_DECAY // default decay for FSRS-4.5 and FSRS-5
} else {
w.params[20]
}
});
let historical_retention = req.as_ref().map(|w| w.historical_retention);
let items = fsrs_items_for_memory_states(
&fsrs,
@ -94,6 +105,7 @@ impl Collection {
if let (Some(req), Some(item)) = (&req, item) {
card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?;
card.desired_retention = desired_retention;
card.decay = decay;
// if rescheduling
if let Some(reviews) = &last_revlog_info {
// and we have a last review time for the card

View file

@ -75,6 +75,7 @@ pub(crate) fn apply_load_balance_and_easy_days(
fn create_review_priority_fn(
review_order: ReviewCardOrder,
deck_size: usize,
params: Vec<f32>,
) -> Option<ReviewPriorityFn> {
// Helper macro to wrap closure in ReviewPriorityFn
macro_rules! wrap {
@ -91,11 +92,12 @@ fn create_review_priority_fn(
// Interval-based ordering
IntervalsAscending => wrap!(|c| c.interval as i32),
IntervalsDescending => wrap!(|c| -(c.interval as i32)),
// Retrievability-based ordering
RetrievabilityAscending => wrap!(|c| (c.retrievability() * 1000.0) as i32),
RetrievabilityAscending => {
wrap!(move |c| (c.retrievability(&params) * 1000.0) as i32)
}
RetrievabilityDescending => {
wrap!(|c| -(c.retrievability() * 1000.0) as i32)
wrap!(move |c| -(c.retrievability(&params) * 1000.0) as i32)
}
// Due date ordering
@ -123,8 +125,22 @@ impl Collection {
.col
.storage
.get_revlog_entries_for_searched_cards_in_card_order()?;
let cards = guard.col.storage.all_searched_cards()?;
let mut cards = guard.col.storage.all_searched_cards()?;
drop(guard);
fn is_included_card(c: &Card) -> bool {
c.queue != CardQueue::Suspended
&& c.queue != CardQueue::PreviewRepeat
&& c.queue != CardQueue::New
}
// calculate any missing memory state
for c in &mut cards {
if is_included_card(c) && c.memory_state.is_none() {
let original = c.clone();
let new_state = self.compute_memory_state(c.id)?.state;
c.memory_state = new_state.map(Into::into);
self.update_card_inner(c, original, self.usn()?)?;
}
}
let days_elapsed = self.timing_today().unwrap().days_elapsed as i32;
let new_cards = cards
.iter()
@ -133,7 +149,7 @@ impl Collection {
+ req.deck_size as usize;
let mut converted_cards = cards
.into_iter()
.filter(|c| c.queue != CardQueue::Suspended && c.queue != CardQueue::PreviewRepeat)
.filter(is_included_card)
.filter_map(|c| Card::convert(c, days_elapsed))
.collect_vec();
let introduced_today_count = self
@ -181,28 +197,26 @@ impl Collection {
.review_order
.try_into()
.ok()
.and_then(|order| create_review_priority_fn(order, deck_size));
.and_then(|order| create_review_priority_fn(order, deck_size, req.params.clone()));
let config = SimulatorConfig {
deck_size,
learn_span: req.days_to_simulate as usize,
max_cost_perday: f32::MAX,
max_ivl: req.max_interval as f32,
learn_costs: p.learn_costs,
review_costs: p.review_costs,
first_rating_prob: p.first_rating_prob,
review_rating_prob: p.review_rating_prob,
first_rating_offsets: p.first_rating_offsets,
first_session_lens: p.first_session_lens,
forget_rating_offset: p.forget_rating_offset,
forget_session_len: p.forget_session_len,
loss_aversion: 1.0,
learn_limit: req.new_limit as usize,
review_limit: req.review_limit as usize,
new_cards_ignore_review_limit: req.new_cards_ignore_review_limit,
suspend_after_lapses: req.suspend_after_lapse_count,
post_scheduling_fn,
review_priority_fn,
learning_step_transitions: p.learning_step_transitions,
relearning_step_transitions: p.relearning_step_transitions,
state_rating_costs: p.state_rating_costs,
learning_step_count: p.learning_step_count,
relearning_step_count: p.relearning_step_count,
};
Ok((config, converted_cards))

View file

@ -320,17 +320,31 @@ impl crate::services::SchedulerService for Collection {
learn_span: simulator_config.learn_span as u32,
max_cost_perday: simulator_config.max_cost_perday,
max_ivl: simulator_config.max_ivl,
learn_costs: simulator_config.learn_costs.to_vec(),
review_costs: simulator_config.review_costs.to_vec(),
first_rating_prob: simulator_config.first_rating_prob.to_vec(),
review_rating_prob: simulator_config.review_rating_prob.to_vec(),
first_rating_offsets: simulator_config.first_rating_offsets.to_vec(),
first_session_lens: simulator_config.first_session_lens.to_vec(),
forget_rating_offset: simulator_config.forget_rating_offset,
forget_session_len: simulator_config.forget_session_len,
loss_aversion: simulator_config.loss_aversion,
loss_aversion: 1.0,
learn_limit: simulator_config.learn_limit as u32,
review_limit: simulator_config.review_limit as u32,
learning_step_transitions: simulator_config
.learning_step_transitions
.iter()
.flatten()
.cloned()
.collect(),
relearning_step_transitions: simulator_config
.relearning_step_transitions
.iter()
.flatten()
.cloned()
.collect(),
state_rating_costs: simulator_config
.state_rating_costs
.iter()
.flatten()
.cloned()
.collect(),
learning_step_count: simulator_config.learning_step_count as u32,
relearning_step_count: simulator_config.relearning_step_count as u32,
})
}

View file

@ -555,7 +555,7 @@ impl SqlWriter<'_> {
}
fn write_all_fields(&mut self, val: &str) {
self.args.push(format!("(?i)^{}$", to_re(val)));
self.args.push(format!("(?is)^{}$", to_re(val)));
write!(self.sql, "regexp_fields(?{}, n.flds)", self.args.len()).unwrap();
}
@ -1081,7 +1081,7 @@ mod test {
s(ctx, "*:te*st"),
(
"(regexp_fields(?1, n.flds))".into(),
vec!["(?i)^te.*st$".into()]
vec!["(?is)^te.*st$".into()]
)
);
// all field search with regex

View file

@ -2,6 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use fsrs::FSRS;
use fsrs::FSRS5_DEFAULT_DECAY;
use crate::card::CardType;
use crate::card::FsrsMemoryState;
@ -37,10 +38,11 @@ impl Collection {
let fsrs_retrievability = card
.memory_state
.zip(Some(days_elapsed))
.map(|(state, days)| {
.zip(Some(card.decay.unwrap_or(FSRS5_DEFAULT_DECAY)))
.map(|((state, days), decay)| {
FSRS::new(None)
.unwrap()
.current_retrievability(state.into(), days)
.current_retrievability(state.into(), days, decay)
});
let original_deck = if card.original_deck_id == DeckId(0) {
@ -75,6 +77,7 @@ impl Collection {
memory_state: card.memory_state.map(Into::into),
fsrs_retrievability,
custom_data: card.custom_data,
fsrs_params: preset.fsrs_params().to_vec(),
preset: preset.name,
original_deck: if original_deck != deck {
Some(original_deck.human_name())

View file

@ -3,6 +3,7 @@
use anki_proto::stats::graphs_response::Retrievability;
use fsrs::FSRS;
use fsrs::FSRS5_DEFAULT_DECAY;
use crate::prelude::TimestampSecs;
use crate::scheduler::timing::SchedTimingToday;
@ -30,7 +31,11 @@ impl GraphsContext {
entry.1 += 1;
if let Some(state) = card.memory_state {
let elapsed_days = card.days_since_last_review(&timing).unwrap_or_default();
let r = fsrs.current_retrievability(state.into(), elapsed_days);
let r = fsrs.current_retrievability(
state.into(),
elapsed_days,
card.decay.unwrap_or(FSRS5_DEFAULT_DECAY),
);
*retrievability
.retrievability

View file

@ -42,6 +42,11 @@ pub(crate) struct CardData {
deserialize_with = "default_on_invalid"
)]
pub(crate) fsrs_desired_retention: Option<f32>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "default_on_invalid"
)]
pub(crate) decay: Option<f32>,
/// A string representation of a JSON object storing optional data
/// associated with the card, so v3 custom scheduling code can persist
@ -57,6 +62,7 @@ impl CardData {
fsrs_stability: card.memory_state.as_ref().map(|m| m.stability),
fsrs_difficulty: card.memory_state.as_ref().map(|m| m.difficulty),
fsrs_desired_retention: card.desired_retention,
decay: card.decay,
custom_data: card.custom_data.clone(),
}
}
@ -87,6 +93,9 @@ impl CardData {
if let Some(v) = &mut self.fsrs_desired_retention {
round_to_places(v, 2)
}
if let Some(v) = &mut self.decay {
round_to_places(v, 3)
}
serde_json::to_string(&self).map_err(Into::into)
}
}
@ -159,11 +168,12 @@ mod test {
fsrs_stability: Some(123.45678),
fsrs_difficulty: Some(1.234567),
fsrs_desired_retention: Some(0.987654),
decay: Some(0.123456),
custom_data: "".to_string(),
};
assert_eq!(
data.convert_to_json().unwrap(),
r#"{"s":123.457,"d":1.235,"dr":0.99}"#
r#"{"s":123.457,"d":1.235,"dr":0.99,"decay":0.123}"#
);
}
}

View file

@ -85,6 +85,7 @@ fn row_to_card(row: &Row) -> result::Result<Card, rusqlite::Error> {
original_position: data.original_position,
memory_state: data.memory_state(),
desired_retention: data.fsrs_desired_retention,
decay: data.decay,
custom_data: data.custom_data,
})
}

View file

@ -11,6 +11,7 @@ use std::sync::Arc;
use fnv::FnvHasher;
use fsrs::FSRS;
use fsrs::FSRS5_DEFAULT_DECAY;
use regex::Regex;
use rusqlite::functions::FunctionFlags;
use rusqlite::params;
@ -325,10 +326,11 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
let review_day = due.saturating_sub(ivl);
days_elapsed.saturating_sub(review_day) as u32
};
let decay = card_data.decay.unwrap_or_default();
Ok(card_data.memory_state().map(|state| {
FSRS::new(None)
.unwrap()
.current_retrievability(state.into(), days_elapsed)
.current_retrievability(state.into(), days_elapsed, decay)
}))
},
)
@ -374,10 +376,11 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result
{
// avoid div by zero
desired_retrievability = desired_retrievability.max(0.0001);
let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY);
let current_retrievability = FSRS::new(None)
.unwrap()
.current_retrievability(state.into(), days_elapsed)
.current_retrievability(state.into(), days_elapsed, decay)
.max(0.0001);
return Ok(Some(

View file

@ -332,6 +332,7 @@ impl From<CardEntry> for Card {
original_position: data.original_position,
memory_state: data.memory_state(),
desired_retention: data.fsrs_desired_retention,
decay: data.decay,
custom_data: data.custom_data,
}
}

View file

@ -71,7 +71,7 @@ impl NormalSyncer<'_> {
let local = self.col.sync_meta()?;
let local_bytes = local.collection_bytes;
let limit = *MAXIMUM_SYNC_PAYLOAD_BYTES_UNCOMPRESSED;
if local.collection_bytes > limit {
if self.server.endpoint.as_str().contains("ankiweb") && local.collection_bytes > limit {
return Err(AnkiError::sync_error(
format!("{local_bytes} > {limit}"),
SyncErrorKind::UploadTooLarge,

View file

@ -15,7 +15,7 @@ use nom::combinator::map;
use nom::sequence::delimited;
use regex::Regex;
use crate::cloze::add_cloze_numbers_in_string;
use crate::cloze::cloze_number_in_fields;
use crate::error::AnkiError;
use crate::error::Result;
use crate::error::TemplateError;
@ -673,11 +673,7 @@ pub fn render_card(
}
fn cloze_is_empty(field_map: &HashMap<&str, Cow<str>>, card_ord: u16) -> bool {
let mut set = HashSet::with_capacity(4);
for field in field_map.values() {
add_cloze_numbers_in_string(field.as_ref(), &mut set);
}
!set.contains(&(card_ord + 1))
!cloze_number_in_fields(field_map.values()).contains(&(card_ord + 1))
}
// Field requirements

View file

@ -71,8 +71,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
});
}
$: disabled =
!alwaysEnabled && (!$focusedInput || !editingInputIsRichText($focusedInput));
$: enabled =
alwaysEnabled ||
($focusedInput &&
editingInputIsRichText($focusedInput) &&
$focusedInput.isClozeField);
$: disabled = !enabled;
const incrementKeyCombination = "Control+Shift+C";
const sameKeyCombination = "Control+Alt+Shift+C";

View file

@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
description: string;
collapsed: boolean;
hidden: boolean;
isClozeField: boolean;
}
export interface EditorFieldAPI {
@ -82,6 +83,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
element,
direction: directionStore,
editingArea: editingArea as EditingAreaAPI,
isClozeField: field.isClozeField,
});
setContextProperty(api);

View file

@ -133,7 +133,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
for (const [index, [, fieldContent]] of fs.entries()) {
fieldStores[index].set(sanitize(fieldContent));
fieldStores[index].set(fieldContent);
}
fieldNames = newFieldNames;
@ -144,6 +144,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
fieldsCollapsed =
sessionOptions[notetypeMeta?.id]?.fieldsCollapsed ?? defaultCollapsed;
}
let clozeFields: boolean[] = [];
export function setClozeFields(defaultClozeFields: boolean[]): void {
clozeFields = defaultClozeFields;
}
let richTextsHidden: boolean[] = [];
let plainTextsHidden: boolean[] = [];
@ -276,6 +280,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
direction: fonts[index][2] ? "rtl" : "ltr",
collapsed: fieldsCollapsed[index],
hidden: hideFieldInOcclusionType(index, ioFields),
isClozeField: clozeFields[index],
})) as FieldData[];
let lastSavedTags: string[] | null = null;
@ -424,7 +429,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} from "../routes/image-occlusion/store";
import CollapseLabel from "./CollapseLabel.svelte";
import * as oldEditorAdapter from "./old-editor-adapter";
import { sanitize } from "$lib/domlib";
$: isIOImageLoaded = false;
$: ioImageLoadedStore.set(isIOImageLoaded);
@ -574,6 +578,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
saveSession,
setFields,
setCollapsed,
setClozeFields,
setPlainTexts,
setDescriptions,
setFonts,
@ -763,6 +768,7 @@ the AddCards dialog) should be implemented in the user of this component.
$focusedInput = null;
}}
bind:this={richTextInputs[index]}
isClozeField={field.isClozeField}
/>
</Collapsible>
</svelte:fragment>

View file

@ -4,8 +4,42 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import ButtonGroup from "$lib/components/ButtonGroup.svelte";
import { bridgeCommand } from "@tslib/bridgecommand";
import { toggleEditorButton } from "../old-editor-adapter";
import { singleCallback } from "@tslib/typing";
import { on } from "@tslib/events";
export let buttons: string[];
const { buttons } = $props<{ buttons: string[] }>();
$effect(() => {
// Each time the buttons are changed...
buttons;
// Add event handlers to each button
const addonButtons = document.querySelectorAll(".anki-addon-button");
const cbs = [...addonButtons].map((button) =>
singleCallback(
on(button, "click", () => {
const command = button.getAttribute("data-command");
if (command) {
bridgeCommand(command);
}
const toggleable = button.getAttribute("data-cantoggle");
if (toggleable === "1") {
toggleEditorButton(button as HTMLButtonElement);
}
return false;
}),
on(button as HTMLButtonElement, "mousedown", (evt) => {
evt.preventDefault();
evt.stopPropagation();
}),
),
);
return singleCallback(...cbs);
});
const radius = "5px";
function getBorderRadius(index: number, length: number): string {

View file

@ -14,6 +14,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: richTextAPI = $focusedInput as RichTextInputAPI;
async function onSurround({ detail }): Promise<void> {
if (!richTextAPI.isClozeField) {
return;
}
const richText = await richTextAPI.element;
const { prefix, suffix } = detail;

View file

@ -15,6 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ClozeButtons from "../ClozeButtons.svelte";
export let isBlock: boolean;
export let isClozeField: boolean;
const dispatch = createEventDispatcher();
</script>
@ -40,7 +41,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</IconButton>
</ButtonGroup>
<ClozeButtons on:surround alwaysEnabled={true} />
{#if isClozeField}
<ClozeButtons on:surround alwaysEnabled={true} />
{/if}
<ButtonGroup>
<IconButton

View file

@ -34,6 +34,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let cleanup: Callback;
let richTextInput: RichTextInputAPI | null = null;
let allowPromise = Promise.resolve();
// Whether the last focused input field corresponds to a cloze field.
let isClozeField: boolean = true;
async function initialize(input: EditingInputAPI | null): Promise<void> {
cleanup?.();
@ -50,6 +52,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
on(container, "movecaretafter" as any, showOnAutofocus),
on(container, "selectall" as any, showSelectAll),
);
isClozeField = input.isClozeField;
}
// Wait if the mathjax overlay is still active
@ -242,6 +245,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<MathjaxButtons
{isBlock}
{isClozeField}
on:setinline={async () => {
isBlock = false;
await updateBlockAttribute();

View file

@ -22,6 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/** The API exposed by the editable component */
editable: ContentEditableAPI;
customStyles: Promise<Record<string, any>>;
isClozeField: boolean;
}
function editingInputIsRichText(
@ -84,6 +85,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let hidden = false;
export const focusFlag = new Flag();
export let isClozeField: boolean;
const { focusedInput } = noteEditorContext.get();
const { content, editingInputs } = editingAreaContext.get();
@ -156,6 +158,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
inputHandler,
editable: {} as ContentEditableAPI,
customStyles,
isClozeField,
};
const allContexts = getAllContexts();
@ -204,6 +207,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
}
$: {
api.isClozeField = isClozeField;
}
onMount(() => {
$editingInputs.push(api);
$editingInputs = $editingInputs;

View file

@ -12,6 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export { className as class };
export let title: string;
export let onTitleClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null;
</script>
<div
@ -24,7 +25,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
style:--container-margin="0"
>
<div class="position-relative">
<h1>{title}</h1>
{#if onTitleClick}
<span
on:click={onTitleClick}
on:keydown={onTitleClick}
role="button"
tabindex="0"
>
<h1>
{title}
</h1>
</span>
{:else}
<h1>
{title}
</h1>
{/if}
<div class="help-badge position-absolute" class:rtl>
<slot name="tooltip" />
</div>

View file

@ -5,5 +5,4 @@ export * from "./content-editable";
export * from "./location";
export * from "./move-nodes";
export * from "./place-caret";
export * from "./sanitize";
export * from "./surround";

View file

@ -1,10 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import DOMPurify from "dompurify";
export function sanitize(html: string): string {
// We need to treat the text as a document fragment, or a style tag
// at the start of input will be discarded.
return DOMPurify.sanitize(html, { FORCE_BODY: true });
}

View file

@ -57,12 +57,6 @@
"path": "node_modules/@tootallnate/once",
"licenseFile": "node_modules/@tootallnate/once/LICENSE"
},
"@types/trusted-types@2.0.7": {
"licenses": "MIT",
"repository": "https://github.com/DefinitelyTyped/DefinitelyTyped",
"path": "node_modules/@types/trusted-types",
"licenseFile": "node_modules/@types/trusted-types/LICENSE"
},
"abab@2.0.6": {
"licenses": "BSD-3-Clause",
"repository": "https://github.com/jsdom/abab",
@ -101,8 +95,8 @@
"repository": "https://github.com/TooTallNate/node-agent-base",
"publisher": "Nathan Rajlich",
"email": "nathan@tootallnate.net",
"path": "node_modules/http-proxy-agent/node_modules/agent-base",
"licenseFile": "node_modules/http-proxy-agent/node_modules/agent-base/README.md"
"path": "node_modules/agent-base",
"licenseFile": "node_modules/agent-base/README.md"
},
"asynckit@0.4.0": {
"licenses": "MIT",
@ -442,14 +436,6 @@
"path": "node_modules/domexception",
"licenseFile": "node_modules/domexception/LICENSE.txt"
},
"dompurify@3.2.5": {
"licenses": "(MPL-2.0 OR Apache-2.0)",
"repository": "https://github.com/cure53/DOMPurify",
"publisher": "Dr.-Ing. Mario Heiderich, Cure53",
"email": "mario@cure53.de",
"path": "node_modules/dompurify",
"licenseFile": "node_modules/dompurify/LICENSE"
},
"empty-npm-package@1.0.0": {
"licenses": "ISC",
"path": "node_modules/canvas"
@ -586,14 +572,6 @@
"path": "node_modules/lodash-es",
"licenseFile": "node_modules/lodash-es/LICENSE"
},
"lru-cache@10.4.3": {
"licenses": "ISC",
"repository": "https://github.com/isaacs/node-lru-cache",
"publisher": "Isaac Z. Schlueter",
"email": "i@izs.me",
"path": "node_modules/lru-cache",
"licenseFile": "node_modules/lru-cache/LICENSE"
},
"marked@5.1.2": {
"licenses": "MIT",
"repository": "https://github.com/markedjs/marked",
@ -790,16 +768,16 @@
"repository": "https://github.com/jsdom/whatwg-url",
"publisher": "Sebastian Mayr",
"email": "github@smayr.name",
"path": "node_modules/jsdom/node_modules/whatwg-url",
"licenseFile": "node_modules/jsdom/node_modules/whatwg-url/LICENSE.txt"
"path": "node_modules/whatwg-url",
"licenseFile": "node_modules/whatwg-url/LICENSE.txt"
},
"whatwg-url@11.0.0": {
"licenses": "MIT",
"repository": "https://github.com/jsdom/whatwg-url",
"publisher": "Sebastian Mayr",
"email": "github@smayr.name",
"path": "node_modules/whatwg-url",
"licenseFile": "node_modules/whatwg-url/LICENSE.txt"
"path": "node_modules/data-urls/node_modules/whatwg-url",
"licenseFile": "node_modules/data-urls/node_modules/whatwg-url/LICENSE.txt"
},
"ws@8.18.0": {
"licenses": "MIT",

View file

@ -18,6 +18,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: fsrsEnabled = stats?.memoryState != null;
$: desiredRetention = stats?.desiredRetention ?? 0.9;
$: decay = (() => {
const paramsLength = stats?.fsrsParams?.length ?? 0;
if (paramsLength === 0) {
return 0.2; // default decay for FSRS-6
}
if (paramsLength < 21) {
return 0.5; // default decay for FSRS-4.5 and FSRS-5
}
return stats?.fsrsParams?.[20] ?? 0.2;
})();
</script>
<Container breakpoint="md" --gutter-inline="1rem" --gutter-block="0.5rem">
@ -33,7 +43,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{/if}
{#if fsrsEnabled}
<Row>
<ForgettingCurve revlog={stats.revlog} {desiredRetention} />
<ForgettingCurve revlog={stats.revlog} {desiredRetention} {decay} />
</Row>
{/if}
{:else}

View file

@ -21,6 +21,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let revlog: RevlogEntry[];
export let desiredRetention: number;
export let decay: number;
let svg: HTMLElement | SVGElement | null = null;
const bounds = defaultGraphBounds();
const title = tr.cardStatsFsrsForgettingCurveTitle();
@ -47,6 +48,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
svg as SVGElement,
bounds,
desiredRetention,
decay,
);
</script>

View file

@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { filterRevlogEntryByReviewKind } from "./forgetting-curve";
export let revlog: RevlogEntry[];
export let fsrsEnabled: boolean = false;
export const fsrsEnabled: boolean = false;
function reviewKindClass(entry: RevlogEntry): string {
switch (entry.reviewKind) {
@ -174,7 +174,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{/each}
</div>
</div>
{#if fsrsEnabled}{/if}
<!-- {#if fsrsEnabled}
<div class="column">
<div class="column-head">{tr2.cardStatsFsrsStability()}</div>
<div class="column-content right">
{#each revlogRows as row, _index}
<div>{row.stability}</div>
{/each}
</div>
</div>
{/if} -->
</div>
{/if}

View file

@ -11,12 +11,11 @@ import { axisBottom, axisLeft, line, max, min, pointer, scaleLinear, scaleTime,
import { type GraphBounds, setDataAvailable } from "../graphs/graph-helpers";
import { hideTooltip, showTooltip } from "../graphs/tooltip-utils.svelte";
const FACTOR = 19 / 81;
const DECAY = -0.5;
const MIN_POINTS = 1000;
function forgettingCurve(stability: number, daysElapsed: number): number {
return Math.pow((daysElapsed / stability) * FACTOR + 1.0, DECAY);
function forgettingCurve(stability: number, daysElapsed: number, decay: number): number {
const factor = Math.pow(0.9, 1 / -decay) - 1;
return Math.pow((daysElapsed / stability) * factor + 1.0, -decay);
}
interface DataPoint {
@ -68,7 +67,7 @@ export function filterRevlog(revlog: RevlogEntry[]): RevlogEntry[] {
return result.filter((entry) => filterRevlogEntryByReviewKind(entry));
}
export function prepareData(revlog: RevlogEntry[], maxDays: number) {
export function prepareData(revlog: RevlogEntry[], maxDays: number, decay: number) {
const data: DataPoint[] = [];
let lastReviewTime = 0;
let lastStability = 0;
@ -97,7 +96,7 @@ export function prepareData(revlog: RevlogEntry[], maxDays: number) {
let elapsedDays = 0;
while (elapsedDays < totalDaysElapsed - step) {
elapsedDays += step;
const retrievability = forgettingCurve(lastStability, elapsedDays);
const retrievability = forgettingCurve(lastStability, elapsedDays, decay);
data.push({
date: new Date((lastReviewTime + elapsedDays * 86400) * 1000),
daysSinceFirstLearn: data[data.length - 1].daysSinceFirstLearn + step,
@ -128,7 +127,7 @@ export function prepareData(revlog: RevlogEntry[], maxDays: number) {
let elapsedDays = 0;
while (elapsedDays < totalDaysSinceLastReview - step) {
elapsedDays += step;
const retrievability = forgettingCurve(lastStability, elapsedDays);
const retrievability = forgettingCurve(lastStability, elapsedDays, decay);
data.push({
date: new Date((lastReviewTime + elapsedDays * 86400) * 1000),
daysSinceFirstLearn: data[data.length - 1].daysSinceFirstLearn + step,
@ -138,7 +137,7 @@ export function prepareData(revlog: RevlogEntry[], maxDays: number) {
});
}
daysSinceFirstLearn += totalDaysSinceLastReview;
const retrievability = forgettingCurve(lastStability, totalDaysSinceLastReview);
const retrievability = forgettingCurve(lastStability, totalDaysSinceLastReview, decay);
data.push({
date: new Date(now * 1000),
daysSinceFirstLearn: daysSinceFirstLearn,
@ -151,7 +150,7 @@ export function prepareData(revlog: RevlogEntry[], maxDays: number) {
let previewDaysElapsed = 0;
while (previewDaysElapsed < previewDays) {
previewDaysElapsed += step;
const retrievability = forgettingCurve(lastStability, elapsedDays + previewDaysElapsed);
const retrievability = forgettingCurve(lastStability, elapsedDays + previewDaysElapsed, decay);
data.push({
date: new Date((now + previewDaysElapsed * 86400) * 1000),
daysSinceFirstLearn: data[data.length - 1].daysSinceFirstLearn + step,
@ -185,6 +184,7 @@ export function renderForgettingCurve(
svgElem: SVGElement,
bounds: GraphBounds,
desiredRetention: number,
decay: number,
) {
const svg = select(svgElem);
const trans = svg.transition().duration(600) as any;
@ -194,7 +194,7 @@ export function renderForgettingCurve(
}
const maxDays = calculateMaxDays(filteredRevlog, timeRange);
const data = prepareData(filteredRevlog, maxDays);
const data = prepareData(filteredRevlog, maxDays, decay);
if (data.length === 0) {
setDataAvailable(svg, false);

View file

@ -47,7 +47,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: computing = computingParams || checkingParams;
$: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
$: roundedRetention = Number($config.desiredRetention.toFixed(2));
$: desiredRetentionWarning = getRetentionWarning(roundedRetention);
$: desiredRetentionWarning = getRetentionWarning(
roundedRetention,
fsrsParams($config),
);
$: retentionWarningClass = getRetentionWarningClass(roundedRetention);
$: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
@ -64,8 +67,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
reviewOrder: $config.reviewOrder,
});
function getRetentionWarning(retention: number): string {
const decay = -0.5;
function getRetentionWarning(retention: number, params: number[]): string {
const decay = params.length > 20 ? -params[20] : -0.5; // default decay for FSRS-4.5 and FSRS-5
const factor = 0.9 ** (1 / decay) - 1;
const stability = 100;
const days = Math.round(
@ -142,7 +145,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
: tr.deckConfigFsrsParamsNoReviews();
setTimeout(() => alert(msg), 200);
} else {
$config.fsrsParams5 = resp.params;
$config.fsrsParams6 = resp.params;
}
if (computeParamsProgress) {
computeParamsProgress.current = computeParamsProgress.total;
@ -244,9 +247,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<div class="ms-1 me-1">
<ParamsInputRow
bind:value={$config.fsrsParams5}
bind:value={$config.fsrsParams6}
defaultValue={[]}
defaults={defaults.fsrsParams5}
defaults={defaults.fsrsParams6}
>
<SettingTitle on:click={() => openHelpModal("modelParams")}>
{tr.deckConfigWeights()}

View file

@ -233,7 +233,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
);
}
let easyDayPercentages = [...$config.easyDaysPercentages];
$: easyDayPercentages = [...$config.easyDaysPercentages];
</script>
<div class="modal" class:show={shown} class:d-block={shown} tabindex="-1">
@ -308,7 +308,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</details>
<details>
<summary>{"Advanced settings"}</summary>
<summary>{tr.deckConfigAdvancedSettings()}</summary>
<SpinBoxRow
bind:value={simulateFsrsRequest.maxInterval}
defaultValue={$config.maximumReviewInterval}
@ -351,7 +351,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<SettingTitle
on:click={() => openHelpModal("simulateFsrsReview")}
>
{"Smooth Graph"}
{tr.deckConfigSmoothGraph()}
</SettingTitle>
</SwitchRow>
@ -363,7 +363,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<SettingTitle
on:click={() => openHelpModal("simulateFsrsReview")}
>
{"Suspend Leeches"}
{tr.deckConfigSuspendLeeches()}
</SettingTitle>
</SwitchRow>
@ -451,8 +451,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
onPresetChange();
}}
>
<!-- {tr.deckConfigApplyChanges()} -->
{"Save to Preset Options"}
{tr.deckConfigSaveOptionsToPreset()}
</button>
{#if processing}

View file

@ -461,7 +461,9 @@ export async function commitEditing(): Promise<void> {
}
export function fsrsParams(config: DeckConfig_Config): number[] {
if (config.fsrsParams5) {
if (config.fsrsParams6) {
return config.fsrsParams6;
} else if (config.fsrsParams5) {
return config.fsrsParams5;
} else {
return config.fsrsParams4;

View file

@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// When title is null (default), the graph is inlined, not having TitledContainer wrapper.
export let title: string | null = null;
export let subtitle: string | null = null;
export let onTitleClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null;
</script>
{#if title == null}
@ -18,7 +19,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<slot />
</div>
{:else}
<TitledContainer class="d-flex flex-column" {title}>
<TitledContainer class="d-flex flex-column" {title} {onTitleClick}>
<slot slot="tooltip" name="tooltip"></slot>
<div class="graph d-flex flex-grow-1 flex-column justify-content-center">
{#if subtitle}
<div class="subtitle">{subtitle}</div>

View file

@ -5,6 +5,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="ts">
import type { GraphsResponse } from "@generated/anki/stats_pb";
import * as tr from "@generated/ftl";
import { HelpPage } from "@tslib/help-page";
import HelpModal from "$lib/components/HelpModal.svelte";
import type Carousel from "bootstrap/js/dist/carousel";
import type Modal from "bootstrap/js/dist/modal";
import type { HelpItem } from "$lib/components/types";
import { type RevlogRange } from "./graph-helpers";
import { DisplayMode, type PeriodTrueRetentionData, Scope } from "./true-retention";
@ -31,13 +36,43 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
});
const retentionHelp = {
trueRetention: {
title: tr.statisticsTrueRetentionTitle(),
help: tr.statisticsTrueRetentionTooltip(),
},
};
const helpSections: HelpItem[] = Object.values(retentionHelp);
let modal: Modal;
let carousel: Carousel;
function openHelpModal(index: number): void {
modal.show();
carousel.to(index);
}
let mode: DisplayMode = $state(DisplayMode.Summary);
const title = tr.statisticsTrueRetentionTitle();
const subtitle = tr.statisticsTrueRetentionSubtitle();
const onTitleClick = () => {
openHelpModal(Object.keys(retentionHelp).indexOf("trueRetention"));
};
</script>
<Graph {title} {subtitle}>
<Graph {title} {subtitle} {onTitleClick}>
<HelpModal
title={tr.statisticsTrueRetentionTitle()}
url={HelpPage.DeckOptions.fsrs}
slot="tooltip"
{helpSections}
on:mount={(e) => {
modal = e.detail.modal;
carousel = e.detail.carousel;
}}
/>
<InputBox>
<label>
<input type="radio" bind:group={mode} value={DisplayMode.Young} />

View file

@ -99,6 +99,7 @@ export class ImportCsvState {
const shouldRefetchMetadata = this.shouldRefetchMetadata(changed);
if (shouldRefetchMetadata) {
const { globalTags, updatedTags } = changed;
changed = await getCsvMetadata({
path: this.path,
delimiter: changed.delimiter,
@ -106,6 +107,9 @@ export class ImportCsvState {
deckId: getDeckId(changed) ?? undefined,
isHtml: changed.isHtml,
});
// carry over tags
changed.globalTags = globalTags;
changed.updatedTags = updatedTags;
}
const globalNotetype = getGlobalNotetype(changed);

View file

@ -1661,13 +1661,6 @@ __metadata:
languageName: node
linkType: hard
"@types/trusted-types@npm:^2.0.7":
version: 2.0.7
resolution: "@types/trusted-types@npm:2.0.7"
checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:^5.60.1":
version: 5.62.0
resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0"
@ -2024,7 +2017,6 @@ __metadata:
cross-env: "npm:^7.0.2"
d3: "npm:^7.0.0"
diff: "npm:^5.0.0"
dompurify: "npm:^3.2.5"
dprint: "npm:^0.47.2"
esbuild: "npm:^0.25.0"
esbuild-sass-plugin: "npm:^2"
@ -2392,9 +2384,9 @@ __metadata:
linkType: hard
"caniuse-lite@npm:^1.0.30001431, caniuse-lite@npm:^1.0.30001524, caniuse-lite@npm:^1.0.30001669":
version: 1.0.30001671
resolution: "caniuse-lite@npm:1.0.30001671"
checksum: 10c0/9bb81be7be641fdcdf4d3722b661d4204cc203a489c16080503a72b1605bd5c1061f8ae2452cc6c15d6957c818182824eb34e6569521051795f42cd14e844f99
version: 1.0.30001714
resolution: "caniuse-lite@npm:1.0.30001714"
checksum: 10c0/b0e3372f018c5c177912f0282af98049057d83c80846293a4e3df728644a622db42a9e8971d6b7708d76e0fd4e9f6d5ce93802cf4e6818de80fdf371dc0f6a06
languageName: node
linkType: hard
@ -3176,18 +3168,6 @@ __metadata:
languageName: node
linkType: hard
"dompurify@npm:^3.2.5":
version: 3.2.5
resolution: "dompurify@npm:3.2.5"
dependencies:
"@types/trusted-types": "npm:^2.0.7"
dependenciesMeta:
"@types/trusted-types":
optional: true
checksum: 10c0/b564167cc588933ad2d25c185296716bdd7124e9d2a75dac76efea831bb22d1230ce5205a1ab6ce4c1010bb32ac35f7a5cb2dd16c78cbf382111f1228362aa59
languageName: node
linkType: hard
"domutils@npm:^3.0.1":
version: 3.1.0
resolution: "domutils@npm:3.1.0"