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
|
||||
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -7,7 +7,7 @@ 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(),
|
||||
finished: state.finished,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ 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()),
|
||||
finished: state.finished,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ impl CardStateUpdater {
|
|||
} else {
|
||||
PreviewState {
|
||||
scheduled_secs: filtered.preview_delay * 60,
|
||||
original_state: normal_state,
|
||||
finished: false,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -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<Option<RevlogEntryPartial>> {
|
||||
// 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<Option<RevlogEntryPartial>> {
|
||||
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)
|
||||
}
|
||||
},
|
||||
}?;
|
||||
|
||||
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
|
||||
|
|
|
@ -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<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;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
next: ReviewState,
|
||||
) -> Result<Option<RevlogEntryPartial>> {
|
||||
self.card.remove_from_filtered_deck_before_reschedule();
|
||||
self.card.queue = CardQueue::Review;
|
||||
self.card.ctype = CardType::Review;
|
||||
self.card.interval = next.scheduled_days;
|
||||
|
|
|
@ -37,7 +37,7 @@ impl FilteredState {
|
|||
|
||||
pub(crate) fn review_state(self) -> Option<ReviewState> {
|
||||
match self {
|
||||
FilteredState::Preview(state) => state.original_state.review_state(),
|
||||
FilteredState::Preview(_) => None,
|
||||
FilteredState::Rescheduling(state) => state.original_state.review_state(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<NewState> for NormalState {
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue