mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00
Merge remote-tracking branch 'upstream/main' into x-axis
This commit is contained in:
commit
908778dd17
24 changed files with 240 additions and 106 deletions
|
@ -236,6 +236,7 @@ Marvin Kopf <marvinkopf@outlook.com>
|
||||||
Kevin Nakamura <grinkers@grinkers.net>
|
Kevin Nakamura <grinkers@grinkers.net>
|
||||||
Bradley Szoke <bradleyszoke@gmail.com>
|
Bradley Szoke <bradleyszoke@gmail.com>
|
||||||
jcznk <https://github.com/jcznk>
|
jcznk <https://github.com/jcznk>
|
||||||
|
Thomas Rixen <thomas.rixen@student.uclouvain.be>
|
||||||
|
|
||||||
********************
|
********************
|
||||||
|
|
||||||
|
|
46
Cargo.lock
generated
46
Cargo.lock
generated
|
@ -130,7 +130,7 @@ dependencies = [
|
||||||
"prost",
|
"prost",
|
||||||
"prost-reflect",
|
"prost-reflect",
|
||||||
"pulldown-cmark 0.13.0",
|
"pulldown-cmark 0.13.0",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.12.20",
|
"reqwest 0.12.20",
|
||||||
|
@ -144,7 +144,7 @@ dependencies = [
|
||||||
"serde_tuple",
|
"serde_tuple",
|
||||||
"sha1",
|
"sha1",
|
||||||
"snafu",
|
"snafu",
|
||||||
"strum 0.27.1",
|
"strum 0.27.2",
|
||||||
"syn 2.0.103",
|
"syn 2.0.103",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -220,7 +220,7 @@ dependencies = [
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"serde",
|
"serde",
|
||||||
"snafu",
|
"snafu",
|
||||||
"strum 0.27.1",
|
"strum 0.27.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -706,7 +706,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"portable-atomic-util",
|
"portable-atomic-util",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -732,7 +732,7 @@ dependencies = [
|
||||||
"hashbrown 0.15.4",
|
"hashbrown 0.15.4",
|
||||||
"log",
|
"log",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"serde",
|
"serde",
|
||||||
"spin 0.10.0",
|
"spin 0.10.0",
|
||||||
"text_placeholder",
|
"text_placeholder",
|
||||||
|
@ -762,12 +762,12 @@ dependencies = [
|
||||||
"csv",
|
"csv",
|
||||||
"derive-new 0.7.0",
|
"derive-new 0.7.0",
|
||||||
"dirs 6.0.0",
|
"dirs 6.0.0",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"sanitize-filename 0.6.0",
|
"sanitize-filename 0.6.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum 0.27.1",
|
"strum 0.27.2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
]
|
]
|
||||||
|
@ -817,7 +817,7 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"paste",
|
"paste",
|
||||||
"portable-atomic-util",
|
"portable-atomic-util",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"seq-macro",
|
"seq-macro",
|
||||||
"spin 0.10.0",
|
"spin 0.10.0",
|
||||||
]
|
]
|
||||||
|
@ -865,7 +865,7 @@ dependencies = [
|
||||||
"half",
|
"half",
|
||||||
"hashbrown 0.15.4",
|
"hashbrown 0.15.4",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"rand_distr",
|
"rand_distr",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
|
@ -959,7 +959,7 @@ dependencies = [
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"rand_distr",
|
"rand_distr",
|
||||||
"rayon",
|
"rayon",
|
||||||
"safetensors",
|
"safetensors",
|
||||||
|
@ -1403,7 +1403,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"sanitize-filename 0.5.0",
|
"sanitize-filename 0.5.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -2214,20 +2214,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fsrs"
|
name = "fsrs"
|
||||||
version = "5.0.0"
|
version = "5.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f590cfcbe25079bb54a39900f45e6e308935bd6067249ce00d265b280465cde2"
|
checksum = "04954cc67c3c11ee342a2ee1f5222bf76d73f7772df08d37dc9a6cdd73c467eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"burn",
|
"burn",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
"ndarray",
|
"ndarray",
|
||||||
"priority-queue",
|
"priority-queue",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"snafu",
|
"snafu",
|
||||||
"strum 0.27.1",
|
"strum 0.27.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2804,7 +2804,7 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"crunchy",
|
"crunchy",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"rand_distr",
|
"rand_distr",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -3658,7 +3658,7 @@ dependencies = [
|
||||||
"linkcheck",
|
"linkcheck",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.12.20",
|
"reqwest 0.12.20",
|
||||||
"strum 0.27.1",
|
"strum 0.27.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -5096,7 +5096,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"lru-slab",
|
"lru-slab",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"rustls",
|
"rustls",
|
||||||
|
@ -5150,9 +5150,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.3",
|
||||||
|
@ -5203,7 +5203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
|
checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.1",
|
"rand 0.9.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -6113,9 +6113,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.27.1"
|
version = "0.27.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strum_macros 0.27.1",
|
"strum_macros 0.27.1",
|
||||||
]
|
]
|
||||||
|
|
|
@ -33,10 +33,8 @@ git = "https://github.com/ankitects/linkcheck.git"
|
||||||
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
||||||
|
|
||||||
[workspace.dependencies.fsrs]
|
[workspace.dependencies.fsrs]
|
||||||
version = "5.0.0"
|
version = "5.1.0"
|
||||||
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
||||||
# branch = "Refactor/expected_workload_via_dp"
|
|
||||||
# rev = "a7f7efc10f0a26b14ee348cc7402155685f2a24f"
|
|
||||||
# path = "../open-spaced-repetition/fsrs-rs"
|
# path = "../open-spaced-repetition/fsrs-rs"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
|
@ -1450,7 +1450,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fsrs",
|
"name": "fsrs",
|
||||||
"version": "5.0.0",
|
"version": "5.1.0",
|
||||||
"authors": "Open Spaced Repetition",
|
"authors": "Open Spaced Repetition",
|
||||||
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
|
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
|
@ -3322,7 +3322,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "rand",
|
"name": "rand",
|
||||||
"version": "0.9.1",
|
"version": "0.9.2",
|
||||||
"authors": "The Rand Project Developers|The Rust Project Developers",
|
"authors": "The Rand Project Developers|The Rust Project Developers",
|
||||||
"repository": "https://github.com/rust-random/rand",
|
"repository": "https://github.com/rust-random/rand",
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
@ -4132,7 +4132,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "strum",
|
"name": "strum",
|
||||||
"version": "0.27.1",
|
"version": "0.27.2",
|
||||||
"authors": "Peter Glotfelty <peter.glotfelty@microsoft.com>",
|
"authors": "Peter Glotfelty <peter.glotfelty@microsoft.com>",
|
||||||
"repository": "https://github.com/Peternator7/strum",
|
"repository": "https://github.com/Peternator7/strum",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
@ -5,6 +5,11 @@ database-check-card-properties =
|
||||||
[one] Fixed { $count } invalid card property.
|
[one] Fixed { $count } invalid card property.
|
||||||
*[other] Fixed { $count } invalid card properties.
|
*[other] Fixed { $count } invalid card properties.
|
||||||
}
|
}
|
||||||
|
database-check-card-last-review-time-empty =
|
||||||
|
{ $count ->
|
||||||
|
[one] Added last review time to { $count } card.
|
||||||
|
*[other] Added last review time to { $count } cards.
|
||||||
|
}
|
||||||
database-check-missing-templates =
|
database-check-missing-templates =
|
||||||
{ $count ->
|
{ $count ->
|
||||||
[one] Deleted { $count } card with missing template.
|
[one] Deleted { $count } card with missing template.
|
||||||
|
|
|
@ -407,6 +407,8 @@ message SimulateFsrsReviewRequest {
|
||||||
deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11;
|
deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11;
|
||||||
optional uint32 suspend_after_lapse_count = 12;
|
optional uint32 suspend_after_lapse_count = 12;
|
||||||
float historical_retention = 13;
|
float historical_retention = 13;
|
||||||
|
uint32 learning_step_count = 14;
|
||||||
|
uint32 relearning_step_count = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SimulateFsrsReviewResponse {
|
message SimulateFsrsReviewResponse {
|
||||||
|
|
|
@ -246,7 +246,7 @@ def backend_exception_to_pylib(err: backend_pb2.BackendError) -> Exception:
|
||||||
return BackendError(err.message, help_page, context, backtrace)
|
return BackendError(err.message, help_page, context, backtrace)
|
||||||
|
|
||||||
elif val == kind.SEARCH_ERROR:
|
elif val == kind.SEARCH_ERROR:
|
||||||
return SearchError(markdown(err.message), help_page, context, backtrace)
|
return SearchError(err.message, help_page, context, backtrace)
|
||||||
|
|
||||||
elif val == kind.UNDO_EMPTY:
|
elif val == kind.UNDO_EMPTY:
|
||||||
return UndoEmpty(err.message, help_page, context, backtrace)
|
return UndoEmpty(err.message, help_page, context, backtrace)
|
||||||
|
|
|
@ -10,6 +10,8 @@ import re
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from markdown import markdown
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.browser
|
import aqt.browser
|
||||||
import aqt.editor
|
import aqt.editor
|
||||||
|
@ -20,7 +22,7 @@ from anki.cards import Card, CardId
|
||||||
from anki.collection import Collection, Config, OpChanges, SearchNode
|
from anki.collection import Collection, Config, OpChanges, SearchNode
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.decks import DeckId
|
from anki.decks import DeckId
|
||||||
from anki.errors import NotFoundError
|
from anki.errors import NotFoundError, SearchError
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.models import NotetypeId
|
from anki.models import NotetypeId
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
|
@ -498,6 +500,8 @@ class Browser(QMainWindow):
|
||||||
text = self.current_search()
|
text = self.current_search()
|
||||||
try:
|
try:
|
||||||
normed = self.col.build_search_string(text)
|
normed = self.col.build_search_string(text)
|
||||||
|
except SearchError as err:
|
||||||
|
showWarning(markdown(str(err)))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
showWarning(str(err))
|
showWarning(str(err))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -126,8 +126,9 @@ impl Card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This uses card.due and card.ivl to infer the elapsed time. If 'set due
|
/// If last_review_date isn't stored in the card, this uses card.due and
|
||||||
/// date' or an add-on has changed the due date, this won't be accurate.
|
/// card.ivl to infer the elapsed time, which won't be accurate if
|
||||||
|
/// 'set due date' or an add-on has changed the due date.
|
||||||
pub(crate) fn seconds_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> {
|
pub(crate) fn seconds_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> {
|
||||||
if let Some(last_review_time) = self.last_review_time {
|
if let Some(last_review_time) = self.last_review_time {
|
||||||
Some(timing.now.elapsed_secs_since(last_review_time) as u32)
|
Some(timing.now.elapsed_secs_since(last_review_time) as u32)
|
||||||
|
|
|
@ -24,6 +24,7 @@ use crate::notetype::NotetypeId;
|
||||||
use crate::notetype::NotetypeKind;
|
use crate::notetype::NotetypeKind;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::ThrottlingProgressHandler;
|
use crate::progress::ThrottlingProgressHandler;
|
||||||
|
use crate::storage::card::CardFixStats;
|
||||||
use crate::timestamp::TimestampMillis;
|
use crate::timestamp::TimestampMillis;
|
||||||
use crate::timestamp::TimestampSecs;
|
use crate::timestamp::TimestampSecs;
|
||||||
|
|
||||||
|
@ -40,6 +41,7 @@ pub struct CheckDatabaseOutput {
|
||||||
notetypes_recovered: usize,
|
notetypes_recovered: usize,
|
||||||
invalid_utf8: usize,
|
invalid_utf8: usize,
|
||||||
invalid_ids: usize,
|
invalid_ids: usize,
|
||||||
|
card_last_review_time_empty: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
@ -69,6 +71,11 @@ impl CheckDatabaseOutput {
|
||||||
if self.card_properties_invalid > 0 {
|
if self.card_properties_invalid > 0 {
|
||||||
probs.push(tr.database_check_card_properties(self.card_properties_invalid));
|
probs.push(tr.database_check_card_properties(self.card_properties_invalid));
|
||||||
}
|
}
|
||||||
|
if self.card_last_review_time_empty > 0 {
|
||||||
|
probs.push(
|
||||||
|
tr.database_check_card_last_review_time_empty(self.card_last_review_time_empty),
|
||||||
|
);
|
||||||
|
}
|
||||||
if self.cards_missing_note > 0 {
|
if self.cards_missing_note > 0 {
|
||||||
probs.push(tr.database_check_card_missing_note(self.cards_missing_note));
|
probs.push(tr.database_check_card_missing_note(self.cards_missing_note));
|
||||||
}
|
}
|
||||||
|
@ -158,14 +165,25 @@ impl Collection {
|
||||||
|
|
||||||
fn check_card_properties(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
fn check_card_properties(&mut self, out: &mut CheckDatabaseOutput) -> Result<()> {
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let (new_cnt, other_cnt) = self.storage.fix_card_properties(
|
let CardFixStats {
|
||||||
|
new_cards_fixed,
|
||||||
|
other_cards_fixed,
|
||||||
|
last_review_time_fixed,
|
||||||
|
} = self.storage.fix_card_properties(
|
||||||
timing.days_elapsed,
|
timing.days_elapsed,
|
||||||
TimestampSecs::now(),
|
TimestampSecs::now(),
|
||||||
self.usn()?,
|
self.usn()?,
|
||||||
self.scheduler_version() == SchedulerVersion::V1,
|
self.scheduler_version() == SchedulerVersion::V1,
|
||||||
)?;
|
)?;
|
||||||
out.card_position_too_high = new_cnt;
|
out.card_position_too_high = new_cards_fixed;
|
||||||
out.card_properties_invalid += other_cnt;
|
out.card_properties_invalid += other_cards_fixed;
|
||||||
|
out.card_last_review_time_empty = last_review_time_fixed;
|
||||||
|
|
||||||
|
// Trigger one-way sync if last_review_time was updated to avoid conflicts
|
||||||
|
if last_review_time_fixed > 0 {
|
||||||
|
self.set_schema_modified()?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anki_proto::generic;
|
use anki_proto::generic;
|
||||||
|
use rayon::iter::IntoParallelIterator;
|
||||||
|
use rayon::iter::ParallelIterator;
|
||||||
|
|
||||||
use crate::collection::Collection;
|
use crate::collection::Collection;
|
||||||
use crate::deckconfig::DeckConfSchema11;
|
use crate::deckconfig::DeckConfSchema11;
|
||||||
|
@ -11,6 +13,7 @@ use crate::deckconfig::DeckConfigId;
|
||||||
use crate::deckconfig::UpdateDeckConfigsRequest;
|
use crate::deckconfig::UpdateDeckConfigsRequest;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::scheduler::fsrs::params::ignore_revlogs_before_date_to_ms;
|
use crate::scheduler::fsrs::params::ignore_revlogs_before_date_to_ms;
|
||||||
|
use crate::scheduler::fsrs::simulator::is_included_card;
|
||||||
|
|
||||||
impl crate::services::DeckConfigService for Collection {
|
impl crate::services::DeckConfigService for Collection {
|
||||||
fn add_or_update_deck_config_legacy(
|
fn add_or_update_deck_config_legacy(
|
||||||
|
@ -103,6 +106,7 @@ impl crate::services::DeckConfigService for Collection {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: anki_proto::deck_config::GetRetentionWorkloadRequest,
|
input: anki_proto::deck_config::GetRetentionWorkloadRequest,
|
||||||
) -> Result<anki_proto::deck_config::GetRetentionWorkloadResponse> {
|
) -> Result<anki_proto::deck_config::GetRetentionWorkloadResponse> {
|
||||||
|
let days_elapsed = self.timing_today().unwrap().days_elapsed as i32;
|
||||||
let guard =
|
let guard =
|
||||||
self.search_cards_into_table(&input.search, crate::search::SortMode::NoOrder)?;
|
self.search_cards_into_table(&input.search, crate::search::SortMode::NoOrder)?;
|
||||||
|
|
||||||
|
@ -112,12 +116,26 @@ impl crate::services::DeckConfigService for Collection {
|
||||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||||
|
|
||||||
let config = guard.col.get_optimal_retention_parameters(revlogs)?;
|
let config = guard.col.get_optimal_retention_parameters(revlogs)?;
|
||||||
|
let cards = guard
|
||||||
|
.col
|
||||||
|
.storage
|
||||||
|
.all_searched_cards()?
|
||||||
|
.into_iter()
|
||||||
|
.filter(is_included_card)
|
||||||
|
.filter_map(|c| crate::card::Card::convert(c.clone(), days_elapsed, c.memory_state?))
|
||||||
|
.collect::<Vec<fsrs::Card>>();
|
||||||
|
|
||||||
let costs = (70u32..=99u32)
|
let costs = (70u32..=99u32)
|
||||||
|
.into_par_iter()
|
||||||
.map(|dr| {
|
.map(|dr| {
|
||||||
Ok((
|
Ok((
|
||||||
dr,
|
dr,
|
||||||
fsrs::expected_workload(&input.w, dr as f32 / 100., &config)?,
|
fsrs::expected_workload_with_existing_cards(
|
||||||
|
&input.w,
|
||||||
|
dr as f32 / 100.,
|
||||||
|
&config,
|
||||||
|
&cards,
|
||||||
|
)?,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.collect::<Result<HashMap<_, _>>>()?;
|
.collect::<Result<HashMap<_, _>>>()?;
|
||||||
|
|
|
@ -84,6 +84,42 @@ impl RevlogEntry {
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if this entry represents a reset operation.
|
||||||
|
/// These entries are created when a card is reset using
|
||||||
|
/// [`Collection::reschedule_cards_as_new`].
|
||||||
|
/// The 0 value of `ease_factor` differentiates it
|
||||||
|
/// from entry created by [`Collection::set_due_date`] that has
|
||||||
|
/// `RevlogReviewKind::Manual` but non-zero `ease_factor`.
|
||||||
|
pub(crate) fn is_reset(&self) -> bool {
|
||||||
|
self.review_kind == RevlogReviewKind::Manual && self.ease_factor == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this entry represents a cramming operation.
|
||||||
|
/// These entries are created when a card is reviewed in a
|
||||||
|
/// filtered deck with "Reschedule cards based on my answers
|
||||||
|
/// in this deck" disabled.
|
||||||
|
/// [`crate::scheduler::answering::CardStateUpdater::apply_preview_state`].
|
||||||
|
/// The 0 value of `ease_factor` distinguishes it from the entry
|
||||||
|
/// created when a card is reviewed before its due date in a
|
||||||
|
/// filtered deck with reschedule enabled or using Grade Now.
|
||||||
|
pub(crate) fn is_cramming(&self) -> bool {
|
||||||
|
self.review_kind == RevlogReviewKind::Filtered && self.ease_factor == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_rating(&self) -> bool {
|
||||||
|
self.button_chosen > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the review entry is not manually rescheduled and not
|
||||||
|
/// cramming. Used to filter out entries that shouldn't be considered
|
||||||
|
/// for statistics and scheduling.
|
||||||
|
pub(crate) fn has_rating_and_affects_scheduling(&self) -> bool {
|
||||||
|
// not rescheduled/set due date/reset
|
||||||
|
self.has_rating()
|
||||||
|
// not cramming
|
||||||
|
&& !self.is_cramming()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
|
|
|
@ -306,15 +306,15 @@ pub(crate) fn fsrs_items_for_memory_states(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LastRevlogInfo {
|
pub(crate) struct LastRevlogInfo {
|
||||||
/// Used to determine the actual elapsed time between the last time the user
|
/// Used to determine the actual elapsed time between the last time the user
|
||||||
/// reviewed the card and now, so that we can determine an accurate period
|
/// reviewed the card and now, so that we can determine an accurate period
|
||||||
/// when the card has subsequently been rescheduled to a different day.
|
/// when the card has subsequently been rescheduled to a different day.
|
||||||
last_reviewed_at: Option<TimestampSecs>,
|
pub(crate) last_reviewed_at: Option<TimestampSecs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a map of cards to info about last review/reschedule.
|
/// Return a map of cards to info about last review.
|
||||||
fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogInfo> {
|
pub(crate) fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogInfo> {
|
||||||
let mut out = HashMap::new();
|
let mut out = HashMap::new();
|
||||||
revlogs
|
revlogs
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -323,8 +323,10 @@ fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogIn
|
||||||
.for_each(|(card_id, group)| {
|
.for_each(|(card_id, group)| {
|
||||||
let mut last_reviewed_at = None;
|
let mut last_reviewed_at = None;
|
||||||
for e in group.into_iter() {
|
for e in group.into_iter() {
|
||||||
if e.button_chosen >= 1 {
|
if e.has_rating_and_affects_scheduling() {
|
||||||
last_reviewed_at = Some(e.id.as_secs());
|
last_reviewed_at = Some(e.id.as_secs());
|
||||||
|
} else if e.is_reset() {
|
||||||
|
last_reviewed_at = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.insert(card_id, LastRevlogInfo { last_reviewed_at });
|
out.insert(card_id, LastRevlogInfo { last_reviewed_at });
|
||||||
|
|
|
@ -394,13 +394,13 @@ pub(crate) fn reviews_for_fsrs(
|
||||||
let mut revlogs_complete = false;
|
let mut revlogs_complete = false;
|
||||||
// Working backwards from the latest review...
|
// Working backwards from the latest review...
|
||||||
for (index, entry) in entries.iter().enumerate().rev() {
|
for (index, entry) in entries.iter().enumerate().rev() {
|
||||||
if entry.review_kind == RevlogReviewKind::Filtered && entry.ease_factor == 0 {
|
if entry.is_cramming() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// For incomplete review histories, initial memory state is based on the first
|
// For incomplete review histories, initial memory state is based on the first
|
||||||
// user-graded review after the cutoff date with interval >= 1d.
|
// user-graded review after the cutoff date with interval >= 1d.
|
||||||
let within_cutoff = entry.id.0 > ignore_revlogs_before.0;
|
let within_cutoff = entry.id.0 > ignore_revlogs_before.0;
|
||||||
let user_graded = matches!(entry.button_chosen, 1..=4);
|
let user_graded = entry.has_rating();
|
||||||
let interday = entry.interval >= 1 || entry.interval <= -86400;
|
let interday = entry.interval >= 1 || entry.interval <= -86400;
|
||||||
if user_graded && within_cutoff && interday {
|
if user_graded && within_cutoff && interday {
|
||||||
first_user_grade_idx = Some(index);
|
first_user_grade_idx = Some(index);
|
||||||
|
@ -409,10 +409,7 @@ pub(crate) fn reviews_for_fsrs(
|
||||||
if user_graded && entry.review_kind == RevlogReviewKind::Learning {
|
if user_graded && entry.review_kind == RevlogReviewKind::Learning {
|
||||||
first_of_last_learn_entries = Some(index);
|
first_of_last_learn_entries = Some(index);
|
||||||
revlogs_complete = true;
|
revlogs_complete = true;
|
||||||
} else if matches!(
|
} else if entry.is_reset() {
|
||||||
(entry.review_kind, entry.ease_factor),
|
|
||||||
(RevlogReviewKind::Manual, 0)
|
|
||||||
) {
|
|
||||||
// Ignore entries prior to a `Reset` if a learning step has come after,
|
// Ignore entries prior to a `Reset` if a learning step has come after,
|
||||||
// but consider revlogs complete.
|
// but consider revlogs complete.
|
||||||
if first_of_last_learn_entries.is_some() {
|
if first_of_last_learn_entries.is_some() {
|
||||||
|
@ -472,16 +469,7 @@ pub(crate) fn reviews_for_fsrs(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out unwanted entries
|
// Filter out unwanted entries
|
||||||
entries.retain(|entry| {
|
entries.retain(|entry| entry.has_rating_and_affects_scheduling());
|
||||||
!(
|
|
||||||
// set due date, reset or rescheduled
|
|
||||||
(entry.review_kind == RevlogReviewKind::Manual || entry.button_chosen == 0)
|
|
||||||
|| // cram
|
|
||||||
(entry.review_kind == RevlogReviewKind::Filtered && entry.ease_factor == 0)
|
|
||||||
|| // rescheduled
|
|
||||||
(entry.review_kind == RevlogReviewKind::Rescheduled)
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Compute delta_t for each entry
|
// Compute delta_t for each entry
|
||||||
let delta_ts = iter::once(0)
|
let delta_ts = iter::once(0)
|
||||||
|
@ -560,10 +548,14 @@ pub(crate) mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry {
|
pub(crate) fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry {
|
||||||
|
let button_chosen = match review_kind {
|
||||||
|
RevlogReviewKind::Manual | RevlogReviewKind::Rescheduled => 0,
|
||||||
|
_ => 3,
|
||||||
|
};
|
||||||
RevlogEntry {
|
RevlogEntry {
|
||||||
review_kind,
|
review_kind,
|
||||||
id: days_ago_ms(days_ago).into(),
|
id: days_ago_ms(days_ago).into(),
|
||||||
button_chosen: 3,
|
button_chosen,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,6 +121,12 @@ fn create_review_priority_fn(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_included_card(c: &Card) -> bool {
|
||||||
|
c.queue != CardQueue::Suspended
|
||||||
|
&& c.queue != CardQueue::PreviewRepeat
|
||||||
|
&& c.ctype != CardType::New
|
||||||
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn simulate_request_to_config(
|
pub fn simulate_request_to_config(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -133,11 +139,6 @@ impl Collection {
|
||||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||||
let mut cards = guard.col.storage.all_searched_cards()?;
|
let mut cards = guard.col.storage.all_searched_cards()?;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
fn is_included_card(c: &Card) -> bool {
|
|
||||||
c.queue != CardQueue::Suspended
|
|
||||||
&& c.queue != CardQueue::PreviewRepeat
|
|
||||||
&& c.ctype != CardType::New
|
|
||||||
}
|
|
||||||
// calculate any missing memory state
|
// calculate any missing memory state
|
||||||
for c in &mut cards {
|
for c in &mut cards {
|
||||||
if is_included_card(c) && c.memory_state.is_none() {
|
if is_included_card(c) && c.memory_state.is_none() {
|
||||||
|
@ -237,8 +238,8 @@ impl Collection {
|
||||||
learning_step_transitions: p.learning_step_transitions,
|
learning_step_transitions: p.learning_step_transitions,
|
||||||
relearning_step_transitions: p.relearning_step_transitions,
|
relearning_step_transitions: p.relearning_step_transitions,
|
||||||
state_rating_costs: p.state_rating_costs,
|
state_rating_costs: p.state_rating_costs,
|
||||||
learning_step_count: p.learning_step_count,
|
learning_step_count: req.learning_step_count as usize,
|
||||||
relearning_step_count: p.relearning_step_count,
|
relearning_step_count: req.relearning_step_count as usize,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((config, converted_cards))
|
Ok((config, converted_cards))
|
||||||
|
@ -306,7 +307,11 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Card {
|
impl Card {
|
||||||
fn convert(card: Card, days_elapsed: i32, memory_state: FsrsMemoryState) -> Option<fsrs::Card> {
|
pub(crate) fn convert(
|
||||||
|
card: Card,
|
||||||
|
days_elapsed: i32,
|
||||||
|
memory_state: FsrsMemoryState,
|
||||||
|
) -> Option<fsrs::Card> {
|
||||||
match card.queue {
|
match card.queue {
|
||||||
CardQueue::DayLearn | CardQueue::Review => {
|
CardQueue::DayLearn | CardQueue::Review => {
|
||||||
let due = card.original_or_current_due();
|
let due = card.original_or_current_due();
|
||||||
|
|
|
@ -57,10 +57,10 @@ const SECOND: f32 = 1.0;
|
||||||
const MINUTE: f32 = 60.0 * SECOND;
|
const MINUTE: f32 = 60.0 * SECOND;
|
||||||
const HOUR: f32 = 60.0 * MINUTE;
|
const HOUR: f32 = 60.0 * MINUTE;
|
||||||
const DAY: f32 = 24.0 * HOUR;
|
const DAY: f32 = 24.0 * HOUR;
|
||||||
const MONTH: f32 = 30.417 * DAY; // 365/12 ≈ 30.417
|
|
||||||
const YEAR: f32 = 365.0 * DAY;
|
const YEAR: f32 = 365.0 * DAY;
|
||||||
|
const MONTH: f32 = YEAR / 12.0;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub(crate) enum TimespanUnit {
|
pub(crate) enum TimespanUnit {
|
||||||
Seconds,
|
Seconds,
|
||||||
Minutes,
|
Minutes,
|
||||||
|
@ -111,6 +111,13 @@ impl Timespan {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_unit(self, unit: TimespanUnit) -> Timespan {
|
||||||
|
Timespan {
|
||||||
|
seconds: self.seconds,
|
||||||
|
unit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Round seconds and days to integers, otherwise
|
/// Round seconds and days to integers, otherwise
|
||||||
/// truncates to one decimal place.
|
/// truncates to one decimal place.
|
||||||
pub fn as_rounded_unit(self) -> f32 {
|
pub fn as_rounded_unit(self) -> f32 {
|
||||||
|
|
|
@ -30,14 +30,24 @@ impl Collection {
|
||||||
|
|
||||||
let (average_secs, total_secs) = average_and_total_secs_strings(&revlog);
|
let (average_secs, total_secs) = average_and_total_secs_strings(&revlog);
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let seconds_elapsed = if let Some(last_review_time) = card.last_review_time {
|
|
||||||
timing.now.elapsed_secs_since(last_review_time) as u32
|
let last_review_time = if let Some(last_review_time) = card.last_review_time {
|
||||||
|
last_review_time
|
||||||
} else {
|
} else {
|
||||||
self.storage
|
let mut new_card = card.clone();
|
||||||
|
let last_review_time = self
|
||||||
|
.storage
|
||||||
.time_of_last_review(card.id)?
|
.time_of_last_review(card.id)?
|
||||||
.map(|ts| timing.now.elapsed_secs_since(ts))
|
.unwrap_or_default();
|
||||||
.unwrap_or_default() as u32
|
|
||||||
|
new_card.last_review_time = Some(last_review_time);
|
||||||
|
|
||||||
|
self.storage.update_card(&new_card)?;
|
||||||
|
last_review_time
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let seconds_elapsed = timing.now.elapsed_secs_since(last_review_time) as u32;
|
||||||
|
|
||||||
let fsrs_retrievability = card
|
let fsrs_retrievability = card
|
||||||
.memory_state
|
.memory_state
|
||||||
.zip(Some(seconds_elapsed))
|
.zip(Some(seconds_elapsed))
|
||||||
|
@ -187,7 +197,7 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn average_and_total_secs_strings(revlog: &[RevlogEntry]) -> (f32, f32) {
|
fn average_and_total_secs_strings(revlog: &[RevlogEntry]) -> (f32, f32) {
|
||||||
let normal_answer_count = revlog.iter().filter(|r| r.button_chosen > 0).count();
|
let normal_answer_count = revlog.iter().filter(|r| r.has_rating()).count();
|
||||||
let total_secs: f32 = revlog
|
let total_secs: f32 = revlog
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| (entry.taken_millis as f32) / 1000.0)
|
.map(|entry| (entry.taken_millis as f32) / 1000.0)
|
||||||
|
|
|
@ -53,10 +53,7 @@ impl GraphsContext {
|
||||||
self.revlog
|
self.revlog
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|review| {
|
.filter(|review| {
|
||||||
// not rescheduled/set due date/reset
|
review.has_rating_and_affects_scheduling()
|
||||||
review.button_chosen > 0
|
|
||||||
// not cramming
|
|
||||||
&& (review.review_kind != RevlogReviewKind::Filtered || review.ease_factor != 0)
|
|
||||||
// cards with an interval ≥ 1 day
|
// cards with an interval ≥ 1 day
|
||||||
&& (review.review_kind == RevlogReviewKind::Review
|
&& (review.review_kind == RevlogReviewKind::Review
|
||||||
|| review.last_interval <= -86400
|
|| review.last_interval <= -86400
|
||||||
|
|
|
@ -5,17 +5,18 @@ use anki_i18n::I18n;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::scheduler::timespan::Timespan;
|
use crate::scheduler::timespan::Timespan;
|
||||||
|
use crate::scheduler::timespan::TimespanUnit;
|
||||||
|
|
||||||
pub fn studied_today(cards: u32, secs: f32, tr: &I18n) -> String {
|
pub fn studied_today(cards: u32, secs: f32, tr: &I18n) -> String {
|
||||||
let span = Timespan::from_secs(secs).natural_span();
|
let span = Timespan::from_secs(secs).natural_span();
|
||||||
let amount = span.as_unit();
|
let unit = std::cmp::min(span.unit(), TimespanUnit::Minutes);
|
||||||
let unit = span.unit().as_str();
|
let amount = span.to_unit(unit).as_unit();
|
||||||
let secs_per_card = if cards > 0 {
|
let secs_per_card = if cards > 0 {
|
||||||
secs / (cards as f32)
|
secs / (cards as f32)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
tr.statistics_studied_today(unit, secs_per_card, amount, cards)
|
tr.statistics_studied_today(unit.as_str(), secs_per_card, amount, cards)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,5 +42,9 @@ mod test {
|
||||||
&studied_today(3, 13.0, &tr).replace('\n', " "),
|
&studied_today(3, 13.0, &tr).replace('\n', " "),
|
||||||
"Studied 3 cards in 13 seconds today (4.33s/card)"
|
"Studied 3 cards in 13 seconds today (4.33s/card)"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&studied_today(300, 5400.0, &tr).replace('\n', " "),
|
||||||
|
"Studied 300 cards in 90 minutes today (18s/card)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ use crate::decks::DeckKind;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::notes::NoteId;
|
use crate::notes::NoteId;
|
||||||
use crate::scheduler::congrats::CongratsInfo;
|
use crate::scheduler::congrats::CongratsInfo;
|
||||||
|
use crate::scheduler::fsrs::memory_state::get_last_revlog_info;
|
||||||
use crate::scheduler::queue::BuryMode;
|
use crate::scheduler::queue::BuryMode;
|
||||||
use crate::scheduler::queue::DueCard;
|
use crate::scheduler::queue::DueCard;
|
||||||
use crate::scheduler::queue::DueCardKind;
|
use crate::scheduler::queue::DueCardKind;
|
||||||
|
@ -42,6 +43,13 @@ use crate::timestamp::TimestampMillis;
|
||||||
use crate::timestamp::TimestampSecs;
|
use crate::timestamp::TimestampSecs;
|
||||||
use crate::types::Usn;
|
use crate::types::Usn;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct CardFixStats {
|
||||||
|
pub new_cards_fixed: usize,
|
||||||
|
pub other_cards_fixed: usize,
|
||||||
|
pub last_review_time_fixed: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl FromSql for CardType {
|
impl FromSql for CardType {
|
||||||
fn column_result(value: ValueRef<'_>) -> result::Result<Self, FromSqlError> {
|
fn column_result(value: ValueRef<'_>) -> result::Result<Self, FromSqlError> {
|
||||||
if let ValueRef::Integer(i) = value {
|
if let ValueRef::Integer(i) = value {
|
||||||
|
@ -365,7 +373,7 @@ impl super::SqliteStorage {
|
||||||
mtime: TimestampSecs,
|
mtime: TimestampSecs,
|
||||||
usn: Usn,
|
usn: Usn,
|
||||||
v1_sched: bool,
|
v1_sched: bool,
|
||||||
) -> Result<(usize, usize)> {
|
) -> Result<CardFixStats> {
|
||||||
let new_cnt = self
|
let new_cnt = self
|
||||||
.db
|
.db
|
||||||
.prepare(include_str!("fix_due_new.sql"))?
|
.prepare(include_str!("fix_due_new.sql"))?
|
||||||
|
@ -390,7 +398,24 @@ impl super::SqliteStorage {
|
||||||
.db
|
.db
|
||||||
.prepare(include_str!("fix_ordinal.sql"))?
|
.prepare(include_str!("fix_ordinal.sql"))?
|
||||||
.execute(params![mtime, usn])?;
|
.execute(params![mtime, usn])?;
|
||||||
Ok((new_cnt, other_cnt))
|
let mut last_review_time_cnt = 0;
|
||||||
|
let revlog = self.get_all_revlog_entries_in_card_order()?;
|
||||||
|
let last_revlog_info = get_last_revlog_info(&revlog);
|
||||||
|
for (card_id, last_revlog_info) in last_revlog_info {
|
||||||
|
let card = self.get_card(card_id)?;
|
||||||
|
if let Some(mut card) = card {
|
||||||
|
if card.ctype != CardType::New && card.last_review_time.is_none() {
|
||||||
|
card.last_review_time = last_revlog_info.last_reviewed_at;
|
||||||
|
self.update_card(&card)?;
|
||||||
|
last_review_time_cnt += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(CardFixStats {
|
||||||
|
new_cards_fixed: new_cnt,
|
||||||
|
other_cards_fixed: other_cnt,
|
||||||
|
last_review_time_fixed: last_review_time_cnt,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn delete_orphaned_cards(&self) -> Result<usize> {
|
pub(crate) fn delete_orphaned_cards(&self) -> Result<usize> {
|
||||||
|
|
|
@ -89,7 +89,7 @@ export function naturalWholeUnit(secs: number): TimespanUnit {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function studiedToday(cards: number, secs: number): string {
|
export function studiedToday(cards: number, secs: number): string {
|
||||||
const unit = naturalUnit(secs);
|
const unit = Math.min(naturalUnit(secs), TimespanUnit.Minutes);
|
||||||
const amount = unitAmount(unit, secs);
|
const amount = unitAmount(unit, secs);
|
||||||
const name = unitName(unit);
|
const name = unitName(unit);
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
easyDaysPercentages: $config.easyDaysPercentages,
|
easyDaysPercentages: $config.easyDaysPercentages,
|
||||||
reviewOrder: $config.reviewOrder,
|
reviewOrder: $config.reviewOrder,
|
||||||
historicalRetention: $config.historicalRetention,
|
historicalRetention: $config.historicalRetention,
|
||||||
|
learningStepCount: $config.learnSteps.length,
|
||||||
|
relearningStepCount: $config.relearnSteps.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;
|
const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;
|
||||||
|
@ -128,7 +130,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let retentionWorloadInfo: undefined | Promise<GetRetentionWorkloadResponse> =
|
let retentionWorkloadInfo: undefined | Promise<GetRetentionWorkloadResponse> =
|
||||||
undefined;
|
undefined;
|
||||||
let lastParams = [...fsrsParams($config)];
|
let lastParams = [...fsrsParams($config)];
|
||||||
|
|
||||||
|
@ -139,7 +141,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
// If the cache is empty and a request has not yet been made to fill it
|
// If the cache is empty and a request has not yet been made to fill it
|
||||||
!retentionWorloadInfo ||
|
!retentionWorkloadInfo ||
|
||||||
// If the parameters have been changed
|
// If the parameters have been changed
|
||||||
lastParams.toString() !== params.toString()
|
lastParams.toString() !== params.toString()
|
||||||
) {
|
) {
|
||||||
|
@ -148,12 +150,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
search: defaultparamSearch,
|
search: defaultparamSearch,
|
||||||
});
|
});
|
||||||
lastParams = [...params];
|
lastParams = [...params];
|
||||||
retentionWorloadInfo = getRetentionWorkload(request);
|
retentionWorkloadInfo = getRetentionWorkload(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
const previous = +startingDesiredRetention * 100;
|
const previous = +startingDesiredRetention * 100;
|
||||||
const after = retention * 100;
|
const after = retention * 100;
|
||||||
const resp = await retentionWorloadInfo;
|
const resp = await retentionWorkloadInfo;
|
||||||
const factor = resp.costs[after] / resp.costs[previous];
|
const factor = resp.costs[after] / resp.costs[previous];
|
||||||
|
|
||||||
desiredRetentionChangeInfo = tr.deckConfigWorkloadFactorChange({
|
desiredRetentionChangeInfo = tr.deckConfigWorkloadFactorChange({
|
||||||
|
@ -218,29 +220,34 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
healthCheck: $healthCheck,
|
healthCheck: $healthCheck,
|
||||||
});
|
});
|
||||||
|
|
||||||
const already_optimal =
|
const alreadyOptimal =
|
||||||
(params.length &&
|
(params.length &&
|
||||||
params.every(
|
params.every(
|
||||||
(n, i) => n.toFixed(4) === resp.params[i].toFixed(4),
|
(n, i) => n.toFixed(4) === resp.params[i].toFixed(4),
|
||||||
)) ||
|
)) ||
|
||||||
resp.params.length === 0;
|
resp.params.length === 0;
|
||||||
|
|
||||||
|
let healthCheckMessage = "";
|
||||||
if (resp.healthCheckPassed !== undefined) {
|
if (resp.healthCheckPassed !== undefined) {
|
||||||
if (resp.healthCheckPassed) {
|
healthCheckMessage = resp.healthCheckPassed
|
||||||
setTimeout(() => alert(tr.deckConfigFsrsGoodFit()), 200);
|
? tr.deckConfigFsrsGoodFit()
|
||||||
} else {
|
: tr.deckConfigFsrsBadFitWarning();
|
||||||
setTimeout(
|
}
|
||||||
() => alert(tr.deckConfigFsrsBadFitWarning()),
|
let alreadyOptimalMessage = "";
|
||||||
200,
|
if (alreadyOptimal) {
|
||||||
);
|
alreadyOptimalMessage = resp.fsrsItems
|
||||||
}
|
|
||||||
} else if (already_optimal) {
|
|
||||||
const msg = resp.fsrsItems
|
|
||||||
? tr.deckConfigFsrsParamsOptimal()
|
? tr.deckConfigFsrsParamsOptimal()
|
||||||
: tr.deckConfigFsrsParamsNoReviews();
|
: tr.deckConfigFsrsParamsNoReviews();
|
||||||
setTimeout(() => alert(msg), 200);
|
|
||||||
}
|
}
|
||||||
if (!already_optimal) {
|
const message = [alreadyOptimalMessage, healthCheckMessage]
|
||||||
|
.filter((a) => a)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
setTimeout(() => alert(message), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyOptimal) {
|
||||||
$config.fsrsParams6 = resp.params;
|
$config.fsrsParams6 = resp.params;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
optimized = true;
|
optimized = true;
|
||||||
|
|
|
@ -627,7 +627,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
.svg-container {
|
.svg-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: calc(100vh - 400px); /* Account for modal header, controls, etc */
|
/* Account for modal header, controls, etc */
|
||||||
|
max-height: max(calc(100vh - 400px), 200px);
|
||||||
aspect-ratio: 600 / 250;
|
aspect-ratio: 600 / 250;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -18,8 +18,8 @@ import {
|
||||||
bin,
|
bin,
|
||||||
cumsum,
|
cumsum,
|
||||||
curveBasis,
|
curveBasis,
|
||||||
interpolateBlues,
|
|
||||||
interpolateGreens,
|
interpolateGreens,
|
||||||
|
interpolateOranges,
|
||||||
interpolatePurples,
|
interpolatePurples,
|
||||||
interpolateReds,
|
interpolateReds,
|
||||||
max,
|
max,
|
||||||
|
@ -181,7 +181,7 @@ export function renderReviews(
|
||||||
const reds = scaleSequential((n) => interpolateReds(cappedRange(n)!)).domain(
|
const reds = scaleSequential((n) => interpolateReds(cappedRange(n)!)).domain(
|
||||||
x.domain() as any,
|
x.domain() as any,
|
||||||
);
|
);
|
||||||
const blues = scaleSequential((n) => interpolateBlues(cappedRange(n)!)).domain(
|
const oranges = scaleSequential((n) => interpolateOranges(cappedRange(n)!)).domain(
|
||||||
x.domain() as any,
|
x.domain() as any,
|
||||||
);
|
);
|
||||||
const purples = scaleSequential((n) => interpolatePurples(cappedRange(n)!)).domain(
|
const purples = scaleSequential((n) => interpolatePurples(cappedRange(n)!)).domain(
|
||||||
|
@ -195,7 +195,7 @@ export function renderReviews(
|
||||||
case BinIndex.Young:
|
case BinIndex.Young:
|
||||||
return lighterGreens;
|
return lighterGreens;
|
||||||
case BinIndex.Learn:
|
case BinIndex.Learn:
|
||||||
return blues;
|
return oranges;
|
||||||
case BinIndex.Relearn:
|
case BinIndex.Relearn:
|
||||||
return reds;
|
return reds;
|
||||||
case BinIndex.Filtered:
|
case BinIndex.Filtered:
|
||||||
|
|
Loading…
Reference in a new issue