diff --git a/pylib/anki/scheduler.py b/pylib/anki/scheduler.py index c35e17970..8ac969d48 100644 --- a/pylib/anki/scheduler.py +++ b/pylib/anki/scheduler.py @@ -239,9 +239,6 @@ class Scheduler: return card.queue def answerButtons(self, card: Card) -> int: - conf = self._cardConf(card) - if card.odid and not conf["resched"]: - return 2 return 4 def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str: diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 09738bacb..3d39dc816 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -857,9 +857,16 @@ def test_preview(): col.reset() # grab the first card c = col.sched.getCard() - assert col.sched.answerButtons(c) == 2 + + if is_2021(): + passing_grade = 4 + else: + passing_grade = 2 + + assert col.sched.answerButtons(c) == passing_grade assert col.sched.nextIvl(c, 1) == 600 - assert col.sched.nextIvl(c, 2) == 0 + assert col.sched.nextIvl(c, passing_grade) == 0 + # failing it will push its due time back due = c.due col.sched.answerCard(c, 1) @@ -870,7 +877,7 @@ def test_preview(): assert c2.id != c.id # passing it will remove it - col.sched.answerCard(c2, 2) + col.sched.answerCard(c2, passing_grade) assert c2.queue == QUEUE_TYPE_NEW assert c2.reps == 0 assert c2.type == CARD_TYPE_NEW diff --git a/rslib/backend.proto b/rslib/backend.proto index 860b223d3..b31c939d4 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -1319,7 +1319,7 @@ message SchedulingState { } message Preview { uint32 scheduled_secs = 1; - Normal original_state = 2; + bool finished = 2; } message ReschedulingFilter { Normal original_state = 1; diff --git a/rslib/src/backend/scheduler/states/preview.rs b/rslib/src/backend/scheduler/states/preview.rs index c09876820..24d3c2877 100644 --- a/rslib/src/backend/scheduler/states/preview.rs +++ b/rslib/src/backend/scheduler/states/preview.rs @@ -7,7 +7,7 @@ impl From 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(), + finished: state.finished, } } } @@ -16,7 +16,7 @@ impl From 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()), + finished: state.finished, } } } diff --git a/rslib/src/scheduler/answering/current.rs b/rslib/src/scheduler/answering/current.rs index 2b62024a4..ff2d3bf46 100644 --- a/rslib/src/scheduler/answering/current.rs +++ b/rslib/src/scheduler/answering/current.rs @@ -49,7 +49,7 @@ impl CardStateUpdater { } else { PreviewState { scheduled_secs: filtered.preview_delay * 60, - original_state: normal_state, + finished: false, } .into() } diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 2b0d2e1d8..3b4f24435 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -5,7 +5,6 @@ mod current; mod learning; mod preview; mod relearning; -mod rescheduling_filter; mod review; mod revlog; @@ -44,7 +43,6 @@ pub struct CardAnswer { pub milliseconds_taken: u32, } -// fixme: 4 buttons for previewing // fixme: log preview review // fixme: undo @@ -107,25 +105,52 @@ impl CardStateUpdater { current: CardState, next: CardState, ) -> Result> { - // any non-preview answer resets card.odue and increases reps - if !matches!(current, CardState::Filtered(FilteredState::Preview(_))) { - self.card.reps += 1; - self.card.original_due = 0; - } + let revlog = match next { + CardState::Normal(normal) => { + // transitioning from filtered state? + if let CardState::Filtered(filtered) = ¤t { + match filtered { + FilteredState::Preview(_) => { + return Err(AnkiError::invalid_input( + "should set finished=true, not return different state", + )); + } + FilteredState::Rescheduling(_) => { + // card needs to be removed from normal filtered deck, then scheduled normally + self.card.remove_from_filtered_deck_before_reschedule(); + } + } + } + // apply normal scheduling + self.apply_normal_study_state(current, normal) + } + CardState::Filtered(filtered) => { + self.ensure_filtered()?; + match filtered { + FilteredState::Preview(next) => self.apply_preview_state(current, next), + FilteredState::Rescheduling(next) => { + self.apply_normal_study_state(current, next.original_state) + } + } + } + }?; + + Ok(revlog) + } + + fn apply_normal_study_state( + &mut self, + current: CardState, + next: NormalState, + ) -> Result> { + self.card.reps += 1; + self.card.original_due = 0; let revlog = match next { - CardState::Normal(normal) => match normal { - NormalState::New(next) => self.apply_new_state(current, next), - NormalState::Learning(next) => self.apply_learning_state(current, next), - NormalState::Review(next) => self.apply_review_state(current, next), - NormalState::Relearning(next) => self.apply_relearning_state(current, next), - }, - CardState::Filtered(filtered) => match filtered { - FilteredState::Preview(next) => self.apply_preview_state(current, next), - FilteredState::Rescheduling(next) => { - self.apply_rescheduling_filter_state(current, next) - } - }, + NormalState::New(next) => self.apply_new_state(current, next), + NormalState::Learning(next) => self.apply_learning_state(current, next), + NormalState::Review(next) => self.apply_review_state(current, next), + NormalState::Relearning(next) => self.apply_relearning_state(current, next), }?; if next.leeched() && self.config.inner.leech_action() == LeechAction::Suspend { @@ -173,6 +198,7 @@ impl Collection { let now = TimestampSecs::now(); let timing = self.timing_for_timestamp(now)?; let secs_until_rollover = (timing.next_day_at - now.0).max(0) as u32; + Ok(vec![ answer_button_time_collapsible( choices diff --git a/rslib/src/scheduler/answering/preview.rs b/rslib/src/scheduler/answering/preview.rs index 9bf385477..4dd1e9df8 100644 --- a/rslib/src/scheduler/answering/preview.rs +++ b/rslib/src/scheduler/answering/preview.rs @@ -3,6 +3,7 @@ use crate::{ card::CardQueue, + config::SchedulerVersion, prelude::*, scheduler::states::{CardState, IntervalKind, PreviewState}, }; @@ -17,7 +18,12 @@ impl CardStateUpdater { current: CardState, next: PreviewState, ) -> Result> { - self.ensure_filtered()?; + if next.finished { + self.card + .remove_from_filtered_deck_restoring_queue(SchedulerVersion::V2); + return Ok(None); + } + self.card.queue = CardQueue::PreviewRepeat; let interval = next.interval_kind(); @@ -48,7 +54,7 @@ mod test { card::CardType, scheduler::{ answering::{CardAnswer, Rating}, - states::{CardState, FilteredState, LearnState, NormalState}, + states::{CardState, FilteredState}, }, timestamp::TimestampMillis, }; @@ -56,42 +62,35 @@ mod test { #[test] fn preview() -> Result<()> { let mut col = open_test_collection(); - dbg!(col.scheduler_version()); let mut c = Card { deck_id: DeckID(1), ctype: CardType::Learn, - queue: CardQueue::Learn, + queue: CardQueue::DayLearn, remaining_steps: 2, + due: 123, ..Default::default() }; col.add_card(&mut c)?; - // set the first (current) step to a day - let deck = col.storage.get_deck(DeckID(1))?.unwrap(); - let mut conf = col - .get_deck_config(DeckConfID(deck.normal()?.config_id), false)? - .unwrap(); - *conf.inner.learn_steps.get_mut(0).unwrap() = 24.0 * 60.0; - col.add_or_update_deck_config(&mut conf, false)?; - // pull the card into a preview deck let mut filtered_deck = Deck::new_filtered(); filtered_deck.filtered_mut()?.reschedule = false; col.add_or_update_deck(&mut filtered_deck)?; assert_eq!(col.rebuild_filtered_deck(filtered_deck.id)?, 1); - // the original state reflects the learning steps, not the card properties let next = col.get_next_card_states(c.id)?; - assert_eq!( + assert!(matches!( next.current, + CardState::Filtered(FilteredState::Preview(_)) + )); + // the exit state should have a 0 second interval, which will show up as (end) + assert!(matches!( + next.easy, CardState::Filtered(FilteredState::Preview(PreviewState { - scheduled_secs: 600, - original_state: NormalState::Learning(LearnState { - remaining_steps: 2, - scheduled_secs: 86_400, - }), + scheduled_secs: 0, + finished: true })) - ); + )); // use Again on the preview col.answer_card(&CardAnswer { @@ -106,8 +105,7 @@ mod test { c = col.storage.get_card(c.id)?.unwrap(); assert_eq!(c.queue, CardQueue::PreviewRepeat); - // and then it should return to its old state once passed - // (based on learning steps) + // hard let next = col.get_next_card_states(c.id)?; col.answer_card(&CardAnswer { card_id: c.id, @@ -118,8 +116,34 @@ mod test { milliseconds_taken: 0, })?; c = col.storage.get_card(c.id)?.unwrap(); + assert_eq!(c.queue, CardQueue::PreviewRepeat); + + // good + let next = col.get_next_card_states(c.id)?; + col.answer_card(&CardAnswer { + card_id: c.id, + current_state: next.current, + new_state: next.good, + rating: Rating::Good, + answered_at: TimestampMillis::now(), + milliseconds_taken: 0, + })?; + c = col.storage.get_card(c.id)?.unwrap(); + assert_eq!(c.queue, CardQueue::PreviewRepeat); + + // and then it should return to its old state once easy selected + let next = col.get_next_card_states(c.id)?; + col.answer_card(&CardAnswer { + card_id: c.id, + current_state: next.current, + new_state: next.easy, + rating: Rating::Easy, + answered_at: TimestampMillis::now(), + milliseconds_taken: 0, + })?; + c = col.storage.get_card(c.id)?.unwrap(); assert_eq!(c.queue, CardQueue::DayLearn); - assert_eq!(c.due, 1); + assert_eq!(c.due, 123); Ok(()) } diff --git a/rslib/src/scheduler/answering/rescheduling_filter.rs b/rslib/src/scheduler/answering/rescheduling_filter.rs deleted file mode 100644 index e5b331cfc..000000000 --- a/rslib/src/scheduler/answering/rescheduling_filter.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -use crate::{ - prelude::*, - scheduler::states::{CardState, ReschedulingFilterState}, -}; - -use super::{CardStateUpdater, RevlogEntryPartial}; - -impl CardStateUpdater { - pub(super) fn apply_rescheduling_filter_state( - &mut self, - current: CardState, - next: ReschedulingFilterState, - ) -> Result> { - self.ensure_filtered()?; - self.apply_study_state(current, next.original_state.into()) - } -} diff --git a/rslib/src/scheduler/answering/review.rs b/rslib/src/scheduler/answering/review.rs index 6df913c29..b948b43c2 100644 --- a/rslib/src/scheduler/answering/review.rs +++ b/rslib/src/scheduler/answering/review.rs @@ -15,7 +15,6 @@ impl CardStateUpdater { current: CardState, next: ReviewState, ) -> Result> { - self.card.remove_from_filtered_deck_before_reschedule(); self.card.queue = CardQueue::Review; self.card.ctype = CardType::Review; self.card.interval = next.scheduled_days; diff --git a/rslib/src/scheduler/states/filtered.rs b/rslib/src/scheduler/states/filtered.rs index a74966df0..b0de406a9 100644 --- a/rslib/src/scheduler/states/filtered.rs +++ b/rslib/src/scheduler/states/filtered.rs @@ -37,7 +37,7 @@ impl FilteredState { pub(crate) fn review_state(self) -> Option { match self { - FilteredState::Preview(state) => state.original_state.review_state(), + FilteredState::Preview(_) => None, FilteredState::Rescheduling(state) => state.original_state.review_state(), } } diff --git a/rslib/src/scheduler/states/normal.rs b/rslib/src/scheduler/states/normal.rs index 9b59c1b05..25c47cb81 100644 --- a/rslib/src/scheduler/states/normal.rs +++ b/rslib/src/scheduler/states/normal.rs @@ -64,6 +64,10 @@ impl NormalState { NormalState::Relearning(RelearnState { review, .. }) => Some(review), } } + + pub(crate) fn leeched(self) -> bool { + self.review_state().map(|r| r.leeched).unwrap_or_default() + } } impl From for NormalState { diff --git a/rslib/src/scheduler/states/preview_filter.rs b/rslib/src/scheduler/states/preview_filter.rs index 97173211c..b22cb2445 100644 --- a/rslib/src/scheduler/states/preview_filter.rs +++ b/rslib/src/scheduler/states/preview_filter.rs @@ -1,12 +1,12 @@ // 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, StateContext}; +use super::{IntervalKind, NextCardStates, StateContext}; #[derive(Debug, Clone, Copy, PartialEq)] pub struct PreviewState { pub scheduled_secs: u32, - pub original_state: NormalState, + pub finished: bool, } impl PreviewState { @@ -22,9 +22,22 @@ impl PreviewState { ..self } .into(), - hard: self.original_state.into(), - good: self.original_state.into(), - easy: self.original_state.into(), + hard: PreviewState { + // ~15 minutes with the default setting + scheduled_secs: ctx.with_learning_fuzz(ctx.preview_step * 90), + ..self + } + .into(), + good: PreviewState { + scheduled_secs: ctx.with_learning_fuzz(ctx.preview_step * 120), + ..self + } + .into(), + easy: PreviewState { + scheduled_secs: 0, + finished: true, + } + .into(), } } } diff --git a/rslib/src/scheduler/timespan.rs b/rslib/src/scheduler/timespan.rs index 4c5ce2749..5c1f34d55 100644 --- a/rslib/src/scheduler/timespan.rs +++ b/rslib/src/scheduler/timespan.rs @@ -22,7 +22,9 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String { /// 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 { + if seconds == 0 { + i18n.tr(TR::SchedulingEnd).into() + } else if seconds < collapse_secs { format!("<{}", string) } else { string