diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index a68402271..f7c049b00 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -37,8 +37,8 @@ service SchedulerService { rpc SetDueDate(SetDueDateRequest) returns (collection.OpChanges); rpc SortCards(SortCardsRequest) returns (collection.OpChangesWithCount); rpc SortDeck(SortDeckRequest) returns (collection.OpChangesWithCount); - rpc GetNextCardStates(cards.CardId) returns (NextCardStates); - rpc DescribeNextStates(NextCardStates) returns (generic.StringList); + rpc GetSchedulingStates(cards.CardId) returns (SchedulingStates); + rpc DescribeNextStates(SchedulingStates) returns (generic.StringList); rpc StateIsLeech(SchedulingState) returns (generic.Bool); rpc UpgradeScheduler(generic.Empty) returns (generic.Empty); rpc CustomStudy(CustomStudyRequest) returns (collection.OpChanges); @@ -92,6 +92,10 @@ message SchedulingState { Normal normal = 1; Filtered filtered = 2; } + // The backend does not populate this field in GetQueuedCards; the front-end + // is expected to populate it based on the provided Card. If it's not set when + // answering a card, the existing custom data will not be updated. + optional string custom_data = 3; } message QueuedCards { @@ -103,7 +107,7 @@ message QueuedCards { message QueuedCard { cards.Card card = 1; Queue queue = 2; - NextCardStates next_states = 3; + SchedulingStates states = 3; } repeated QueuedCard cards = 1; @@ -218,7 +222,7 @@ message SortDeckRequest { bool randomize = 2; } -message NextCardStates { +message SchedulingStates { SchedulingState current = 1; SchedulingState again = 2; SchedulingState hard = 3; @@ -240,7 +244,6 @@ message CardAnswer { Rating rating = 4; int64 answered_at_millis = 5; uint32 milliseconds_taken = 6; - string custom_data = 7; } message CustomStudyRequest { @@ -304,9 +307,3 @@ message RepositionDefaultsResponse { bool random = 1; bool shift = 2; } - -// Data required to support the v3 scheduler's custom scheduling feature -message CustomScheduling { - NextCardStates states = 1; - string custom_data = 2; -} diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py index faf5c1ccf..6854542bc 100644 --- a/pylib/anki/scheduler/v3.py +++ b/pylib/anki/scheduler/v3.py @@ -29,9 +29,8 @@ from anki.utils import int_time QueuedCards = scheduler_pb2.QueuedCards SchedulingState = scheduler_pb2.SchedulingState -NextStates = scheduler_pb2.NextCardStates +SchedulingStates = scheduler_pb2.SchedulingStates CardAnswer = scheduler_pb2.CardAnswer -CustomScheduling = scheduler_pb2.CustomScheduling class Scheduler(SchedulerBaseWithLegacy): @@ -54,7 +53,7 @@ class Scheduler(SchedulerBaseWithLegacy): fetch_limit=fetch_limit, intraday_learning_only=intraday_learning_only ) - def describe_next_states(self, next_states: NextStates) -> Sequence[str]: + def describe_next_states(self, next_states: SchedulingStates) -> Sequence[str]: "Labels for each of the answer buttons." return self.col._backend.describe_next_states(next_states) @@ -65,8 +64,7 @@ class Scheduler(SchedulerBaseWithLegacy): self, *, card: Card, - states: NextStates, - custom_data: str, + states: SchedulingStates, rating: CardAnswer.Rating.V, ) -> CardAnswer: "Build input for answer_card()." @@ -85,7 +83,6 @@ class Scheduler(SchedulerBaseWithLegacy): card_id=card.id, current_state=states.current, new_state=new_state, - custom_data=custom_data, rating=rating, answered_at_millis=int_time(1000), milliseconds_taken=card.time_taken(capped=False), @@ -150,7 +147,7 @@ class Scheduler(SchedulerBaseWithLegacy): def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str: "Return the next interval for CARD as a string." - states = self.col._backend.get_next_card_states(card.id) + states = self.col._backend.get_scheduling_states(card.id) return self.col._backend.describe_next_states(states)[ease - 1] # Answering a card (legacy API) @@ -168,11 +165,9 @@ class Scheduler(SchedulerBaseWithLegacy): else: raise Exception("invalid ease") - states = self.col._backend.get_next_card_states(card.id) + states = self.col._backend.get_scheduling_states(card.id) changes = self.answer_card( - self.build_answer( - card=card, states=states, custom_data=card.custom_data, rating=rating - ) + self.build_answer(card=card, states=states, rating=rating) ) # tests assume card will be mutated, so we need to reload it @@ -224,7 +219,7 @@ class Scheduler(SchedulerBaseWithLegacy): def nextIvl(self, card: Card, ease: int) -> Any: "Don't use this - it is only required by tests, and will be moved in the future." - states = self.col._backend.get_next_card_states(card.id) + states = self.col._backend.get_scheduling_states(card.id) if ease == BUTTON_ONE: new_state = states.again elif ease == BUTTON_TWO: diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index f28236dd1..76a00f43e 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -27,7 +27,7 @@ from anki import hooks from anki._vendor import stringcase from anki.collection import OpChanges from anki.decks import DeckConfigsForUpdate, UpdateDeckConfigs -from anki.scheduler.v3 import CustomScheduling +from anki.scheduler_pb2 import SchedulingStates from anki.utils import dev_mode from aqt.changenotetype import ChangeNotetypeDialog from aqt.deckoptions import DeckOptionsDialog @@ -412,18 +412,18 @@ def update_deck_configs() -> bytes: return b"" -def get_custom_scheduling() -> bytes: - if scheduling := aqt.mw.reviewer.get_custom_scheduling(): - return scheduling.SerializeToString() +def get_scheduling_states() -> bytes: + if states := aqt.mw.reviewer.get_scheduling_states(): + return states.SerializeToString() else: return b"" -def set_custom_scheduling() -> bytes: +def set_scheduling_states() -> bytes: key = request.headers.get("key", "") - input = CustomScheduling() - input.ParseFromString(request.data) - aqt.mw.reviewer.set_custom_scheduling(key, input) + states = SchedulingStates() + states.ParseFromString(request.data) + aqt.mw.reviewer.set_scheduling_states(key, states) return b"" @@ -455,8 +455,8 @@ post_handler_list = [ congrats_info, get_deck_configs_for_update, update_deck_configs, - get_custom_scheduling, - set_custom_scheduling, + get_scheduling_states, + set_scheduling_states, change_notetype, import_csv, ] diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 09b3e32cb..4ad68a626 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -17,8 +17,9 @@ from anki import hooks from anki.cards import Card, CardId from anki.collection import Config, OpChanges, OpChangesWithCount from anki.scheduler.base import ScheduleCardsAsNew -from anki.scheduler.v3 import CardAnswer, CustomScheduling, NextStates, QueuedCards +from anki.scheduler.v3 import CardAnswer, QueuedCards from anki.scheduler.v3 import Scheduler as V3Scheduler +from anki.scheduler.v3 import SchedulingStates from anki.tags import MARKED_TAG from anki.types import assert_exhaustive from aqt import AnkiQt, gui_hooks @@ -74,22 +75,23 @@ def replay_audio(card: Card, question_side: bool) -> None: @dataclass class V3CardInfo: - """2021 test scheduler info. + """Stores the top of the card queue for the v3 scheduler. - next_states is copied from the top card on initialization, and can be - mutated to alter the default scheduling. + This includes current and potential next states of the displayed card, + which may be mutated by a user's custom scheduling. """ queued_cards: QueuedCards - next_states: NextStates - custom_data: str + states: SchedulingStates @staticmethod def from_queue(queued_cards: QueuedCards) -> V3CardInfo: + top_card = queued_cards.cards[0] + states = top_card.states + states.current.custom_data = top_card.card.custom_data return V3CardInfo( queued_cards=queued_cards, - next_states=queued_cards.cards[0].next_states, - custom_data=queued_cards.cards[0].card.custom_data, + states=states, ) def top_card(self) -> QueuedCards.QueuedCard: @@ -262,19 +264,18 @@ class Reviewer: self.card = Card(self.mw.col, backend_card=self._v3.top_card().card) self.card.start_timer() - def get_custom_scheduling(self) -> CustomScheduling | None: + def get_scheduling_states(self) -> SchedulingStates | None: if v3 := self._v3: - return CustomScheduling(states=v3.next_states, custom_data=v3.custom_data) + return v3.states else: return None - def set_custom_scheduling(self, key: str, scheduling: CustomScheduling) -> None: + def set_scheduling_states(self, key: str, states: SchedulingStates) -> None: if key != self._state_mutation_key: return if v3 := self._v3: - v3.next_states = scheduling.states - v3.custom_data = scheduling.custom_data + v3.states = states def _run_state_mutation_hook(self) -> None: if self._v3 and (js := self._state_mutation_js): @@ -436,8 +437,7 @@ class Reviewer: if (v3 := self._v3) and (sched := cast(V3Scheduler, self.mw.col.sched)): answer = sched.build_answer( card=self.card, - states=v3.next_states, - custom_data=v3.custom_data, + states=v3.states, rating=v3.rating_from_ease(ease), ) @@ -771,7 +771,7 @@ time = %(time)d; if v3 := self._v3: assert isinstance(self.mw.col.sched, V3Scheduler) - labels = self.mw.col.sched.describe_next_states(v3.next_states) + labels = self.mw.col.sched.describe_next_states(v3.states) else: labels = None diff --git a/rslib/src/backend/scheduler/answering.rs b/rslib/src/backend/scheduler/answering.rs index 7e69010cc..d10559fef 100644 --- a/rslib/src/backend/scheduler/answering.rs +++ b/rslib/src/backend/scheduler/answering.rs @@ -1,6 +1,8 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use std::mem; + use crate::{ pb, prelude::*, @@ -11,15 +13,17 @@ use crate::{ }; impl From for CardAnswer { - fn from(answer: pb::CardAnswer) -> Self { + fn from(mut answer: pb::CardAnswer) -> Self { + let mut new_state = mem::take(&mut answer.new_state).unwrap_or_default(); + let custom_data = mem::take(&mut new_state.custom_data); CardAnswer { card_id: CardId(answer.card_id), rating: answer.rating().into(), current_state: answer.current_state.unwrap_or_default().into(), - new_state: answer.new_state.unwrap_or_default().into(), + new_state: new_state.into(), answered_at: TimestampMillis(answer.answered_at_millis), milliseconds_taken: answer.milliseconds_taken, - custom_data: answer.custom_data, + custom_data, } } } @@ -39,7 +43,7 @@ impl From for pb::queued_cards::QueuedCard { fn from(queued_card: QueuedCard) -> Self { Self { card: Some(queued_card.card.into()), - next_states: Some(queued_card.next_states.into()), + states: Some(queued_card.states.into()), queue: match queued_card.kind { crate::scheduler::queue::QueueEntryKind::New => pb::queued_cards::Queue::New, crate::scheduler::queue::QueueEntryKind::Review => pb::queued_cards::Queue::Review, diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs index dceb6cd19..635ab0db3 100644 --- a/rslib/src/backend/scheduler/mod.rs +++ b/rslib/src/backend/scheduler/mod.rs @@ -11,7 +11,7 @@ use crate::{ prelude::*, scheduler::{ new::NewCardDueOrder, - states::{CardState, NextCardStates}, + states::{CardState, SchedulingStates}, }, stats::studied_today, }; @@ -168,14 +168,14 @@ impl SchedulerService for Backend { }) } - fn get_next_card_states(&self, input: pb::CardId) -> Result { + fn get_scheduling_states(&self, input: pb::CardId) -> Result { let cid: CardId = input.into(); - self.with_col(|col| col.get_next_card_states(cid)) + self.with_col(|col| col.get_scheduling_states(cid)) .map(Into::into) } - fn describe_next_states(&self, input: pb::NextCardStates) -> Result { - let states: NextCardStates = input.into(); + fn describe_next_states(&self, input: pb::SchedulingStates) -> Result { + let states: SchedulingStates = input.into(); self.with_col(|col| col.describe_next_states(states)) .map(Into::into) } diff --git a/rslib/src/backend/scheduler/states/mod.rs b/rslib/src/backend/scheduler/states/mod.rs index d21421fba..e627bd17e 100644 --- a/rslib/src/backend/scheduler/states/mod.rs +++ b/rslib/src/backend/scheduler/states/mod.rs @@ -12,12 +12,12 @@ mod review; use crate::{ pb, - scheduler::states::{CardState, NewState, NextCardStates, NormalState}, + scheduler::states::{CardState, NewState, NormalState, SchedulingStates}, }; -impl From for pb::NextCardStates { - fn from(choices: NextCardStates) -> Self { - pb::NextCardStates { +impl From for pb::SchedulingStates { + fn from(choices: SchedulingStates) -> Self { + pb::SchedulingStates { current: Some(choices.current.into()), again: Some(choices.again.into()), hard: Some(choices.hard.into()), @@ -27,9 +27,9 @@ impl From for pb::NextCardStates { } } -impl From for NextCardStates { - fn from(choices: pb::NextCardStates) -> Self { - NextCardStates { +impl From for SchedulingStates { + fn from(choices: pb::SchedulingStates) -> Self { + SchedulingStates { current: choices.current.unwrap_or_default().into(), again: choices.again.unwrap_or_default().into(), hard: choices.hard.unwrap_or_default().into(), @@ -46,6 +46,7 @@ impl From for pb::SchedulingState { CardState::Normal(state) => pb::scheduling_state::Value::Normal(state.into()), CardState::Filtered(state) => pb::scheduling_state::Value::Filtered(state.into()), }), + custom_data: None, } } } diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 003f64dbc..21ec1d3b0 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -13,7 +13,7 @@ use revlog::RevlogEntryPartial; use super::{ states::{ - steps::LearningSteps, CardState, FilteredState, NextCardStates, NormalState, StateContext, + steps::LearningSteps, CardState, FilteredState, NormalState, SchedulingStates, StateContext, }, timespan::answer_button_time_collapsible, timing::SchedTimingToday, @@ -41,7 +41,7 @@ pub struct CardAnswer { pub rating: Rating, pub answered_at: TimestampMillis, pub milliseconds_taken: u32, - pub custom_data: String, + pub custom_data: Option, } impl CardAnswer { @@ -189,7 +189,7 @@ impl Rating { impl Collection { /// Return the next states that will be applied for each answer button. - pub fn get_next_card_states(&mut self, cid: CardId) -> Result { + pub fn get_scheduling_states(&mut self, cid: CardId) -> Result { let card = self.storage.get_card(cid)?.ok_or(AnkiError::NotFound)?; let ctx = self.card_state_updater(card)?; let current = ctx.current_card_state(); @@ -198,7 +198,7 @@ impl Collection { } /// Describe the next intervals, to display on the answer buttons. - pub fn describe_next_states(&mut self, choices: NextCardStates) -> Result> { + pub fn describe_next_states(&mut self, choices: SchedulingStates) -> Result> { let collapse_time = self.learn_ahead_secs(); let now = TimestampSecs::now(); let timing = self.timing_for_timestamp(now)?; @@ -274,8 +274,10 @@ impl Collection { self.maybe_bury_siblings(&original, &updater.config)?; let timing = updater.timing; let mut card = updater.into_card(); - card.custom_data = answer.custom_data.clone(); - card.validate_custom_data()?; + if let Some(data) = answer.custom_data.take() { + card.custom_data = data; + card.validate_custom_data()?; + } self.update_card_inner(&mut card, original, usn)?; if answer.new_state.leeched() { self.add_leech_tag(card.note_id)?; @@ -411,18 +413,18 @@ pub mod test_helpers { fn answer(&mut self, get_state: F, rating: Rating) -> Result where - F: FnOnce(&NextCardStates) -> CardState, + F: FnOnce(&SchedulingStates) -> CardState, { let queued = self.get_next_card()?.unwrap(); - let new_state = get_state(&queued.next_states); + let new_state = get_state(&queued.states); self.answer_card(&mut CardAnswer { card_id: queued.card.id, - current_state: queued.next_states.current, + current_state: queued.states.current, new_state, rating, answered_at: TimestampMillis::now(), milliseconds_taken: 0, - custom_data: String::new(), + custom_data: None, })?; Ok(PostAnswerState { card_id: queued.card.id, @@ -456,7 +458,7 @@ mod test { }; fn current_state(col: &mut Collection, card_id: CardId) -> CardState { - col.get_next_card_states(card_id).unwrap().current + col.get_scheduling_states(card_id).unwrap().current } // make sure the 'current' state for a card matches the diff --git a/rslib/src/scheduler/answering/preview.rs b/rslib/src/scheduler/answering/preview.rs index 70c50850b..42387748d 100644 --- a/rslib/src/scheduler/answering/preview.rs +++ b/rslib/src/scheduler/answering/preview.rs @@ -70,7 +70,7 @@ mod test { col.add_or_update_deck(&mut filtered_deck)?; assert_eq!(col.rebuild_filtered_deck(filtered_deck.id)?.output, 1); - let next = col.get_next_card_states(c.id)?; + let next = col.get_scheduling_states(c.id)?; assert!(matches!( next.current, CardState::Filtered(FilteredState::Preview(_)) @@ -92,14 +92,14 @@ mod test { rating: Rating::Again, answered_at: TimestampMillis::now(), milliseconds_taken: 0, - custom_data: String::new(), + custom_data: None, })?; c = col.storage.get_card(c.id)?.unwrap(); assert_eq!(c.queue, CardQueue::PreviewRepeat); // hard - let next = col.get_next_card_states(c.id)?; + let next = col.get_scheduling_states(c.id)?; col.answer_card(&mut CardAnswer { card_id: c.id, current_state: next.current, @@ -107,13 +107,13 @@ mod test { rating: Rating::Hard, answered_at: TimestampMillis::now(), milliseconds_taken: 0, - custom_data: String::new(), + custom_data: None, })?; c = col.storage.get_card(c.id)?.unwrap(); assert_eq!(c.queue, CardQueue::PreviewRepeat); // good - let next = col.get_next_card_states(c.id)?; + let next = col.get_scheduling_states(c.id)?; col.answer_card(&mut CardAnswer { card_id: c.id, current_state: next.current, @@ -121,13 +121,13 @@ mod test { rating: Rating::Good, answered_at: TimestampMillis::now(), milliseconds_taken: 0, - custom_data: String::new(), + 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_next_card_states(c.id)?; + let next = col.get_scheduling_states(c.id)?; col.answer_card(&mut CardAnswer { card_id: c.id, current_state: next.current, @@ -135,7 +135,7 @@ mod test { rating: Rating::Easy, answered_at: TimestampMillis::now(), milliseconds_taken: 0, - custom_data: String::new(), + custom_data: None, })?; c = col.storage.get_card(c.id)?.unwrap(); assert_eq!(c.queue, CardQueue::DayLearn); diff --git a/rslib/src/scheduler/queue/mod.rs b/rslib/src/scheduler/queue/mod.rs index 5bbacca5a..308220364 100644 --- a/rslib/src/scheduler/queue/mod.rs +++ b/rslib/src/scheduler/queue/mod.rs @@ -15,7 +15,7 @@ pub(crate) use learning::LearningQueueEntry; pub(crate) use main::{MainQueueEntry, MainQueueEntryKind}; use self::undo::QueueUpdate; -use super::{states::NextCardStates, timing::SchedTimingToday}; +use super::{states::SchedulingStates, timing::SchedTimingToday}; use crate::{prelude::*, timestamp::TimestampSecs}; #[derive(Debug)] @@ -49,7 +49,7 @@ impl Counts { pub struct QueuedCard { pub card: Card, pub kind: QueueEntryKind, - pub next_states: NextCardStates, + pub states: SchedulingStates, } #[derive(Debug)] @@ -96,11 +96,11 @@ impl Collection { } // fixme: pass in card instead of id - let next_states = self.get_next_card_states(card.id)?; + let next_states = self.get_scheduling_states(card.id)?; Ok(QueuedCard { card, - next_states, + states: next_states, kind: entry.kind(), }) }) diff --git a/rslib/src/scheduler/states/filtered.rs b/rslib/src/scheduler/states/filtered.rs index 0845360d6..18b07eeba 100644 --- a/rslib/src/scheduler/states/filtered.rs +++ b/rslib/src/scheduler/states/filtered.rs @@ -2,7 +2,8 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{ - IntervalKind, NextCardStates, PreviewState, ReschedulingFilterState, ReviewState, StateContext, + IntervalKind, PreviewState, ReschedulingFilterState, ReviewState, SchedulingStates, + StateContext, }; use crate::revlog::RevlogReviewKind; @@ -27,7 +28,7 @@ impl FilteredState { } } - pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates { + pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates { match self { FilteredState::Preview(state) => state.next_states(ctx), FilteredState::Rescheduling(state) => state.next_states(ctx), diff --git a/rslib/src/scheduler/states/learning.rs b/rslib/src/scheduler/states/learning.rs index 57223bc36..7215a931e 100644 --- a/rslib/src/scheduler/states/learning.rs +++ b/rslib/src/scheduler/states/learning.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use super::{interval_kind::IntervalKind, CardState, NextCardStates, ReviewState, StateContext}; +use super::{interval_kind::IntervalKind, CardState, ReviewState, SchedulingStates, StateContext}; use crate::revlog::RevlogReviewKind; #[derive(Debug, Clone, Copy, PartialEq)] @@ -19,8 +19,8 @@ impl LearnState { RevlogReviewKind::Learning } - pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates { - NextCardStates { + pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates { + SchedulingStates { current: self.into(), again: self.answer_again(ctx).into(), hard: self.answer_hard(ctx), diff --git a/rslib/src/scheduler/states/mod.rs b/rslib/src/scheduler/states/mod.rs index d85784d8a..75a95bd33 100644 --- a/rslib/src/scheduler/states/mod.rs +++ b/rslib/src/scheduler/states/mod.rs @@ -47,7 +47,7 @@ impl CardState { } } - pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates { + pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates { match self { CardState::Normal(state) => state.next_states(ctx), CardState::Filtered(state) => state.next_states(ctx), @@ -139,7 +139,7 @@ impl<'a> StateContext<'a> { } #[derive(Debug, Clone)] -pub struct NextCardStates { +pub struct SchedulingStates { pub current: CardState, pub again: CardState, pub hard: CardState, diff --git a/rslib/src/scheduler/states/normal.rs b/rslib/src/scheduler/states/normal.rs index 7043b24dc..3b9f776a2 100644 --- a/rslib/src/scheduler/states/normal.rs +++ b/rslib/src/scheduler/states/normal.rs @@ -2,7 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{ - interval_kind::IntervalKind, LearnState, NewState, NextCardStates, RelearnState, ReviewState, + interval_kind::IntervalKind, LearnState, NewState, RelearnState, ReviewState, SchedulingStates, StateContext, }; use crate::revlog::RevlogReviewKind; @@ -34,7 +34,7 @@ impl NormalState { } } - pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates { + pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates { match self { NormalState::New(_) => { // New state acts like answering a failed learning card @@ -44,7 +44,7 @@ impl NormalState { } .next_states(ctx); // .. but with current as New, not Learning - NextCardStates { + SchedulingStates { current: self.into(), ..next_states } diff --git a/rslib/src/scheduler/states/preview_filter.rs b/rslib/src/scheduler/states/preview_filter.rs index 055df9d65..6303f8f0e 100644 --- a/rslib/src/scheduler/states/preview_filter.rs +++ b/rslib/src/scheduler/states/preview_filter.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use super::{IntervalKind, NextCardStates, StateContext}; +use super::{IntervalKind, SchedulingStates, StateContext}; use crate::revlog::RevlogReviewKind; #[derive(Debug, Clone, Copy, PartialEq)] @@ -19,8 +19,8 @@ impl PreviewState { RevlogReviewKind::Filtered } - pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates { - NextCardStates { + pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates { + SchedulingStates { current: self.into(), again: PreviewState { scheduled_secs: ctx.preview_step * 60, diff --git a/rslib/src/scheduler/states/relearning.rs b/rslib/src/scheduler/states/relearning.rs index a295717db..d42e643e1 100644 --- a/rslib/src/scheduler/states/relearning.rs +++ b/rslib/src/scheduler/states/relearning.rs @@ -2,7 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{ - interval_kind::IntervalKind, CardState, LearnState, NextCardStates, ReviewState, StateContext, + interval_kind::IntervalKind, CardState, LearnState, ReviewState, SchedulingStates, StateContext, }; use crate::revlog::RevlogReviewKind; @@ -21,8 +21,8 @@ impl RelearnState { RevlogReviewKind::Relearning } - pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates { - NextCardStates { + pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates { + SchedulingStates { current: self.into(), again: self.answer_again(ctx), hard: self.answer_hard(ctx), diff --git a/rslib/src/scheduler/states/rescheduling_filter.rs b/rslib/src/scheduler/states/rescheduling_filter.rs index 8a2216eea..0520d01bd 100644 --- a/rslib/src/scheduler/states/rescheduling_filter.rs +++ b/rslib/src/scheduler/states/rescheduling_filter.rs @@ -2,7 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{ - interval_kind::IntervalKind, normal::NormalState, CardState, NextCardStates, StateContext, + interval_kind::IntervalKind, normal::NormalState, CardState, SchedulingStates, StateContext, }; use crate::revlog::RevlogReviewKind; @@ -20,10 +20,10 @@ impl ReschedulingFilterState { self.original_state.revlog_kind() } - pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates { + pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates { let normal = self.original_state.next_states(ctx); if ctx.in_filtered_deck { - NextCardStates { + SchedulingStates { current: self.into(), again: maybe_wrap(normal.again), hard: maybe_wrap(normal.hard), diff --git a/rslib/src/scheduler/states/review.rs b/rslib/src/scheduler/states/review.rs index 11dad02c6..5bcd41c4e 100644 --- a/rslib/src/scheduler/states/review.rs +++ b/rslib/src/scheduler/states/review.rs @@ -2,7 +2,8 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{ - interval_kind::IntervalKind, CardState, LearnState, NextCardStates, RelearnState, StateContext, + interval_kind::IntervalKind, CardState, LearnState, RelearnState, SchedulingStates, + StateContext, }; use crate::revlog::RevlogReviewKind; @@ -52,10 +53,10 @@ impl ReviewState { } } - pub(crate) fn next_states(self, ctx: &StateContext) -> NextCardStates { + pub(crate) fn next_states(self, ctx: &StateContext) -> SchedulingStates { let (hard_interval, good_interval, easy_interval) = self.passing_review_intervals(ctx); - NextCardStates { + SchedulingStates { current: self.into(), again: self.answer_again(ctx), hard: self.answer_hard(hard_interval).into(), diff --git a/ts/reviewer/answering.ts b/ts/reviewer/answering.ts index f3a633763..c3d86dfff 100644 --- a/ts/reviewer/answering.ts +++ b/ts/reviewer/answering.ts @@ -4,38 +4,60 @@ import { postRequest } from "../lib/postrequest"; import { Scheduler } from "../lib/proto"; -async function getCustomScheduling(): Promise { - return Scheduler.CustomScheduling.decode( - await postRequest("/_anki/getCustomScheduling", ""), +interface CustomDataStates { + again: Record; + hard: Record; + good: Record; + easy: Record; +} + +async function getSchedulingStates(): Promise { + return Scheduler.SchedulingStates.decode( + await postRequest("/_anki/getSchedulingStates", ""), ); } -async function setCustomScheduling( +async function setSchedulingStates( key: string, - scheduling: Scheduler.CustomScheduling, + states: Scheduler.SchedulingStates, ): Promise { - const bytes = Scheduler.CustomScheduling.encode(scheduling).finish(); - await postRequest("/_anki/setCustomScheduling", bytes, { key }); + const bytes = Scheduler.SchedulingStates.encode(states).finish(); + await postRequest("/_anki/setSchedulingStates", bytes, { key }); +} + +function unpackCustomData(states: Scheduler.SchedulingStates): CustomDataStates { + const toObject = (s: string): Record => { + try { + return JSON.parse(s); + } catch { + return {}; + } + }; + return { + again: toObject(states.current!.customData!), + hard: toObject(states.current!.customData!), + good: toObject(states.current!.customData!), + easy: toObject(states.current!.customData!), + }; +} + +function packCustomData( + states: Scheduler.SchedulingStates, + customData: CustomDataStates, +) { + states.again!.customData = JSON.stringify(customData.again); + states.hard!.customData = JSON.stringify(customData.hard); + states.good!.customData = JSON.stringify(customData.good); + states.easy!.customData = JSON.stringify(customData.easy); } export async function mutateNextCardStates( key: string, - mutator: ( - states: Scheduler.NextCardStates, - customData: Record, - ) => void, + mutator: (states: Scheduler.SchedulingStates, customData: CustomDataStates) => void, ): Promise { - const scheduling = await getCustomScheduling(); - let customData = {}; - try { - customData = JSON.parse(scheduling.customData); - } catch { - // can't be parsed - } - - mutator(scheduling.states!, customData); - - scheduling.customData = JSON.stringify(customData); - - await setCustomScheduling(key, scheduling); + const states = await getSchedulingStates(); + const customData = unpackCustomData(states); + mutator(states, customData); + packCustomData(states, customData); + await setSchedulingStates(key, states); }