diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl
index a3777645d..145c27cb3 100644
--- a/ftl/core/decks.ftl
+++ b/ftl/core/decks.ftl
@@ -25,7 +25,9 @@ decks-order-due = Order due
decks-please-select-something = Please select something.
decks-random = Random
decks-relative-overdueness = Relative overdueness
-decks-repeat-failed-cards-after = Repeat failed cards after
+decks-repeat-failed-cards-after = Delay Repeat failed cards after
+# e.g. "Delay for Again", "Delay for Hard", "Delay for Good"
+decks-delay-for-button = Delay for { $button }
decks-reschedule-cards-based-on-my-answers = Reschedule cards based on my answers in this deck
decks-study = Study
decks-study-deck = Study Deck
diff --git a/proto/anki/decks.proto b/proto/anki/decks.proto
index aff2a9d21..ca360aed9 100644
--- a/proto/anki/decks.proto
+++ b/proto/anki/decks.proto
@@ -110,7 +110,11 @@ message Deck {
// v1 scheduler only
repeated float delays = 3;
// v2 scheduler only
- uint32 preview_delay = 4;
+ uint32 preview_again_mins = 4;
+ // recent v3 scheduler only; 0 means card will be returned
+ uint32 preview_hard_mins = 5;
+ // recent v3 scheduler only; 0 means card will be returned
+ uint32 preview_good_mins = 6;
}
// a container to store the deck specifics in the DB
// as a tagged enum
diff --git a/pylib/tests/test_schedv3.py b/pylib/tests/test_schedv3.py
index 996f0450d..cc1fcc1d0 100644
--- a/pylib/tests/test_schedv3.py
+++ b/pylib/tests/test_schedv3.py
@@ -740,7 +740,7 @@ def test_preview():
passing_grade = 4
assert col.sched.answerButtons(c) == passing_grade
- assert col.sched.nextIvl(c, 1) == 600
+ assert col.sched.nextIvl(c, 1) == 60
assert col.sched.nextIvl(c, passing_grade) == 0
# failing it will push its due time back
diff --git a/qt/aqt/filtered_deck.py b/qt/aqt/filtered_deck.py
index af87b2e6d..56f5aa14b 100644
--- a/qt/aqt/filtered_deck.py
+++ b/qt/aqt/filtered_deck.py
@@ -100,6 +100,16 @@ class FilteredDeckConfigDialog(QDialog):
self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.FILTERED_DECK)
)
+ self.form.again_delay_label.setText(
+ tr.decks_delay_for_button(button=tr.studying_again())
+ )
+ self.form.hard_delay_label.setText(
+ tr.decks_delay_for_button(button=tr.studying_hard())
+ )
+ self.form.good_delay_label.setText(
+ tr.decks_delay_for_button(button=tr.studying_good())
+ )
+
restoreGeom(self, self.GEOMETRY_KEY)
def load_deck_and_show(self, deck: FilteredDeckForUpdate) -> None:
@@ -132,10 +142,9 @@ class FilteredDeckConfigDialog(QDialog):
form.order.setCurrentIndex(term1.order)
form.limit.setValue(term1.limit)
- form.steps.setVisible(False)
- form.stepsOn.setVisible(False)
-
- form.previewDelay.setValue(config.preview_delay)
+ form.preview_again.setValue(config.preview_again_mins)
+ form.preview_hard.setValue(config.preview_hard_mins)
+ form.preview_good.setValue(config.preview_good_mins)
if len(config.search_terms) > 1:
term2: FilteredDeckConfig.SearchTerm = config.search_terms[1]
@@ -268,7 +277,9 @@ class FilteredDeckConfigDialog(QDialog):
del config.search_terms[:]
config.search_terms.extend(terms)
- config.preview_delay = form.previewDelay.value()
+ config.preview_again_mins = form.preview_again.value()
+ config.preview_hard_mins = form.preview_hard.value()
+ config.preview_good_mins = form.preview_good.value()
return True
@@ -293,32 +304,3 @@ class FilteredDeckConfigDialog(QDialog):
add_or_update_filtered_deck(parent=self, deck=self.deck).success(
success
).run_in_background()
-
- # Step load/save
- ########################################################
- # fixme: remove once we drop support for v1
-
- def listToUser(self, values: list[Union[float, int]]) -> str:
- return " ".join(
- [str(int(val)) if int(val) == val else str(val) for val in values]
- )
-
- def userToList(self, line: QLineEdit, minSize: int = 1) -> list[float] | None:
- items = str(line.text()).split(" ")
- ret = []
- for item in items:
- if not item:
- continue
- try:
- i = float(item)
- if i <= 0:
- raise Exception("0 invalid")
- ret.append(i)
- except:
- # invalid, don't update
- showWarning(tr.scheduling_steps_must_be_numbers())
- return None
- if len(ret) < minSize:
- showWarning(tr.scheduling_at_least_one_step_is_required())
- return None
- return ret
diff --git a/qt/aqt/forms/filtered_deck.ui b/qt/aqt/forms/filtered_deck.ui
index 40e7bb8d8..4399ba993 100644
--- a/qt/aqt/forms/filtered_deck.ui
+++ b/qt/aqt/forms/filtered_deck.ui
@@ -6,8 +6,8 @@
0
0
- 757
- 589
+ 526
+ 567
@@ -198,13 +198,67 @@
actions_options
- -
-
+
-
+
+
+
-
+
+
+ good delay
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ again delay
+
+
+
+ -
+
+
+ -
+
+
+ decks_minutes
+
+
+
+ -
+
+
+ decks_minutes
+
+
+
+ -
+
+
+ hard delay
+
+
+
+ -
+
+
+ decks_minutes
+
+
+
+
+
+
+ -
+
- decks_reschedule_cards_based_on_my_answers
-
-
- true
+ decks_create_even_if_empty
@@ -215,50 +269,13 @@
- -
-
-
-
-
-
-
- decks_repeat_failed_cards_after
-
-
-
- -
-
-
- -
-
-
- decks_minutes
-
-
-
-
-
-
- -
-
-
- false
-
+
-
+
- 1 10
+ decks_reschedule_cards_based_on_my_answers
-
-
- -
-
-
- decks_custom_steps_in_minutes
-
-
-
- -
-
-
- decks_create_even_if_empty
+
+ true
@@ -331,19 +348,18 @@
name
- search_button
search
limit
order
- search_button_2
search_2
limit_2
order_2
resched
- previewDelay
+ preview_again
+ preview_hard
+ preview_good
secondFilter
- stepsOn
- steps
+ allow_empty
@@ -395,21 +411,5 @@
-
- stepsOn
- toggled(bool)
- steps
- setEnabled(bool)
-
-
- 194
- 351
-
-
- 190
- 378
-
-
-
diff --git a/rslib/src/decks/filtered.rs b/rslib/src/decks/filtered.rs
index c9dfad09c..9df710775 100644
--- a/rslib/src/decks/filtered.rs
+++ b/rslib/src/decks/filtered.rs
@@ -22,7 +22,8 @@ impl Deck {
limit: 20,
order: FilteredSearchOrder::Due as i32,
});
- filt.preview_delay = 10;
+ filt.preview_again_mins = 1;
+ filt.preview_hard_mins = 10;
filt.reschedule = true;
Deck {
id: DeckId(0),
diff --git a/rslib/src/decks/schema11.rs b/rslib/src/decks/schema11.rs
index 45a5e14fb..2efe0cf03 100644
--- a/rslib/src/decks/schema11.rs
+++ b/rslib/src/decks/schema11.rs
@@ -156,8 +156,12 @@ pub struct FilteredDeckSchema11 {
delays: Option>,
// new scheduler
+ #[serde(default, rename = "previewDelay")]
+ preview_again_mins: u32,
#[serde(default)]
- preview_delay: u32,
+ preview_hard_mins: u32,
+ #[serde(default)]
+ preview_good_mins: u32,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default, Clone)]
pub struct DeckTodaySchema11 {
@@ -328,7 +332,9 @@ impl From for FilteredDeck {
reschedule: deck.resched,
search_terms: deck.terms.into_iter().map(Into::into).collect(),
delays: deck.delays.unwrap_or_default(),
- preview_delay: deck.preview_delay,
+ preview_again_mins: deck.preview_again_mins,
+ preview_hard_mins: deck.preview_hard_mins,
+ preview_good_mins: deck.preview_good_mins,
}
}
}
@@ -367,7 +373,9 @@ impl From for DeckSchema11 {
} else {
Some(filt.delays.clone())
},
- preview_delay: filt.preview_delay,
+ preview_again_mins: filt.preview_again_mins,
+ preview_hard_mins: filt.preview_hard_mins,
+ preview_good_mins: filt.preview_good_mins,
common: deck.into(),
}),
}
diff --git a/rslib/src/scheduler/answering/current.rs b/rslib/src/scheduler/answering/current.rs
index 2fcc92f63..67ef476f1 100644
--- a/rslib/src/scheduler/answering/current.rs
+++ b/rslib/src/scheduler/answering/current.rs
@@ -51,7 +51,7 @@ impl CardStateUpdater {
.into()
} else {
PreviewState {
- scheduled_secs: filtered.preview_delay * 60,
+ scheduled_secs: filtered.preview_again_mins * 60,
finished: false,
}
.into()
diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs
index 501217e80..bad9f3dca 100644
--- a/rslib/src/scheduler/answering/mod.rs
+++ b/rslib/src/scheduler/answering/mod.rs
@@ -91,10 +91,14 @@ impl CardStateUpdater {
lapse_multiplier: self.config.inner.lapse_multiplier,
minimum_lapse_interval: self.config.inner.minimum_lapse_interval,
in_filtered_deck: self.deck.is_filtered(),
- preview_step: if let DeckKind::Filtered(deck) = &self.deck.kind {
- deck.preview_delay
+ preview_delays: if let DeckKind::Filtered(deck) = &self.deck.kind {
+ PreviewDelays {
+ again: deck.preview_again_mins,
+ hard: deck.preview_hard_mins,
+ good: deck.preview_good_mins,
+ }
} else {
- 0
+ Default::default()
},
fsrs_next_states: self.fsrs_next_states.clone(),
}
@@ -185,6 +189,13 @@ impl CardStateUpdater {
}
}
+#[derive(Debug, Default)]
+pub(crate) struct PreviewDelays {
+ pub again: u32,
+ pub hard: u32,
+ pub good: u32,
+}
+
impl Rating {
fn as_number(self) -> u8 {
match self {
diff --git a/rslib/src/scheduler/answering/preview.rs b/rslib/src/scheduler/answering/preview.rs
index c52920ce9..00da45b19 100644
--- a/rslib/src/scheduler/answering/preview.rs
+++ b/rslib/src/scheduler/answering/preview.rs
@@ -79,6 +79,13 @@ mod test {
finished: true
}))
));
+ assert!(matches!(
+ next.good,
+ CardState::Filtered(FilteredState::Preview(PreviewState {
+ scheduled_secs: 0,
+ finished: true
+ }))
+ ));
// use Again on the preview
col.answer_card(&mut CardAnswer {
@@ -108,7 +115,8 @@ mod test {
c = col.storage.get_card(c.id)?.unwrap();
assert_eq!(c.queue, CardQueue::PreviewRepeat);
- // good
+ // and then it should return to its old state once good or easy selected,
+ // with the default filtered config
let next = col.get_scheduling_states(c.id)?;
col.answer_card(&mut CardAnswer {
card_id: c.id,
@@ -120,20 +128,6 @@ mod test {
custom_data: None,
})?;
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_scheduling_states(c.id)?;
- col.answer_card(&mut CardAnswer {
- card_id: c.id,
- current_state: next.current,
- new_state: next.easy,
- rating: Rating::Easy,
- answered_at: TimestampMillis::now(),
- milliseconds_taken: 0,
- custom_data: None,
- })?;
- c = col.storage.get_card(c.id)?.unwrap();
assert_eq!(c.queue, CardQueue::DayLearn);
assert_eq!(c.due, 123);
diff --git a/rslib/src/scheduler/filtered/custom_study.rs b/rslib/src/scheduler/filtered/custom_study.rs
index b7d6542ff..fda0901a2 100644
--- a/rslib/src/scheduler/filtered/custom_study.rs
+++ b/rslib/src/scheduler/filtered/custom_study.rs
@@ -208,7 +208,9 @@ fn custom_study_config(
order: order as i32,
}],
delays: vec![],
- preview_delay: 10,
+ preview_again_mins: 1,
+ preview_hard_mins: 10,
+ preview_good_mins: 0,
}
}
diff --git a/rslib/src/scheduler/states/mod.rs b/rslib/src/scheduler/states/mod.rs
index ae99f4e13..23eb9a261 100644
--- a/rslib/src/scheduler/states/mod.rs
+++ b/rslib/src/scheduler/states/mod.rs
@@ -26,6 +26,7 @@ pub use review::ReviewState;
use self::steps::LearningSteps;
use crate::revlog::RevlogReviewKind;
+use crate::scheduler::answering::PreviewDelays;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CardState {
@@ -106,7 +107,7 @@ pub(crate) struct StateContext<'a> {
// filtered
pub in_filtered_deck: bool,
- pub preview_step: u32,
+ pub preview_delays: PreviewDelays,
}
impl<'a> StateContext<'a> {
@@ -136,7 +137,11 @@ impl<'a> StateContext<'a> {
lapse_multiplier: 0.0,
minimum_lapse_interval: 1,
in_filtered_deck: false,
- preview_step: 10,
+ preview_delays: PreviewDelays {
+ again: 1,
+ hard: 10,
+ good: 0,
+ },
fsrs_next_states: None,
}
}
diff --git a/rslib/src/scheduler/states/preview_filter.rs b/rslib/src/scheduler/states/preview_filter.rs
index ba904b61b..a4f16632a 100644
--- a/rslib/src/scheduler/states/preview_filter.rs
+++ b/rslib/src/scheduler/states/preview_filter.rs
@@ -1,6 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+use super::CardState;
use super::IntervalKind;
use super::SchedulingStates;
use super::StateContext;
@@ -24,27 +25,25 @@ impl PreviewState {
pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates {
SchedulingStates {
current: self.into(),
- again: PreviewState {
- scheduled_secs: ctx.preview_step * 60,
- ..self
- }
- .into(),
- hard: PreviewState {
- // ~15 minutes with the default setting
- scheduled_secs: ctx.preview_step * 90,
- ..self
- }
- .into(),
- good: PreviewState {
- scheduled_secs: ctx.preview_step * 120,
- ..self
- }
- .into(),
- easy: PreviewState {
- scheduled_secs: 0,
- finished: true,
- }
- .into(),
+ again: delay_or_return(ctx.preview_delays.again),
+ hard: delay_or_return(ctx.preview_delays.hard),
+ good: delay_or_return(ctx.preview_delays.good),
+ easy: delay_or_return(0),
}
}
}
+
+fn delay_or_return(minutes: u32) -> CardState {
+ if minutes == 0 {
+ PreviewState {
+ scheduled_secs: 0,
+ finished: true,
+ }
+ } else {
+ PreviewState {
+ scheduled_secs: minutes * 60,
+ finished: false,
+ }
+ }
+ .into()
+}