Allow user to configure hard/good buttons when rescheduling off

Closes #2858
This commit is contained in:
Damien Elmes 2023-12-08 11:04:34 +10:00
parent fdcdc14f6b
commit e778cba089
13 changed files with 163 additions and 155 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>757</width>
<height>589</height>
<width>526</width>
<height>567</height>
</rect>
</property>
<property name="windowTitle">
@ -198,13 +198,67 @@
<string>actions_options</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2" alignment="Qt::AlignLeft">
<widget class="QCheckBox" name="resched">
<item row="1" column="0">
<widget class="QWidget" name="previewDelayWidget" native="true">
<layout class="QGridLayout" name="gridLayout_5">
<item row="3" column="0">
<widget class="QLabel" name="good_delay_label">
<property name="text">
<string notr="true">good delay</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QSpinBox" name="preview_again"/>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="preview_hard"/>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="again_delay_label">
<property name="text">
<string notr="true">again delay</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QSpinBox" name="preview_good"/>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_13">
<property name="text">
<string>decks_minutes</string>
</property>
</widget>
</item>
<item row="3" column="3" colspan="2">
<widget class="QLabel" name="label_9">
<property name="text">
<string>decks_minutes</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="hard_delay_label">
<property name="text">
<string notr="true">hard delay</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLabel" name="label_8">
<property name="text">
<string>decks_minutes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0" alignment="Qt::AlignLeft">
<widget class="QCheckBox" name="allow_empty">
<property name="text">
<string>decks_reschedule_cards_based_on_my_answers</string>
</property>
<property name="checked">
<bool>true</bool>
<string>decks_create_even_if_empty</string>
</property>
</widget>
</item>
@ -215,50 +269,13 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QWidget" name="previewDelayWidget" native="true">
<layout class="QHBoxLayout" name="previewDelayBox">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>decks_repeat_failed_cards_after</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="previewDelay"/>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>decks_minutes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0">
<widget class="QLineEdit" name="steps">
<property name="enabled">
<bool>false</bool>
</property>
<item row="0" column="0" colspan="2" alignment="Qt::AlignLeft">
<widget class="QCheckBox" name="resched">
<property name="text">
<string notr="true">1 10</string>
<string>decks_reschedule_cards_based_on_my_answers</string>
</property>
</widget>
</item>
<item row="3" column="0" alignment="Qt::AlignLeft">
<widget class="QCheckBox" name="stepsOn">
<property name="text">
<string>decks_custom_steps_in_minutes</string>
</property>
</widget>
</item>
<item row="5" column="0" alignment="Qt::AlignLeft">
<widget class="QCheckBox" name="allow_empty">
<property name="text">
<string>decks_create_even_if_empty</string>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
@ -331,19 +348,18 @@
</widget>
<tabstops>
<tabstop>name</tabstop>
<tabstop>search_button</tabstop>
<tabstop>search</tabstop>
<tabstop>limit</tabstop>
<tabstop>order</tabstop>
<tabstop>search_button_2</tabstop>
<tabstop>search_2</tabstop>
<tabstop>limit_2</tabstop>
<tabstop>order_2</tabstop>
<tabstop>resched</tabstop>
<tabstop>previewDelay</tabstop>
<tabstop>preview_again</tabstop>
<tabstop>preview_hard</tabstop>
<tabstop>preview_good</tabstop>
<tabstop>secondFilter</tabstop>
<tabstop>stepsOn</tabstop>
<tabstop>steps</tabstop>
<tabstop>allow_empty</tabstop>
</tabstops>
<resources/>
<connections>
@ -395,21 +411,5 @@
</hint>
</hints>
</connection>
<connection>
<sender>stepsOn</sender>
<signal>toggled(bool)</signal>
<receiver>steps</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>194</x>
<y>351</y>
</hint>
<hint type="destinationlabel">
<x>190</x>
<y>378</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -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),

View file

@ -156,8 +156,12 @@ pub struct FilteredDeckSchema11 {
delays: Option<Vec<f32>>,
// 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<FilteredDeckSchema11> 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<Deck> 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(),
}),
}

View file

@ -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()

View file

@ -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 {

View file

@ -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);

View file

@ -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,
}
}

View file

@ -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,
}
}

View file

@ -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()
}