mirror of
https://github.com/ankitects/anki.git
synced 2025-12-14 15:20:58 -05:00
Ensure state mutator runs after card is rendered (#2421)
* Ensure state mutator runs after card is rendered * Ensure ease buttons only show when states are ready * Pass context into states mutator * Revert queuing of state mutator hook Now that context data is exposed users shouldn't rely on the question having been rendered anymore. * Use callbacks instead of signals and timeout ... to track whether the states mutator ran or failed. * Make mutator async * Remove State enum * Reduce requests and compute seed on backend
This commit is contained in:
parent
de9e2cfc40
commit
bd88c6d352
8 changed files with 84 additions and 19 deletions
|
|
@ -108,6 +108,7 @@ message QueuedCards {
|
|||
cards.Card card = 1;
|
||||
Queue queue = 2;
|
||||
SchedulingStates states = 3;
|
||||
SchedulingContext context = 4;
|
||||
}
|
||||
|
||||
repeated QueuedCard cards = 1;
|
||||
|
|
@ -282,6 +283,16 @@ message CustomStudyRequest {
|
|||
}
|
||||
}
|
||||
|
||||
message SchedulingContext {
|
||||
string deck_name = 1;
|
||||
uint64 seed = 2;
|
||||
}
|
||||
|
||||
message SchedulingStatesWithContext {
|
||||
SchedulingStates states = 1;
|
||||
SchedulingContext context = 2;
|
||||
}
|
||||
|
||||
message CustomStudyDefaultsRequest {
|
||||
int64 deck_id = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ from anki.utils import int_time
|
|||
QueuedCards = scheduler_pb2.QueuedCards
|
||||
SchedulingState = scheduler_pb2.SchedulingState
|
||||
SchedulingStates = scheduler_pb2.SchedulingStates
|
||||
SchedulingContext = scheduler_pb2.SchedulingContext
|
||||
SchedulingStatesWithContext = scheduler_pb2.SchedulingStatesWithContext
|
||||
CardAnswer = scheduler_pb2.CardAnswer
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import aqt.operations
|
|||
from anki import hooks
|
||||
from anki.collection import OpChanges
|
||||
from anki.decks import UpdateDeckConfigs
|
||||
from anki.scheduler.v3 import SchedulingStatesWithContext
|
||||
from anki.scheduler_pb2 import SchedulingStates
|
||||
from anki.utils import dev_mode
|
||||
from aqt.changenotetype import ChangeNotetypeDialog
|
||||
|
|
@ -408,11 +409,11 @@ def update_deck_configs() -> bytes:
|
|||
return b""
|
||||
|
||||
|
||||
def get_scheduling_states() -> bytes:
|
||||
if states := aqt.mw.reviewer.get_scheduling_states():
|
||||
return states.SerializeToString()
|
||||
else:
|
||||
return b""
|
||||
def get_scheduling_states_with_context() -> bytes:
|
||||
return SchedulingStatesWithContext(
|
||||
states=aqt.mw.reviewer.get_scheduling_states(),
|
||||
context=aqt.mw.reviewer.get_scheduling_context(),
|
||||
).SerializeToString()
|
||||
|
||||
|
||||
def set_scheduling_states() -> bytes:
|
||||
|
|
@ -451,7 +452,7 @@ post_handler_list = [
|
|||
congrats_info,
|
||||
get_deck_configs_for_update,
|
||||
update_deck_configs,
|
||||
get_scheduling_states,
|
||||
get_scheduling_states_with_context,
|
||||
set_scheduling_states,
|
||||
change_notetype,
|
||||
import_csv,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from anki.collection import Config, OpChanges, OpChangesWithCount
|
|||
from anki.scheduler.base import ScheduleCardsAsNew
|
||||
from anki.scheduler.v3 import CardAnswer, QueuedCards
|
||||
from anki.scheduler.v3 import Scheduler as V3Scheduler
|
||||
from anki.scheduler.v3 import SchedulingStates
|
||||
from anki.scheduler.v3 import SchedulingContext, SchedulingStates
|
||||
from anki.tags import MARKED_TAG
|
||||
from anki.types import assert_exhaustive
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
|
|
@ -83,6 +83,7 @@ class V3CardInfo:
|
|||
|
||||
queued_cards: QueuedCards
|
||||
states: SchedulingStates
|
||||
context: SchedulingContext
|
||||
|
||||
@staticmethod
|
||||
def from_queue(queued_cards: QueuedCards) -> V3CardInfo:
|
||||
|
|
@ -90,8 +91,7 @@ class V3CardInfo:
|
|||
states = top_card.states
|
||||
states.current.custom_data = top_card.card.custom_data
|
||||
return V3CardInfo(
|
||||
queued_cards=queued_cards,
|
||||
states=states,
|
||||
queued_cards=queued_cards, states=states, context=top_card.context
|
||||
)
|
||||
|
||||
def top_card(self) -> QueuedCards.QueuedCard:
|
||||
|
|
@ -143,6 +143,7 @@ class Reviewer:
|
|||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||
self._card_info = ReviewerCardInfo(self.mw)
|
||||
self._previous_card_info = PreviousReviewerCardInfo(self.mw)
|
||||
self._states_mutated = True
|
||||
hooks.card_did_leech.append(self.onLeech)
|
||||
|
||||
def show(self) -> None:
|
||||
|
|
@ -267,8 +268,12 @@ class Reviewer:
|
|||
def get_scheduling_states(self) -> SchedulingStates | None:
|
||||
if v3 := self._v3:
|
||||
return v3.states
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_scheduling_context(self) -> SchedulingContext | None:
|
||||
if v3 := self._v3:
|
||||
return v3.context
|
||||
return None
|
||||
|
||||
def set_scheduling_states(self, key: str, states: SchedulingStates) -> None:
|
||||
if key != self._state_mutation_key:
|
||||
|
|
@ -278,9 +283,16 @@ class Reviewer:
|
|||
v3.states = states
|
||||
|
||||
def _run_state_mutation_hook(self) -> None:
|
||||
def on_eval(result: Any) -> None:
|
||||
if result is None:
|
||||
# eval failed, usually a syntax error
|
||||
self._states_mutated = True
|
||||
|
||||
if self._v3 and (js := self._state_mutation_js):
|
||||
self.web.eval(
|
||||
f"anki.mutateNextCardStates('{self._state_mutation_key}', (states, customData) => {{ {js} }})"
|
||||
self._states_mutated = False
|
||||
self.web.evalWithCallback(
|
||||
RUN_STATE_MUTATION.format(key=self._state_mutation_key, js=js),
|
||||
on_eval,
|
||||
)
|
||||
|
||||
# Audio
|
||||
|
|
@ -546,6 +558,8 @@ class Reviewer:
|
|||
play_clicked_audio(url, self.card)
|
||||
elif url.startswith("updateToolbar"):
|
||||
self.mw.toolbarWeb.update_background_image()
|
||||
elif url == "statesMutated":
|
||||
self._states_mutated = True
|
||||
else:
|
||||
print("unrecognized anki link:", url)
|
||||
|
||||
|
|
@ -692,6 +706,9 @@ time = %(time)d;
|
|||
self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime))
|
||||
|
||||
def _showEaseButtons(self) -> None:
|
||||
if not self._states_mutated:
|
||||
self.mw.progress.single_shot(50, self._showEaseButtons)
|
||||
return
|
||||
middle = self._answerButtons()
|
||||
self.bottom.web.eval(f"showAnswer({json.dumps(middle)});")
|
||||
|
||||
|
|
@ -1034,3 +1051,9 @@ time = %(time)d;
|
|||
onDelete = delete_current_note
|
||||
onMark = toggle_mark_on_current_note
|
||||
setFlag = set_flag_on_current_card
|
||||
|
||||
|
||||
RUN_STATE_MUTATION = """
|
||||
anki.mutateNextCardStates('{key}', async (states, customData, ctx) => {{ {js} }})
|
||||
.finally(() => bridgeCommand('statesMutated'));
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ impl From<QueuedCard> for pb::scheduler::queued_cards::QueuedCard {
|
|||
Self {
|
||||
card: Some(queued_card.card.into()),
|
||||
states: Some(queued_card.states.into()),
|
||||
context: Some(queued_card.context),
|
||||
queue: match queued_card.kind {
|
||||
crate::scheduler::queue::QueueEntryKind::New => {
|
||||
pb::scheduler::queued_cards::Queue::New
|
||||
|
|
|
|||
|
|
@ -184,6 +184,13 @@ impl Card {
|
|||
.max(1) as u32;
|
||||
(remaining != new_remaining).then_some(new_remaining)
|
||||
}
|
||||
|
||||
/// Supposedly unique across all reviews in the collection.
|
||||
pub fn review_seed(&self) -> u64 {
|
||||
(self.id.0 as u64)
|
||||
.rotate_left(8)
|
||||
.wrapping_add(self.reps as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub(crate) use main::MainQueueEntryKind;
|
|||
use self::undo::QueueUpdate;
|
||||
use super::states::SchedulingStates;
|
||||
use super::timing::SchedTimingToday;
|
||||
use crate::pb::scheduler::SchedulingContext;
|
||||
use crate::prelude::*;
|
||||
use crate::timestamp::TimestampSecs;
|
||||
|
||||
|
|
@ -56,6 +57,7 @@ pub struct QueuedCard {
|
|||
pub card: Card,
|
||||
pub kind: QueueEntryKind,
|
||||
pub states: SchedulingStates,
|
||||
pub context: SchedulingContext,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -116,6 +118,7 @@ impl Collection {
|
|||
let next_states = self.get_scheduling_states(card.id)?;
|
||||
|
||||
Ok(QueuedCard {
|
||||
context: SchedulingContext::new(self, &card)?,
|
||||
card,
|
||||
states: next_states,
|
||||
kind: entry.kind(),
|
||||
|
|
@ -131,6 +134,18 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
impl SchedulingContext {
|
||||
fn new(col: &mut Collection, card: &Card) -> Result<Self> {
|
||||
Ok(Self {
|
||||
deck_name: col
|
||||
.get_deck(card.deck_id)?
|
||||
.or_not_found(card.deck_id)?
|
||||
.human_name(),
|
||||
seed: card.review_seed(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CardQueues {
|
||||
/// An iterator over the card queues, in the order the cards will
|
||||
/// be presented.
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ interface CustomDataStates {
|
|||
easy: Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function getSchedulingStates(): Promise<Scheduler.SchedulingStates> {
|
||||
return Scheduler.SchedulingStates.decode(
|
||||
await postRequest("/_anki/getSchedulingStates", ""),
|
||||
async function getSchedulingStatesWithContext(): Promise<Scheduler.SchedulingStatesWithContext> {
|
||||
return Scheduler.SchedulingStatesWithContext.decode(
|
||||
await postRequest("/_anki/getSchedulingStatesWithContext", ""),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -53,11 +53,16 @@ function packCustomData(
|
|||
|
||||
export async function mutateNextCardStates(
|
||||
key: string,
|
||||
mutator: (states: Scheduler.SchedulingStates, customData: CustomDataStates) => void,
|
||||
mutator: (
|
||||
states: Scheduler.SchedulingStates,
|
||||
customData: CustomDataStates,
|
||||
ctx: Scheduler.SchedulingContext,
|
||||
) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const states = await getSchedulingStates();
|
||||
const statesWithContext = await getSchedulingStatesWithContext();
|
||||
const states = statesWithContext.states!;
|
||||
const customData = unpackCustomData(states);
|
||||
mutator(states, customData);
|
||||
await mutator(states, customData, statesWithContext.context!);
|
||||
packCustomData(states, customData);
|
||||
await setSchedulingStates(key, states);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue