Enable state-dependent custom scheduling data (#2049)

* Enable state-dependent custom scheduling data

* Next(Card)States -> SchedulingStates

The fact that `current` was included in `next` always bothered me,
and custom data is part of the card state, so that was a bit confusing
too.

* Store custom_data in SchedulingState

* Make custom_data optional when answering

Avoids having to send it 4 extra times to the frontend, and avoids the
legacy answerCard() API clobbering the stored data.

Co-authored-by: Damien Elmes <gpg@ankiweb.net>
This commit is contained in:
RumovZ 2022-09-05 08:48:01 +02:00 committed by GitHub
parent 0bcb3a3564
commit e39fb74e82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 157 additions and 134 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<pb::CardAnswer> 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<QueuedCard> 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,

View file

@ -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<pb::NextCardStates> {
fn get_scheduling_states(&self, input: pb::CardId) -> Result<pb::SchedulingStates> {
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<pb::StringList> {
let states: NextCardStates = input.into();
fn describe_next_states(&self, input: pb::SchedulingStates) -> Result<pb::StringList> {
let states: SchedulingStates = input.into();
self.with_col(|col| col.describe_next_states(states))
.map(Into::into)
}

View file

@ -12,12 +12,12 @@ mod review;
use crate::{
pb,
scheduler::states::{CardState, NewState, NextCardStates, NormalState},
scheduler::states::{CardState, NewState, NormalState, SchedulingStates},
};
impl From<NextCardStates> for pb::NextCardStates {
fn from(choices: NextCardStates) -> Self {
pb::NextCardStates {
impl From<SchedulingStates> 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<NextCardStates> for pb::NextCardStates {
}
}
impl From<pb::NextCardStates> for NextCardStates {
fn from(choices: pb::NextCardStates) -> Self {
NextCardStates {
impl From<pb::SchedulingStates> 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<CardState> 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,
}
}
}

View file

@ -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<String>,
}
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<NextCardStates> {
pub fn get_scheduling_states(&mut self, cid: CardId) -> Result<SchedulingStates> {
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<Vec<String>> {
pub fn describe_next_states(&mut self, choices: SchedulingStates) -> Result<Vec<String>> {
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<F>(&mut self, get_state: F, rating: Rating) -> Result<PostAnswerState>
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,38 +4,60 @@
import { postRequest } from "../lib/postrequest";
import { Scheduler } from "../lib/proto";
async function getCustomScheduling(): Promise<Scheduler.CustomScheduling> {
return Scheduler.CustomScheduling.decode(
await postRequest("/_anki/getCustomScheduling", ""),
interface CustomDataStates {
again: Record<string, unknown>;
hard: Record<string, unknown>;
good: Record<string, unknown>;
easy: Record<string, unknown>;
}
async function getSchedulingStates(): Promise<Scheduler.SchedulingStates> {
return Scheduler.SchedulingStates.decode(
await postRequest("/_anki/getSchedulingStates", ""),
);
}
async function setCustomScheduling(
async function setSchedulingStates(
key: string,
scheduling: Scheduler.CustomScheduling,
states: Scheduler.SchedulingStates,
): Promise<void> {
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<string, unknown> => {
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<string, unknown>,
) => void,
mutator: (states: Scheduler.SchedulingStates, customData: CustomDataStates) => void,
): Promise<void> {
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);
}