mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 23:42:23 -04:00
initial work on moving v2 card answering into backend
Not plugged into the Python code yet. Still a work in progress. Other changes: - move a bunch of From implementations out of the giant backend/mod.rs file into separate submodules. - reorder backend methods to match proto order - fix some clippy lints
This commit is contained in:
parent
54fa322f3d
commit
ab790c1d14
41 changed files with 2258 additions and 398 deletions
|
@ -115,6 +115,9 @@ service BackendService {
|
||||||
rpc SetDueDate(SetDueDateIn) returns (Empty);
|
rpc SetDueDate(SetDueDateIn) returns (Empty);
|
||||||
rpc SortCards(SortCardsIn) returns (Empty);
|
rpc SortCards(SortCardsIn) returns (Empty);
|
||||||
rpc SortDeck(SortDeckIn) returns (Empty);
|
rpc SortDeck(SortDeckIn) returns (Empty);
|
||||||
|
rpc GetNextCardStates(CardID) returns (NextCardStates);
|
||||||
|
rpc DescribeNextStates(NextCardStates) returns (StringList);
|
||||||
|
rpc AnswerCard(AnswerCardIn) returns (Empty);
|
||||||
|
|
||||||
// stats
|
// stats
|
||||||
|
|
||||||
|
@ -268,7 +271,7 @@ message DeckConfigInner {
|
||||||
float interval_multiplier = 15;
|
float interval_multiplier = 15;
|
||||||
|
|
||||||
uint32 maximum_review_interval = 16;
|
uint32 maximum_review_interval = 16;
|
||||||
uint32 minimum_review_interval = 17;
|
uint32 minimum_lapse_interval = 17;
|
||||||
|
|
||||||
uint32 graduating_interval_good = 18;
|
uint32 graduating_interval_good = 18;
|
||||||
uint32 graduating_interval_easy = 19;
|
uint32 graduating_interval_easy = 19;
|
||||||
|
@ -1263,3 +1266,73 @@ message RenderMarkdownIn {
|
||||||
string markdown = 1;
|
string markdown = 1;
|
||||||
bool sanitize = 2;
|
bool sanitize = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SchedulingState {
|
||||||
|
message New {
|
||||||
|
uint32 position = 1;
|
||||||
|
}
|
||||||
|
message Learning {
|
||||||
|
uint32 remaining_steps = 1;
|
||||||
|
uint32 scheduled_secs = 2;
|
||||||
|
}
|
||||||
|
message Review {
|
||||||
|
uint32 scheduled_days = 1;
|
||||||
|
uint32 elapsed_days = 2;
|
||||||
|
float ease_factor = 3;
|
||||||
|
uint32 lapses = 4;
|
||||||
|
}
|
||||||
|
message Relearning {
|
||||||
|
Review review = 1;
|
||||||
|
Learning learning = 2;
|
||||||
|
}
|
||||||
|
message Normal {
|
||||||
|
oneof value {
|
||||||
|
New new = 1;
|
||||||
|
Learning learning = 2;
|
||||||
|
Review review = 3;
|
||||||
|
Relearning relearning = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message Preview {
|
||||||
|
uint32 scheduled_secs = 1;
|
||||||
|
Normal original_state = 2;
|
||||||
|
}
|
||||||
|
message ReschedulingFilter {
|
||||||
|
Normal original_state = 1;
|
||||||
|
}
|
||||||
|
message Filtered {
|
||||||
|
oneof value {
|
||||||
|
Preview preview = 1;
|
||||||
|
ReschedulingFilter rescheduling = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof value {
|
||||||
|
Normal normal = 1;
|
||||||
|
Filtered filtered = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message NextCardStates {
|
||||||
|
SchedulingState current = 1;
|
||||||
|
SchedulingState again = 2;
|
||||||
|
SchedulingState hard = 3;
|
||||||
|
SchedulingState good = 4;
|
||||||
|
SchedulingState easy = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AnswerCardIn {
|
||||||
|
enum Rating {
|
||||||
|
AGAIN = 0;
|
||||||
|
HARD = 1;
|
||||||
|
GOOD = 2;
|
||||||
|
EASY = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64 card_id = 1;
|
||||||
|
SchedulingState current_state = 2;
|
||||||
|
SchedulingState new_state = 3;
|
||||||
|
Rating rating = 4;
|
||||||
|
int64 answered_at = 5;
|
||||||
|
uint32 milliseconds_taken = 6;
|
||||||
|
}
|
||||||
|
|
76
rslib/src/backend/generic.rs
Normal file
76
rslib/src/backend/generic.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, prelude::*};
|
||||||
|
|
||||||
|
impl From<Vec<u8>> for pb::Json {
|
||||||
|
fn from(json: Vec<u8>) -> Self {
|
||||||
|
pb::Json { json }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for pb::String {
|
||||||
|
fn from(val: String) -> Self {
|
||||||
|
pb::String { val }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for pb::Int64 {
|
||||||
|
fn from(val: i64) -> Self {
|
||||||
|
pb::Int64 { val }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for pb::UInt32 {
|
||||||
|
fn from(val: u32) -> Self {
|
||||||
|
pb::UInt32 { val }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<()> for pb::Empty {
|
||||||
|
fn from(_val: ()) -> Self {
|
||||||
|
pb::Empty {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::CardId> for CardID {
|
||||||
|
fn from(cid: pb::CardId) -> Self {
|
||||||
|
CardID(cid.cid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Vec<CardID>> for pb::CardIDs {
|
||||||
|
fn into(self) -> Vec<CardID> {
|
||||||
|
self.cids.into_iter().map(CardID).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::NoteId> for NoteID {
|
||||||
|
fn from(nid: pb::NoteId) -> Self {
|
||||||
|
NoteID(nid.nid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::NoteTypeId> for NoteTypeID {
|
||||||
|
fn from(ntid: pb::NoteTypeId) -> Self {
|
||||||
|
NoteTypeID(ntid.ntid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::DeckId> for DeckID {
|
||||||
|
fn from(did: pb::DeckId) -> Self {
|
||||||
|
DeckID(did.did)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::DeckConfigId> for DeckConfID {
|
||||||
|
fn from(dcid: pb::DeckConfigId) -> Self {
|
||||||
|
DeckConfID(dcid.dcid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<String>> for pb::StringList {
|
||||||
|
fn from(vals: Vec<String>) -> Self {
|
||||||
|
pb::StringList { vals }
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ use crate::{
|
||||||
collection::{open_collection, Collection},
|
collection::{open_collection, Collection},
|
||||||
config::SortKind,
|
config::SortKind,
|
||||||
dbcheck::DatabaseCheckProgress,
|
dbcheck::DatabaseCheckProgress,
|
||||||
deckconf::{DeckConf, DeckConfID, DeckConfSchema11},
|
deckconf::{DeckConf, DeckConfSchema11},
|
||||||
decks::{Deck, DeckID, DeckSchema11},
|
decks::{Deck, DeckID, DeckSchema11},
|
||||||
err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind},
|
err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind},
|
||||||
i18n::{tr_args, I18n, TR},
|
i18n::{tr_args, I18n, TR},
|
||||||
|
@ -28,13 +28,13 @@ use crate::{
|
||||||
media::MediaManager,
|
media::MediaManager,
|
||||||
notes::{Note, NoteID},
|
notes::{Note, NoteID},
|
||||||
notetype::{
|
notetype::{
|
||||||
all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeID, NoteTypeSchema11,
|
all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeSchema11, RenderCardOutput,
|
||||||
RenderCardOutput,
|
|
||||||
},
|
},
|
||||||
sched::{
|
sched::{
|
||||||
new::NewCardSortOrder,
|
new::NewCardSortOrder,
|
||||||
parse_due_date_str,
|
parse_due_date_str,
|
||||||
timespan::{answer_button_time, time_span},
|
states::NextCardStates,
|
||||||
|
timespan::{answer_button_time, answer_button_time_collapsible, time_span},
|
||||||
},
|
},
|
||||||
search::{
|
search::{
|
||||||
concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node,
|
concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node,
|
||||||
|
@ -69,7 +69,9 @@ use std::{
|
||||||
use tokio::runtime::{self, Runtime};
|
use tokio::runtime::{self, Runtime};
|
||||||
|
|
||||||
mod dbproxy;
|
mod dbproxy;
|
||||||
|
mod generic;
|
||||||
mod http_sync_server;
|
mod http_sync_server;
|
||||||
|
mod sched;
|
||||||
|
|
||||||
struct ThrottlingProgressHandler {
|
struct ThrottlingProgressHandler {
|
||||||
state: Arc<Mutex<ProgressState>>,
|
state: Arc<Mutex<ProgressState>>,
|
||||||
|
@ -218,82 +220,6 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result<Backend, String> {
|
||||||
Ok(Backend::new(i18n, input.server))
|
Ok(Backend::new(i18n, input.server))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<u8>> for pb::Json {
|
|
||||||
fn from(json: Vec<u8>) -> Self {
|
|
||||||
pb::Json { json }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for pb::String {
|
|
||||||
fn from(val: String) -> Self {
|
|
||||||
pb::String { val }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<i64> for pb::Int64 {
|
|
||||||
fn from(val: i64) -> Self {
|
|
||||||
pb::Int64 { val }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u32> for pb::UInt32 {
|
|
||||||
fn from(val: u32) -> Self {
|
|
||||||
pb::UInt32 { val }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<()> for pb::Empty {
|
|
||||||
fn from(_val: ()) -> Self {
|
|
||||||
pb::Empty {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<pb::CardId> for CardID {
|
|
||||||
fn from(cid: pb::CardId) -> Self {
|
|
||||||
CardID(cid.cid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl pb::CardIDs {
|
|
||||||
fn into_native(self) -> Vec<CardID> {
|
|
||||||
self.cids.into_iter().map(CardID).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<pb::NoteId> for NoteID {
|
|
||||||
fn from(nid: pb::NoteId) -> Self {
|
|
||||||
NoteID(nid.nid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl pb::search_node::IdList {
|
|
||||||
fn into_id_string(self) -> String {
|
|
||||||
self.ids
|
|
||||||
.iter()
|
|
||||||
.map(|i| i.to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<pb::NoteTypeId> for NoteTypeID {
|
|
||||||
fn from(ntid: pb::NoteTypeId) -> Self {
|
|
||||||
NoteTypeID(ntid.ntid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<pb::DeckId> for DeckID {
|
|
||||||
fn from(did: pb::DeckId) -> Self {
|
|
||||||
DeckID(did.did)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<pb::DeckConfigId> for DeckConfID {
|
|
||||||
fn from(dcid: pb::DeckConfigId) -> Self {
|
|
||||||
DeckConfID(dcid.dcid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<pb::SearchNode> for Node {
|
impl TryFrom<pb::SearchNode> for Node {
|
||||||
type Error = AnkiError;
|
type Error = AnkiError;
|
||||||
|
|
||||||
|
@ -436,62 +362,6 @@ impl BackendService for Backend {
|
||||||
|
|
||||||
// card rendering
|
// card rendering
|
||||||
|
|
||||||
fn render_existing_card(
|
|
||||||
&self,
|
|
||||||
input: pb::RenderExistingCardIn,
|
|
||||||
) -> BackendResult<pb::RenderCardOut> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
col.render_existing_card(CardID(input.card_id), input.browser)
|
|
||||||
.map(Into::into)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_uncommitted_card(
|
|
||||||
&self,
|
|
||||||
input: pb::RenderUncommittedCardIn,
|
|
||||||
) -> BackendResult<pb::RenderCardOut> {
|
|
||||||
let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?;
|
|
||||||
let template = schema11.into();
|
|
||||||
let mut note = input
|
|
||||||
.note
|
|
||||||
.ok_or_else(|| AnkiError::invalid_input("missing note"))?
|
|
||||||
.into();
|
|
||||||
let ord = input.card_ord as u16;
|
|
||||||
let fill_empty = input.fill_empty;
|
|
||||||
self.with_col(|col| {
|
|
||||||
col.render_uncommitted_card(&mut note, &template, ord, fill_empty)
|
|
||||||
.map(Into::into)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_empty_cards(&self, _input: pb::Empty) -> Result<pb::EmptyCardsReport> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mut empty = col.empty_cards()?;
|
|
||||||
let report = col.empty_cards_report(&mut empty)?;
|
|
||||||
|
|
||||||
let mut outnotes = vec![];
|
|
||||||
for (_ntid, notes) in empty {
|
|
||||||
outnotes.extend(notes.into_iter().map(|e| {
|
|
||||||
pb::empty_cards_report::NoteWithEmptyCards {
|
|
||||||
note_id: e.nid.0,
|
|
||||||
will_delete_note: e.empty.len() == e.current_count,
|
|
||||||
card_ids: e.empty.into_iter().map(|(_ord, id)| id.0).collect(),
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
Ok(pb::EmptyCardsReport {
|
|
||||||
report,
|
|
||||||
notes: outnotes,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_av_tags(&self, input: pb::String) -> BackendResult<pb::String> {
|
|
||||||
Ok(pb::String {
|
|
||||||
val: strip_av_tags(&input.val).into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> BackendResult<pb::ExtractAvTagsOut> {
|
fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> BackendResult<pb::ExtractAvTagsOut> {
|
||||||
let (text, tags) = extract_av_tags(&input.text, input.question_side);
|
let (text, tags) = extract_av_tags(&input.text, input.question_side);
|
||||||
let pt_tags = tags
|
let pt_tags = tags
|
||||||
|
@ -544,6 +414,62 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_empty_cards(&self, _input: pb::Empty) -> Result<pb::EmptyCardsReport> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mut empty = col.empty_cards()?;
|
||||||
|
let report = col.empty_cards_report(&mut empty)?;
|
||||||
|
|
||||||
|
let mut outnotes = vec![];
|
||||||
|
for (_ntid, notes) in empty {
|
||||||
|
outnotes.extend(notes.into_iter().map(|e| {
|
||||||
|
pb::empty_cards_report::NoteWithEmptyCards {
|
||||||
|
note_id: e.nid.0,
|
||||||
|
will_delete_note: e.empty.len() == e.current_count,
|
||||||
|
card_ids: e.empty.into_iter().map(|(_ord, id)| id.0).collect(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Ok(pb::EmptyCardsReport {
|
||||||
|
report,
|
||||||
|
notes: outnotes,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_existing_card(
|
||||||
|
&self,
|
||||||
|
input: pb::RenderExistingCardIn,
|
||||||
|
) -> BackendResult<pb::RenderCardOut> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.render_existing_card(CardID(input.card_id), input.browser)
|
||||||
|
.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_uncommitted_card(
|
||||||
|
&self,
|
||||||
|
input: pb::RenderUncommittedCardIn,
|
||||||
|
) -> BackendResult<pb::RenderCardOut> {
|
||||||
|
let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?;
|
||||||
|
let template = schema11.into();
|
||||||
|
let mut note = input
|
||||||
|
.note
|
||||||
|
.ok_or_else(|| AnkiError::invalid_input("missing note"))?
|
||||||
|
.into();
|
||||||
|
let ord = input.card_ord as u16;
|
||||||
|
let fill_empty = input.fill_empty;
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.render_uncommitted_card(&mut note, &template, ord, fill_empty)
|
||||||
|
.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_av_tags(&self, input: pb::String) -> BackendResult<pb::String> {
|
||||||
|
Ok(pb::String {
|
||||||
|
val: strip_av_tags(&input.val).into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// searching
|
// searching
|
||||||
//-----------------------------------------------
|
//-----------------------------------------------
|
||||||
|
|
||||||
|
@ -675,10 +601,8 @@ impl BackendService for Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_buried_and_suspended_cards(&self, input: pb::CardIDs) -> BackendResult<Empty> {
|
fn restore_buried_and_suspended_cards(&self, input: pb::CardIDs) -> BackendResult<Empty> {
|
||||||
self.with_col(|col| {
|
let cids: Vec<_> = input.into();
|
||||||
col.unbury_or_unsuspend_cards(&input.into_native())
|
self.with_col(|col| col.unbury_or_unsuspend_cards(&cids).map(Into::into))
|
||||||
.map(Into::into)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unbury_cards_in_current_deck(
|
fn unbury_cards_in_current_deck(
|
||||||
|
@ -747,6 +671,46 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_next_card_states(&self, input: pb::CardId) -> BackendResult<pb::NextCardStates> {
|
||||||
|
let cid: CardID = input.into();
|
||||||
|
self.with_col(|col| col.get_next_card_states(cid))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn describe_next_states(&self, input: pb::NextCardStates) -> BackendResult<pb::StringList> {
|
||||||
|
let collapse_time = self.with_col(|col| Ok(col.learn_ahead_secs()))?;
|
||||||
|
let choices: NextCardStates = input.into();
|
||||||
|
|
||||||
|
Ok(vec![
|
||||||
|
answer_button_time_collapsible(
|
||||||
|
choices.again.interval_kind().as_seconds(),
|
||||||
|
collapse_time,
|
||||||
|
&self.i18n,
|
||||||
|
),
|
||||||
|
answer_button_time_collapsible(
|
||||||
|
choices.hard.interval_kind().as_seconds(),
|
||||||
|
collapse_time,
|
||||||
|
&self.i18n,
|
||||||
|
),
|
||||||
|
answer_button_time_collapsible(
|
||||||
|
choices.good.interval_kind().as_seconds(),
|
||||||
|
collapse_time,
|
||||||
|
&self.i18n,
|
||||||
|
),
|
||||||
|
answer_button_time_collapsible(
|
||||||
|
choices.easy.interval_kind().as_seconds(),
|
||||||
|
collapse_time,
|
||||||
|
&self.i18n,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_card(&self, input: pb::AnswerCardIn) -> BackendResult<pb::Empty> {
|
||||||
|
self.with_col(|col| col.answer_card(&input.into()))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
// statistics
|
// statistics
|
||||||
//-----------------------------------------------
|
//-----------------------------------------------
|
||||||
|
|
||||||
|
@ -768,9 +732,102 @@ impl BackendService for Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
// decks
|
// media
|
||||||
//-----------------------------------------------
|
//-----------------------------------------------
|
||||||
|
|
||||||
|
fn check_media(&self, _input: pb::Empty) -> Result<pb::CheckMediaOut> {
|
||||||
|
let mut handler = self.new_progress_handler();
|
||||||
|
let progress_fn =
|
||||||
|
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
col.transact(None, |ctx| {
|
||||||
|
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
||||||
|
let mut output = checker.check()?;
|
||||||
|
|
||||||
|
let report = checker.summarize_output(&mut output);
|
||||||
|
|
||||||
|
Ok(pb::CheckMediaOut {
|
||||||
|
unused: output.unused,
|
||||||
|
missing: output.missing,
|
||||||
|
report,
|
||||||
|
have_trash: output.trash_count > 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> BackendResult<Empty> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
let mut ctx = mgr.dbctx();
|
||||||
|
mgr.remove_files(&mut ctx, &input.fnames)
|
||||||
|
})
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_media_file(&self, input: pb::AddMediaFileIn) -> BackendResult<pb::String> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
let mut ctx = mgr.dbctx();
|
||||||
|
Ok(mgr
|
||||||
|
.add_file(&mut ctx, &input.desired_name, &input.data)?
|
||||||
|
.to_string()
|
||||||
|
.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_trash(&self, _input: Empty) -> BackendResult<Empty> {
|
||||||
|
let mut handler = self.new_progress_handler();
|
||||||
|
let progress_fn =
|
||||||
|
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
||||||
|
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
col.transact(None, |ctx| {
|
||||||
|
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
||||||
|
|
||||||
|
checker.empty_trash()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_trash(&self, _input: Empty) -> BackendResult<Empty> {
|
||||||
|
let mut handler = self.new_progress_handler();
|
||||||
|
let progress_fn =
|
||||||
|
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
|
||||||
|
col.transact(None, |ctx| {
|
||||||
|
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
||||||
|
|
||||||
|
checker.restore_trash()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decks
|
||||||
|
//----------------------------------------------------
|
||||||
|
|
||||||
|
fn add_or_update_deck_legacy(&self, input: pb::AddOrUpdateDeckLegacyIn) -> Result<pb::DeckId> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let schema11: DeckSchema11 = serde_json::from_slice(&input.deck)?;
|
||||||
|
let mut deck: Deck = schema11.into();
|
||||||
|
if input.preserve_usn_and_mtime {
|
||||||
|
col.transact(None, |col| {
|
||||||
|
let usn = col.usn()?;
|
||||||
|
col.add_or_update_single_deck(&mut deck, usn)
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
col.add_or_update_deck(&mut deck)?;
|
||||||
|
}
|
||||||
|
Ok(pb::DeckId { did: deck.id.0 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn deck_tree(&self, input: pb::DeckTreeIn) -> Result<pb::DeckTreeNode> {
|
fn deck_tree(&self, input: pb::DeckTreeIn) -> Result<pb::DeckTreeNode> {
|
||||||
let lim = if input.top_deck_id > 0 {
|
let lim = if input.top_deck_id > 0 {
|
||||||
Some(DeckID(input.top_deck_id))
|
Some(DeckID(input.top_deck_id))
|
||||||
|
@ -796,17 +853,12 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_deck_legacy(&self, input: pb::DeckId) -> Result<pb::Json> {
|
fn get_all_decks_legacy(&self, _input: Empty) -> BackendResult<pb::Json> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let deck: DeckSchema11 = col
|
let decks = col.storage.get_all_decks_as_schema11()?;
|
||||||
.storage
|
serde_json::to_vec(&decks).map_err(Into::into)
|
||||||
.get_deck(input.into())?
|
|
||||||
.ok_or(AnkiError::NotFound)?
|
|
||||||
.into();
|
|
||||||
serde_json::to_vec(&deck)
|
|
||||||
.map_err(Into::into)
|
|
||||||
.map(Into::into)
|
|
||||||
})
|
})
|
||||||
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_deck_id_by_name(&self, input: pb::String) -> Result<pb::DeckId> {
|
fn get_deck_id_by_name(&self, input: pb::String) -> Result<pb::DeckId> {
|
||||||
|
@ -818,12 +870,17 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_all_decks_legacy(&self, _input: Empty) -> BackendResult<pb::Json> {
|
fn get_deck_legacy(&self, input: pb::DeckId) -> Result<pb::Json> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let decks = col.storage.get_all_decks_as_schema11()?;
|
let deck: DeckSchema11 = col
|
||||||
serde_json::to_vec(&decks).map_err(Into::into)
|
.storage
|
||||||
|
.get_deck(input.into())?
|
||||||
|
.ok_or(AnkiError::NotFound)?
|
||||||
|
.into();
|
||||||
|
serde_json::to_vec(&deck)
|
||||||
|
.map_err(Into::into)
|
||||||
|
.map(Into::into)
|
||||||
})
|
})
|
||||||
.map(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_deck_names(&self, input: pb::GetDeckNamesIn) -> Result<pb::DeckNames> {
|
fn get_deck_names(&self, input: pb::GetDeckNamesIn) -> Result<pb::DeckNames> {
|
||||||
|
@ -842,22 +899,6 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_or_update_deck_legacy(&self, input: pb::AddOrUpdateDeckLegacyIn) -> Result<pb::DeckId> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let schema11: DeckSchema11 = serde_json::from_slice(&input.deck)?;
|
|
||||||
let mut deck: Deck = schema11.into();
|
|
||||||
if input.preserve_usn_and_mtime {
|
|
||||||
col.transact(None, |col| {
|
|
||||||
let usn = col.usn()?;
|
|
||||||
col.add_or_update_single_deck(&mut deck, usn)
|
|
||||||
})?;
|
|
||||||
} else {
|
|
||||||
col.add_or_update_deck(&mut deck)?;
|
|
||||||
}
|
|
||||||
Ok(pb::DeckId { did: deck.id.0 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_deck_legacy(&self, input: pb::Bool) -> BackendResult<pb::Json> {
|
fn new_deck_legacy(&self, input: pb::Bool) -> BackendResult<pb::Json> {
|
||||||
let deck = if input.val {
|
let deck = if input.val {
|
||||||
Deck::new_filtered()
|
Deck::new_filtered()
|
||||||
|
@ -917,6 +958,15 @@ impl BackendService for Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_deck_config_legacy(&self, input: pb::DeckConfigId) -> BackendResult<pb::Json> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let conf = col.get_deck_config(input.into(), true)?.unwrap();
|
||||||
|
let conf: DeckConfSchema11 = conf.into();
|
||||||
|
Ok(serde_json::to_vec(&conf)?)
|
||||||
|
})
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
fn new_deck_config_legacy(&self, _input: Empty) -> BackendResult<pb::Json> {
|
fn new_deck_config_legacy(&self, _input: Empty) -> BackendResult<pb::Json> {
|
||||||
serde_json::to_vec(&DeckConfSchema11::default())
|
serde_json::to_vec(&DeckConfSchema11::default())
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
|
@ -928,15 +978,6 @@ impl BackendService for Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_deck_config_legacy(&self, input: pb::DeckConfigId) -> BackendResult<pb::Json> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let conf = col.get_deck_config(input.into(), true)?.unwrap();
|
|
||||||
let conf: DeckConfSchema11 = conf.into();
|
|
||||||
Ok(serde_json::to_vec(&conf)?)
|
|
||||||
})
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cards
|
// cards
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -1080,18 +1121,6 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn field_names_for_notes(
|
|
||||||
&self,
|
|
||||||
input: pb::FieldNamesForNotesIn,
|
|
||||||
) -> BackendResult<pb::FieldNamesForNotesOut> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let nids: Vec<_> = input.nids.into_iter().map(NoteID).collect();
|
|
||||||
col.storage
|
|
||||||
.field_names_for_notes(&nids)
|
|
||||||
.map(|fields| pb::FieldNamesForNotesOut { fields })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> BackendResult<Empty> {
|
fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> BackendResult<Empty> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.transact(None, |col| {
|
col.transact(None, |col| {
|
||||||
|
@ -1105,6 +1134,18 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn field_names_for_notes(
|
||||||
|
&self,
|
||||||
|
input: pb::FieldNamesForNotesIn,
|
||||||
|
) -> BackendResult<pb::FieldNamesForNotesOut> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let nids: Vec<_> = input.nids.into_iter().map(NoteID).collect();
|
||||||
|
col.storage
|
||||||
|
.field_names_for_notes(&nids)
|
||||||
|
.map(|fields| pb::FieldNamesForNotesOut { fields })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn note_is_duplicate_or_empty(
|
fn note_is_duplicate_or_empty(
|
||||||
&self,
|
&self,
|
||||||
input: pb::Note,
|
input: pb::Note,
|
||||||
|
@ -1129,6 +1170,22 @@ impl BackendService for Backend {
|
||||||
// notetypes
|
// notetypes
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn add_or_update_notetype(
|
||||||
|
&self,
|
||||||
|
input: pb::AddOrUpdateNotetypeIn,
|
||||||
|
) -> BackendResult<pb::NoteTypeId> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let legacy: NoteTypeSchema11 = serde_json::from_slice(&input.json)?;
|
||||||
|
let mut nt: NoteType = legacy.into();
|
||||||
|
if nt.id.0 == 0 {
|
||||||
|
col.add_notetype(&mut nt)?;
|
||||||
|
} else {
|
||||||
|
col.update_notetype(&mut nt, input.preserve_usn_and_mtime)?;
|
||||||
|
}
|
||||||
|
Ok(pb::NoteTypeId { ntid: nt.id.0 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> BackendResult<pb::Json> {
|
fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> BackendResult<pb::Json> {
|
||||||
// fixme: use individual functions instead of full vec
|
// fixme: use individual functions instead of full vec
|
||||||
let mut all = all_stock_notetypes(&self.i18n);
|
let mut all = all_stock_notetypes(&self.i18n);
|
||||||
|
@ -1140,6 +1197,17 @@ impl BackendService for Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_notetype_legacy(&self, input: pb::NoteTypeId) -> BackendResult<pb::Json> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let schema11: NoteTypeSchema11 = col
|
||||||
|
.storage
|
||||||
|
.get_notetype(input.into())?
|
||||||
|
.ok_or(AnkiError::NotFound)?
|
||||||
|
.into();
|
||||||
|
Ok(serde_json::to_vec(&schema11)?).map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn get_notetype_names(&self, _input: Empty) -> BackendResult<pb::NoteTypeNames> {
|
fn get_notetype_names(&self, _input: Empty) -> BackendResult<pb::NoteTypeNames> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let entries: Vec<_> = col
|
let entries: Vec<_> = col
|
||||||
|
@ -1168,17 +1236,6 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_notetype_legacy(&self, input: pb::NoteTypeId) -> BackendResult<pb::Json> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let schema11: NoteTypeSchema11 = col
|
|
||||||
.storage
|
|
||||||
.get_notetype(input.into())?
|
|
||||||
.ok_or(AnkiError::NotFound)?
|
|
||||||
.into();
|
|
||||||
Ok(serde_json::to_vec(&schema11)?).map(Into::into)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_notetype_id_by_name(&self, input: pb::String) -> BackendResult<pb::NoteTypeId> {
|
fn get_notetype_id_by_name(&self, input: pb::String) -> BackendResult<pb::NoteTypeId> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.storage
|
col.storage
|
||||||
|
@ -1188,120 +1245,14 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_or_update_notetype(
|
|
||||||
&self,
|
|
||||||
input: pb::AddOrUpdateNotetypeIn,
|
|
||||||
) -> BackendResult<pb::NoteTypeId> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let legacy: NoteTypeSchema11 = serde_json::from_slice(&input.json)?;
|
|
||||||
let mut nt: NoteType = legacy.into();
|
|
||||||
if nt.id.0 == 0 {
|
|
||||||
col.add_notetype(&mut nt)?;
|
|
||||||
} else {
|
|
||||||
col.update_notetype(&mut nt, input.preserve_usn_and_mtime)?;
|
|
||||||
}
|
|
||||||
Ok(pb::NoteTypeId { ntid: nt.id.0 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_notetype(&self, input: pb::NoteTypeId) -> BackendResult<Empty> {
|
fn remove_notetype(&self, input: pb::NoteTypeId) -> BackendResult<Empty> {
|
||||||
self.with_col(|col| col.remove_notetype(input.into()))
|
self.with_col(|col| col.remove_notetype(input.into()))
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
// media
|
|
||||||
//-------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn add_media_file(&self, input: pb::AddMediaFileIn) -> BackendResult<pb::String> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
let mut ctx = mgr.dbctx();
|
|
||||||
Ok(mgr
|
|
||||||
.add_file(&mut ctx, &input.desired_name, &input.data)?
|
|
||||||
.to_string()
|
|
||||||
.into())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn empty_trash(&self, _input: Empty) -> BackendResult<Empty> {
|
|
||||||
let mut handler = self.new_progress_handler();
|
|
||||||
let progress_fn =
|
|
||||||
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
|
||||||
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
col.transact(None, |ctx| {
|
|
||||||
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
|
||||||
|
|
||||||
checker.empty_trash()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_trash(&self, _input: Empty) -> BackendResult<Empty> {
|
|
||||||
let mut handler = self.new_progress_handler();
|
|
||||||
let progress_fn =
|
|
||||||
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
|
|
||||||
col.transact(None, |ctx| {
|
|
||||||
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
|
||||||
|
|
||||||
checker.restore_trash()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> BackendResult<Empty> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
let mut ctx = mgr.dbctx();
|
|
||||||
mgr.remove_files(&mut ctx, &input.fnames)
|
|
||||||
})
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_media(&self, _input: pb::Empty) -> Result<pb::CheckMediaOut> {
|
|
||||||
let mut handler = self.new_progress_handler();
|
|
||||||
let progress_fn =
|
|
||||||
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
col.transact(None, |ctx| {
|
|
||||||
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
|
||||||
let mut output = checker.check()?;
|
|
||||||
|
|
||||||
let report = checker.summarize_output(&mut output);
|
|
||||||
|
|
||||||
Ok(pb::CheckMediaOut {
|
|
||||||
unused: output.unused,
|
|
||||||
missing: output.missing,
|
|
||||||
report,
|
|
||||||
have_trash: output.trash_count > 0,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// collection
|
// collection
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
fn check_database(&self, _input: pb::Empty) -> BackendResult<pb::CheckDatabaseOut> {
|
|
||||||
let mut handler = self.new_progress_handler();
|
|
||||||
let progress_fn = move |progress, throttle| {
|
|
||||||
handler.update(Progress::DatabaseCheck(progress), throttle);
|
|
||||||
};
|
|
||||||
self.with_col(|col| {
|
|
||||||
col.check_database(progress_fn)
|
|
||||||
.map(|problems| pb::CheckDatabaseOut {
|
|
||||||
problems: problems.to_i18n_strings(&col.i18n),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_collection(&self, input: pb::OpenCollectionIn) -> BackendResult<Empty> {
|
fn open_collection(&self, input: pb::OpenCollectionIn) -> BackendResult<Empty> {
|
||||||
let mut col = self.col.lock().unwrap();
|
let mut col = self.col.lock().unwrap();
|
||||||
if col.is_some() {
|
if col.is_some() {
|
||||||
|
@ -1350,31 +1301,22 @@ impl BackendService for Backend {
|
||||||
Ok(().into())
|
Ok(().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_database(&self, _input: pb::Empty) -> BackendResult<pb::CheckDatabaseOut> {
|
||||||
|
let mut handler = self.new_progress_handler();
|
||||||
|
let progress_fn = move |progress, throttle| {
|
||||||
|
handler.update(Progress::DatabaseCheck(progress), throttle);
|
||||||
|
};
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.check_database(progress_fn)
|
||||||
|
.map(|problems| pb::CheckDatabaseOut {
|
||||||
|
problems: problems.to_i18n_strings(&col.i18n),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// sync
|
// sync
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
fn sync_login(&self, input: pb::SyncLoginIn) -> BackendResult<pb::SyncAuth> {
|
|
||||||
self.sync_login_inner(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_status(&self, input: pb::SyncAuth) -> BackendResult<pb::SyncStatusOut> {
|
|
||||||
self.sync_status_inner(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_collection(&self, input: pb::SyncAuth) -> BackendResult<pb::SyncCollectionOut> {
|
|
||||||
self.sync_collection_inner(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn full_upload(&self, input: pb::SyncAuth) -> BackendResult<Empty> {
|
|
||||||
self.full_sync_inner(input, true)?;
|
|
||||||
Ok(().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn full_download(&self, input: pb::SyncAuth) -> BackendResult<Empty> {
|
|
||||||
self.full_sync_inner(input, false)?;
|
|
||||||
Ok(().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_media(&self, input: pb::SyncAuth) -> BackendResult<Empty> {
|
fn sync_media(&self, input: pb::SyncAuth) -> BackendResult<Empty> {
|
||||||
self.sync_media_inner(input).map(Into::into)
|
self.sync_media_inner(input).map(Into::into)
|
||||||
}
|
}
|
||||||
|
@ -1399,6 +1341,28 @@ impl BackendService for Backend {
|
||||||
self.with_col(|col| col.before_upload().map(Into::into))
|
self.with_col(|col| col.before_upload().map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_login(&self, input: pb::SyncLoginIn) -> BackendResult<pb::SyncAuth> {
|
||||||
|
self.sync_login_inner(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_status(&self, input: pb::SyncAuth) -> BackendResult<pb::SyncStatusOut> {
|
||||||
|
self.sync_status_inner(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_collection(&self, input: pb::SyncAuth) -> BackendResult<pb::SyncCollectionOut> {
|
||||||
|
self.sync_collection_inner(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_upload(&self, input: pb::SyncAuth) -> BackendResult<Empty> {
|
||||||
|
self.full_sync_inner(input, true)?;
|
||||||
|
Ok(().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_download(&self, input: pb::SyncAuth) -> BackendResult<Empty> {
|
||||||
|
self.full_sync_inner(input, false)?;
|
||||||
|
Ok(().into())
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_server_method(&self, input: pb::SyncServerMethodIn) -> BackendResult<pb::Json> {
|
fn sync_server_method(&self, input: pb::SyncServerMethodIn) -> BackendResult<pb::Json> {
|
||||||
let req = SyncRequest::from_method_and_data(input.method(), input.data)?;
|
let req = SyncRequest::from_method_and_data(input.method(), input.data)?;
|
||||||
self.sync_server_method_inner(req).map(Into::into)
|
self.sync_server_method_inner(req).map(Into::into)
|
||||||
|
@ -1450,6 +1414,10 @@ impl BackendService for Backend {
|
||||||
// tags
|
// tags
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn clear_unused_tags(&self, _input: pb::Empty) -> BackendResult<pb::Empty> {
|
||||||
|
self.with_col(|col| col.transact(None, |col| col.clear_unused_tags().map(Into::into)))
|
||||||
|
}
|
||||||
|
|
||||||
fn all_tags(&self, _input: Empty) -> BackendResult<pb::StringList> {
|
fn all_tags(&self, _input: Empty) -> BackendResult<pb::StringList> {
|
||||||
Ok(pb::StringList {
|
Ok(pb::StringList {
|
||||||
vals: self.with_col(|col| {
|
vals: self.with_col(|col| {
|
||||||
|
@ -1472,10 +1440,6 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_unused_tags(&self, _input: pb::Empty) -> BackendResult<pb::Empty> {
|
|
||||||
self.with_col(|col| col.transact(None, |col| col.clear_unused_tags().map(Into::into)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_tag(&self, tag: pb::String) -> BackendResult<pb::Empty> {
|
fn clear_tag(&self, tag: pb::String) -> BackendResult<pb::Empty> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.transact(None, |col| {
|
col.transact(None, |col| {
|
||||||
|
@ -1536,15 +1500,6 @@ impl BackendService for Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_preferences(&self, _input: Empty) -> BackendResult<pb::Preferences> {
|
|
||||||
self.with_col(|col| col.get_preferences())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_preferences(&self, input: pb::Preferences) -> BackendResult<Empty> {
|
|
||||||
self.with_col(|col| col.transact(None, |col| col.set_preferences(input)))
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_config_bool(&self, input: pb::config::Bool) -> BackendResult<pb::Bool> {
|
fn get_config_bool(&self, input: pb::config::Bool) -> BackendResult<pb::Bool> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
Ok(pb::Bool {
|
Ok(pb::Bool {
|
||||||
|
@ -1570,6 +1525,15 @@ impl BackendService for Backend {
|
||||||
self.with_col(|col| col.transact(None, |col| col.set_string(input)))
|
self.with_col(|col| col.transact(None, |col| col.set_string(input)))
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_preferences(&self, _input: Empty) -> BackendResult<pb::Preferences> {
|
||||||
|
self.with_col(|col| col.get_preferences())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_preferences(&self, input: pb::Preferences) -> BackendResult<Empty> {
|
||||||
|
self.with_col(|col| col.transact(None, |col| col.set_preferences(input)))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
|
@ -2172,3 +2136,13 @@ impl From<NormalSyncProgress> for Progress {
|
||||||
Progress::NormalSync(p)
|
Progress::NormalSync(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl pb::search_node::IdList {
|
||||||
|
fn into_id_string(self) -> String {
|
||||||
|
self.ids
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
32
rslib/src/backend/sched/answering.rs
Normal file
32
rslib/src/backend/sched/answering.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend_proto as pb,
|
||||||
|
prelude::*,
|
||||||
|
sched::answering::{CardAnswer, Rating},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl From<pb::AnswerCardIn> for CardAnswer {
|
||||||
|
fn from(answer: pb::AnswerCardIn) -> Self {
|
||||||
|
CardAnswer {
|
||||||
|
card_id: CardID(answer.card_id),
|
||||||
|
rating: answer.rating().into(),
|
||||||
|
current_state: answer.current_state.unwrap_or_default().into(),
|
||||||
|
new_state: answer.new_state.unwrap_or_default().into(),
|
||||||
|
answered_at: TimestampSecs(answer.answered_at),
|
||||||
|
milliseconds_taken: answer.milliseconds_taken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::answer_card_in::Rating> for Rating {
|
||||||
|
fn from(rating: pb::answer_card_in::Rating) -> Self {
|
||||||
|
match rating {
|
||||||
|
pb::answer_card_in::Rating::Again => Rating::Again,
|
||||||
|
pb::answer_card_in::Rating::Hard => Rating::Hard,
|
||||||
|
pb::answer_card_in::Rating::Good => Rating::Good,
|
||||||
|
pb::answer_card_in::Rating::Easy => Rating::Easy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
rslib/src/backend/sched/mod.rs
Normal file
5
rslib/src/backend/sched/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
mod answering;
|
||||||
|
mod states;
|
35
rslib/src/backend/sched/states/filtered.rs
Normal file
35
rslib/src/backend/sched/states/filtered.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, sched::states::FilteredState};
|
||||||
|
|
||||||
|
impl From<FilteredState> for pb::scheduling_state::Filtered {
|
||||||
|
fn from(state: FilteredState) -> Self {
|
||||||
|
pb::scheduling_state::Filtered {
|
||||||
|
value: Some(match state {
|
||||||
|
FilteredState::Preview(state) => {
|
||||||
|
pb::scheduling_state::filtered::Value::Preview(state.into())
|
||||||
|
}
|
||||||
|
FilteredState::Rescheduling(state) => {
|
||||||
|
pb::scheduling_state::filtered::Value::Rescheduling(state.into())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::scheduling_state::Filtered> for FilteredState {
|
||||||
|
fn from(state: pb::scheduling_state::Filtered) -> Self {
|
||||||
|
match state
|
||||||
|
.value
|
||||||
|
.unwrap_or_else(|| pb::scheduling_state::filtered::Value::Preview(Default::default()))
|
||||||
|
{
|
||||||
|
pb::scheduling_state::filtered::Value::Preview(state) => {
|
||||||
|
FilteredState::Preview(state.into())
|
||||||
|
}
|
||||||
|
pb::scheduling_state::filtered::Value::Rescheduling(state) => {
|
||||||
|
FilteredState::Rescheduling(state.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
rslib/src/backend/sched/states/learning.rs
Normal file
22
rslib/src/backend/sched/states/learning.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, sched::states::LearnState};
|
||||||
|
|
||||||
|
impl From<pb::scheduling_state::Learning> for LearnState {
|
||||||
|
fn from(state: pb::scheduling_state::Learning) -> Self {
|
||||||
|
LearnState {
|
||||||
|
remaining_steps: state.remaining_steps,
|
||||||
|
scheduled_secs: state.scheduled_secs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LearnState> for pb::scheduling_state::Learning {
|
||||||
|
fn from(state: LearnState) -> Self {
|
||||||
|
pb::scheduling_state::Learning {
|
||||||
|
remaining_steps: state.remaining_steps,
|
||||||
|
scheduled_secs: state.scheduled_secs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
rslib/src/backend/sched/states/mod.rs
Normal file
66
rslib/src/backend/sched/states/mod.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
mod filtered;
|
||||||
|
mod learning;
|
||||||
|
mod new;
|
||||||
|
mod normal;
|
||||||
|
mod preview;
|
||||||
|
mod relearning;
|
||||||
|
mod rescheduling;
|
||||||
|
mod review;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend_proto as pb,
|
||||||
|
sched::states::{CardState, NewState, NextCardStates, NormalState},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl From<NextCardStates> for pb::NextCardStates {
|
||||||
|
fn from(choices: NextCardStates) -> Self {
|
||||||
|
pb::NextCardStates {
|
||||||
|
current: Some(choices.current.into()),
|
||||||
|
again: Some(choices.again.into()),
|
||||||
|
hard: Some(choices.hard.into()),
|
||||||
|
good: Some(choices.good.into()),
|
||||||
|
easy: Some(choices.easy.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::NextCardStates> for NextCardStates {
|
||||||
|
fn from(choices: pb::NextCardStates) -> Self {
|
||||||
|
NextCardStates {
|
||||||
|
current: choices.current.unwrap_or_default().into(),
|
||||||
|
again: choices.again.unwrap_or_default().into(),
|
||||||
|
hard: choices.hard.unwrap_or_default().into(),
|
||||||
|
good: choices.good.unwrap_or_default().into(),
|
||||||
|
easy: choices.easy.unwrap_or_default().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CardState> for pb::SchedulingState {
|
||||||
|
fn from(state: CardState) -> Self {
|
||||||
|
pb::SchedulingState {
|
||||||
|
value: Some(match state {
|
||||||
|
CardState::Normal(state) => pb::scheduling_state::Value::Normal(state.into()),
|
||||||
|
CardState::Filtered(state) => pb::scheduling_state::Value::Filtered(state.into()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::SchedulingState> for CardState {
|
||||||
|
fn from(state: pb::SchedulingState) -> Self {
|
||||||
|
if let Some(value) = state.value {
|
||||||
|
match value {
|
||||||
|
pb::scheduling_state::Value::Normal(normal) => CardState::Normal(normal.into()),
|
||||||
|
pb::scheduling_state::Value::Filtered(filtered) => {
|
||||||
|
CardState::Filtered(filtered.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CardState::Normal(NormalState::New(NewState::default()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
rslib/src/backend/sched/states/new.rs
Normal file
20
rslib/src/backend/sched/states/new.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, sched::states::NewState};
|
||||||
|
|
||||||
|
impl From<pb::scheduling_state::New> for NewState {
|
||||||
|
fn from(state: pb::scheduling_state::New) -> Self {
|
||||||
|
NewState {
|
||||||
|
position: state.position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NewState> for pb::scheduling_state::New {
|
||||||
|
fn from(state: NewState) -> Self {
|
||||||
|
pb::scheduling_state::New {
|
||||||
|
position: state.position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
rslib/src/backend/sched/states/normal.rs
Normal file
41
rslib/src/backend/sched/states/normal.rs
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
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, sched::states::NormalState};
|
||||||
|
|
||||||
|
impl From<NormalState> for pb::scheduling_state::Normal {
|
||||||
|
fn from(state: NormalState) -> Self {
|
||||||
|
pb::scheduling_state::Normal {
|
||||||
|
value: Some(match state {
|
||||||
|
NormalState::New(state) => pb::scheduling_state::normal::Value::New(state.into()),
|
||||||
|
NormalState::Learning(state) => {
|
||||||
|
pb::scheduling_state::normal::Value::Learning(state.into())
|
||||||
|
}
|
||||||
|
NormalState::Review(state) => {
|
||||||
|
pb::scheduling_state::normal::Value::Review(state.into())
|
||||||
|
}
|
||||||
|
NormalState::Relearning(state) => {
|
||||||
|
pb::scheduling_state::normal::Value::Relearning(state.into())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::scheduling_state::Normal> for NormalState {
|
||||||
|
fn from(state: pb::scheduling_state::Normal) -> Self {
|
||||||
|
match state
|
||||||
|
.value
|
||||||
|
.unwrap_or_else(|| pb::scheduling_state::normal::Value::New(Default::default()))
|
||||||
|
{
|
||||||
|
pb::scheduling_state::normal::Value::New(state) => NormalState::New(state.into()),
|
||||||
|
pb::scheduling_state::normal::Value::Learning(state) => {
|
||||||
|
NormalState::Learning(state.into())
|
||||||
|
}
|
||||||
|
pb::scheduling_state::normal::Value::Review(state) => NormalState::Review(state.into()),
|
||||||
|
pb::scheduling_state::normal::Value::Relearning(state) => {
|
||||||
|
NormalState::Relearning(state.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
rslib/src/backend/sched/states/preview.rs
Normal file
22
rslib/src/backend/sched/states/preview.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, sched::states::PreviewState};
|
||||||
|
|
||||||
|
impl From<pb::scheduling_state::Preview> for PreviewState {
|
||||||
|
fn from(state: pb::scheduling_state::Preview) -> Self {
|
||||||
|
PreviewState {
|
||||||
|
scheduled_secs: state.scheduled_secs,
|
||||||
|
original_state: state.original_state.unwrap_or_default().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PreviewState> for pb::scheduling_state::Preview {
|
||||||
|
fn from(state: PreviewState) -> Self {
|
||||||
|
pb::scheduling_state::Preview {
|
||||||
|
scheduled_secs: state.scheduled_secs,
|
||||||
|
original_state: Some(state.original_state.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
rslib/src/backend/sched/states/relearning.rs
Normal file
22
rslib/src/backend/sched/states/relearning.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, sched::states::RelearnState};
|
||||||
|
|
||||||
|
impl From<pb::scheduling_state::Relearning> for RelearnState {
|
||||||
|
fn from(state: pb::scheduling_state::Relearning) -> Self {
|
||||||
|
RelearnState {
|
||||||
|
review: state.review.unwrap_or_default().into(),
|
||||||
|
learning: state.learning.unwrap_or_default().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RelearnState> for pb::scheduling_state::Relearning {
|
||||||
|
fn from(state: RelearnState) -> Self {
|
||||||
|
pb::scheduling_state::Relearning {
|
||||||
|
review: Some(state.review.into()),
|
||||||
|
learning: Some(state.learning.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
rslib/src/backend/sched/states/rescheduling.rs
Normal file
20
rslib/src/backend/sched/states/rescheduling.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, sched::states::ReschedulingFilterState};
|
||||||
|
|
||||||
|
impl From<pb::scheduling_state::ReschedulingFilter> for ReschedulingFilterState {
|
||||||
|
fn from(state: pb::scheduling_state::ReschedulingFilter) -> Self {
|
||||||
|
ReschedulingFilterState {
|
||||||
|
original_state: state.original_state.unwrap_or_default().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReschedulingFilterState> for pb::scheduling_state::ReschedulingFilter {
|
||||||
|
fn from(state: ReschedulingFilterState) -> Self {
|
||||||
|
pb::scheduling_state::ReschedulingFilter {
|
||||||
|
original_state: Some(state.original_state.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
rslib/src/backend/sched/states/review.rs
Normal file
26
rslib/src/backend/sched/states/review.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{backend_proto as pb, sched::states::ReviewState};
|
||||||
|
|
||||||
|
impl From<pb::scheduling_state::Review> for ReviewState {
|
||||||
|
fn from(state: pb::scheduling_state::Review) -> Self {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days: state.scheduled_days,
|
||||||
|
elapsed_days: state.elapsed_days,
|
||||||
|
ease_factor: state.ease_factor,
|
||||||
|
lapses: state.lapses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReviewState> for pb::scheduling_state::Review {
|
||||||
|
fn from(state: ReviewState) -> Self {
|
||||||
|
pb::scheduling_state::Review {
|
||||||
|
scheduled_days: state.scheduled_days,
|
||||||
|
elapsed_days: state.elapsed_days,
|
||||||
|
ease_factor: state.ease_factor,
|
||||||
|
lapses: state.lapses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use crate::decks::DeckID;
|
|
||||||
use crate::define_newtype;
|
use crate::define_newtype;
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{AnkiError, Result};
|
||||||
use crate::notes::NoteID;
|
use crate::notes::NoteID;
|
||||||
|
@ -9,6 +8,7 @@ use crate::{
|
||||||
collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn,
|
collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn,
|
||||||
undo::Undoable,
|
undo::Undoable,
|
||||||
};
|
};
|
||||||
|
use crate::{deckconf::DeckConf, decks::DeckID};
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
@ -107,6 +107,17 @@ impl Card {
|
||||||
self.remove_from_filtered_deck_restoring_queue(sched);
|
self.remove_from_filtered_deck_restoring_queue(sched);
|
||||||
self.deck_id = deck;
|
self.deck_id = deck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the total number of steps left to do, ignoring the
|
||||||
|
/// "steps today" number packed into the DB representation.
|
||||||
|
pub fn remaining_steps(&self) -> u32 {
|
||||||
|
self.remaining_steps % 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return ease factor as a multiplier (eg 2.5)
|
||||||
|
pub fn ease_factor(&self) -> f32 {
|
||||||
|
(self.ease_factor as f32) / 1000.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct UpdateCardUndo(Card);
|
pub(crate) struct UpdateCardUndo(Card);
|
||||||
|
@ -122,15 +133,17 @@ impl Undoable for UpdateCardUndo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Card {
|
impl Card {
|
||||||
pub fn new(nid: NoteID, ord: u16, deck_id: DeckID, due: i32) -> Self {
|
pub fn new(note_id: NoteID, template_idx: u16, deck_id: DeckID, due: i32) -> Self {
|
||||||
let mut card = Card::default();
|
Card {
|
||||||
card.note_id = nid;
|
note_id,
|
||||||
card.template_idx = ord;
|
template_idx,
|
||||||
card.deck_id = deck_id;
|
deck_id,
|
||||||
card.due = due;
|
due,
|
||||||
card
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn get_and_update_card<F, T>(&mut self, cid: CardID, func: F) -> Result<Card>
|
pub(crate) fn get_and_update_card<F, T>(&mut self, cid: CardID, func: F) -> Result<Card>
|
||||||
|
@ -217,6 +230,17 @@ impl Collection {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get deck config for the given card. If missing, return default values.
|
||||||
|
pub(crate) fn deck_config_for_card(&mut self, card: &Card) -> Result<DeckConf> {
|
||||||
|
if let Some(deck) = self.get_deck(card.original_or_current_deck_id())? {
|
||||||
|
if let Some(conf_id) = deck.config_id() {
|
||||||
|
return Ok(self.get_deck_config(conf_id, true)?.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DeckConf::default())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -5,6 +5,7 @@ use crate::{
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
define_newtype,
|
define_newtype,
|
||||||
err::{AnkiError, Result},
|
err::{AnkiError, Result},
|
||||||
|
sched::states::review::INITIAL_EASE_FACTOR,
|
||||||
timestamp::{TimestampMillis, TimestampSecs},
|
timestamp::{TimestampMillis, TimestampSecs},
|
||||||
types::Usn,
|
types::Usn,
|
||||||
};
|
};
|
||||||
|
@ -15,7 +16,7 @@ pub use crate::backend_proto::{
|
||||||
};
|
};
|
||||||
pub use schema11::{DeckConfSchema11, NewCardOrderSchema11};
|
pub use schema11::{DeckConfSchema11, NewCardOrderSchema11};
|
||||||
/// Old deck config and cards table store 250% as 2500.
|
/// Old deck config and cards table store 250% as 2500.
|
||||||
pub const INITIAL_EASE_FACTOR_THOUSANDS: u16 = 2500;
|
pub(crate) const INITIAL_EASE_FACTOR_THOUSANDS: u16 = (INITIAL_EASE_FACTOR * 1000.0) as u16;
|
||||||
|
|
||||||
mod schema11;
|
mod schema11;
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@ impl Default for DeckConf {
|
||||||
lapse_multiplier: 0.0,
|
lapse_multiplier: 0.0,
|
||||||
interval_multiplier: 1.0,
|
interval_multiplier: 1.0,
|
||||||
maximum_review_interval: 36_500,
|
maximum_review_interval: 36_500,
|
||||||
minimum_review_interval: 1,
|
minimum_lapse_interval: 1,
|
||||||
graduating_interval_good: 1,
|
graduating_interval_good: 1,
|
||||||
graduating_interval_easy: 4,
|
graduating_interval_easy: 4,
|
||||||
new_card_order: NewCardOrder::Due as i32,
|
new_card_order: NewCardOrder::Due as i32,
|
||||||
|
|
|
@ -243,7 +243,7 @@ impl From<DeckConfSchema11> for DeckConf {
|
||||||
lapse_multiplier: c.lapse.mult,
|
lapse_multiplier: c.lapse.mult,
|
||||||
interval_multiplier: c.rev.ivl_fct,
|
interval_multiplier: c.rev.ivl_fct,
|
||||||
maximum_review_interval: c.rev.max_ivl,
|
maximum_review_interval: c.rev.max_ivl,
|
||||||
minimum_review_interval: c.lapse.min_int,
|
minimum_lapse_interval: c.lapse.min_int,
|
||||||
graduating_interval_good: c.new.ints.good as u32,
|
graduating_interval_good: c.new.ints.good as u32,
|
||||||
graduating_interval_easy: c.new.ints.easy as u32,
|
graduating_interval_easy: c.new.ints.easy as u32,
|
||||||
new_card_order: match c.new.order {
|
new_card_order: match c.new.order {
|
||||||
|
@ -327,7 +327,7 @@ impl From<DeckConf> for DeckConfSchema11 {
|
||||||
_ => LeechAction::Suspend,
|
_ => LeechAction::Suspend,
|
||||||
},
|
},
|
||||||
leech_fails: i.leech_threshold,
|
leech_fails: i.leech_threshold,
|
||||||
min_int: i.minimum_review_interval,
|
min_int: i.minimum_lapse_interval,
|
||||||
mult: i.lapse_multiplier,
|
mult: i.lapse_multiplier,
|
||||||
other: lapse_other,
|
other: lapse_other,
|
||||||
},
|
},
|
||||||
|
|
|
@ -37,11 +37,6 @@ pub struct Deck {
|
||||||
|
|
||||||
impl Deck {
|
impl Deck {
|
||||||
pub fn new_normal() -> Deck {
|
pub fn new_normal() -> Deck {
|
||||||
let mut norm = NormalDeck::default();
|
|
||||||
norm.config_id = 1;
|
|
||||||
// enable in the future
|
|
||||||
// norm.markdown_description = true;
|
|
||||||
|
|
||||||
Deck {
|
Deck {
|
||||||
id: DeckID(0),
|
id: DeckID(0),
|
||||||
name: "".into(),
|
name: "".into(),
|
||||||
|
@ -52,7 +47,12 @@ impl Deck {
|
||||||
browser_collapsed: true,
|
browser_collapsed: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
kind: DeckKind::Normal(norm),
|
kind: DeckKind::Normal(NormalDeck {
|
||||||
|
config_id: 1,
|
||||||
|
// enable in the future
|
||||||
|
// markdown_description = true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ use reqwest::StatusCode;
|
||||||
use std::{io, num::ParseIntError, str::Utf8Error};
|
use std::{io, num::ParseIntError, str::Utf8Error};
|
||||||
use tempfile::PathPersistError;
|
use tempfile::PathPersistError;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AnkiError>;
|
pub type Result<T, E = AnkiError> = std::result::Result<T, E>;
|
||||||
|
|
||||||
#[derive(Debug, Fail, PartialEq)]
|
#[derive(Debug, Fail, PartialEq)]
|
||||||
pub enum AnkiError {
|
pub enum AnkiError {
|
||||||
|
|
|
@ -75,6 +75,14 @@ impl Card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn original_or_current_deck_id(&self) -> DeckID {
|
||||||
|
if self.original_deck_id.0 > 0 {
|
||||||
|
self.original_deck_id
|
||||||
|
} else {
|
||||||
|
self.deck_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
|
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
|
||||||
if self.original_deck_id.0 == 0 {
|
if self.original_deck_id.0 == 0 {
|
||||||
// not in a filtered deck
|
// not in a filtered deck
|
||||||
|
|
|
@ -58,10 +58,6 @@ pub struct NoteType {
|
||||||
|
|
||||||
impl Default for NoteType {
|
impl Default for NoteType {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut conf = NoteTypeConfig::default();
|
|
||||||
conf.css = DEFAULT_CSS.into();
|
|
||||||
conf.latex_pre = DEFAULT_LATEX_HEADER.into();
|
|
||||||
conf.latex_post = DEFAULT_LATEX_FOOTER.into();
|
|
||||||
NoteType {
|
NoteType {
|
||||||
id: NoteTypeID(0),
|
id: NoteTypeID(0),
|
||||||
name: "".into(),
|
name: "".into(),
|
||||||
|
@ -69,7 +65,12 @@ impl Default for NoteType {
|
||||||
usn: Usn(0),
|
usn: Usn(0),
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
templates: vec![],
|
templates: vec![],
|
||||||
config: conf,
|
config: NoteTypeConfig {
|
||||||
|
css: DEFAULT_CSS.into(),
|
||||||
|
latex_pre: DEFAULT_LATEX_HEADER.into(),
|
||||||
|
latex_post: DEFAULT_LATEX_FOOTER.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,9 +76,10 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// no existing card; synthesize one
|
// no existing card; synthesize one
|
||||||
let mut card = Card::default();
|
Ok(Card {
|
||||||
card.template_idx = card_ord;
|
template_idx: card_ord,
|
||||||
Ok(card)
|
..Default::default()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_card_inner(
|
fn render_card_inner(
|
||||||
|
|
|
@ -43,8 +43,10 @@ fn fieldref<S: AsRef<str>>(name: S) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn basic(i18n: &I18n) -> NoteType {
|
pub(crate) fn basic(i18n: &I18n) -> NoteType {
|
||||||
let mut nt = NoteType::default();
|
let mut nt = NoteType {
|
||||||
nt.name = i18n.tr(TR::NotetypesBasicName).into();
|
name: i18n.tr(TR::NotetypesBasicName).into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let front = i18n.tr(TR::NotetypesFrontField);
|
let front = i18n.tr(TR::NotetypesFrontField);
|
||||||
let back = i18n.tr(TR::NotetypesBackField);
|
let back = i18n.tr(TR::NotetypesBackField);
|
||||||
nt.add_field(front.as_ref());
|
nt.add_field(front.as_ref());
|
||||||
|
|
|
@ -43,6 +43,7 @@ pub enum RevlogReviewKind {
|
||||||
Relearning = 2,
|
Relearning = 2,
|
||||||
EarlyReview = 3,
|
EarlyReview = 3,
|
||||||
Manual = 4,
|
Manual = 4,
|
||||||
|
// Preview = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RevlogReviewKind {
|
impl Default for RevlogReviewKind {
|
||||||
|
|
506
rslib/src/sched/answering.rs
Normal file
506
rslib/src/sched/answering.rs
Normal file
|
@ -0,0 +1,506 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend_proto,
|
||||||
|
card::{CardQueue, CardType},
|
||||||
|
deckconf::DeckConf,
|
||||||
|
decks::{Deck, DeckKind},
|
||||||
|
prelude::*,
|
||||||
|
revlog::{RevlogEntry, RevlogReviewKind},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
cutoff::SchedTimingToday,
|
||||||
|
states::{
|
||||||
|
steps::LearningSteps, CardState, FilteredState, IntervalKind, LearnState, NewState,
|
||||||
|
NextCardStates, NormalState, PreviewState, RelearnState, ReschedulingFilterState,
|
||||||
|
ReviewState, StateContext,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum Rating {
|
||||||
|
Again,
|
||||||
|
Hard,
|
||||||
|
Good,
|
||||||
|
Easy,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CardAnswer {
|
||||||
|
pub card_id: CardID,
|
||||||
|
pub current_state: CardState,
|
||||||
|
pub new_state: CardState,
|
||||||
|
pub rating: Rating,
|
||||||
|
pub answered_at: TimestampSecs,
|
||||||
|
pub milliseconds_taken: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: suspension
|
||||||
|
// fixme: fuzz learning intervals, graduating intervals
|
||||||
|
// fixme: 4 buttons for previewing
|
||||||
|
// fixme: log previewing
|
||||||
|
// fixme: - undo
|
||||||
|
|
||||||
|
/// Information needed when answering a card.
|
||||||
|
struct AnswerContext {
|
||||||
|
deck: Deck,
|
||||||
|
config: DeckConf,
|
||||||
|
timing: SchedTimingToday,
|
||||||
|
now: TimestampSecs,
|
||||||
|
fuzz_seed: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnswerContext {
|
||||||
|
fn state_context(&self) -> StateContext<'_> {
|
||||||
|
StateContext {
|
||||||
|
fuzz_seed: self.fuzz_seed,
|
||||||
|
steps: self.learn_steps(),
|
||||||
|
graduating_interval_good: self.config.inner.graduating_interval_good,
|
||||||
|
graduating_interval_easy: self.config.inner.graduating_interval_easy,
|
||||||
|
hard_multiplier: self.config.inner.hard_multiplier,
|
||||||
|
easy_multiplier: self.config.inner.easy_multiplier,
|
||||||
|
interval_multiplier: self.config.inner.interval_multiplier,
|
||||||
|
maximum_review_interval: self.config.inner.maximum_review_interval,
|
||||||
|
relearn_steps: self.relearn_steps(),
|
||||||
|
lapse_multiplier: self.config.inner.lapse_multiplier,
|
||||||
|
minimum_lapse_interval: self.config.inner.minimum_lapse_interval,
|
||||||
|
in_filtered_deck: self.deck.is_filtered(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn secs_until_rollover(&self) -> u32 {
|
||||||
|
(self.timing.next_day_at - self.now.0).max(0) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normal_study_state(
|
||||||
|
&self,
|
||||||
|
ctype: CardType,
|
||||||
|
due: i32,
|
||||||
|
interval: u32,
|
||||||
|
lapses: u32,
|
||||||
|
ease_factor: f32,
|
||||||
|
remaining_steps: u32,
|
||||||
|
) -> NormalState {
|
||||||
|
match ctype {
|
||||||
|
CardType::New => NormalState::New(NewState {
|
||||||
|
position: due.max(0) as u32,
|
||||||
|
}),
|
||||||
|
CardType::Learn => {
|
||||||
|
LearnState {
|
||||||
|
scheduled_secs: self.learn_steps().current_delay_secs(remaining_steps),
|
||||||
|
remaining_steps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
CardType::Review => ReviewState {
|
||||||
|
scheduled_days: interval,
|
||||||
|
elapsed_days: ((interval as i32) - (due - self.timing.days_elapsed as i32)).max(0)
|
||||||
|
as u32,
|
||||||
|
ease_factor,
|
||||||
|
lapses,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
CardType::Relearn => RelearnState {
|
||||||
|
learning: LearnState {
|
||||||
|
scheduled_secs: self.relearn_steps().current_delay_secs(remaining_steps),
|
||||||
|
remaining_steps,
|
||||||
|
},
|
||||||
|
review: ReviewState {
|
||||||
|
scheduled_days: interval,
|
||||||
|
elapsed_days: interval,
|
||||||
|
ease_factor,
|
||||||
|
lapses,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: context depends on deck conf, but card passed in later - needs rethink
|
||||||
|
fn current_card_state(&self, card: &Card) -> CardState {
|
||||||
|
let interval = card.interval;
|
||||||
|
let lapses = card.lapses;
|
||||||
|
let ease_factor = card.ease_factor();
|
||||||
|
let remaining_steps = card.remaining_steps();
|
||||||
|
let ctype = card.ctype;
|
||||||
|
|
||||||
|
let due = match &self.deck.kind {
|
||||||
|
DeckKind::Normal(_) => {
|
||||||
|
// if not in a filtered deck, ensure due time is not before today,
|
||||||
|
// which avoids tripping up test_nextIvl() in the Python tests
|
||||||
|
if matches!(ctype, CardType::Review) {
|
||||||
|
card.due.min(self.timing.days_elapsed as i32)
|
||||||
|
} else {
|
||||||
|
card.due
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeckKind::Filtered(_) => {
|
||||||
|
if card.original_due != 0 {
|
||||||
|
card.original_due
|
||||||
|
} else {
|
||||||
|
// v2 scheduler resets original_due on first answer
|
||||||
|
card.due
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let normal_state =
|
||||||
|
self.normal_study_state(ctype, due, interval, lapses, ease_factor, remaining_steps);
|
||||||
|
|
||||||
|
match &self.deck.kind {
|
||||||
|
// normal decks have normal state
|
||||||
|
DeckKind::Normal(_) => normal_state.into(),
|
||||||
|
// filtered decks wrap the normal state
|
||||||
|
DeckKind::Filtered(filtered) => {
|
||||||
|
if filtered.reschedule {
|
||||||
|
ReschedulingFilterState {
|
||||||
|
original_state: normal_state,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
PreviewState {
|
||||||
|
scheduled_secs: filtered.preview_delay * 60,
|
||||||
|
original_state: normal_state,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn learn_steps(&self) -> LearningSteps<'_> {
|
||||||
|
LearningSteps::new(&self.config.inner.learn_steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relearn_steps(&self) -> LearningSteps<'_> {
|
||||||
|
LearningSteps::new(&self.config.inner.relearn_steps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Card {
|
||||||
|
fn apply_study_state(
|
||||||
|
&mut self,
|
||||||
|
current: CardState,
|
||||||
|
next: CardState,
|
||||||
|
ctx: &AnswerContext,
|
||||||
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
|
// any non-preview answer resets card.odue and increases reps
|
||||||
|
if !matches!(current, CardState::Filtered(FilteredState::Preview(_))) {
|
||||||
|
self.reps += 1;
|
||||||
|
self.original_due = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
match next {
|
||||||
|
CardState::Normal(normal) => match normal {
|
||||||
|
NormalState::New(next) => self.apply_new_state(current, next, ctx),
|
||||||
|
NormalState::Learning(next) => self.apply_learning_state(current, next, ctx),
|
||||||
|
NormalState::Review(next) => self.apply_review_state(current, next, ctx),
|
||||||
|
NormalState::Relearning(next) => self.apply_relearning_state(current, next, ctx),
|
||||||
|
},
|
||||||
|
CardState::Filtered(filtered) => match filtered {
|
||||||
|
FilteredState::Preview(next) => self.apply_preview_state(current, next, ctx),
|
||||||
|
FilteredState::Rescheduling(next) => {
|
||||||
|
self.apply_rescheduling_state(current, next, ctx)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_new_state(
|
||||||
|
&mut self,
|
||||||
|
current: CardState,
|
||||||
|
next: NewState,
|
||||||
|
ctx: &AnswerContext,
|
||||||
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
|
self.ctype = CardType::New;
|
||||||
|
self.queue = CardQueue::New;
|
||||||
|
self.due = next.position as i32;
|
||||||
|
|
||||||
|
Ok(RevlogEntryPartial::maybe_new(
|
||||||
|
current,
|
||||||
|
next.into(),
|
||||||
|
0.0,
|
||||||
|
ctx.secs_until_rollover(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_learning_state(
|
||||||
|
&mut self,
|
||||||
|
current: CardState,
|
||||||
|
next: LearnState,
|
||||||
|
ctx: &AnswerContext,
|
||||||
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
|
self.remaining_steps = next.remaining_steps;
|
||||||
|
self.ctype = CardType::Learn;
|
||||||
|
|
||||||
|
let interval = next
|
||||||
|
.interval_kind()
|
||||||
|
.maybe_as_days(ctx.secs_until_rollover());
|
||||||
|
match interval {
|
||||||
|
IntervalKind::InSecs(secs) => {
|
||||||
|
self.queue = CardQueue::Learn;
|
||||||
|
self.due = TimestampSecs::now().0 as i32 + secs as i32;
|
||||||
|
}
|
||||||
|
IntervalKind::InDays(days) => {
|
||||||
|
self.queue = CardQueue::DayLearn;
|
||||||
|
self.due = (ctx.timing.days_elapsed + days) as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RevlogEntryPartial::maybe_new(
|
||||||
|
current,
|
||||||
|
next.into(),
|
||||||
|
0.0,
|
||||||
|
ctx.secs_until_rollover(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_review_state(
|
||||||
|
&mut self,
|
||||||
|
current: CardState,
|
||||||
|
next: ReviewState,
|
||||||
|
ctx: &AnswerContext,
|
||||||
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
|
self.remove_from_filtered_deck_before_reschedule();
|
||||||
|
|
||||||
|
self.queue = CardQueue::Review;
|
||||||
|
self.ctype = CardType::Review;
|
||||||
|
self.interval = next.scheduled_days;
|
||||||
|
self.due = (ctx.timing.days_elapsed + next.scheduled_days) as i32;
|
||||||
|
self.ease_factor = (next.ease_factor * 1000.0).round() as u16;
|
||||||
|
self.lapses = next.lapses;
|
||||||
|
|
||||||
|
Ok(RevlogEntryPartial::maybe_new(
|
||||||
|
current,
|
||||||
|
next.into(),
|
||||||
|
next.ease_factor,
|
||||||
|
ctx.secs_until_rollover(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_relearning_state(
|
||||||
|
&mut self,
|
||||||
|
current: CardState,
|
||||||
|
next: RelearnState,
|
||||||
|
ctx: &AnswerContext,
|
||||||
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
|
self.interval = next.review.scheduled_days;
|
||||||
|
self.remaining_steps = next.learning.remaining_steps;
|
||||||
|
self.ctype = CardType::Relearn;
|
||||||
|
|
||||||
|
let interval = next
|
||||||
|
.interval_kind()
|
||||||
|
.maybe_as_days(ctx.secs_until_rollover());
|
||||||
|
match interval {
|
||||||
|
IntervalKind::InSecs(secs) => {
|
||||||
|
self.queue = CardQueue::Learn;
|
||||||
|
self.due = TimestampSecs::now().0 as i32 + secs as i32;
|
||||||
|
}
|
||||||
|
IntervalKind::InDays(days) => {
|
||||||
|
self.queue = CardQueue::DayLearn;
|
||||||
|
self.due = (ctx.timing.days_elapsed + days) as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RevlogEntryPartial::maybe_new(
|
||||||
|
current,
|
||||||
|
next.into(),
|
||||||
|
next.review.ease_factor,
|
||||||
|
ctx.secs_until_rollover(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixme: check learning card moved into preview
|
||||||
|
// restores correctly in both learn and day-learn case
|
||||||
|
fn apply_preview_state(
|
||||||
|
&mut self,
|
||||||
|
current: CardState,
|
||||||
|
next: PreviewState,
|
||||||
|
ctx: &AnswerContext,
|
||||||
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
|
self.ensure_filtered()?;
|
||||||
|
self.queue = CardQueue::PreviewRepeat;
|
||||||
|
|
||||||
|
let interval = next.interval_kind();
|
||||||
|
match interval {
|
||||||
|
IntervalKind::InSecs(secs) => {
|
||||||
|
self.due = TimestampSecs::now().0 as i32 + secs as i32;
|
||||||
|
}
|
||||||
|
IntervalKind::InDays(_days) => {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RevlogEntryPartial::maybe_new(
|
||||||
|
current,
|
||||||
|
next.into(),
|
||||||
|
0.0,
|
||||||
|
ctx.secs_until_rollover(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixme: better name
|
||||||
|
fn apply_rescheduling_state(
|
||||||
|
&mut self,
|
||||||
|
current: CardState,
|
||||||
|
next: ReschedulingFilterState,
|
||||||
|
ctx: &AnswerContext,
|
||||||
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
|
self.ensure_filtered()?;
|
||||||
|
self.apply_study_state(current, next.original_state.into(), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_filtered(&self) -> Result<()> {
|
||||||
|
if self.original_deck_id.0 == 0 {
|
||||||
|
Err(AnkiError::invalid_input(
|
||||||
|
"card answering can't transition into filtered state",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rating {
|
||||||
|
fn as_number(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Rating::Again => 1,
|
||||||
|
Rating::Hard => 2,
|
||||||
|
Rating::Good => 3,
|
||||||
|
Rating::Easy => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct RevlogEntryPartial {
|
||||||
|
interval: IntervalKind,
|
||||||
|
last_interval: IntervalKind,
|
||||||
|
ease_factor: f32,
|
||||||
|
review_kind: RevlogReviewKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RevlogEntryPartial {
|
||||||
|
fn maybe_new(
|
||||||
|
current: CardState,
|
||||||
|
next: CardState,
|
||||||
|
ease_factor: f32,
|
||||||
|
secs_until_rollover: u32,
|
||||||
|
) -> Option<Self> {
|
||||||
|
current.revlog_kind().map(|review_kind| {
|
||||||
|
let next_interval = next.interval_kind().maybe_as_days(secs_until_rollover);
|
||||||
|
let current_interval = current.interval_kind().maybe_as_days(secs_until_rollover);
|
||||||
|
|
||||||
|
RevlogEntryPartial {
|
||||||
|
interval: next_interval,
|
||||||
|
last_interval: current_interval,
|
||||||
|
ease_factor,
|
||||||
|
review_kind,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_revlog_entry(
|
||||||
|
self,
|
||||||
|
usn: Usn,
|
||||||
|
cid: CardID,
|
||||||
|
button_chosen: u8,
|
||||||
|
taken_millis: u32,
|
||||||
|
) -> RevlogEntry {
|
||||||
|
RevlogEntry {
|
||||||
|
id: TimestampMillis::now(),
|
||||||
|
cid,
|
||||||
|
usn,
|
||||||
|
button_chosen,
|
||||||
|
interval: self.interval.as_revlog_interval(),
|
||||||
|
last_interval: self.last_interval.as_revlog_interval(),
|
||||||
|
ease_factor: (self.ease_factor * 1000.0).round() as u32,
|
||||||
|
taken_millis,
|
||||||
|
review_kind: self.review_kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub fn answer_card(&mut self, answer: &CardAnswer) -> Result<()> {
|
||||||
|
self.transact(None, |col| col.answer_card_inner(answer))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_card_inner(&mut self, answer: &CardAnswer) -> Result<()> {
|
||||||
|
let mut card = self
|
||||||
|
.storage
|
||||||
|
.get_card(answer.card_id)?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
let answer_context = self.answer_context(&card)?;
|
||||||
|
let current_state = answer_context.current_card_state(&card);
|
||||||
|
if current_state != answer.current_state {
|
||||||
|
// fixme: unique error
|
||||||
|
return Err(AnkiError::invalid_input(format!(
|
||||||
|
"card was modified: {:#?} {:#?}",
|
||||||
|
current_state, answer.current_state,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let original = card.clone();
|
||||||
|
let usn = self.usn()?;
|
||||||
|
|
||||||
|
if let Some(revlog_partial) =
|
||||||
|
card.apply_study_state(current_state, answer.new_state, &answer_context)?
|
||||||
|
{
|
||||||
|
let button_chosen = answer.rating.as_number();
|
||||||
|
let revlog = revlog_partial.into_revlog_entry(
|
||||||
|
usn,
|
||||||
|
answer.card_id,
|
||||||
|
button_chosen,
|
||||||
|
answer.milliseconds_taken,
|
||||||
|
);
|
||||||
|
self.storage.add_revlog_entry(&revlog)?;
|
||||||
|
}
|
||||||
|
self.update_card(&mut card, &original, usn)?;
|
||||||
|
|
||||||
|
// fixme: we're reusing code used by python, which means re-feteching the target deck
|
||||||
|
// - might want to avoid that in the future
|
||||||
|
self.update_deck_stats(
|
||||||
|
answer_context.timing.days_elapsed,
|
||||||
|
usn,
|
||||||
|
backend_proto::UpdateStatsIn {
|
||||||
|
deck_id: answer_context.deck.id.0,
|
||||||
|
new_delta: if matches!(current_state, CardState::Normal(NormalState::New(_))) {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
review_delta: if matches!(current_state, CardState::Normal(NormalState::Review(_)))
|
||||||
|
{
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
millisecond_delta: answer.milliseconds_taken as i32,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_context(&mut self, card: &Card) -> Result<AnswerContext> {
|
||||||
|
let timing = self.timing_today()?;
|
||||||
|
Ok(AnswerContext {
|
||||||
|
// fixme: fetching deck twice
|
||||||
|
deck: self
|
||||||
|
.storage
|
||||||
|
.get_deck(card.deck_id)?
|
||||||
|
.ok_or(AnkiError::NotFound)?,
|
||||||
|
config: self.deck_config_for_card(card)?,
|
||||||
|
timing,
|
||||||
|
now: TimestampSecs::now(),
|
||||||
|
fuzz_seed: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_next_card_states(&mut self, cid: CardID) -> Result<NextCardStates> {
|
||||||
|
let card = self.storage.get_card(cid)?.ok_or(AnkiError::NotFound)?;
|
||||||
|
let ctx = self.answer_context(&card)?;
|
||||||
|
let current = ctx.current_card_state(&card);
|
||||||
|
let state_ctx = ctx.state_context();
|
||||||
|
Ok(current.next_states(&state_ctx))
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,27 +32,4 @@ impl Card {
|
||||||
self.ease_factor = INITIAL_EASE_FACTOR_THOUSANDS;
|
self.ease_factor = INITIAL_EASE_FACTOR_THOUSANDS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn all_remaining_steps(&self) -> u32 {
|
|
||||||
self.remaining_steps % 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn remaining_steps_today(&self) -> u32 {
|
|
||||||
self.remaining_steps / 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn current_learning_delay_seconds(&self, delays: &[u32]) -> Option<u32> {
|
|
||||||
if self.queue == CardQueue::Learn {
|
|
||||||
let remaining = self.all_remaining_steps();
|
|
||||||
delays
|
|
||||||
.iter()
|
|
||||||
.nth_back(remaining.saturating_sub(0) as usize)
|
|
||||||
.or(Some(&0))
|
|
||||||
.map(|n| n * 60)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,14 @@
|
||||||
|
|
||||||
use crate::{collection::Collection, config::SchedulerVersion, err::Result, prelude::*};
|
use crate::{collection::Collection, config::SchedulerVersion, err::Result, prelude::*};
|
||||||
|
|
||||||
|
pub mod answering;
|
||||||
pub mod bury_and_suspend;
|
pub mod bury_and_suspend;
|
||||||
pub(crate) mod congrats;
|
pub(crate) mod congrats;
|
||||||
pub mod cutoff;
|
pub mod cutoff;
|
||||||
mod learning;
|
mod learning;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
mod reviews;
|
mod reviews;
|
||||||
|
pub mod states;
|
||||||
pub mod timespan;
|
pub mod timespan;
|
||||||
|
|
||||||
use chrono::FixedOffset;
|
use chrono::FixedOffset;
|
||||||
|
|
35
rslib/src/sched/states/filtered.rs
Normal file
35
rslib/src/sched/states/filtered.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::revlog::RevlogReviewKind;
|
||||||
|
|
||||||
|
use super::{IntervalKind, NextCardStates, PreviewState, ReschedulingFilterState, StateContext};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum FilteredState {
|
||||||
|
Preview(PreviewState),
|
||||||
|
Rescheduling(ReschedulingFilterState),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilteredState {
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
match self {
|
||||||
|
FilteredState::Preview(state) => state.interval_kind(),
|
||||||
|
FilteredState::Rescheduling(state) => state.interval_kind(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn revlog_kind(self) -> Option<RevlogReviewKind> {
|
||||||
|
match self {
|
||||||
|
FilteredState::Preview(_state) => None,
|
||||||
|
FilteredState::Rescheduling(state) => Some(state.revlog_kind()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates {
|
||||||
|
match self {
|
||||||
|
FilteredState::Preview(state) => state.next_states(),
|
||||||
|
FilteredState::Rescheduling(state) => state.next_states(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
rslib/src/sched/states/interval_kind.rs
Normal file
38
rslib/src/sched/states/interval_kind.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub(crate) enum IntervalKind {
|
||||||
|
InSecs(u32),
|
||||||
|
InDays(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntervalKind {
|
||||||
|
/// Convert seconds-based intervals that pass the day barrier into days.
|
||||||
|
pub(crate) fn maybe_as_days(self, secs_until_rollover: u32) -> Self {
|
||||||
|
match self {
|
||||||
|
IntervalKind::InSecs(secs) => {
|
||||||
|
if secs >= secs_until_rollover {
|
||||||
|
IntervalKind::InDays(((secs - secs_until_rollover) / 86_400) + 1)
|
||||||
|
} else {
|
||||||
|
IntervalKind::InSecs(secs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_seconds(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
IntervalKind::InSecs(secs) => secs,
|
||||||
|
IntervalKind::InDays(days) => days * 86_400,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_revlog_interval(self) -> i32 {
|
||||||
|
match self {
|
||||||
|
IntervalKind::InDays(days) => days as i32,
|
||||||
|
IntervalKind::InSecs(secs) => -(secs as i32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
rslib/src/sched/states/learning.rs
Normal file
78
rslib/src/sched/states/learning.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::revlog::RevlogReviewKind;
|
||||||
|
|
||||||
|
use super::{interval_kind::IntervalKind, CardState, NextCardStates, ReviewState, StateContext};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct LearnState {
|
||||||
|
pub remaining_steps: u32,
|
||||||
|
pub scheduled_secs: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LearnState {
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
IntervalKind::InSecs(self.scheduled_secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn revlog_kind(self) -> RevlogReviewKind {
|
||||||
|
RevlogReviewKind::Learning
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates {
|
||||||
|
NextCardStates {
|
||||||
|
current: self.into(),
|
||||||
|
again: self.answer_again(ctx).into(),
|
||||||
|
hard: self.answer_hard(ctx),
|
||||||
|
good: self.answer_good(ctx),
|
||||||
|
easy: self.answer_easy(ctx).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_again(self, ctx: &StateContext) -> LearnState {
|
||||||
|
LearnState {
|
||||||
|
remaining_steps: ctx.steps.remaining_for_failed(),
|
||||||
|
scheduled_secs: ctx.steps.again_delay_secs_learn(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_hard(self, ctx: &StateContext) -> CardState {
|
||||||
|
if let Some(hard_delay) = ctx.steps.hard_delay_secs(self.remaining_steps) {
|
||||||
|
LearnState {
|
||||||
|
scheduled_secs: hard_delay,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days: ctx.graduating_interval_good,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_good(self, ctx: &StateContext) -> CardState {
|
||||||
|
if let Some(good_delay) = ctx.steps.good_delay_secs(self.remaining_steps) {
|
||||||
|
LearnState {
|
||||||
|
remaining_steps: ctx.steps.remaining_for_good(self.remaining_steps),
|
||||||
|
scheduled_secs: good_delay,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days: ctx.graduating_interval_good,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_easy(self, ctx: &StateContext) -> ReviewState {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days: ctx.graduating_interval_easy,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
129
rslib/src/sched/states/mod.rs
Normal file
129
rslib/src/sched/states/mod.rs
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
pub(crate) mod filtered;
|
||||||
|
pub(crate) mod interval_kind;
|
||||||
|
pub(crate) mod learning;
|
||||||
|
pub(crate) mod new;
|
||||||
|
pub(crate) mod normal;
|
||||||
|
pub(crate) mod preview_filter;
|
||||||
|
pub(crate) mod relearning;
|
||||||
|
pub(crate) mod rescheduling_filter;
|
||||||
|
pub(crate) mod review;
|
||||||
|
pub(crate) mod steps;
|
||||||
|
|
||||||
|
pub use {
|
||||||
|
filtered::FilteredState, learning::LearnState, new::NewState, normal::NormalState,
|
||||||
|
preview_filter::PreviewState, relearning::RelearnState,
|
||||||
|
rescheduling_filter::ReschedulingFilterState, review::ReviewState,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) use interval_kind::IntervalKind;
|
||||||
|
|
||||||
|
use crate::revlog::RevlogReviewKind;
|
||||||
|
|
||||||
|
use self::steps::LearningSteps;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum CardState {
|
||||||
|
Normal(NormalState),
|
||||||
|
Filtered(FilteredState),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CardState {
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
match self {
|
||||||
|
CardState::Normal(normal) => normal.interval_kind(),
|
||||||
|
CardState::Filtered(filtered) => filtered.interval_kind(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn revlog_kind(self) -> Option<RevlogReviewKind> {
|
||||||
|
match self {
|
||||||
|
CardState::Normal(normal) => Some(normal.revlog_kind()),
|
||||||
|
CardState::Filtered(filtered) => filtered.revlog_kind(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates {
|
||||||
|
match self {
|
||||||
|
CardState::Normal(state) => state.next_states(&ctx),
|
||||||
|
CardState::Filtered(state) => state.next_states(&ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info required during state transitions.
|
||||||
|
pub(crate) struct StateContext<'a> {
|
||||||
|
pub fuzz_seed: Option<u64>,
|
||||||
|
|
||||||
|
// learning
|
||||||
|
pub steps: LearningSteps<'a>,
|
||||||
|
pub graduating_interval_good: u32,
|
||||||
|
pub graduating_interval_easy: u32,
|
||||||
|
|
||||||
|
// reviewing
|
||||||
|
pub hard_multiplier: f32,
|
||||||
|
pub easy_multiplier: f32,
|
||||||
|
pub interval_multiplier: f32,
|
||||||
|
pub maximum_review_interval: u32,
|
||||||
|
|
||||||
|
// relearning
|
||||||
|
pub relearn_steps: LearningSteps<'a>,
|
||||||
|
pub lapse_multiplier: f32,
|
||||||
|
pub minimum_lapse_interval: u32,
|
||||||
|
|
||||||
|
// filtered
|
||||||
|
pub in_filtered_deck: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NextCardStates {
|
||||||
|
pub current: CardState,
|
||||||
|
pub again: CardState,
|
||||||
|
pub hard: CardState,
|
||||||
|
pub good: CardState,
|
||||||
|
pub easy: CardState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NewState> for CardState {
|
||||||
|
fn from(state: NewState) -> Self {
|
||||||
|
CardState::Normal(state.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReviewState> for CardState {
|
||||||
|
fn from(state: ReviewState) -> Self {
|
||||||
|
CardState::Normal(state.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LearnState> for CardState {
|
||||||
|
fn from(state: LearnState) -> Self {
|
||||||
|
CardState::Normal(state.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RelearnState> for CardState {
|
||||||
|
fn from(state: RelearnState) -> Self {
|
||||||
|
CardState::Normal(state.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NormalState> for CardState {
|
||||||
|
fn from(state: NormalState) -> Self {
|
||||||
|
CardState::Normal(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PreviewState> for CardState {
|
||||||
|
fn from(state: PreviewState) -> Self {
|
||||||
|
CardState::Filtered(FilteredState::Preview(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReschedulingFilterState> for CardState {
|
||||||
|
fn from(state: ReschedulingFilterState) -> Self {
|
||||||
|
CardState::Filtered(FilteredState::Rescheduling(state))
|
||||||
|
}
|
||||||
|
}
|
23
rslib/src/sched/states/new.rs
Normal file
23
rslib/src/sched/states/new.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::revlog::RevlogReviewKind;
|
||||||
|
|
||||||
|
use super::interval_kind::IntervalKind;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct NewState {
|
||||||
|
pub position: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewState {
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
// todo: consider packing the due number in here; it would allow us to restore the
|
||||||
|
// original position of cards - though not as cheaply as if it were a card property.
|
||||||
|
IntervalKind::InSecs(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn revlog_kind(self) -> RevlogReviewKind {
|
||||||
|
RevlogReviewKind::Learning
|
||||||
|
}
|
||||||
|
}
|
82
rslib/src/sched/states/normal.rs
Normal file
82
rslib/src/sched/states/normal.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::revlog::RevlogReviewKind;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
interval_kind::IntervalKind, LearnState, NewState, NextCardStates, RelearnState, ReviewState,
|
||||||
|
StateContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum NormalState {
|
||||||
|
New(NewState),
|
||||||
|
Learning(LearnState),
|
||||||
|
Review(ReviewState),
|
||||||
|
Relearning(RelearnState),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NormalState {
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
match self {
|
||||||
|
NormalState::New(state) => state.interval_kind(),
|
||||||
|
NormalState::Learning(state) => state.interval_kind(),
|
||||||
|
NormalState::Review(state) => state.interval_kind(),
|
||||||
|
NormalState::Relearning(state) => state.interval_kind(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn revlog_kind(self) -> RevlogReviewKind {
|
||||||
|
match self {
|
||||||
|
NormalState::New(state) => state.revlog_kind(),
|
||||||
|
NormalState::Learning(state) => state.revlog_kind(),
|
||||||
|
NormalState::Review(state) => state.revlog_kind(),
|
||||||
|
NormalState::Relearning(state) => state.revlog_kind(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates {
|
||||||
|
match self {
|
||||||
|
NormalState::New(_) => {
|
||||||
|
// New state acts like answering a failed learning card
|
||||||
|
let next_states = LearnState {
|
||||||
|
remaining_steps: ctx.steps.remaining_for_failed(),
|
||||||
|
scheduled_secs: 0,
|
||||||
|
}
|
||||||
|
.next_states(ctx);
|
||||||
|
// .. but with current as New, not Learning
|
||||||
|
NextCardStates {
|
||||||
|
current: self.into(),
|
||||||
|
..next_states
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NormalState::Learning(state) => state.next_states(ctx),
|
||||||
|
NormalState::Review(state) => state.next_states(ctx),
|
||||||
|
NormalState::Relearning(state) => state.next_states(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NewState> for NormalState {
|
||||||
|
fn from(state: NewState) -> Self {
|
||||||
|
NormalState::New(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReviewState> for NormalState {
|
||||||
|
fn from(state: ReviewState) -> Self {
|
||||||
|
NormalState::Review(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LearnState> for NormalState {
|
||||||
|
fn from(state: LearnState) -> Self {
|
||||||
|
NormalState::Learning(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RelearnState> for NormalState {
|
||||||
|
fn from(state: RelearnState) -> Self {
|
||||||
|
NormalState::Relearning(state)
|
||||||
|
}
|
||||||
|
}
|
26
rslib/src/sched/states/preview_filter.rs
Normal file
26
rslib/src/sched/states/preview_filter.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use super::{IntervalKind, NextCardStates, NormalState};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct PreviewState {
|
||||||
|
pub scheduled_secs: u32,
|
||||||
|
pub original_state: NormalState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreviewState {
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
IntervalKind::InSecs(self.scheduled_secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_states(self) -> NextCardStates {
|
||||||
|
NextCardStates {
|
||||||
|
current: self.into(),
|
||||||
|
again: self.into(),
|
||||||
|
hard: self.original_state.into(),
|
||||||
|
good: self.original_state.into(),
|
||||||
|
easy: self.original_state.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
rslib/src/sched/states/relearning.rs
Normal file
105
rslib/src/sched/states/relearning.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::revlog::RevlogReviewKind;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
interval_kind::IntervalKind, CardState, LearnState, NextCardStates, ReviewState, StateContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct RelearnState {
|
||||||
|
pub learning: LearnState,
|
||||||
|
pub review: ReviewState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelearnState {
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
self.learning.interval_kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn revlog_kind(self) -> RevlogReviewKind {
|
||||||
|
RevlogReviewKind::Relearning
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates {
|
||||||
|
NextCardStates {
|
||||||
|
current: self.into(),
|
||||||
|
again: self.answer_again(ctx),
|
||||||
|
hard: self.answer_hard(ctx),
|
||||||
|
good: self.answer_good(ctx),
|
||||||
|
easy: self.answer_easy().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_again(self, ctx: &StateContext) -> CardState {
|
||||||
|
if let Some(learn_interval) = ctx.relearn_steps.again_delay_secs_relearn() {
|
||||||
|
RelearnState {
|
||||||
|
learning: LearnState {
|
||||||
|
remaining_steps: ctx.relearn_steps.remaining_for_failed(),
|
||||||
|
scheduled_secs: learn_interval,
|
||||||
|
},
|
||||||
|
review: ReviewState {
|
||||||
|
scheduled_days: self.review.failing_review_interval(ctx),
|
||||||
|
elapsed_days: 0,
|
||||||
|
..self.review
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
self.review.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_hard(self, ctx: &StateContext) -> CardState {
|
||||||
|
if let Some(learn_interval) = ctx
|
||||||
|
.relearn_steps
|
||||||
|
.hard_delay_secs(self.learning.remaining_steps)
|
||||||
|
{
|
||||||
|
RelearnState {
|
||||||
|
learning: LearnState {
|
||||||
|
scheduled_secs: learn_interval,
|
||||||
|
..self.learning
|
||||||
|
},
|
||||||
|
review: ReviewState {
|
||||||
|
elapsed_days: 0,
|
||||||
|
..self.review
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
self.review.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_good(self, ctx: &StateContext) -> CardState {
|
||||||
|
if let Some(learn_interval) = ctx
|
||||||
|
.relearn_steps
|
||||||
|
.good_delay_secs(self.learning.remaining_steps)
|
||||||
|
{
|
||||||
|
RelearnState {
|
||||||
|
learning: LearnState {
|
||||||
|
scheduled_secs: learn_interval,
|
||||||
|
remaining_steps: ctx
|
||||||
|
.relearn_steps
|
||||||
|
.remaining_for_good(self.learning.remaining_steps),
|
||||||
|
},
|
||||||
|
review: ReviewState {
|
||||||
|
elapsed_days: 0,
|
||||||
|
..self.review
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
self.review.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_easy(self) -> ReviewState {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days: self.review.scheduled_days + 1,
|
||||||
|
elapsed_days: 0,
|
||||||
|
..self.review
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
rslib/src/sched/states/rescheduling_filter.rs
Normal file
60
rslib/src/sched/states/rescheduling_filter.rs
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
|
||||||
|
|
||||||
|
use crate::revlog::RevlogReviewKind;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
interval_kind::IntervalKind, normal::NormalState, CardState, NextCardStates, StateContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct ReschedulingFilterState {
|
||||||
|
pub original_state: NormalState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReschedulingFilterState {
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
self.original_state.interval_kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn revlog_kind(self) -> RevlogReviewKind {
|
||||||
|
self.original_state.revlog_kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates {
|
||||||
|
let normal = self.original_state.next_states(&ctx);
|
||||||
|
if ctx.in_filtered_deck {
|
||||||
|
NextCardStates {
|
||||||
|
current: self.into(),
|
||||||
|
again: maybe_wrap(normal.again),
|
||||||
|
hard: maybe_wrap(normal.hard),
|
||||||
|
good: maybe_wrap(normal.good),
|
||||||
|
easy: maybe_wrap(normal.easy),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// card is marked as filtered, but not in a filtered deck; convert to normal
|
||||||
|
normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The review state is returned unchanged because cards are returned to
|
||||||
|
/// their original deck in that state; other normal states are wrapped
|
||||||
|
/// in the filtered state. Providing a filtered state is an error.
|
||||||
|
fn maybe_wrap(state: CardState) -> CardState {
|
||||||
|
match state {
|
||||||
|
CardState::Normal(normal) => {
|
||||||
|
if matches!(normal, NormalState::Review(_)) {
|
||||||
|
normal.into()
|
||||||
|
} else {
|
||||||
|
ReschedulingFilterState {
|
||||||
|
original_state: normal,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CardState::Filtered(_) => {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
234
rslib/src/sched/states/review.rs
Normal file
234
rslib/src/sched/states/review.rs
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use rand::prelude::*;
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
|
||||||
|
use crate::revlog::RevlogReviewKind;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
interval_kind::IntervalKind, CardState, LearnState, NextCardStates, RelearnState, StateContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const INITIAL_EASE_FACTOR: f32 = 2.5;
|
||||||
|
pub const MINIMUM_EASE_FACTOR: f32 = 1.3;
|
||||||
|
pub const EASE_FACTOR_AGAIN_DELTA: f32 = -0.2;
|
||||||
|
pub const EASE_FACTOR_HARD_DELTA: f32 = -0.15;
|
||||||
|
pub const EASE_FACTOR_EASY_DELTA: f32 = 0.15;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct ReviewState {
|
||||||
|
pub scheduled_days: u32,
|
||||||
|
pub elapsed_days: u32,
|
||||||
|
pub ease_factor: f32,
|
||||||
|
pub lapses: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ReviewState {
|
||||||
|
fn default() -> Self {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days: 0,
|
||||||
|
elapsed_days: 0,
|
||||||
|
ease_factor: INITIAL_EASE_FACTOR,
|
||||||
|
lapses: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReviewState {
|
||||||
|
pub(crate) fn days_late(&self) -> i32 {
|
||||||
|
self.elapsed_days as i32 - self.scheduled_days as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn interval_kind(self) -> IntervalKind {
|
||||||
|
// fixme: maybe use elapsed days in the future? would only
|
||||||
|
// make sense for revlog's lastIvl, not for future interval
|
||||||
|
IntervalKind::InDays(self.scheduled_days)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn revlog_kind(self) -> RevlogReviewKind {
|
||||||
|
if self.days_late() < 0 {
|
||||||
|
RevlogReviewKind::EarlyReview
|
||||||
|
} else {
|
||||||
|
RevlogReviewKind::Review
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates {
|
||||||
|
let (hard_interval, good_interval, easy_interval) = self.passing_review_intervals(ctx);
|
||||||
|
|
||||||
|
NextCardStates {
|
||||||
|
current: self.into(),
|
||||||
|
again: self.answer_again(ctx),
|
||||||
|
hard: self.answer_hard(hard_interval).into(),
|
||||||
|
good: self.answer_good(good_interval).into(),
|
||||||
|
easy: self.answer_easy(easy_interval).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn failing_review_interval(self, ctx: &StateContext) -> u32 {
|
||||||
|
// fixme: floor() is for python
|
||||||
|
(((self.scheduled_days as f32) * ctx.lapse_multiplier).floor() as u32)
|
||||||
|
.max(ctx.minimum_lapse_interval)
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_again(self, ctx: &StateContext) -> CardState {
|
||||||
|
let again_review = ReviewState {
|
||||||
|
scheduled_days: self.failing_review_interval(ctx),
|
||||||
|
elapsed_days: 0,
|
||||||
|
ease_factor: (self.ease_factor + EASE_FACTOR_AGAIN_DELTA).max(MINIMUM_EASE_FACTOR),
|
||||||
|
lapses: self.lapses + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(learn_interval) = ctx.relearn_steps.again_delay_secs_relearn() {
|
||||||
|
RelearnState {
|
||||||
|
learning: LearnState {
|
||||||
|
remaining_steps: ctx.relearn_steps.remaining_for_failed(),
|
||||||
|
scheduled_secs: learn_interval,
|
||||||
|
},
|
||||||
|
review: again_review,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
again_review.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_hard(self, scheduled_days: u32) -> ReviewState {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days,
|
||||||
|
elapsed_days: 0,
|
||||||
|
ease_factor: (self.ease_factor + EASE_FACTOR_HARD_DELTA).max(MINIMUM_EASE_FACTOR),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_good(self, scheduled_days: u32) -> ReviewState {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days,
|
||||||
|
elapsed_days: 0,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn answer_easy(self, scheduled_days: u32) -> ReviewState {
|
||||||
|
ReviewState {
|
||||||
|
scheduled_days,
|
||||||
|
elapsed_days: 0,
|
||||||
|
ease_factor: self.ease_factor + EASE_FACTOR_EASY_DELTA,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the intervals for hard, good and easy, each of which depends on the previous.
|
||||||
|
fn passing_review_intervals(self, ctx: &StateContext) -> (u32, u32, u32) {
|
||||||
|
if self.days_late() < 0 {
|
||||||
|
self.passing_early_review_intervals(ctx)
|
||||||
|
} else {
|
||||||
|
self.passing_nonearly_review_intervals(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn passing_nonearly_review_intervals(self, ctx: &StateContext) -> (u32, u32, u32) {
|
||||||
|
let current_interval = self.scheduled_days as f32;
|
||||||
|
let days_late = self.days_late().max(0) as f32;
|
||||||
|
let hard_factor = ctx.hard_multiplier;
|
||||||
|
let hard_minimum = if hard_factor <= 1.0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.scheduled_days + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// fixme: floor() is to match python
|
||||||
|
|
||||||
|
let hard_interval =
|
||||||
|
constrain_passing_interval(ctx, current_interval * hard_factor, hard_minimum);
|
||||||
|
let good_interval = constrain_passing_interval(
|
||||||
|
ctx,
|
||||||
|
(current_interval + (days_late / 2.0).floor()) * self.ease_factor,
|
||||||
|
hard_interval + 1,
|
||||||
|
);
|
||||||
|
let easy_interval = constrain_passing_interval(
|
||||||
|
ctx,
|
||||||
|
(current_interval + days_late) * self.ease_factor * ctx.easy_multiplier,
|
||||||
|
good_interval + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
(hard_interval, good_interval, easy_interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mostly direct port from the Python version for now, so we can confirm implementation
|
||||||
|
/// is correct.
|
||||||
|
/// FIXME: this needs reworking in the future; it overly penalizes reviews done
|
||||||
|
/// shortly before the due date.
|
||||||
|
fn passing_early_review_intervals(self, ctx: &StateContext) -> (u32, u32, u32) {
|
||||||
|
let scheduled = self.scheduled_days as f32;
|
||||||
|
let elapsed = (self.scheduled_days as f32) + (self.days_late() as f32);
|
||||||
|
|
||||||
|
let hard_interval = {
|
||||||
|
let factor = ctx.hard_multiplier;
|
||||||
|
let half_usual = factor / 2.0;
|
||||||
|
constrain_passing_interval(ctx, (elapsed * factor).max(scheduled * half_usual), 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let good_interval =
|
||||||
|
constrain_passing_interval(ctx, (elapsed * self.ease_factor).max(scheduled), 0);
|
||||||
|
|
||||||
|
let easy_interval = {
|
||||||
|
// currently flooring() f64s to match python output
|
||||||
|
let easy_mult = ctx.easy_multiplier as f64;
|
||||||
|
let reduced_bonus = easy_mult - (easy_mult - 1.0) / 2.0;
|
||||||
|
constrain_passing_interval(
|
||||||
|
ctx,
|
||||||
|
((elapsed as f64 * self.ease_factor as f64).max(scheduled as f64) * reduced_bonus)
|
||||||
|
.floor() as f32,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
(hard_interval, good_interval, easy_interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fuzz_range(interval: f32, factor: f32, minimum: f32) -> (f32, f32) {
|
||||||
|
let delta = (interval * factor).max(minimum).max(1.0);
|
||||||
|
(interval - delta, interval + delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform the provided hard/good/easy interval.
|
||||||
|
/// - Apply configured interval multiplier.
|
||||||
|
/// - Apply fuzz.
|
||||||
|
/// - Ensure it is at least `minimum`, and at least 1.
|
||||||
|
/// - Ensure it is at or below the configured maximum interval.
|
||||||
|
fn constrain_passing_interval(ctx: &StateContext, interval: f32, minimum: u32) -> u32 {
|
||||||
|
// fixme: floor is to match python
|
||||||
|
let interval = interval.floor();
|
||||||
|
with_review_fuzz(ctx.fuzz_seed, interval * ctx.interval_multiplier)
|
||||||
|
.max(minimum)
|
||||||
|
.min(ctx.maximum_review_interval)
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_review_fuzz(seed: Option<u64>, interval: f32) -> u32 {
|
||||||
|
// fixme: floor() is to match python
|
||||||
|
let interval = interval.floor();
|
||||||
|
if let Some(seed) = seed {
|
||||||
|
let mut rng = StdRng::seed_from_u64(seed);
|
||||||
|
let (lower, upper) = if interval < 2.0 {
|
||||||
|
(1.0, 1.0)
|
||||||
|
} else if interval < 3.0 {
|
||||||
|
(2.0, 3.0)
|
||||||
|
} else if interval < 7.0 {
|
||||||
|
fuzz_range(interval, 0.25, 0.0)
|
||||||
|
} else if interval < 30.0 {
|
||||||
|
fuzz_range(interval, 0.15, 2.0)
|
||||||
|
} else {
|
||||||
|
fuzz_range(interval, 0.05, 4.0)
|
||||||
|
};
|
||||||
|
rng.gen_range(lower, upper + 1.0)
|
||||||
|
} else {
|
||||||
|
interval
|
||||||
|
}
|
||||||
|
.round() as u32
|
||||||
|
}
|
82
rslib/src/sched/states/steps.rs
Normal file
82
rslib/src/sched/states/steps.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
const DEFAULT_SECS_IF_MISSING: u32 = 60;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub(crate) struct LearningSteps<'a> {
|
||||||
|
steps: &'a [f32],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_secs(v: f32) -> u32 {
|
||||||
|
(v * 60.0) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LearningSteps<'a> {
|
||||||
|
pub(crate) fn new(steps: &[f32]) -> LearningSteps<'_> {
|
||||||
|
LearningSteps { steps }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strip off 'learning today', and ensure index is in bounds.
|
||||||
|
fn get_index(self, remaining: u32) -> usize {
|
||||||
|
let total = self.steps.len();
|
||||||
|
total
|
||||||
|
.saturating_sub((remaining % 1000) as usize)
|
||||||
|
.min(total.saturating_sub(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn secs_at_index(&self, index: usize) -> Option<u32> {
|
||||||
|
self.steps.get(index).copied().map(to_secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cards in learning must always have at least one learning step.
|
||||||
|
pub(crate) fn again_delay_secs_learn(&self) -> u32 {
|
||||||
|
self.secs_at_index(0).unwrap_or(DEFAULT_SECS_IF_MISSING)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn again_delay_secs_relearn(&self) -> Option<u32> {
|
||||||
|
self.secs_at_index(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixme: the logic here is not ideal, but tries to match
|
||||||
|
// the current python code
|
||||||
|
|
||||||
|
pub(crate) fn hard_delay_secs(self, remaining: u32) -> Option<u32> {
|
||||||
|
let idx = self.get_index(remaining);
|
||||||
|
if let Some(current) = self
|
||||||
|
.secs_at_index(idx)
|
||||||
|
// if current is invalid, try first step
|
||||||
|
.or_else(|| self.steps.first().copied().map(to_secs))
|
||||||
|
{
|
||||||
|
let next = if self.steps.len() > 1 {
|
||||||
|
self.secs_at_index(idx + 1).unwrap_or(60)
|
||||||
|
} else {
|
||||||
|
current * 2
|
||||||
|
}
|
||||||
|
.max(current);
|
||||||
|
|
||||||
|
Some((current + next) / 2)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn good_delay_secs(self, remaining: u32) -> Option<u32> {
|
||||||
|
let idx = self.get_index(remaining);
|
||||||
|
self.secs_at_index(idx + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn current_delay_secs(self, remaining: u32) -> u32 {
|
||||||
|
let idx = self.get_index(remaining);
|
||||||
|
self.secs_at_index(idx).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remaining_for_good(self, remaining: u32) -> u32 {
|
||||||
|
let idx = self.get_index(remaining);
|
||||||
|
self.steps.len().saturating_sub(idx + 1) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remaining_for_failed(self) -> u32 {
|
||||||
|
self.steps.len() as u32
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,17 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
|
||||||
i18n.trn(key, args)
|
i18n.trn(key, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Short string like '4d' to place above answer buttons.
|
||||||
|
/// Times within the collapse time are represented like '<10m'
|
||||||
|
pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, i18n: &I18n) -> String {
|
||||||
|
let string = answer_button_time(seconds as f32, i18n);
|
||||||
|
if seconds < collapse_secs {
|
||||||
|
format!("<{}", string)
|
||||||
|
} else {
|
||||||
|
string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Describe the given seconds using the largest appropriate unit.
|
/// Describe the given seconds using the largest appropriate unit.
|
||||||
/// If precise is true, show to two decimal places, eg
|
/// If precise is true, show to two decimal places, eg
|
||||||
/// eg 70 seconds -> "1.17 minutes"
|
/// eg 70 seconds -> "1.17 minutes"
|
||||||
|
|
|
@ -417,7 +417,7 @@ fn render_into(
|
||||||
current_text: "".into(),
|
current_text: "".into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Replacement { key, filters } if key == "" && !filters.is_empty() => {
|
Replacement { key, filters } if key.is_empty() && !filters.is_empty() => {
|
||||||
// if a filter is provided, we accept an empty field name to
|
// if a filter is provided, we accept an empty field name to
|
||||||
// mean 'pass an empty string to the filter, and it will add
|
// mean 'pass an empty string to the filter, and it will add
|
||||||
// its own text'
|
// its own text'
|
||||||
|
|
|
@ -117,7 +117,7 @@ pub fn strip_av_tags(text: &str) -> Cow<str> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract audio tags from string, replacing them with [anki:play] refs
|
/// Extract audio tags from string, replacing them with [anki:play] refs
|
||||||
pub fn extract_av_tags<'a>(text: &'a str, question_side: bool) -> (Cow<'a, str>, Vec<AVTag>) {
|
pub fn extract_av_tags(text: &str, question_side: bool) -> (Cow<str>, Vec<AVTag>) {
|
||||||
let mut tags = vec![];
|
let mut tags = vec![];
|
||||||
let context = if question_side { 'q' } else { 'a' };
|
let context = if question_side { 'q' } else { 'a' };
|
||||||
let replaced_text = AV_TAGS.replace_all(text, |caps: &Captures| {
|
let replaced_text = AV_TAGS.replace_all(text, |caps: &Captures| {
|
||||||
|
|
Loading…
Reference in a new issue