mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
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:
parent
0bcb3a3564
commit
e39fb74e82
19 changed files with 157 additions and 134 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue