mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
qt's js code now shares ts/node_modules; more graph work
This commit is contained in:
parent
2a3ef80c90
commit
329186f140
22 changed files with 556 additions and 349 deletions
10
Makefile
10
Makefile
|
@ -53,7 +53,7 @@ DEVFLAGS := $(BUILDFLAGS)
|
|||
RUNFLAGS :=
|
||||
CHECKABLE_PY := pylib qt
|
||||
CHECKABLE_RS := rslib rspy
|
||||
DEVEL := rslib rspy pylib qt
|
||||
DEVEL := rslib rspy pylib ts qt
|
||||
|
||||
.PHONY: all
|
||||
all: run
|
||||
|
@ -115,10 +115,14 @@ qt/po/repo:
|
|||
$(MAKE) pull-i18n
|
||||
|
||||
.PHONY: build
|
||||
build: clean-dist build-rspy build-pylib build-qt add-buildhash
|
||||
build: clean-dist build-ts build-rspy build-pylib build-qt add-buildhash
|
||||
@echo
|
||||
@echo "Build complete."
|
||||
|
||||
.PHONY: build-ts
|
||||
build-ts:
|
||||
$(SUBMAKE) -C ts build
|
||||
|
||||
.PHONY: build-rspy
|
||||
build-rspy: pyenv buildhash
|
||||
@set -eu -o pipefail ${SHELLFLAGS}; \
|
||||
|
@ -154,7 +158,7 @@ check: pyenv buildhash prepare
|
|||
@set -eu -o pipefail ${SHELLFLAGS}; \
|
||||
.github/scripts/trailing-newlines.sh; \
|
||||
. "${ACTIVATE_SCRIPT}"; \
|
||||
for dir in $(CHECKABLE_RS) $(CHECKABLE_PY); do \
|
||||
for dir in $(CHECKABLE_RS) ts $(CHECKABLE_PY); do \
|
||||
$(SUBMAKE) -C $$dir check; \
|
||||
done;
|
||||
@echo
|
||||
|
|
|
@ -1037,6 +1037,12 @@ message ButtonsGraphData {
|
|||
}
|
||||
|
||||
message RevlogEntry {
|
||||
enum ReviewKind {
|
||||
LEARNING = 0;
|
||||
REVIEW = 1;
|
||||
RELEARNING = 2;
|
||||
EARLY_REVIEW = 4;
|
||||
}
|
||||
int64 id = 1;
|
||||
int64 cid = 2;
|
||||
int32 usn = 3;
|
||||
|
@ -1045,5 +1051,5 @@ message RevlogEntry {
|
|||
int32 last_interval = 6;
|
||||
uint32 ease_factor = 7;
|
||||
uint32 taken_millis = 8;
|
||||
uint32 review_kind = 9;
|
||||
ReviewKind review_kind = 9;
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ all: check
|
|||
TSDEPS := $(wildcard ts/src/*.ts) $(wildcard ts/scss/*.scss)
|
||||
|
||||
.build/js: $(TSDEPS)
|
||||
(cd ts && npm i && npm run build)
|
||||
(cd ts && make build)
|
||||
python ./tools/extract_scss_colors.py
|
||||
@touch $@
|
||||
|
||||
|
@ -81,7 +81,7 @@ check: .build/pyaudio $(BUILD_STEPS) .build/mypy .build/test .build/fmt .build/i
|
|||
fix: $(BUILD_STEPS)
|
||||
isort $(ISORTARGS)
|
||||
python -m black $(BLACKARGS)
|
||||
(cd ts && npm run pretty)
|
||||
(cd ts && make fix)
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
|
@ -93,7 +93,7 @@ clean:
|
|||
JSDEPS := $(patsubst ts/src/%.ts, web/%.js, $(TSDEPS))
|
||||
|
||||
.build/ts-fmt: $(TSDEPS)
|
||||
(cd ts && npm i && npm run check-pretty)
|
||||
(cd ts && make check)
|
||||
@touch $@
|
||||
|
||||
# Checking python
|
||||
|
|
|
@ -182,7 +182,7 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
cmd = self.path[len("/_anki/") :]
|
||||
|
||||
if cmd == "graphData":
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
content_length = int(self.headers["Content-Length"])
|
||||
body = self.rfile.read(content_length)
|
||||
data = graph_data(self.mw.col, **from_json_bytes(body))
|
||||
else:
|
||||
|
@ -198,13 +198,14 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
self.wfile.write(data)
|
||||
|
||||
|
||||
def graph_data(col: Collection, search: str, days: str) -> bytes:
|
||||
def graph_data(col: Collection, search: str, days: int) -> bytes:
|
||||
try:
|
||||
return col.backend.graphs(search=search, days=days)
|
||||
except Exception as e:
|
||||
# likely searching error
|
||||
print(e)
|
||||
return b''
|
||||
return b""
|
||||
|
||||
|
||||
# work around Windows machines with incorrect mime type
|
||||
RequestHandler.extensions_map[".css"] = "text/css"
|
||||
|
|
34
qt/ts/Makefile
Normal file
34
qt/ts/Makefile
Normal file
|
@ -0,0 +1,34 @@
|
|||
SHELL := /bin/bash
|
||||
|
||||
ifndef SHELLFLAGS
|
||||
SHELLFLAGS :=
|
||||
endif
|
||||
|
||||
.SHELLFLAGS := -eu -o pipefail ${SHELLFLAGS} -c
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
ifndef OS
|
||||
OS := unknown
|
||||
endif
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
.SUFFIXES:
|
||||
|
||||
BIN := ../../ts/node_modules/.bin
|
||||
|
||||
PHONY: all
|
||||
all: check
|
||||
|
||||
PHONY: build
|
||||
build:
|
||||
$(BIN)/tsc --build
|
||||
$(BIN)/sass --no-source-map scss:../aqt_data/web
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
$(BIN)/prettier --check src/*.ts
|
||||
|
||||
.PHONY: fix
|
||||
fix:
|
||||
$(BIN)/prettier --write src/*.ts
|
185
qt/ts/package-lock.json
generated
185
qt/ts/package-lock.json
generated
|
@ -1,185 +0,0 @@
|
|||
{
|
||||
"name": "anki-dtop-js",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/jquery": {
|
||||
"version": "3.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz",
|
||||
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"@types/jqueryui": {
|
||||
"version": "1.12.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.9.tgz",
|
||||
"integrity": "sha512-bHE7BiG+5Sviy/eA9Npz5HHF3hv40XjaEbpYtSJPaNwuyxhSJ0qWlE8C5DgNMfobVOZ2aSTrM1iGDCGmvlbxOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/jquery": "*"
|
||||
}
|
||||
},
|
||||
"@types/mathjax": {
|
||||
"version": "0.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/mathjax/-/mathjax-0.0.36.tgz",
|
||||
"integrity": "sha512-TqDJc2GWuTqd/m+G/FbNkN+/TF2OCCHvcawmhIrUaZkdVquMdNZmNiNUkupNg9qctorXXkVLVSogZv1DhmgLmg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/sizzle": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
|
||||
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
|
||||
"dev": true
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
||||
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
|
||||
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
|
||||
"dev": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
|
||||
"integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.1",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.1.2",
|
||||
"glob-parent": "~5.1.0",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.3.0"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
|
||||
"integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
|
||||
"integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
||||
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz",
|
||||
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
||||
"dev": true
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
|
||||
"integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.25.0.tgz",
|
||||
"integrity": "sha512-uQMjye0Y70SEDGO56n0j91tauqS9E1BmpKHtiYNQScXDHeaE9uHwNEqQNFf4Bes/3DHMNinB6u79JsG10XWNyw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
|
||||
"integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "anki-dtop-js",
|
||||
"version": "1.0.0",
|
||||
"description": "Anki desktop js support files",
|
||||
"scripts": {
|
||||
"build": "tsc --build && sass --no-source-map scss:../aqt_data/web",
|
||||
"pretty": "prettier --write src/*.ts",
|
||||
"check-pretty": "prettier --check src/*.ts"
|
||||
},
|
||||
"private": true,
|
||||
"author": "Ankitects Pty Ltd",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@types/jquery": "^3.3.31",
|
||||
"@types/jqueryui": "^1.12.9",
|
||||
"@types/mathjax": "0.0.36",
|
||||
"prettier": "^1.19.1",
|
||||
"sass": "^1.25.0",
|
||||
"typescript": "^3.7.3"
|
||||
}
|
||||
}
|
|
@ -1,21 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"es6",
|
||||
"dom"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"outDir": "../aqt_data/web",
|
||||
"strict": true,
|
||||
/* Enable all strict type-checking options. */
|
||||
"noImplicitAny": false,
|
||||
/* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"strictNullChecks": false,
|
||||
/* Enable strict null checks. */
|
||||
"noImplicitThis": false,
|
||||
/* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
"esModuleInterop": true
|
||||
}
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"lib": ["es6", "dom"],
|
||||
"rootDir": "src",
|
||||
"outDir": "../aqt_data/web",
|
||||
"typeRoots": ["../../ts/node_modules/@types"],
|
||||
"strict": true,
|
||||
/* Enable all strict type-checking options. */
|
||||
"noImplicitAny": false,
|
||||
/* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"strictNullChecks": false,
|
||||
/* Enable strict null checks. */
|
||||
"noImplicitThis": false,
|
||||
/* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,103 +3,6 @@
|
|||
|
||||
use crate::{backend_proto as pb, prelude::*, revlog::RevlogEntry};
|
||||
|
||||
// impl GraphsContext {
|
||||
// fn observe_button_stats_for_review(&mut self, review: &RevlogEntry) {
|
||||
// let mut button_num = review.button_chosen as usize;
|
||||
// if button_num == 0 {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let buttons = &mut self.stats.buttons;
|
||||
// let category = match review.review_kind {
|
||||
// RevlogReviewKind::Learning | RevlogReviewKind::Relearning => {
|
||||
// // V1 scheduler only had 3 buttons in learning
|
||||
// if button_num == 4 && self.scheduler == SchedulerVersion::V1 {
|
||||
// button_num = 3;
|
||||
// }
|
||||
|
||||
// &mut buttons.learn
|
||||
// }
|
||||
// RevlogReviewKind::Review | RevlogReviewKind::EarlyReview => {
|
||||
// if review.last_interval < 21 {
|
||||
// &mut buttons.young
|
||||
// } else {
|
||||
// &mut buttons.mature
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// if let Some(count) = category.get_mut(button_num - 1) {
|
||||
// *count += 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn observe_hour_stats_for_review(&mut self, review: &RevlogEntry) {
|
||||
// match review.review_kind {
|
||||
// RevlogReviewKind::Learning
|
||||
// | RevlogReviewKind::Review
|
||||
// | RevlogReviewKind::Relearning => {
|
||||
// let hour_idx = (((review.id.0 / 1000) + self.local_offset_secs) / 3600) % 24;
|
||||
// let hour = &mut self.stats.hours[hour_idx as usize];
|
||||
|
||||
// hour.review_count += 1;
|
||||
// if review.button_chosen != 1 {
|
||||
// hour.correct_count += 1;
|
||||
// }
|
||||
// }
|
||||
// RevlogReviewKind::EarlyReview => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn observe_today_stats_for_review(&mut self, review: &RevlogEntry) {
|
||||
// if review.id.0 < self.today_rolled_over_at_millis {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let today = &mut self.stats.today;
|
||||
|
||||
// // total
|
||||
// today.answer_count += 1;
|
||||
// today.answer_millis += review.taken_millis;
|
||||
|
||||
// // correct
|
||||
// if review.button_chosen > 1 {
|
||||
// today.correct_count += 1;
|
||||
// }
|
||||
|
||||
// // mature
|
||||
// if review.last_interval >= 21 {
|
||||
// today.mature_count += 1;
|
||||
// if review.button_chosen > 1 {
|
||||
// today.mature_correct += 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // type counts
|
||||
// match review.review_kind {
|
||||
// RevlogReviewKind::Learning => today.learn_count += 1,
|
||||
// RevlogReviewKind::Review => today.review_count += 1,
|
||||
// RevlogReviewKind::Relearning => today.relearn_count += 1,
|
||||
// RevlogReviewKind::EarlyReview => today.early_review_count += 1,
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn observe_card_stats_for_card(&mut self, card: &Card) {
|
||||
// counts by type
|
||||
// match card.queue {
|
||||
// CardQueue::New => cstats.new_count += 1,
|
||||
// CardQueue::Review if card.ivl >= 21 => cstats.mature_count += 1,
|
||||
// CardQueue::Review | CardQueue::Learn | CardQueue::DayLearn => {
|
||||
// cstats.young_or_learning_count += 1
|
||||
// }
|
||||
// CardQueue::Suspended | CardQueue::UserBuried | CardQueue::SchedBuried => {
|
||||
// cstats.suspended_or_buried_count += 1
|
||||
// }
|
||||
// CardQueue::PreviewRepeat => {}
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn graph_data_for_search(
|
||||
&mut self,
|
||||
|
@ -155,7 +58,7 @@ impl From<RevlogEntry> for pb::RevlogEntry {
|
|||
last_interval: e.last_interval,
|
||||
ease_factor: e.ease_factor,
|
||||
taken_millis: e.taken_millis,
|
||||
review_kind: e.review_kind as u32,
|
||||
review_kind: e.review_kind as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
ts/Makefile
12
ts/Makefile
|
@ -38,14 +38,22 @@ dev: .build/proto
|
|||
PHONY: build
|
||||
build: .build/build
|
||||
|
||||
PHONY: develop
|
||||
develop: .build/build
|
||||
|
||||
.build/build: .build/proto $(BUILDDEPS) $(wildcard src/*/*.svelte src/*/*.ts)
|
||||
npm i
|
||||
npm run build
|
||||
@touch $@
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
check: .build/build
|
||||
npm run check
|
||||
|
||||
.PHONY: fix
|
||||
fix:
|
||||
fix: .build/build
|
||||
npm run fix
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf .build node_modules src/backend/*
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"name": "anki",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"author": "Ankitects Pty Ltd and contributors",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Anki JS support files",
|
||||
"devDependencies": {
|
||||
"@pyoner/svelte-types": "^3.4.4-2",
|
||||
"@types/d3-array": "^2.0.0",
|
||||
|
@ -10,9 +14,12 @@
|
|||
"@types/d3-selection": "^1.4.1",
|
||||
"@types/d3-shape": "^1.3.2",
|
||||
"@types/d3-transition": "^1.1.6",
|
||||
"@types/jquery": "^3.5.0",
|
||||
"@types/jqueryui": "^1.12.13",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.throttle": "^4.1.6",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/mathjax": "0.0.36",
|
||||
"@typescript-eslint/eslint-plugin": "^2.11.0",
|
||||
"@typescript-eslint/parser": "^2.11.0",
|
||||
"cross-env": "^7.0.2",
|
||||
|
@ -23,6 +30,7 @@
|
|||
"html-webpack-plugin": "^4.3.0",
|
||||
"prettier": "^2.0.0",
|
||||
"prettier-plugin-svelte": "^1.1.0",
|
||||
"sass": "^1.26.9",
|
||||
"style-loader": "^1.2.1",
|
||||
"svelte": "^3.23.2",
|
||||
"svelte-loader": "^2.13.6",
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
import pb from "../backend/proto";
|
||||
import HistogramGraph from "./HistogramGraph.svelte";
|
||||
|
||||
export let data: pb.BackendProto.GraphsOut | null = null;
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
let histogramData = null as HistogramData | null;
|
||||
let range = AddedRange.Month;
|
||||
|
||||
let addedData: GraphData | null = null;
|
||||
$: if (data) {
|
||||
$: if (sourceData) {
|
||||
console.log("gathering data");
|
||||
addedData = gatherData(data);
|
||||
addedData = gatherData(sourceData);
|
||||
}
|
||||
|
||||
$: if (addedData) {
|
||||
|
|
32
ts/src/stats/ButtonsGraph.svelte
Normal file
32
ts/src/stats/ButtonsGraph.svelte
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script lang="typescript">
|
||||
import { defaultGraphBounds } from "./graphs";
|
||||
import AxisTicks from "./AxisTicks.svelte";
|
||||
import AxisLabels from "./AxisLabels.svelte";
|
||||
import { gatherData, GraphData, renderButtons } from "./buttons";
|
||||
import pb from "../backend/proto";
|
||||
import HistogramGraph from "./HistogramGraph.svelte";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
const bounds = defaultGraphBounds();
|
||||
const xText = "";
|
||||
const yText = "Times pressed";
|
||||
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
|
||||
$: if (sourceData) {
|
||||
console.log("gathering data");
|
||||
renderButtons(svg as SVGElement, bounds, gatherData(sourceData));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="graph">
|
||||
<h1>Answer Buttons</h1>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
<g class="bars" />
|
||||
<g class="hoverzone" />
|
||||
<AxisTicks {bounds} />
|
||||
<AxisLabels {bounds} {xText} {yText} />
|
||||
</svg>
|
||||
</div>
|
17
ts/src/stats/CardCounts.svelte
Normal file
17
ts/src/stats/CardCounts.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="typescript">
|
||||
import { gatherData, CardCounts } from "./card-counts";
|
||||
import pb from "../backend/proto";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
let cardCounts: CardCounts | null = null;
|
||||
$: if (sourceData) {
|
||||
console.log("gathering data");
|
||||
cardCounts = gatherData(sourceData);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="graph">
|
||||
<h1>Card Counts</h1>
|
||||
{JSON.stringify(cardCounts)}
|
||||
</div>
|
|
@ -4,14 +4,14 @@
|
|||
import pb from "../backend/proto";
|
||||
import HistogramGraph from "./HistogramGraph.svelte";
|
||||
|
||||
export let data: pb.BackendProto.GraphsOut | null = null;
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
let histogramData = null as HistogramData | null;
|
||||
|
||||
$: if (data) {
|
||||
$: if (sourceData) {
|
||||
console.log("gathering data");
|
||||
histogramData = prepareData(gatherData(data));
|
||||
histogramData = prepareData(gatherData(sourceData));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -11,8 +11,11 @@
|
|||
import IntervalsGraph from "./IntervalsGraph.svelte";
|
||||
import EaseGraph from "./EaseGraph.svelte";
|
||||
import AddedGraph from "./AddedGraph.svelte";
|
||||
import TodayStats from "./TodayStats.svelte";
|
||||
import ButtonsGraph from "./ButtonsGraph.svelte";
|
||||
import CardCounts from "./CardCounts.svelte";
|
||||
|
||||
let data: pb.BackendProto.GraphsOut | null = null;
|
||||
let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
enum SearchRange {
|
||||
Deck = 1,
|
||||
|
@ -29,7 +32,7 @@
|
|||
|
||||
const refresh = async () => {
|
||||
console.log(`search is ${search}`);
|
||||
data = await getGraphData(search, days);
|
||||
sourceData = await getGraphData(search, days);
|
||||
};
|
||||
|
||||
$: {
|
||||
|
@ -108,6 +111,9 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<AddedGraph {data} />
|
||||
<IntervalsGraph {data} />
|
||||
<EaseGraph {data} />
|
||||
<TodayStats {sourceData} />
|
||||
<CardCounts {sourceData} />
|
||||
<AddedGraph {sourceData} />
|
||||
<IntervalsGraph {sourceData} />
|
||||
<EaseGraph {sourceData} />
|
||||
<ButtonsGraph {sourceData} />
|
||||
|
|
|
@ -9,16 +9,16 @@
|
|||
import pb from "../backend/proto";
|
||||
import HistogramGraph from "./HistogramGraph.svelte";
|
||||
|
||||
export let data: pb.BackendProto.GraphsOut | null = null;
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
let range = IntervalRange.Percentile95;
|
||||
let histogramData = null as HistogramData | null;
|
||||
|
||||
let intervalData: IntervalGraphData | null = null;
|
||||
$: if (data) {
|
||||
$: if (sourceData) {
|
||||
console.log("gathering data");
|
||||
intervalData = gatherIntervalData(data);
|
||||
intervalData = gatherIntervalData(sourceData);
|
||||
}
|
||||
|
||||
$: if (intervalData) {
|
||||
|
|
17
ts/src/stats/TodayStats.svelte
Normal file
17
ts/src/stats/TodayStats.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="typescript">
|
||||
import { gatherData, TodayData } from "./today";
|
||||
import pb from "../backend/proto";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
let todayData: TodayData | null = null;
|
||||
$: if (sourceData) {
|
||||
console.log("gathering data");
|
||||
todayData = gatherData(sourceData);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="graph">
|
||||
<h1>Today</h1>
|
||||
{JSON.stringify(todayData)}
|
||||
</div>
|
195
ts/src/stats/buttons.ts
Normal file
195
ts/src/stats/buttons.ts
Normal file
|
@ -0,0 +1,195 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
@typescript-eslint/no-explicit-any: "off",
|
||||
*/
|
||||
|
||||
import pb from "../backend/proto";
|
||||
import { interpolateRdYlGn } from "d3-scale-chromatic";
|
||||
import "d3-transition";
|
||||
import { select, mouse } from "d3-selection";
|
||||
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
|
||||
import { axisBottom, axisLeft } from "d3-axis";
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import { GraphBounds } from "./graphs";
|
||||
|
||||
type ButtonCounts = [number, number, number, number];
|
||||
|
||||
export interface GraphData {
|
||||
learning: ButtonCounts;
|
||||
young: ButtonCounts;
|
||||
mature: ButtonCounts;
|
||||
}
|
||||
|
||||
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
|
||||
|
||||
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
||||
const learning: ButtonCounts = [0, 0, 0, 0];
|
||||
const young: ButtonCounts = [0, 0, 0, 0];
|
||||
const mature: ButtonCounts = [0, 0, 0, 0];
|
||||
|
||||
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
||||
let buttonNum = review.buttonChosen;
|
||||
if (buttonNum <= 0 || buttonNum > 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let buttons = learning;
|
||||
switch (review.reviewKind) {
|
||||
case ReviewKind.LEARNING:
|
||||
case ReviewKind.RELEARNING:
|
||||
// V1 scheduler only had 3 buttons in learning
|
||||
if (buttonNum === 4 && data.schedulerVersion === 1) {
|
||||
buttonNum = 3;
|
||||
}
|
||||
break;
|
||||
|
||||
case ReviewKind.REVIEW:
|
||||
case ReviewKind.EARLY_REVIEW:
|
||||
if (review.lastInterval < 21) {
|
||||
buttons = young;
|
||||
} else {
|
||||
buttons = mature;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
buttons[buttonNum - 1] += 1;
|
||||
}
|
||||
return { learning, young, mature };
|
||||
}
|
||||
|
||||
interface Datum {
|
||||
buttonNum: string;
|
||||
group: "learning" | "young" | "mature";
|
||||
count: number;
|
||||
}
|
||||
|
||||
function tooltipText(d: Datum): string {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
|
||||
export function renderButtons(
|
||||
svgElem: SVGElement,
|
||||
bounds: GraphBounds,
|
||||
sourceData: GraphData
|
||||
): void {
|
||||
const data = [
|
||||
...sourceData.learning.map((count: number, idx: number) => {
|
||||
return {
|
||||
buttonNum: (idx + 1).toString(),
|
||||
group: "learning",
|
||||
count,
|
||||
} as Datum;
|
||||
}),
|
||||
...sourceData.young.map((count: number, idx: number) => {
|
||||
return {
|
||||
buttonNum: (idx + 1).toString(),
|
||||
group: "young",
|
||||
count,
|
||||
} as Datum;
|
||||
}),
|
||||
...sourceData.mature.map((count: number, idx: number) => {
|
||||
return {
|
||||
buttonNum: (idx + 1).toString(),
|
||||
group: "mature",
|
||||
count,
|
||||
} as Datum;
|
||||
}),
|
||||
];
|
||||
|
||||
console.log(data);
|
||||
|
||||
const yMax = Math.max(...data.map((d) => d.count));
|
||||
|
||||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
|
||||
const xGroup = scaleBand()
|
||||
.domain(["learning", "young", "mature"])
|
||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||
svg.select<SVGGElement>(".x-ticks").transition(trans).call(
|
||||
axisBottom(xGroup)
|
||||
// .ticks()
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
|
||||
const xButton = scaleBand()
|
||||
.domain(["1", "2", "3", "4"])
|
||||
.range([0, xGroup.bandwidth()])
|
||||
.paddingOuter(1)
|
||||
.paddingInner(0.1);
|
||||
|
||||
const colour = scaleSequential(interpolateRdYlGn).domain([1, 4]);
|
||||
|
||||
// y scale
|
||||
|
||||
const y = scaleLinear()
|
||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||
.domain([0, yMax]);
|
||||
svg.select<SVGGElement>(".y-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
axisLeft(y)
|
||||
.ticks(bounds.height / 80)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
|
||||
// x bars
|
||||
|
||||
const updateBar = (sel: any): any => {
|
||||
return sel
|
||||
.attr("width", xButton.bandwidth())
|
||||
.attr("opacity", (d: Datum) => {
|
||||
switch (d.group) {
|
||||
case "learning":
|
||||
return 0.3;
|
||||
case "young":
|
||||
return 0.5;
|
||||
case "mature":
|
||||
return 1;
|
||||
}
|
||||
})
|
||||
.transition(trans)
|
||||
.attr("x", (d: Datum) => xGroup(d.group)! + xButton(d.buttonNum)!)
|
||||
.attr("y", (d: Datum) => y(d.count)!)
|
||||
.attr("height", (d: Datum) => y(0) - y(d.count))
|
||||
.attr("fill", (d: Datum) => colour(parseInt(d.buttonNum)));
|
||||
};
|
||||
|
||||
svg.select("g.bars")
|
||||
.selectAll("rect")
|
||||
.data(data)
|
||||
.join(
|
||||
(enter) =>
|
||||
enter
|
||||
.append("rect")
|
||||
.attr("rx", 1)
|
||||
.attr("x", (d: Datum) => xGroup(d.group)! + xButton(d.buttonNum)!)
|
||||
.attr("y", y(0))
|
||||
.attr("height", 0)
|
||||
.call(updateBar),
|
||||
(update) => update.call(updateBar),
|
||||
(remove) =>
|
||||
remove.call((remove) =>
|
||||
remove.transition(trans).attr("height", 0).attr("y", y(0))
|
||||
)
|
||||
);
|
||||
|
||||
// hover/tooltip
|
||||
svg.select("g.hoverzone")
|
||||
.selectAll("rect")
|
||||
.data(data)
|
||||
.join("rect")
|
||||
.attr("x", (d: Datum) => xGroup(d.group)! + xButton(d.buttonNum)!)
|
||||
.attr("y", () => y(yMax!))
|
||||
.attr("width", xButton.bandwidth())
|
||||
.attr("height", () => y(0) - y(yMax!))
|
||||
.on("mousemove", function (this: any, d: Datum) {
|
||||
const [x, y] = mouse(document.body);
|
||||
showTooltip(tooltipText(d), x, y);
|
||||
})
|
||||
.on("mouseout", hideTooltip);
|
||||
}
|
60
ts/src/stats/card-counts.ts
Normal file
60
ts/src/stats/card-counts.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import pb from "../backend/proto";
|
||||
import { CardQueue } from "../cards";
|
||||
|
||||
export interface CardCounts {
|
||||
totalCards: number;
|
||||
totalNotes: number;
|
||||
newCards: number;
|
||||
young: number;
|
||||
mature: number;
|
||||
suspended: number;
|
||||
buried: number;
|
||||
}
|
||||
|
||||
export function gatherData(data: pb.BackendProto.GraphsOut): CardCounts {
|
||||
const totalNotes = data.noteCount;
|
||||
const totalCards = data.cards.length;
|
||||
let newCards = 0;
|
||||
let young = 0;
|
||||
let mature = 0;
|
||||
let suspended = 0;
|
||||
let buried = 0;
|
||||
|
||||
for (const card of data.cards as pb.BackendProto.Card[]) {
|
||||
switch (card.queue) {
|
||||
case CardQueue.New:
|
||||
newCards += 1;
|
||||
break;
|
||||
case CardQueue.Review:
|
||||
if (card.ivl >= 21) {
|
||||
mature += 1;
|
||||
break;
|
||||
}
|
||||
// young falls through
|
||||
case CardQueue.Learn:
|
||||
case CardQueue.DayLearn:
|
||||
young += 1;
|
||||
break;
|
||||
case CardQueue.Suspended:
|
||||
suspended += 1;
|
||||
break;
|
||||
case CardQueue.SchedBuried:
|
||||
case CardQueue.UserBuried:
|
||||
buried += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalCards,
|
||||
totalNotes,
|
||||
newCards,
|
||||
young,
|
||||
mature,
|
||||
suspended,
|
||||
buried,
|
||||
};
|
||||
}
|
41
ts/src/stats/hours.ts
Normal file
41
ts/src/stats/hours.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import pb from "../backend/proto";
|
||||
|
||||
type ButtonCounts = [number, number, number, number];
|
||||
|
||||
interface Hour {
|
||||
hour: number;
|
||||
totalCount: number;
|
||||
correctCount: number;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
hours: Hour[];
|
||||
}
|
||||
|
||||
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
|
||||
|
||||
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
||||
const hours = Array(24).map((n: number) => {
|
||||
return { hour: n, totalCount: 0, correctCount: 0 } as Hour;
|
||||
});
|
||||
|
||||
// fixme: relative to midnight, not rollover
|
||||
|
||||
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
||||
if (review.reviewKind == ReviewKind.EARLY_REVIEW) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hour =
|
||||
(((review.id as number) / 1000 + data.localOffsetSecs) / 3600) % 24;
|
||||
hours[hour].totalCount += 1;
|
||||
if (review.buttonChosen != 1) {
|
||||
hours[hour].correctCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { hours };
|
||||
}
|
83
ts/src/stats/today.ts
Normal file
83
ts/src/stats/today.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import pb from "../backend/proto";
|
||||
|
||||
export interface TodayData {
|
||||
answerCount: number;
|
||||
answerMillis: number;
|
||||
correctCount: number;
|
||||
matureCorrect: number;
|
||||
matureCount: number;
|
||||
learnCount: number;
|
||||
reviewCount: number;
|
||||
relearnCount: number;
|
||||
earlyReviewCount: number;
|
||||
}
|
||||
|
||||
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
|
||||
|
||||
export function gatherData(data: pb.BackendProto.GraphsOut): TodayData {
|
||||
let answerCount = 0;
|
||||
let answerMillis = 0;
|
||||
let correctCount = 0;
|
||||
let matureCorrect = 0;
|
||||
let matureCount = 0;
|
||||
let learnCount = 0;
|
||||
let reviewCount = 0;
|
||||
let relearnCount = 0;
|
||||
let earlyReviewCount = 0;
|
||||
|
||||
const startOfTodayMillis = (data.nextDayAtSecs - 86400) * 1000;
|
||||
|
||||
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
||||
if (review.id < startOfTodayMillis) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// total
|
||||
answerCount += 1;
|
||||
answerMillis += review.takenMillis;
|
||||
|
||||
// correct
|
||||
if (review.buttonChosen > 1) {
|
||||
correctCount += 1;
|
||||
}
|
||||
|
||||
// mature
|
||||
if (review.lastInterval >= 21) {
|
||||
matureCount += 1;
|
||||
if (review.buttonChosen > 1) {
|
||||
matureCorrect += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// type counts
|
||||
switch (review.reviewKind) {
|
||||
case ReviewKind.LEARNING:
|
||||
learnCount += 1;
|
||||
break;
|
||||
case ReviewKind.REVIEW:
|
||||
reviewCount += 1;
|
||||
break;
|
||||
case ReviewKind.RELEARNING:
|
||||
relearnCount += 1;
|
||||
break;
|
||||
case ReviewKind.EARLY_REVIEW:
|
||||
earlyReviewCount += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
answerCount,
|
||||
answerMillis,
|
||||
correctCount,
|
||||
matureCorrect,
|
||||
matureCount,
|
||||
learnCount,
|
||||
reviewCount,
|
||||
relearnCount,
|
||||
earlyReviewCount,
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue