mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
switch to 4 buttons when previewing in test scheduler
- Currently we just use 1.5x and 2x the normal preview delay; we could change this in the future. - Don't try to capture the current state; just use a flag to denote exit status. - Show (end) when exiting
This commit is contained in:
parent
c74a71a6d7
commit
de7baa80bd
13 changed files with 132 additions and 80 deletions
|
@ -239,9 +239,6 @@ class Scheduler:
|
||||||
return card.queue
|
return card.queue
|
||||||
|
|
||||||
def answerButtons(self, card: Card) -> int:
|
def answerButtons(self, card: Card) -> int:
|
||||||
conf = self._cardConf(card)
|
|
||||||
if card.odid and not conf["resched"]:
|
|
||||||
return 2
|
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str:
|
def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str:
|
||||||
|
|
|
@ -857,9 +857,16 @@ def test_preview():
|
||||||
col.reset()
|
col.reset()
|
||||||
# grab the first card
|
# grab the first card
|
||||||
c = col.sched.getCard()
|
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, 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
|
# failing it will push its due time back
|
||||||
due = c.due
|
due = c.due
|
||||||
col.sched.answerCard(c, 1)
|
col.sched.answerCard(c, 1)
|
||||||
|
@ -870,7 +877,7 @@ def test_preview():
|
||||||
assert c2.id != c.id
|
assert c2.id != c.id
|
||||||
|
|
||||||
# passing it will remove it
|
# passing it will remove it
|
||||||
col.sched.answerCard(c2, 2)
|
col.sched.answerCard(c2, passing_grade)
|
||||||
assert c2.queue == QUEUE_TYPE_NEW
|
assert c2.queue == QUEUE_TYPE_NEW
|
||||||
assert c2.reps == 0
|
assert c2.reps == 0
|
||||||
assert c2.type == CARD_TYPE_NEW
|
assert c2.type == CARD_TYPE_NEW
|
||||||
|
|
|
@ -1319,7 +1319,7 @@ message SchedulingState {
|
||||||
}
|
}
|
||||||
message Preview {
|
message Preview {
|
||||||
uint32 scheduled_secs = 1;
|
uint32 scheduled_secs = 1;
|
||||||
Normal original_state = 2;
|
bool finished = 2;
|
||||||
}
|
}
|
||||||
message ReschedulingFilter {
|
message ReschedulingFilter {
|
||||||
Normal original_state = 1;
|
Normal original_state = 1;
|
||||||
|
|
|
@ -7,7 +7,7 @@ impl From<pb::scheduling_state::Preview> for PreviewState {
|
||||||
fn from(state: pb::scheduling_state::Preview) -> Self {
|
fn from(state: pb::scheduling_state::Preview) -> Self {
|
||||||
PreviewState {
|
PreviewState {
|
||||||
scheduled_secs: state.scheduled_secs,
|
scheduled_secs: state.scheduled_secs,
|
||||||
original_state: state.original_state.unwrap_or_default().into(),
|
finished: state.finished,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ impl From<PreviewState> for pb::scheduling_state::Preview {
|
||||||
fn from(state: PreviewState) -> Self {
|
fn from(state: PreviewState) -> Self {
|
||||||
pb::scheduling_state::Preview {
|
pb::scheduling_state::Preview {
|
||||||
scheduled_secs: state.scheduled_secs,
|
scheduled_secs: state.scheduled_secs,
|
||||||
original_state: Some(state.original_state.into()),
|
finished: state.finished,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ impl CardStateUpdater {
|
||||||
} else {
|
} else {
|
||||||
PreviewState {
|
PreviewState {
|
||||||
scheduled_secs: filtered.preview_delay * 60,
|
scheduled_secs: filtered.preview_delay * 60,
|
||||||
original_state: normal_state,
|
finished: false,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ mod current;
|
||||||
mod learning;
|
mod learning;
|
||||||
mod preview;
|
mod preview;
|
||||||
mod relearning;
|
mod relearning;
|
||||||
mod rescheduling_filter;
|
|
||||||
mod review;
|
mod review;
|
||||||
mod revlog;
|
mod revlog;
|
||||||
|
|
||||||
|
@ -44,7 +43,6 @@ pub struct CardAnswer {
|
||||||
pub milliseconds_taken: u32,
|
pub milliseconds_taken: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixme: 4 buttons for previewing
|
|
||||||
// fixme: log preview review
|
// fixme: log preview review
|
||||||
// fixme: undo
|
// fixme: undo
|
||||||
|
|
||||||
|
@ -107,25 +105,52 @@ impl CardStateUpdater {
|
||||||
current: CardState,
|
current: CardState,
|
||||||
next: CardState,
|
next: CardState,
|
||||||
) -> Result<Option<RevlogEntryPartial>> {
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
// any non-preview answer resets card.odue and increases reps
|
let revlog = match next {
|
||||||
if !matches!(current, CardState::Filtered(FilteredState::Preview(_))) {
|
CardState::Normal(normal) => {
|
||||||
self.card.reps += 1;
|
// transitioning from filtered state?
|
||||||
self.card.original_due = 0;
|
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<Option<RevlogEntryPartial>> {
|
||||||
|
self.card.reps += 1;
|
||||||
|
self.card.original_due = 0;
|
||||||
|
|
||||||
let revlog = match next {
|
let revlog = match next {
|
||||||
CardState::Normal(normal) => match normal {
|
|
||||||
NormalState::New(next) => self.apply_new_state(current, next),
|
NormalState::New(next) => self.apply_new_state(current, next),
|
||||||
NormalState::Learning(next) => self.apply_learning_state(current, next),
|
NormalState::Learning(next) => self.apply_learning_state(current, next),
|
||||||
NormalState::Review(next) => self.apply_review_state(current, next),
|
NormalState::Review(next) => self.apply_review_state(current, next),
|
||||||
NormalState::Relearning(next) => self.apply_relearning_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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
if next.leeched() && self.config.inner.leech_action() == LeechAction::Suspend {
|
if next.leeched() && self.config.inner.leech_action() == LeechAction::Suspend {
|
||||||
|
@ -173,6 +198,7 @@ impl Collection {
|
||||||
let now = TimestampSecs::now();
|
let now = TimestampSecs::now();
|
||||||
let timing = self.timing_for_timestamp(now)?;
|
let timing = self.timing_for_timestamp(now)?;
|
||||||
let secs_until_rollover = (timing.next_day_at - now.0).max(0) as u32;
|
let secs_until_rollover = (timing.next_day_at - now.0).max(0) as u32;
|
||||||
|
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
answer_button_time_collapsible(
|
answer_button_time_collapsible(
|
||||||
choices
|
choices
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
card::CardQueue,
|
card::CardQueue,
|
||||||
|
config::SchedulerVersion,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
scheduler::states::{CardState, IntervalKind, PreviewState},
|
scheduler::states::{CardState, IntervalKind, PreviewState},
|
||||||
};
|
};
|
||||||
|
@ -17,7 +18,12 @@ impl CardStateUpdater {
|
||||||
current: CardState,
|
current: CardState,
|
||||||
next: PreviewState,
|
next: PreviewState,
|
||||||
) -> Result<Option<RevlogEntryPartial>> {
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
self.ensure_filtered()?;
|
if next.finished {
|
||||||
|
self.card
|
||||||
|
.remove_from_filtered_deck_restoring_queue(SchedulerVersion::V2);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
self.card.queue = CardQueue::PreviewRepeat;
|
self.card.queue = CardQueue::PreviewRepeat;
|
||||||
|
|
||||||
let interval = next.interval_kind();
|
let interval = next.interval_kind();
|
||||||
|
@ -48,7 +54,7 @@ mod test {
|
||||||
card::CardType,
|
card::CardType,
|
||||||
scheduler::{
|
scheduler::{
|
||||||
answering::{CardAnswer, Rating},
|
answering::{CardAnswer, Rating},
|
||||||
states::{CardState, FilteredState, LearnState, NormalState},
|
states::{CardState, FilteredState},
|
||||||
},
|
},
|
||||||
timestamp::TimestampMillis,
|
timestamp::TimestampMillis,
|
||||||
};
|
};
|
||||||
|
@ -56,42 +62,35 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn preview() -> Result<()> {
|
fn preview() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = open_test_collection();
|
||||||
dbg!(col.scheduler_version());
|
|
||||||
let mut c = Card {
|
let mut c = Card {
|
||||||
deck_id: DeckID(1),
|
deck_id: DeckID(1),
|
||||||
ctype: CardType::Learn,
|
ctype: CardType::Learn,
|
||||||
queue: CardQueue::Learn,
|
queue: CardQueue::DayLearn,
|
||||||
remaining_steps: 2,
|
remaining_steps: 2,
|
||||||
|
due: 123,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
col.add_card(&mut c)?;
|
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
|
// pull the card into a preview deck
|
||||||
let mut filtered_deck = Deck::new_filtered();
|
let mut filtered_deck = Deck::new_filtered();
|
||||||
filtered_deck.filtered_mut()?.reschedule = false;
|
filtered_deck.filtered_mut()?.reschedule = false;
|
||||||
col.add_or_update_deck(&mut filtered_deck)?;
|
col.add_or_update_deck(&mut filtered_deck)?;
|
||||||
assert_eq!(col.rebuild_filtered_deck(filtered_deck.id)?, 1);
|
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)?;
|
let next = col.get_next_card_states(c.id)?;
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
next.current,
|
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 {
|
CardState::Filtered(FilteredState::Preview(PreviewState {
|
||||||
scheduled_secs: 600,
|
scheduled_secs: 0,
|
||||||
original_state: NormalState::Learning(LearnState {
|
finished: true
|
||||||
remaining_steps: 2,
|
|
||||||
scheduled_secs: 86_400,
|
|
||||||
}),
|
|
||||||
}))
|
}))
|
||||||
);
|
));
|
||||||
|
|
||||||
// use Again on the preview
|
// use Again on the preview
|
||||||
col.answer_card(&CardAnswer {
|
col.answer_card(&CardAnswer {
|
||||||
|
@ -106,8 +105,7 @@ mod test {
|
||||||
c = col.storage.get_card(c.id)?.unwrap();
|
c = col.storage.get_card(c.id)?.unwrap();
|
||||||
assert_eq!(c.queue, CardQueue::PreviewRepeat);
|
assert_eq!(c.queue, CardQueue::PreviewRepeat);
|
||||||
|
|
||||||
// and then it should return to its old state once passed
|
// hard
|
||||||
// (based on learning steps)
|
|
||||||
let next = col.get_next_card_states(c.id)?;
|
let next = col.get_next_card_states(c.id)?;
|
||||||
col.answer_card(&CardAnswer {
|
col.answer_card(&CardAnswer {
|
||||||
card_id: c.id,
|
card_id: c.id,
|
||||||
|
@ -118,8 +116,34 @@ mod test {
|
||||||
milliseconds_taken: 0,
|
milliseconds_taken: 0,
|
||||||
})?;
|
})?;
|
||||||
c = col.storage.get_card(c.id)?.unwrap();
|
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.queue, CardQueue::DayLearn);
|
||||||
assert_eq!(c.due, 1);
|
assert_eq!(c.due, 123);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Option<RevlogEntryPartial>> {
|
|
||||||
self.ensure_filtered()?;
|
|
||||||
self.apply_study_state(current, next.original_state.into())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@ impl CardStateUpdater {
|
||||||
current: CardState,
|
current: CardState,
|
||||||
next: ReviewState,
|
next: ReviewState,
|
||||||
) -> Result<Option<RevlogEntryPartial>> {
|
) -> Result<Option<RevlogEntryPartial>> {
|
||||||
self.card.remove_from_filtered_deck_before_reschedule();
|
|
||||||
self.card.queue = CardQueue::Review;
|
self.card.queue = CardQueue::Review;
|
||||||
self.card.ctype = CardType::Review;
|
self.card.ctype = CardType::Review;
|
||||||
self.card.interval = next.scheduled_days;
|
self.card.interval = next.scheduled_days;
|
||||||
|
|
|
@ -37,7 +37,7 @@ impl FilteredState {
|
||||||
|
|
||||||
pub(crate) fn review_state(self) -> Option<ReviewState> {
|
pub(crate) fn review_state(self) -> Option<ReviewState> {
|
||||||
match self {
|
match self {
|
||||||
FilteredState::Preview(state) => state.original_state.review_state(),
|
FilteredState::Preview(_) => None,
|
||||||
FilteredState::Rescheduling(state) => state.original_state.review_state(),
|
FilteredState::Rescheduling(state) => state.original_state.review_state(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,10 @@ impl NormalState {
|
||||||
NormalState::Relearning(RelearnState { review, .. }) => Some(review),
|
NormalState::Relearning(RelearnState { review, .. }) => Some(review),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn leeched(self) -> bool {
|
||||||
|
self.review_state().map(|r| r.leeched).unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<NewState> for NormalState {
|
impl From<NewState> for NormalState {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// 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 super::{IntervalKind, NextCardStates, NormalState, StateContext};
|
use super::{IntervalKind, NextCardStates, StateContext};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct PreviewState {
|
pub struct PreviewState {
|
||||||
pub scheduled_secs: u32,
|
pub scheduled_secs: u32,
|
||||||
pub original_state: NormalState,
|
pub finished: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreviewState {
|
impl PreviewState {
|
||||||
|
@ -22,9 +22,22 @@ impl PreviewState {
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
hard: self.original_state.into(),
|
hard: PreviewState {
|
||||||
good: self.original_state.into(),
|
// ~15 minutes with the default setting
|
||||||
easy: self.original_state.into(),
|
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,9 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
|
||||||
/// Times within the collapse time are represented like '<10m'
|
/// Times within the collapse time are represented like '<10m'
|
||||||
pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, i18n: &I18n) -> String {
|
pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, i18n: &I18n) -> String {
|
||||||
let string = answer_button_time(seconds as f32, i18n);
|
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)
|
format!("<{}", string)
|
||||||
} else {
|
} else {
|
||||||
string
|
string
|
||||||
|
|
Loading…
Reference in a new issue