Switch CardInfoDialog to ts page (#1414)

* Only collect card stats on the backend ...

... instead of rendering an HTML string using askama.

* Add ts page Card Info

* Update test for new `col.card_stats()`

* Remove obsolete CardStats code

* Use new ts page in `CardInfoDialog`

* Align start and end instead of left and right

Curiously, `text-align: start` does not work for `th` tags if assigned
via classes.

* Adopt ts refactorings after rebase

#1405 and #1409

* Clean up `ts/card-info/BUILD.bazel`

* Port card info logic from Rust to TS

* Move repeated field to the top

https://github.com/ankitects/anki/pull/1414#discussion_r725402730

* Convert pseudo classes to interfaces

* CardInfoPage -> CardInfo

* Make revlog in card info optional

* Add legacy support for old card stats

* Check for undefined instead of falsy

* Make Revlog separate component

* drop askama dependency (dae)

* Fix nightmode for legacy card stats
This commit is contained in:
RumovZ 2021-10-14 11:22:47 +02:00 committed by GitHub
parent 7128de895f
commit 3672b0fe73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 648 additions and 1705 deletions

129
Cargo.lock generated
View file

@ -55,7 +55,6 @@ version = "0.0.0"
dependencies = [
"ammonia",
"anki_i18n",
"askama",
"async-trait",
"blake3",
"bytes",
@ -73,7 +72,7 @@ dependencies = [
"itertools",
"lazy_static",
"linkcheck",
"nom 7.0.0",
"nom",
"num-integer",
"num_enum",
"once_cell",
@ -158,64 +157,12 @@ dependencies = [
"nodrop",
]
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd"
[[package]]
name = "askama"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134"
dependencies = [
"askama_derive",
"askama_escape",
"askama_shared",
]
[[package]]
name = "askama_derive"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522"
dependencies = [
"askama_shared",
"proc-macro2",
"syn",
]
[[package]]
name = "askama_escape"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb"
[[package]]
name = "askama_shared"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc"
dependencies = [
"askama_escape",
"humansize",
"nom 6.1.2",
"num-traits",
"percent-encoding",
"proc-macro2",
"quote",
"serde",
"syn",
"toml",
]
[[package]]
name = "async-trait"
version = "0.1.51"
@ -256,18 +203,6 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitvec"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "blake3"
version = "1.0.0"
@ -650,12 +585,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futf"
version = "0.1.4"
@ -925,12 +854,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
[[package]]
name = "humansize"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
[[package]]
name = "humantime"
version = "2.1.0"
@ -1127,19 +1050,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec 0.5.2",
"bitflags",
"cfg-if",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.103"
@ -1346,19 +1256,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "6.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
dependencies = [
"bitvec",
"funty",
"lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "nom"
version = "7.0.0"
@ -1902,12 +1799,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
name = "rand"
version = "0.7.3"
@ -2473,12 +2364,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "string_cache"
version = "0.8.1"
@ -2542,12 +2427,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.2.0"
@ -3162,12 +3041,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "xml5ever"
version = "0.16.1"

View file

@ -101,16 +101,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.arrayvec-0.4.12.bazel"),
)
maybe(
http_archive,
name = "raze__arrayvec__0_5_2",
url = "https://crates.io/api/v1/crates/arrayvec/0.5.2/download",
type = "tar.gz",
sha256 = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b",
strip_prefix = "arrayvec-0.5.2",
build_file = Label("//cargo/remote:BUILD.arrayvec-0.5.2.bazel"),
)
maybe(
http_archive,
name = "raze__arrayvec__0_7_1",
@ -121,46 +111,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.arrayvec-0.7.1.bazel"),
)
maybe(
http_archive,
name = "raze__askama__0_10_5",
url = "https://crates.io/api/v1/crates/askama/0.10.5/download",
type = "tar.gz",
sha256 = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134",
strip_prefix = "askama-0.10.5",
build_file = Label("//cargo/remote:BUILD.askama-0.10.5.bazel"),
)
maybe(
http_archive,
name = "raze__askama_derive__0_10_5",
url = "https://crates.io/api/v1/crates/askama_derive/0.10.5/download",
type = "tar.gz",
sha256 = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522",
strip_prefix = "askama_derive-0.10.5",
build_file = Label("//cargo/remote:BUILD.askama_derive-0.10.5.bazel"),
)
maybe(
http_archive,
name = "raze__askama_escape__0_10_1",
url = "https://crates.io/api/v1/crates/askama_escape/0.10.1/download",
type = "tar.gz",
sha256 = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb",
strip_prefix = "askama_escape-0.10.1",
build_file = Label("//cargo/remote:BUILD.askama_escape-0.10.1.bazel"),
)
maybe(
http_archive,
name = "raze__askama_shared__0_11_1",
url = "https://crates.io/api/v1/crates/askama_shared/0.11.1/download",
type = "tar.gz",
sha256 = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc",
strip_prefix = "askama_shared-0.11.1",
build_file = Label("//cargo/remote:BUILD.askama_shared-0.11.1.bazel"),
)
maybe(
http_archive,
name = "raze__async_trait__0_1_51",
@ -211,16 +161,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.bitflags-1.3.2.bazel"),
)
maybe(
http_archive,
name = "raze__bitvec__0_19_5",
url = "https://crates.io/api/v1/crates/bitvec/0.19.5/download",
type = "tar.gz",
sha256 = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321",
strip_prefix = "bitvec-0.19.5",
build_file = Label("//cargo/remote:BUILD.bitvec-0.19.5.bazel"),
)
maybe(
http_archive,
name = "raze__blake3__1_0_0",
@ -641,16 +581,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.form_urlencoded-1.0.1.bazel"),
)
maybe(
http_archive,
name = "raze__funty__1_1_0",
url = "https://crates.io/api/v1/crates/funty/1.1.0/download",
type = "tar.gz",
sha256 = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7",
strip_prefix = "funty-1.1.0",
build_file = Label("//cargo/remote:BUILD.funty-1.1.0.bazel"),
)
maybe(
http_archive,
name = "raze__futf__0_1_4",
@ -921,16 +851,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.httpdate-1.0.1.bazel"),
)
maybe(
http_archive,
name = "raze__humansize__1_1_1",
url = "https://crates.io/api/v1/crates/humansize/1.1.1/download",
type = "tar.gz",
sha256 = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026",
strip_prefix = "humansize-1.1.1",
build_file = Label("//cargo/remote:BUILD.humansize-1.1.1.bazel"),
)
maybe(
http_archive,
name = "raze__humantime__2_1_0",
@ -1121,16 +1041,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.lazy_static-1.4.0.bazel"),
)
maybe(
http_archive,
name = "raze__lexical_core__0_7_6",
url = "https://crates.io/api/v1/crates/lexical-core/0.7.6/download",
type = "tar.gz",
sha256 = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe",
strip_prefix = "lexical-core-0.7.6",
build_file = Label("//cargo/remote:BUILD.lexical-core-0.7.6.bazel"),
)
maybe(
http_archive,
name = "raze__libc__0_2_103",
@ -1351,16 +1261,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.nodrop-0.1.14.bazel"),
)
maybe(
http_archive,
name = "raze__nom__6_1_2",
url = "https://crates.io/api/v1/crates/nom/6.1.2/download",
type = "tar.gz",
sha256 = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2",
strip_prefix = "nom-6.1.2",
build_file = Label("//cargo/remote:BUILD.nom-6.1.2.bazel"),
)
maybe(
http_archive,
name = "raze__nom__7_0_0",
@ -1901,16 +1801,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.quote-1.0.9.bazel"),
)
maybe(
http_archive,
name = "raze__radium__0_5_3",
url = "https://crates.io/api/v1/crates/radium/0.5.3/download",
type = "tar.gz",
sha256 = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8",
strip_prefix = "radium-0.5.3",
build_file = Label("//cargo/remote:BUILD.radium-0.5.3.bazel"),
)
maybe(
http_archive,
name = "raze__rand__0_7_3",
@ -2441,16 +2331,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.stable_deref_trait-1.2.0.bazel"),
)
maybe(
http_archive,
name = "raze__static_assertions__1_1_0",
url = "https://crates.io/api/v1/crates/static_assertions/1.1.0/download",
type = "tar.gz",
sha256 = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f",
strip_prefix = "static_assertions-1.1.0",
build_file = Label("//cargo/remote:BUILD.static_assertions-1.1.0.bazel"),
)
maybe(
http_archive,
name = "raze__string_cache__0_8_1",
@ -2511,16 +2391,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.take_mut-0.2.2.bazel"),
)
maybe(
http_archive,
name = "raze__tap__1_0_1",
url = "https://crates.io/api/v1/crates/tap/1.0.1/download",
type = "tar.gz",
sha256 = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369",
strip_prefix = "tap-1.0.1",
build_file = Label("//cargo/remote:BUILD.tap-1.0.1.bazel"),
)
maybe(
http_archive,
name = "raze__tempfile__3_2_0",
@ -3201,16 +3071,6 @@ def raze_fetch_remote_crates():
build_file = Label("//cargo/remote:BUILD.winreg-0.7.0.bazel"),
)
maybe(
http_archive,
name = "raze__wyz__0_2_0",
url = "https://crates.io/api/v1/crates/wyz/0.2.0/download",
type = "tar.gz",
sha256 = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214",
strip_prefix = "wyz-0.2.0",
build_file = Label("//cargo/remote:BUILD.wyz-0.2.0.bazel"),
)
maybe(
http_archive,
name = "raze__xml5ever__0_16_1",

View file

@ -107,15 +107,6 @@
"license_file": null,
"description": "A vector with fixed capacity, backed by an array (it can be stored on the stack too). Implements fixed capacity ArrayVec and ArrayString."
},
{
"name": "arrayvec",
"version": "0.5.2",
"authors": "bluss",
"repository": "https://github.com/bluss/arrayvec",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "A vector with fixed capacity, backed by an array (it can be stored on the stack too). Implements fixed capacity ArrayVec and ArrayString."
},
{
"name": "arrayvec",
"version": "0.7.1",
@ -125,42 +116,6 @@
"license_file": null,
"description": "A vector with fixed capacity, backed by an array (it can be stored on the stack too). Implements fixed capacity ArrayVec and ArrayString."
},
{
"name": "askama",
"version": "0.10.5",
"authors": "Dirkjan Ochtman <dirkjan@ochtman.nl>",
"repository": "https://github.com/djc/askama",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "Type-safe, compiled Jinja-like templates for Rust"
},
{
"name": "askama_derive",
"version": "0.10.5",
"authors": "Dirkjan Ochtman <dirkjan@ochtman.nl>",
"repository": "https://github.com/djc/askama",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "Procedural macro package for Askama"
},
{
"name": "askama_escape",
"version": "0.10.1",
"authors": "Dirkjan Ochtman <dirkjan@ochtman.nl>",
"repository": "https://github.com/djc/askama",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "Optimized HTML escaping code, extracted from Askama"
},
{
"name": "askama_shared",
"version": "0.11.1",
"authors": "Dirkjan Ochtman <dirkjan@ochtman.nl>",
"repository": "https://github.com/djc/askama",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "Shared code for Askama"
},
{
"name": "async-trait",
"version": "0.1.51",
@ -206,15 +161,6 @@
"license_file": null,
"description": "A macro to generate structures which behave like bitflags."
},
{
"name": "bitvec",
"version": "0.19.5",
"authors": "myrrlyn <self@myrrlyn.dev>",
"repository": "https://github.com/myrrlyn/bitvec",
"license": "MIT",
"license_file": null,
"description": "A crate for manipulating memory, bit by bit"
},
{
"name": "blake3",
"version": "1.0.0",
@ -593,15 +539,6 @@
"license_file": null,
"description": "Parser and serializer for the application/x-www-form-urlencoded syntax, as used by HTML forms."
},
{
"name": "funty",
"version": "1.1.0",
"authors": "myrrlyn <self@myrrlyn.dev>",
"repository": "https://github.com/myrrlyn/funty",
"license": "MIT",
"license_file": null,
"description": "Trait generalization over the primitive types"
},
{
"name": "futf",
"version": "0.1.4",
@ -845,15 +782,6 @@
"license_file": null,
"description": "HTTP date parsing and formatting"
},
{
"name": "humansize",
"version": "1.1.1",
"authors": "Leopold Arkham <leopold.arkham@gmail.com>",
"repository": "https://github.com/LeopoldArkham/humansize",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "A configurable crate to easily represent file sizes in a human-readable format."
},
{
"name": "humantime",
"version": "2.1.0",
@ -1025,15 +953,6 @@
"license_file": null,
"description": "A macro for declaring lazily evaluated statics in Rust."
},
{
"name": "lexical-core",
"version": "0.7.6",
"authors": "Alex Huszagh <ahuszagh@gmail.com>",
"repository": "https://github.com/Alexhuszagh/rust-lexical/tree/master/lexical-core",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "Lexical, to- and from-string conversion routines."
},
{
"name": "libc",
"version": "0.2.103",
@ -1232,15 +1151,6 @@
"license_file": null,
"description": "A wrapper type to inhibit drop (destructor). ***Deprecated: Use ManuallyDrop or MaybeUninit instead!***"
},
{
"name": "nom",
"version": "6.1.2",
"authors": "contact@geoffroycouprie.com",
"repository": "https://github.com/Geal/nom",
"license": "MIT",
"license_file": null,
"description": "A byte-oriented, zero-copy, parser combinators library"
},
{
"name": "nom",
"version": "7.0.0",
@ -1727,15 +1637,6 @@
"license_file": null,
"description": "Quasi-quoting macro quote!(...)"
},
{
"name": "radium",
"version": "0.5.3",
"authors": "Nika Layzell <nika@thelayzells.com>|myrrlyn <self@myrrlyn.dev>",
"repository": "https://github.com/mystor/radium",
"license": "MIT",
"license_file": null,
"description": "Helper traits for working with maybe-atomic values"
},
{
"name": "rand",
"version": "0.7.3",
@ -2222,15 +2123,6 @@
"license_file": null,
"description": "An unsafe marker trait for types like Box and Rc that dereference to a stable address even when moved, and hence can be used with libraries such as owning_ref and rental."
},
{
"name": "static_assertions",
"version": "1.1.0",
"authors": "Nikolai Vazquez",
"repository": "https://github.com/nvzqz/static-assertions-rs",
"license": "Apache-2.0 OR MIT",
"license_file": null,
"description": "Compile-time assertions to ensure that invariants are met."
},
{
"name": "string_cache",
"version": "0.8.1",
@ -2285,15 +2177,6 @@
"license_file": null,
"description": "Take a T from a &mut T temporarily"
},
{
"name": "tap",
"version": "1.0.1",
"authors": "Elliott Linder <elliott.darfink@gmail.com>|myrrlyn <self@myrrlyn.dev>",
"repository": "https://github.com/myrrlyn/tap",
"license": "MIT",
"license_file": null,
"description": "Generic extensions for tapping values in Rust"
},
{
"name": "tempfile",
"version": "3.2.0",
@ -2906,15 +2789,6 @@
"license_file": null,
"description": "Rust bindings to MS Windows Registry API"
},
{
"name": "wyz",
"version": "0.2.0",
"authors": "myrrlyn <self@myrrlyn.dev>",
"repository": "https://github.com/myrrlyn/wyz",
"license": "MIT",
"license_file": null,
"description": "myrrlyns utility collection"
},
{
"name": "xml5ever",
"version": "0.16.1",

View file

@ -1,62 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT OR Apache-2.0"
])
# Generated Targets
# Unsupported target "arraystring" with type "bench" omitted
# Unsupported target "extend" with type "bench" omitted
rust_library(
name = "arrayvec",
srcs = glob(["**/*.rs"]),
crate_features = [
"array-sizes-33-128",
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.5.2",
# buildifier: leave-alone
deps = [
],
)
# Unsupported target "serde" with type "test" omitted
# Unsupported target "tests" with type "test" omitted

View file

@ -1,63 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT OR Apache-2.0"
])
# Generated Targets
rust_library(
name = "askama",
srcs = glob(["**/*.rs"]),
crate_features = [
"config",
"default",
"humansize",
"num-traits",
"urlencode",
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
proc_macro_deps = [
"@raze__askama_derive__0_10_5//:askama_derive",
],
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.10.5",
# buildifier: leave-alone
deps = [
"@raze__askama_escape__0_10_1//:askama_escape",
"@raze__askama_shared__0_11_1//:askama_shared",
],
)

View file

@ -1,56 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT OR Apache-2.0"
])
# Generated Targets
rust_library(
name = "askama_derive",
srcs = glob(["**/*.rs"]),
crate_features = [
],
crate_root = "src/lib.rs",
crate_type = "proc-macro",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.10.5",
# buildifier: leave-alone
deps = [
"@raze__askama_shared__0_11_1//:askama_shared",
"@raze__proc_macro2__1_0_29//:proc_macro2",
"@raze__syn__1_0_77//:syn",
],
)

View file

@ -1,55 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT OR Apache-2.0"
])
# Generated Targets
# Unsupported target "all" with type "bench" omitted
rust_library(
name = "askama_escape",
srcs = glob(["**/*.rs"]),
crate_features = [
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.10.1",
# buildifier: leave-alone
deps = [
],
)

View file

@ -1,69 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT OR Apache-2.0"
])
# Generated Targets
rust_library(
name = "askama_shared",
srcs = glob(["**/*.rs"]),
crate_features = [
"config",
"humansize",
"num-traits",
"percent-encoding",
"serde",
"toml",
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.11.1",
# buildifier: leave-alone
deps = [
"@raze__askama_escape__0_10_1//:askama_escape",
"@raze__humansize__1_1_1//:humansize",
"@raze__nom__6_1_2//:nom",
"@raze__num_traits__0_2_14//:num_traits",
"@raze__percent_encoding__2_1_0//:percent_encoding",
"@raze__proc_macro2__1_0_29//:proc_macro2",
"@raze__quote__1_0_9//:quote",
"@raze__serde__1_0_130//:serde",
"@raze__syn__1_0_77//:syn",
"@raze__toml__0_5_8//:toml",
],
)

View file

@ -1,65 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT"
])
# Generated Targets
# Unsupported target "macros" with type "bench" omitted
# Unsupported target "memcpy" with type "bench" omitted
# Unsupported target "slice" with type "bench" omitted
rust_library(
name = "bitvec",
srcs = glob(["**/*.rs"]),
crate_features = [
"alloc",
"std",
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.19.5",
# buildifier: leave-alone
deps = [
"@raze__funty__1_1_0//:funty",
"@raze__radium__0_5_3//:radium",
"@raze__tap__1_0_1//:tap",
"@raze__wyz__0_2_0//:wyz",
],
)

View file

@ -1,53 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT"
])
# Generated Targets
rust_library(
name = "funty",
srcs = glob(["**/*.rs"]),
crate_features = [
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "1.1.0",
# buildifier: leave-alone
deps = [
],
)

View file

@ -1,57 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT OR Apache-2.0"
])
# Generated Targets
# Unsupported target "custom_options" with type "example" omitted
# Unsupported target "sizes" with type "example" omitted
rust_library(
name = "humansize",
srcs = glob(["**/*.rs"]),
crate_features = [
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2015",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "1.1.1",
# buildifier: leave-alone
deps = [
],
)

View file

@ -1,102 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT OR Apache-2.0"
])
# Generated Targets
# buildifier: disable=out-of-order-load
# buildifier: disable=load-on-top
load(
"@rules_rust//cargo:cargo_build_script.bzl",
"cargo_build_script",
)
cargo_build_script(
name = "lexical_core_build_script",
srcs = glob(["**/*.rs"]),
build_script_env = {
},
crate_features = [
"arrayvec",
"correct",
"default",
"ryu",
"static_assertions",
"std",
"table",
],
crate_root = "build.rs",
data = glob(["**"]),
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.7.6",
visibility = ["//visibility:private"],
deps = [
],
)
rust_library(
name = "lexical_core",
srcs = glob(["**/*.rs"]),
crate_features = [
"arrayvec",
"correct",
"default",
"ryu",
"static_assertions",
"std",
"table",
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.7.6",
# buildifier: leave-alone
deps = [
":lexical_core_build_script",
"@raze__arrayvec__0_5_2//:arrayvec",
"@raze__bitflags__1_3_2//:bitflags",
"@raze__cfg_if__1_0_0//:cfg_if",
"@raze__ryu__1_0_5//:ryu",
"@raze__static_assertions__1_1_0//:static_assertions",
],
)

View file

@ -1,162 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT"
])
# Generated Targets
# buildifier: disable=out-of-order-load
# buildifier: disable=load-on-top
load(
"@rules_rust//cargo:cargo_build_script.bzl",
"cargo_build_script",
)
cargo_build_script(
name = "nom_build_script",
srcs = glob(["**/*.rs"]),
build_script_env = {
},
crate_features = [
"alloc",
"bitvec",
"default",
"funty",
"lexical",
"lexical-core",
"std",
],
crate_root = "build.rs",
data = glob(["**"]),
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "6.1.2",
visibility = ["//visibility:private"],
deps = [
"@raze__version_check__0_9_3//:version_check",
],
)
# Unsupported target "arithmetic" with type "bench" omitted
# Unsupported target "http" with type "bench" omitted
# Unsupported target "ini" with type "bench" omitted
# Unsupported target "ini_complete" with type "bench" omitted
# Unsupported target "ini_str" with type "bench" omitted
# Unsupported target "json" with type "bench" omitted
# Unsupported target "number" with type "bench" omitted
# Unsupported target "json" with type "example" omitted
# Unsupported target "s_expression" with type "example" omitted
# Unsupported target "string" with type "example" omitted
rust_library(
name = "nom",
srcs = glob(["**/*.rs"]),
crate_features = [
"alloc",
"bitvec",
"default",
"funty",
"lexical",
"lexical-core",
"std",
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "6.1.2",
# buildifier: leave-alone
deps = [
":nom_build_script",
"@raze__bitvec__0_19_5//:bitvec",
"@raze__funty__1_1_0//:funty",
"@raze__lexical_core__0_7_6//:lexical_core",
"@raze__memchr__2_4_1//:memchr",
],
)
# Unsupported target "arithmetic" with type "test" omitted
# Unsupported target "arithmetic_ast" with type "test" omitted
# Unsupported target "bitstream" with type "test" omitted
# Unsupported target "blockbuf-arithmetic" with type "test" omitted
# Unsupported target "css" with type "test" omitted
# Unsupported target "custom_errors" with type "test" omitted
# Unsupported target "escaped" with type "test" omitted
# Unsupported target "float" with type "test" omitted
# Unsupported target "fnmut" with type "test" omitted
# Unsupported target "inference" with type "test" omitted
# Unsupported target "ini" with type "test" omitted
# Unsupported target "ini_str" with type "test" omitted
# Unsupported target "issues" with type "test" omitted
# Unsupported target "json" with type "test" omitted
# Unsupported target "mp4" with type "test" omitted
# Unsupported target "multiline" with type "test" omitted
# Unsupported target "named_args" with type "test" omitted
# Unsupported target "overflow" with type "test" omitted
# Unsupported target "reborrow_fold" with type "test" omitted
# Unsupported target "test1" with type "test" omitted

View file

@ -42,7 +42,6 @@ cargo_build_script(
build_script_env = {
},
crate_features = [
"default",
"std",
],
crate_root = "build.rs",
@ -66,7 +65,6 @@ rust_library(
name = "num_traits",
srcs = glob(["**/*.rs"]),
crate_features = [
"default",
"std",
],
crate_root = "src/lib.rs",

View file

@ -43,7 +43,6 @@ cargo_build_script(
},
crate_features = [
"abi3",
"abi3-py38",
"abi3-py39",
"default",
"extension-module",
@ -93,7 +92,6 @@ rust_library(
srcs = glob(["**/*.rs"]),
crate_features = [
"abi3",
"abi3-py38",
"abi3-py39",
"default",
"extension-module",

View file

@ -44,7 +44,6 @@ cargo_build_script(
},
crate_features = [
"abi3",
"abi3-py38",
"abi3-py39",
"default",
"resolve-config",
@ -70,7 +69,6 @@ rust_library(
srcs = glob(["**/*.rs"]),
crate_features = [
"abi3",
"abi3-py38",
"abi3-py39",
"default",
"resolve-config",

View file

@ -1,83 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT"
])
# Generated Targets
# buildifier: disable=out-of-order-load
# buildifier: disable=load-on-top
load(
"@rules_rust//cargo:cargo_build_script.bzl",
"cargo_build_script",
)
cargo_build_script(
name = "radium_build_script",
srcs = glob(["**/*.rs"]),
build_script_env = {
},
crate_features = [
],
crate_root = "build.rs",
data = glob(["**"]),
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.5.3",
visibility = ["//visibility:private"],
deps = [
],
)
rust_library(
name = "radium",
srcs = glob(["**/*.rs"]),
crate_features = [
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.5.3",
# buildifier: leave-alone
deps = [
":radium_build_script",
],
)

View file

@ -1,53 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT OR Apache-2.0"
])
# Generated Targets
rust_library(
name = "static_assertions",
srcs = glob(["**/*.rs"]),
crate_features = [
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2015",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "1.1.0",
# buildifier: leave-alone
deps = [
],
)

View file

@ -1,53 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT"
])
# Generated Targets
rust_library(
name = "tap",
srcs = glob(["**/*.rs"]),
crate_features = [
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2015",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "1.0.1",
# buildifier: leave-alone
deps = [
],
)

View file

@ -1,54 +0,0 @@
"""
@generated
cargo-raze crate build file.
DO NOT EDIT! Replaced on runs of cargo-raze
"""
# buildifier: disable=load
load("@bazel_skylib//lib:selects.bzl", "selects")
# buildifier: disable=load
load(
"@rules_rust//rust:rust.bzl",
"rust_binary",
"rust_library",
"rust_test",
)
package(default_visibility = [
# Public for visibility by "@raze__crate__version//" targets.
#
# Prefer access through "//cargo", which limits external
# visibility to explicit Cargo.toml dependencies.
"//visibility:public",
])
licenses([
"notice", # MIT from expression "MIT"
])
# Generated Targets
rust_library(
name = "wyz",
srcs = glob(["**/*.rs"]),
crate_features = [
"alloc",
],
crate_root = "src/lib.rs",
crate_type = "lib",
data = [],
edition = "2018",
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-raze",
"manual",
],
version = "0.2.0",
# buildifier: leave-alone
deps = [
],
)

View file

@ -9,12 +9,45 @@ import "anki/generic.proto";
import "anki/cards.proto";
service StatsService {
rpc CardStats(cards.CardId) returns (generic.String);
rpc CardStats(cards.CardId) returns (CardStatsResponse);
rpc Graphs(GraphsRequest) returns (GraphsResponse);
rpc GetGraphPreferences(generic.Empty) returns (GraphPreferences);
rpc SetGraphPreferences(GraphPreferences) returns (generic.Empty);
}
message CardStatsResponse {
message StatsRevlogEntry {
int64 time = 1;
RevlogEntry.ReviewKind review_kind = 2;
uint32 button_chosen = 3;
// seconds
uint32 interval = 4;
// per mill
uint32 ease = 5;
float taken_secs = 6;
}
repeated StatsRevlogEntry revlog = 1;
int64 card_id = 2;
int64 note_id = 3;
string deck = 4;
// Unix timestamps
int64 added = 5;
generic.Int64 first_review = 6;
generic.Int64 latest_review = 7;
generic.Int64 due_date = 8;
generic.Int32 due_position = 9;
// days
uint32 interval = 10;
// per mill
uint32 ease = 11;
uint32 reviews = 12;
uint32 lapses = 13;
float average_secs = 14;
float total_secs = 15;
string card_type = 16;
string notetype = 17;
}
message GraphsRequest {
string search = 1;
uint32 days = 2;

View file

@ -62,7 +62,13 @@ SKIP_UNROLL_INPUT = {
}
SKIP_UNROLL_OUTPUT = {"GetPreferences"}
SKIP_DECODE = {"Graphs", "GetGraphPreferences", "GetChangeNotetypeInfo", "CompleteTag"}
SKIP_DECODE = {
"Graphs",
"GetGraphPreferences",
"GetChangeNotetypeInfo",
"CompleteTag",
"CardStats",
}
def python_type(field):

View file

@ -24,6 +24,7 @@ SearchNode = search_pb2.SearchNode
Progress = collection_pb2.Progress
EmptyCardsReport = card_rendering_pb2.EmptyCardsReport
GraphPreferences = stats_pb2.GraphPreferences
CardStats = stats_pb2.CardStatsResponse
Preferences = config_pb2.Preferences
UndoStatus = collection_pb2.UndoStatus
OpChanges = collection_pb2.OpChanges
@ -807,23 +808,8 @@ class Collection(DeprecatedNamesMixin):
return CollectionStats(self)
def card_stats(self, card_id: CardId, include_revlog: bool) -> str:
import anki.stats as st
if include_revlog:
revlog_style = "margin-top: 2em;"
else:
revlog_style = "display: none;"
style = f"""<style>
.revlog-learn {{ color: {st.colLearn} }}
.revlog-review {{ color: {st.colMature} }}
.revlog-relearn {{ color: {st.colRelearn} }}
.revlog-ease1 {{ color: {st.colRelearn} }}
table.review-log {{ {revlog_style} }}
</style>"""
return style + self._backend.card_stats(card_id)
def card_stats_data(self, card_id: CardId) -> bytes:
return self._backend.card_stats(card_id)
def studied_today(self) -> str:
return self._backend.studied_today()
@ -1149,9 +1135,17 @@ table.review-log {{ {revlog_style} }}
def _remNotes(self, ids: list[NoteId]) -> None:
pass
@deprecated(replaced_by=card_stats)
@deprecated(replaced_by=card_stats_data)
def card_stats(self, card_id: CardId, include_revlog: bool) -> str:
from anki.stats import _legacy_card_stats
return _legacy_card_stats(self, card_id, include_revlog)
@deprecated(replaced_by=card_stats_data)
def cardStats(self, card: Card) -> str:
return self.card_stats(card.id, include_revlog=False)
from anki.stats import _legacy_card_stats
return _legacy_card_stats(self, card.id, False)
@deprecated(replaced_by=after_note_updates)
def updateFieldCache(self, nids: list[NoteId]) -> None:

View file

@ -7,6 +7,7 @@ from __future__ import annotations
import datetime
import json
import random
import time
from typing import Sequence
@ -14,17 +15,36 @@ import anki.cards
import anki.collection
from anki.consts import *
from anki.lang import FormatTimeSpan
from anki.utils import ids2str
from anki.utils import base62, ids2str
# Card stats
##########################################################################
_legacy_nightmode = False
def _legacy_card_stats(
col: anki.collection.Collection, card_id: anki.cards.CardId, include_revlog: bool
) -> str:
"A quick hack to preserve compatibility with the old HTML string API."
random_id = f"cardinfo-{base62(random.randint(0, 2 ** 64 - 1))}"
return f"""
<div id="{random_id}"></div>
<script src="js/vendor/bootstrap.bundle.min.js"></script>
<link href="pages/card-info-base.css" rel="stylesheet" />
<link href="pages/card-info.css" rel="stylesheet" />
<script src="pages/card-info.js"></script>
<script>
if ({1 if _legacy_nightmode else 0}) {{
document.documentElement.className = "night-mode";
}}
anki.cardInfo(document.getElementById('{random_id}'), {card_id}, {include_revlog});
</script>
"""
class CardStats:
"""
New code should just call collection.card_stats() directly - this class
is only left around for backwards compatibility.
"""
"""Do not use - this class is only left around for backwards compatibility."""
def __init__(self, col: anki.collection.Collection, card: anki.cards.Card) -> None:
if col:
@ -33,7 +53,7 @@ class CardStats:
self.txt = ""
def report(self, include_revlog: bool = False) -> str:
return self.col.card_stats(self.card.id, include_revlog=include_revlog)
return _legacy_card_stats(self.col, self.card.id, include_revlog)
# legacy

View file

@ -4,6 +4,7 @@
import os
import tempfile
from anki.collection import CardStats
from tests.shared import getEmptyCol
@ -14,12 +15,15 @@ def test_stats():
col.addNote(note)
c = note.cards()[0]
# card stats
assert col.card_stats(c.id, include_revlog=True)
card_stats = CardStats()
card_stats.ParseFromString(col.card_stats_data(c.id))
assert card_stats.note_id == note.id
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 3)
col.sched.answerCard(c, 2)
assert col.card_stats(c.id, include_revlog=True)
card_stats.ParseFromString(col.card_stats_data(c.id))
assert len(card_stats.revlog) == 2
def test_graphs_empty():

View file

@ -4,36 +4,53 @@
from __future__ import annotations
import aqt
from anki.cards import Card
from anki.stats import CardStats
from anki.cards import Card, CardId
from aqt.qt import *
from aqt.utils import disable_help_button, qconnect, restoreGeom, saveGeom
from aqt.utils import (
addCloseShortcut,
disable_help_button,
qconnect,
restoreGeom,
saveGeom,
)
from aqt.webview import AnkiWebView
class CardInfoDialog(QDialog):
TITLE = "browser card info"
GEOMETRY_KEY = "revlog"
silentlyClose = True
def __init__(self, parent: QWidget, mw: aqt.AnkiQt, card: Card) -> None:
super().__init__(parent)
disable_help_button(self)
cs = CardStats(mw.col, card)
info = cs.report(include_revlog=True)
l = QVBoxLayout()
l.setContentsMargins(0, 0, 0, 0)
w = AnkiWebView(title="browser card info")
l.addWidget(w)
w.stdHtml(info + "<p>", context=self)
bb = QDialogButtonBox(QDialogButtonBox.Close)
l.addWidget(bb)
qconnect(bb.rejected, self.reject)
self.setLayout(l)
self.setWindowModality(Qt.WindowModal)
self.resize(500, 400)
restoreGeom(self, "revlog")
self.mw = mw
self._setup_ui(card.id)
self.show()
def _setup_ui(self, card_id: CardId) -> None:
self.setWindowModality(Qt.ApplicationModal)
self.mw.garbage_collect_on_dialog_finish(self)
disable_help_button(self)
restoreGeom(self, self.GEOMETRY_KEY)
addCloseShortcut(self)
self.web = AnkiWebView(title=self.TITLE)
self.web.setVisible(False)
self.web.load_ts_page("card-info")
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.web)
buttons = QDialogButtonBox(QDialogButtonBox.Close)
buttons.setContentsMargins(10, 0, 10, 10)
layout.addWidget(buttons)
qconnect(buttons.rejected, self.reject)
self.setLayout(layout)
self.web.eval(
f"anki.cardInfo(document.getElementById('main'), {card_id}, true);"
)
def reject(self) -> None:
saveGeom(self, "revlog")
self.web = None
saveGeom(self, self.GEOMETRY_KEY)
return QDialog.reject(self)

View file

@ -5,6 +5,7 @@ _pages = [
"congrats",
"deck-options",
"change-notetype",
"card-info",
]
[copy_files_into_group(

View file

@ -20,6 +20,7 @@ from waitress.server import create_server
import aqt
from anki import hooks
from anki.cards import CardId
from anki.collection import GraphPreferences, OpChanges
from anki.decks import UpdateDeckConfigs
from anki.models import NotetypeNames
@ -411,6 +412,10 @@ def complete_tag() -> bytes:
return aqt.mw.col.tags.complete_tag(request.data)
def card_stats() -> bytes:
return aqt.mw.col.card_stats_data(CardId(int(request.data)))
# these require a collection
post_handlers = {
"graphData": graph_data,
@ -426,6 +431,7 @@ post_handlers = {
"i18nResources": i18n_resources,
"congratsInfo": congrats_info,
"completeTag": complete_tag,
"cardStats": card_stats,
}

View file

@ -246,6 +246,7 @@ QTabWidget {{ background-color: {}; }}
s.colCram = self.color(colors.SUSPENDED_BG)
s.colSusp = self.color(colors.SUSPENDED_BG)
s.colMature = self.color(colors.REVIEW_COUNT)
s._legacy_nightmode = self._night_mode_preference
theme_manager = ThemeManager()

View file

@ -45,7 +45,6 @@ _anki_compile_data = glob([
]) + [
"Cargo.toml", # prevents a warning about num_enum
"//:buildinfo.txt",
"templates/.empty", # required for askama
]
_anki_features = [
@ -73,7 +72,6 @@ rust_library(
deps = [
":build_script",
"//rslib/cargo:ammonia",
"//rslib/cargo:askama",
"//rslib/cargo:blake3",
"//rslib/cargo:bytes",
"//rslib/cargo:chrono",

View file

@ -30,7 +30,6 @@ anki_i18n = { path="i18n" }
nom = "7.0.0"
proc-macro-nested = "0.1.7"
slog-term = "2.8.0"
askama = "0.10.5"
blake3 = "1.0.0"
bytes = "1.1.0"
chrono = "0.4.19"

View file

@ -21,15 +21,6 @@ alias(
],
)
alias(
name = "askama",
actual = "@raze__askama__0_10_5//:askama",
tags = [
"cargo-raze",
"manual",
],
)
alias(
name = "async_trait",
actual = "@raze__async_trait__0_1_51//:async_trait",

View file

@ -3,12 +3,11 @@
use super::Backend;
pub(super) use crate::backend_proto::stats_service::Service as StatsService;
use crate::{backend_proto as pb, prelude::*};
use crate::{backend_proto as pb, prelude::*, revlog::RevlogReviewKind};
impl StatsService for Backend {
fn card_stats(&self, input: pb::CardId) -> Result<pb::String> {
fn card_stats(&self, input: pb::CardId) -> Result<pb::CardStatsResponse> {
self.with_col(|col| col.card_stats(input.into()))
.map(Into::into)
}
fn graphs(&self, input: pb::GraphsRequest) -> Result<pb::GraphsResponse> {
@ -24,3 +23,15 @@ impl StatsService for Backend {
.map(Into::into)
}
}
impl From<RevlogReviewKind> for i32 {
fn from(kind: RevlogReviewKind) -> Self {
(match kind {
RevlogReviewKind::Learning => pb::revlog_entry::ReviewKind::Learning,
RevlogReviewKind::Review => pb::revlog_entry::ReviewKind::Review,
RevlogReviewKind::Relearning => pb::revlog_entry::ReviewKind::Relearning,
RevlogReviewKind::Filtered => pb::revlog_entry::ReviewKind::Filtered,
RevlogReviewKind::Manual => pb::revlog_entry::ReviewKind::Manual,
}) as i32
}
}

View file

@ -1,68 +1,10 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use askama::Template;
use chrono::prelude::*;
use crate::{
card::CardQueue,
i18n::I18n,
prelude::*,
revlog::{RevlogEntry, RevlogReviewKind},
scheduler::timespan::time_span,
};
struct CardStats {
added: TimestampSecs,
first_review: Option<TimestampSecs>,
latest_review: Option<TimestampSecs>,
due: Due,
interval_secs: u32,
ease: u32,
reviews: u32,
lapses: u32,
average_secs: f32,
total_secs: f32,
card_type: String,
notetype: String,
deck: String,
nid: NoteId,
cid: CardId,
revlog: Vec<RevlogEntry>,
}
#[derive(Template)]
#[template(path = "../src/stats/card_stats.html")]
struct CardStatsTemplate {
stats: Vec<(String, String)>,
revlog: Vec<RevlogText>,
revlog_titles: RevlogText,
}
enum Due {
Time(TimestampSecs),
Position(i32),
Unknown,
}
struct RevlogText {
time: String,
kind: String,
kind_class: String,
rating: String,
rating_class: String,
interval: String,
ease: String,
taken_secs: String,
}
use crate::{backend_proto as pb, card::CardQueue, prelude::*, revlog::RevlogEntry};
impl Collection {
pub fn card_stats(&mut self, cid: CardId) -> Result<String> {
let stats = self.gather_card_stats(cid)?;
Ok(self.card_stats_to_string(stats))
}
fn gather_card_stats(&mut self, cid: CardId) -> Result<CardStats> {
pub fn card_stats(&mut self, cid: CardId) -> Result<pb::CardStatsResponse> {
let card = self.storage.get_card(cid)?.ok_or(AnkiError::NotFound)?;
let note = self
.storage
@ -75,184 +17,92 @@ impl Collection {
.storage
.get_deck(card.deck_id)?
.ok_or(AnkiError::NotFound)?;
let revlog = self.storage.get_revlog_entries_for_card(card.id)?;
let average_secs;
let total_secs;
let normal_answer_count = revlog.iter().filter(|r| r.button_chosen > 0).count();
if normal_answer_count == 0 {
average_secs = 0.0;
total_secs = 0.0;
} else {
total_secs = revlog
.iter()
.map(|e| (e.taken_millis as f32) / 1000.0)
.sum();
average_secs = total_secs / normal_answer_count as f32;
}
let due = if card.original_due != 0 {
card.original_due
} else {
card.due
};
let due = match card.queue {
CardQueue::New => Due::Position(due),
CardQueue::Learn => Due::Time(TimestampSecs::now()),
CardQueue::Review | CardQueue::DayLearn => Due::Time({
let days_remaining = due - (self.timing_today()?.days_elapsed as i32);
let mut due = TimestampSecs::now();
due.0 += (days_remaining as i64) * 86_400;
due
let (average_secs, total_secs) = average_and_total_secs_strings(&revlog);
let (due_date, due_position) = self.due_date_and_position_strings(&card)?;
Ok(pb::CardStatsResponse {
card_id: card.id.into(),
note_id: card.note_id.into(),
deck: deck.human_name(),
added: card.id.as_secs().0,
first_review: revlog.first().map(|entry| pb::generic::Int64 {
val: entry.id.as_secs().0,
}),
_ => Due::Unknown,
};
Ok(CardStats {
added: card.id.as_secs(),
first_review: revlog.first().map(|e| e.id.as_secs()),
latest_review: revlog.last().map(|e| e.id.as_secs()),
due,
interval_secs: card.interval * 86_400,
ease: (card.ease_factor as u32) / 10,
latest_review: revlog.last().map(|entry| pb::generic::Int64 {
val: entry.id.as_secs().0,
}),
due_date,
due_position,
interval: card.interval,
ease: card.ease_factor as u32,
reviews: card.reps,
lapses: card.lapses,
average_secs,
total_secs,
card_type: nt.get_template(card.template_idx)?.name.clone(),
notetype: nt.name.clone(),
deck: deck.human_name(),
nid: card.note_id,
cid: card.id,
revlog: revlog.into_iter().map(Into::into).collect(),
revlog: revlog
.iter()
.rev()
.map(|entry| stats_revlog_entry(entry))
.collect(),
})
}
fn card_stats_to_string(&mut self, cs: CardStats) -> String {
let tr = &self.tr;
let mut stats = vec![(tr.card_stats_added().into(), cs.added.date_string())];
if let Some(first) = cs.first_review {
stats.push((tr.card_stats_first_review().into(), first.date_string()))
}
if let Some(last) = cs.latest_review {
stats.push((tr.card_stats_latest_review().into(), last.date_string()))
}
match cs.due {
Due::Time(secs) => {
stats.push((tr.statistics_due_date().into(), secs.date_string()));
}
Due::Position(pos) => {
stats.push((tr.card_stats_new_card_position().into(), pos.to_string()));
}
Due::Unknown => {}
fn due_date_and_position_strings(
&mut self,
card: &Card,
) -> Result<(Option<pb::generic::Int64>, Option<pb::generic::Int32>)> {
let due = if card.original_due != 0 {
card.original_due
} else {
card.due
};
if cs.interval_secs > 0 {
stats.push((
tr.card_stats_interval().into(),
time_span(cs.interval_secs as f32, tr, true),
));
}
if cs.ease > 0 {
stats.push((tr.card_stats_ease().into(), format!("{}%", cs.ease)));
}
stats.push((tr.card_stats_review_count().into(), cs.reviews.to_string()));
stats.push((tr.card_stats_lapse_count().into(), cs.lapses.to_string()));
if cs.total_secs > 0.0 {
stats.push((
tr.card_stats_average_time().into(),
time_span(cs.average_secs, tr, true),
));
stats.push((
tr.card_stats_total_time().into(),
time_span(cs.total_secs, tr, true),
));
}
stats.push((tr.card_stats_card_template().into(), cs.card_type));
stats.push((tr.card_stats_note_type().into(), cs.notetype));
stats.push((tr.card_stats_deck_name().into(), cs.deck));
stats.push((tr.card_stats_card_id().into(), cs.cid.0.to_string()));
stats.push((tr.card_stats_note_id().into(), cs.nid.0.to_string()));
let revlog = cs
.revlog
.into_iter()
.rev()
.map(|e| revlog_to_text(e, tr))
.collect();
let revlog_titles = RevlogText {
time: tr.card_stats_review_log_date().into(),
kind: tr.card_stats_review_log_type().into(),
kind_class: "".to_string(),
rating: tr.card_stats_review_log_rating().into(),
interval: tr.card_stats_interval().into(),
ease: tr.card_stats_ease().into(),
rating_class: "".to_string(),
taken_secs: tr.card_stats_review_log_time_taken().into(),
};
CardStatsTemplate {
stats,
revlog,
revlog_titles,
}
.render()
.unwrap()
Ok(match card.queue {
CardQueue::New => (None, Some(pb::generic::Int32 { val: due })),
CardQueue::Learn => (
Some(pb::generic::Int64 {
val: TimestampSecs::now().0,
}),
None,
),
CardQueue::Review | CardQueue::DayLearn => (
{
let days_remaining = due - (self.timing_today()?.days_elapsed as i32);
let mut due = TimestampSecs::now();
due.0 += (days_remaining as i64) * 86_400;
Some(pb::generic::Int64 { val: due.0 })
},
None,
),
_ => (None, None),
})
}
}
fn revlog_to_text(e: RevlogEntry, tr: &I18n) -> RevlogText {
let dt = Local.timestamp(e.id.as_secs().0, 0);
let time = dt.format("<b>%Y-%m-%d</b> @ %H:%M").to_string();
let kind = match e.review_kind {
RevlogReviewKind::Learning => tr.card_stats_review_log_type_learn().into(),
RevlogReviewKind::Review => tr.card_stats_review_log_type_review().into(),
RevlogReviewKind::Relearning => tr.card_stats_review_log_type_relearn().into(),
RevlogReviewKind::Filtered => tr.card_stats_review_log_type_filtered().into(),
RevlogReviewKind::Manual => tr.card_stats_review_log_type_manual().into(),
};
let kind_class = match e.review_kind {
RevlogReviewKind::Learning => String::from("revlog-learn"),
RevlogReviewKind::Review => String::from("revlog-review"),
RevlogReviewKind::Relearning => String::from("revlog-relearn"),
RevlogReviewKind::Filtered => String::from("revlog-filtered"),
RevlogReviewKind::Manual => String::from("revlog-manual"),
};
let rating = e.button_chosen.to_string();
let interval = if e.interval == 0 {
String::from("")
fn average_and_total_secs_strings(revlog: &[RevlogEntry]) -> (f32, f32) {
let normal_answer_count = revlog.iter().filter(|r| r.button_chosen > 0).count();
let total_secs: f32 = revlog
.iter()
.map(|entry| (entry.taken_millis as f32) / 1000.0)
.sum();
if normal_answer_count == 0 || total_secs == 0.0 {
(0.0, 0.0)
} else {
let interval_secs = e.interval_secs();
time_span(interval_secs as f32, tr, true)
};
let ease = if e.ease_factor > 0 {
format!("{}%", e.ease_factor / 10)
} else {
"".to_string()
};
let rating_class = if e.button_chosen == 1 {
String::from("revlog-ease1")
} else {
"".to_string()
};
let taken_secs = tr
.statistics_seconds_taken((e.taken_millis / 1000) as i32)
.into();
(total_secs / normal_answer_count as f32, total_secs)
}
}
RevlogText {
time,
kind,
kind_class,
rating,
rating_class,
interval,
ease,
taken_secs,
fn stats_revlog_entry(entry: &RevlogEntry) -> pb::card_stats_response::StatsRevlogEntry {
pb::card_stats_response::StatsRevlogEntry {
time: entry.id.as_secs().0,
review_kind: entry.review_kind.into(),
button_chosen: entry.button_chosen as u32,
interval: entry.interval_secs(),
ease: entry.ease_factor,
taken_secs: entry.taken_millis as f32 / 1000.,
}
}

View file

@ -1,34 +0,0 @@
<table class="card-stats" width="100%">
{% for row in stats %}
<tr>
<td align="left" style="padding-right: 3px;">
<b>{{ row.0 }}</b>
</td>
<td>{{ row.1 }}</td>
</tr>
{% endfor %}
</table>
{% if !revlog.is_empty() %}
<table class="review-log" width="100%">
<tr>
<th>{{ revlog_titles.time }}</th>
<th align="right">{{ revlog_titles.kind }}</th>
<th align="center">{{ revlog_titles.rating }}</th>
<th>{{ revlog_titles.interval }}</th>
<th align="right">{{ revlog_titles.ease }}</th>
<th align="right">{{ revlog_titles.taken_secs }}</th>
</tr>
{% for entry in revlog %}
<tr>
<td>{{ entry.time|safe }}</td>
<td align="right" class="{{ entry.kind_class }}">{{ entry.kind }}</td>
<td align="center" class="{{ entry.rating_class }}">{{ entry.rating }}</td>
<td>{{ entry.interval }}</td>
<td align="right">{{ entry.ease }}</td>
<td align="right">{{ entry.taken_secs }}</td>
</tr>
{% endfor %}
</table>
{% endif %}

View file

69
ts/card-info/BUILD.bazel Normal file
View file

@ -0,0 +1,69 @@
load("//ts:prettier.bzl", "prettier_test")
load("//ts:eslint.bzl", "eslint_test")
load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check")
load("//ts:esbuild.bzl", "esbuild")
load("//ts:compile_sass.bzl", "compile_sass")
load("//ts:typescript.bzl", "typescript")
compile_sass(
srcs = ["card-info-base.scss"],
group = "base_css",
visibility = ["//visibility:public"],
deps = [
"//sass:base_lib",
"//sass:scrollbar_lib",
"//sass/bootstrap",
],
)
compile_svelte()
typescript(
name = "index",
deps = [
":svelte",
"//ts/components",
"//ts/lib",
"@npm//@fluent",
],
)
esbuild(
name = "card-info",
args = {
"globalName": "anki",
"loader": {".svg": "text"},
},
entry_point = "index.ts",
output_css = "card-info.css",
visibility = ["//visibility:public"],
deps = [
":base_css",
":index",
":svelte",
"//ts/components",
"//ts/lib",
"@npm//protobufjs",
],
)
exports_files(["card-info.html"])
# Tests
################
prettier_test()
eslint_test()
svelte_check(
name = "svelte_check",
srcs = glob([
"*.ts",
"*.svelte",
]) + [
"//sass:button_mixins_lib",
"//sass/bootstrap",
"//ts/components",
],
)

View file

@ -0,0 +1,110 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr2 from "../lib/ftl";
import { Stats, unwrapOptionalNumber } from "../lib/proto";
import { Timestamp, timeSpan, DAY } from "../lib/time";
import Revlog from "./Revlog.svelte";
export let stats: Stats.CardStatsResponse;
function dateString(timestamp: number): string {
return new Timestamp(timestamp).dateString();
}
interface StatsRow {
label: string;
value: string | number;
}
const statsRows: StatsRow[] = [];
statsRows.push({ label: tr2.cardStatsAdded(), value: dateString(stats.added) });
const firstReview = unwrapOptionalNumber(stats.firstReview);
if (firstReview !== undefined) {
statsRows.push({
label: tr2.cardStatsFirstReview(),
value: dateString(firstReview),
});
}
const latestReview = unwrapOptionalNumber(stats.latestReview);
if (latestReview !== undefined) {
statsRows.push({
label: tr2.cardStatsLatestReview(),
value: dateString(latestReview),
});
}
const dueDate = unwrapOptionalNumber(stats.dueDate);
if (dueDate !== undefined) {
statsRows.push({ label: tr2.statisticsDueDate(), value: dateString(dueDate) });
}
const duePosition = unwrapOptionalNumber(stats.duePosition);
if (duePosition !== undefined) {
statsRows.push({
label: tr2.cardStatsNewCardPosition(),
value: dateString(duePosition),
});
}
if (stats.interval) {
statsRows.push({
label: tr2.cardStatsInterval(),
value: timeSpan(stats.interval * DAY),
});
}
if (stats.ease) {
statsRows.push({ label: tr2.cardStatsEase(), value: `${stats.ease / 10}%` });
}
statsRows.push({ label: tr2.cardStatsReviewCount(), value: stats.reviews });
statsRows.push({ label: tr2.cardStatsLapseCount(), value: stats.lapses });
if (stats.totalSecs) {
statsRows.push({
label: tr2.cardStatsAverageTime(),
value: timeSpan(stats.averageSecs),
});
statsRows.push({
label: tr2.cardStatsTotalTime(),
value: timeSpan(stats.totalSecs),
});
}
statsRows.push({ label: tr2.cardStatsCardTemplate(), value: stats.cardType });
statsRows.push({ label: tr2.cardStatsNoteType(), value: stats.notetype });
statsRows.push({ label: tr2.cardStatsDeckName(), value: stats.deck });
statsRows.push({ label: tr2.cardStatsCardId(), value: stats.cardId });
statsRows.push({ label: tr2.cardStatsNoteId(), value: stats.noteId });
</script>
<div class="container">
<div>
<table class="stats-table">
{#each statsRows as row, _index}
<tr>
<th style="text-align:start">{row.label}</th>
<td>{row.value}</td>
</tr>
{/each}
</table>
<Revlog {stats} />
</div>
</div>
<style>
.container {
display: flex;
justify-content: center;
white-space: nowrap;
}
.stats-table {
width: 100%;
text-align: start;
}
</style>

141
ts/card-info/Revlog.svelte Normal file
View file

@ -0,0 +1,141 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr2 from "../lib/ftl";
import { Stats } from "../lib/proto";
import { Timestamp, timeSpan } from "../lib/time";
export let stats: Stats.CardStatsResponse;
type IStatsRevlogEntry = Stats.CardStatsResponse.IStatsRevlogEntry;
function reviewKindClass(entry: IStatsRevlogEntry): string {
switch (entry.reviewKind) {
case Stats.RevlogEntry.ReviewKind.LEARNING:
return "revlog-learn";
case Stats.RevlogEntry.ReviewKind.REVIEW:
return "revlog-review";
case Stats.RevlogEntry.ReviewKind.RELEARNING:
return "revlog-relearn";
}
return "";
}
function reviewKindLabel(entry: IStatsRevlogEntry): string {
switch (entry.reviewKind) {
case Stats.RevlogEntry.ReviewKind.LEARNING:
return tr2.cardStatsReviewLogTypeLearn();
case Stats.RevlogEntry.ReviewKind.REVIEW:
return tr2.cardStatsReviewLogTypeReview();
case Stats.RevlogEntry.ReviewKind.RELEARNING:
return tr2.cardStatsReviewLogTypeRelearn();
case Stats.RevlogEntry.ReviewKind.FILTERED:
return tr2.cardStatsReviewLogTypeFiltered();
case Stats.RevlogEntry.ReviewKind.MANUAL:
return tr2.cardStatsReviewLogTypeManual();
}
}
function ratingClass(entry: IStatsRevlogEntry): string {
if (entry.buttonChosen === 1) {
return "revlog-ease1";
}
return "";
}
interface RevlogRow {
date: string;
time: string;
reviewKind: string;
reviewKindClass: string;
rating: number;
ratingClass: string;
interval: string;
ease: string;
takenSecs: string;
}
function revlogRowFromEntry(entry: IStatsRevlogEntry): RevlogRow {
const timestamp = new Timestamp(entry.time!);
return {
date: timestamp.dateString(),
time: timestamp.timeString(),
reviewKind: reviewKindLabel(entry),
reviewKindClass: reviewKindClass(entry),
rating: entry.buttonChosen!,
ratingClass: ratingClass(entry),
interval: timeSpan(entry.interval!),
ease: entry.ease ? `${entry.ease / 10}%` : "",
takenSecs: timeSpan(entry.takenSecs!, true),
};
}
const revlogRows: RevlogRow[] = stats.revlog.map((entry) =>
revlogRowFromEntry(entry)
);
</script>
{#if stats.revlog.length}
<div class="revlog-container">
<table class="revlog-table">
<tr>
<th>{tr2.cardStatsReviewLogDate()}</th>
<th>{tr2.cardStatsReviewLogType()}</th>
<th>{tr2.cardStatsReviewLogRating()}</th>
<th>{tr2.cardStatsInterval()}</th>
<th>{tr2.cardStatsEase()}</th>
<th>{tr2.cardStatsReviewLogTimeTaken()}</th>
</tr>
{#each revlogRows as row, _index}
<tr>
<td class="left"><b>{row.date}</b> @ {row.time}</td>
<td class="center {row.reviewKindClass}">
{row.reviewKind}
</td>
<td class="center {row.ratingClass}">{row.rating}</td>
<td class="center">{row.interval}</td>
<td class="center">{row.ease}</td>
<td class="right">{row.takenSecs}</td>
</tr>
{/each}
</table>
</div>
{/if}
<style>
.left {
text-align: start;
}
.right {
text-align: end;
}
.center {
text-align: center;
}
.revlog-container {
margin: 4em -2em 0 -2em;
}
.revlog-table {
width: 100%;
border-spacing: 2em 0em;
}
.revlog-learn {
color: var(--new-count);
}
.revlog-review {
color: var(--review-count);
}
.revlog-relearn,
.revlog-ease1 {
color: var(--learn-count);
}
</style>

View file

@ -0,0 +1,6 @@
@use "core";
@use "scrollbar";
.night-mode {
@include scrollbar.night-mode;
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" id="viewport" content="width=device-width" />
<link href="card-info-base.css" rel="stylesheet" />
<link href="card-info.css" rel="stylesheet" />
<script src="../js/vendor/bootstrap.bundle.min.js"></script>
<script src="card-info.js"></script>
</head>
<body>
<div id="main"></div>
</body>
</html>

33
ts/card-info/index.ts Normal file
View file

@ -0,0 +1,33 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { getCardStats } from "./lib";
import { setupI18n, ModuleName } from "../lib/i18n";
import { checkNightMode } from "../lib/nightmode";
import CardInfo from "./CardInfo.svelte";
export async function cardInfo(
target: HTMLDivElement,
cardId: number,
includeRevlog: boolean
): Promise<CardInfo> {
checkNightMode();
const [stats] = await Promise.all([
getCardStats(cardId),
setupI18n({
modules: [
ModuleName.CARD_STATS,
ModuleName.SCHEDULING,
ModuleName.STATISTICS,
],
}),
]);
if (!includeRevlog) {
stats.revlog = [];
}
return new CardInfo({
target,
props: { stats },
});
}

11
ts/card-info/lib.ts Normal file
View file

@ -0,0 +1,11 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { Stats } from "../lib/proto";
import { postRequest } from "../lib/postrequest";
export async function getCardStats(cardId: number): Promise<Stats.CardStatsResponse> {
return Stats.CardStatsResponse.decode(
await postRequest("/_anki/cardStats", JSON.stringify(cardId))
);
}

View file

@ -0,0 +1,5 @@
{
"extends": "../tsconfig.json",
"include": ["*"],
"references": [{ "path": "../lib" }]
}

View file

@ -5,9 +5,28 @@ import { anki } from "./backend_proto";
import Cards = anki.cards;
import DeckConfig = anki.deckconfig;
import Generic = anki.generic;
import Notetypes = anki.notetypes;
import Scheduler = anki.scheduler;
import Stats = anki.stats;
import Tags = anki.tags;
export { Stats, Cards, DeckConfig, Notetypes, Scheduler, Tags };
export function unwrapOptionalNumber(
msg:
| Generic.IInt64
| Generic.IUInt32
| Generic.IInt32
| Generic.OptionalInt32
| Generic.OptionalUInt32
| null
| undefined
): number | undefined {
if (msg && msg !== null) {
if (msg.val !== null) {
return msg.val;
}
}
return undefined;
}

View file

@ -177,3 +177,27 @@ export function dayLabel(daysStart: number, daysEnd: number): string {
}
}
}
/** Helper for converting Unix timestamps to date strings. */
export class Timestamp {
private date: Date;
constructor(seconds: number) {
this.date = new Date(seconds * 1000);
}
/** YYYY-MM-DD */
dateString(): string {
const year = this.date.getFullYear();
const month = ("0" + (this.date.getMonth() + 1)).slice(-2);
const date = ("0" + this.date.getDate()).slice(-2);
return `${year}-${month}-${date}`;
}
/** HH:MM */
timeString(): string {
const hours = ("0" + this.date.getHours()).slice(-2);
const minutes = ("0" + this.date.getMinutes()).slice(-2);
return `${hours}:${minutes}`;
}
}