From e778cba089fe85125a5470ab9dcc06f73f184d25 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Dec 2023 11:04:34 +1000 Subject: [PATCH] Allow user to configure hard/good buttons when rescheduling off Closes #2858 --- ftl/core/decks.ftl | 4 +- proto/anki/decks.proto | 6 +- pylib/tests/test_schedv3.py | 2 +- qt/aqt/filtered_deck.py | 50 +++---- qt/aqt/forms/filtered_deck.ui | 142 +++++++++---------- rslib/src/decks/filtered.rs | 3 +- rslib/src/decks/schema11.rs | 14 +- rslib/src/scheduler/answering/current.rs | 2 +- rslib/src/scheduler/answering/mod.rs | 17 ++- rslib/src/scheduler/answering/preview.rs | 24 ++-- rslib/src/scheduler/filtered/custom_study.rs | 4 +- rslib/src/scheduler/states/mod.rs | 9 +- rslib/src/scheduler/states/preview_filter.rs | 41 +++--- 13 files changed, 163 insertions(+), 155 deletions(-) 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() +}