diff --git a/pylib/.pylintrc b/pylib/.pylintrc index 27c32f9d0..c12747171 100644 --- a/pylib/.pylintrc +++ b/pylib/.pylintrc @@ -7,7 +7,8 @@ ignored-classes= BrowserColumns, BrowserRow, FormatTimespanIn, - AnswerCardIn, + CardAnswer, + QueuedCards, UnburyDeckIn, BuryOrSuspendCardsIn, NoteIsDuplicateOrEmptyOut, diff --git a/pylib/anki/_backend/genbackend.py b/pylib/anki/_backend/genbackend.py index 7c79a3ebb..018ee45c0 100755 --- a/pylib/anki/_backend/genbackend.py +++ b/pylib/anki/_backend/genbackend.py @@ -34,7 +34,12 @@ LABEL_REQUIRED = 2 LABEL_REPEATED = 3 # messages we don't want to unroll in codegen -SKIP_UNROLL_INPUT = {"TranslateString", "SetPreferences", "UpdateDeckConfigs"} +SKIP_UNROLL_INPUT = { + "TranslateString", + "SetPreferences", + "UpdateDeckConfigs", + "AnswerCard", +} SKIP_UNROLL_OUTPUT = {"GetPreferences"} SKIP_DECODE = {"Graphs", "GetGraphPreferences"} diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 491df5bc0..eb4b43d17 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -28,6 +28,7 @@ from anki.sound import AVTag # types CardId = NewType("CardId", int) +BackendCard = _pb.Card class Card: @@ -43,7 +44,10 @@ class Card: type: CardType def __init__( - self, col: anki.collection.Collection, id: Optional[CardId] = None + self, + col: anki.collection.Collection, + id: Optional[CardId] = None, + backend_card: Optional[BackendCard] = None, ) -> None: self.col = col.weakref() self.timerStarted = None @@ -52,6 +56,8 @@ class Card: # existing card self.id = id self.load() + elif backend_card: + self._load_from_backend_card(backend_card) else: # new card with defaults self._load_from_backend_card(_pb.Card()) diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py index 6a3a79d0f..a1489a984 100644 --- a/pylib/anki/scheduler/v3.py +++ b/pylib/anki/scheduler/v3.py @@ -11,18 +11,21 @@ as '2' internally. from __future__ import annotations -from typing import Tuple, Union +from typing import Literal, Sequence, Tuple, Union import anki._backend.backend_pb2 as _pb -from anki.cards import Card +from anki.cards import Card, CardId +from anki.collection import OpChanges from anki.consts import * from anki.scheduler.base import CongratsInfo from anki.scheduler.legacy import SchedulerBaseWithLegacy from anki.types import assert_exhaustive from anki.utils import intTime -QueuedCards = _pb.GetQueuedCardsOut.QueuedCards +QueuedCards = _pb.QueuedCards SchedulingState = _pb.SchedulingState +NextStates = _pb.NextCardStates +CardAnswer = _pb.CardAnswer class Scheduler(SchedulerBaseWithLegacy): @@ -34,16 +37,13 @@ class Scheduler(SchedulerBaseWithLegacy): # Fetching the next card ########################################################################## - def reset(self) -> None: - # backend automatically resets queues as operations are performed - pass - def get_queued_cards( self, *, fetch_limit: int = 1, intraday_learning_only: bool = False, ) -> Union[QueuedCards, CongratsInfo]: + "Returns one or more card ids, or the congratulations screen info." info = self.col._backend.get_queued_cards( fetch_limit=fetch_limit, intraday_learning_only=intraday_learning_only ) @@ -56,6 +56,57 @@ class Scheduler(SchedulerBaseWithLegacy): assert_exhaustive(kind) assert False + def next_states(self, card_id: CardId) -> NextStates: + "New states corresponding to each answer button press." + return self.col._backend.get_next_card_states(card_id) + + def describe_next_states(self, next_states: NextStates) -> Sequence[str]: + "Labels for each of the answer buttons." + return self.col._backend.describe_next_states(next_states) + + # Answering a card + ########################################################################## + + def build_answer( + self, *, card: Card, states: NextStates, rating: CardAnswer.Rating.V + ) -> CardAnswer: + "Build input for answer_card()." + if rating == CardAnswer.AGAIN: + new_state = states.again + elif rating == CardAnswer.HARD: + new_state = states.hard + elif rating == CardAnswer.GOOD: + new_state = states.good + elif rating == CardAnswer.EASY: + new_state = states.easy + else: + assert False, "invalid rating" + + return CardAnswer( + card_id=card.id, + current_state=states.current, + new_state=new_state, + rating=rating, + answered_at_millis=intTime(1000), + milliseconds_taken=card.timeTaken(), + ) + + def answer_card(self, input: CardAnswer) -> OpChanges: + "Update card to provided state, and remove it from queue." + self.reps += 1 + return self.col._backend.answer_card(input=input) + + def state_is_leech(self, new_state: SchedulingState) -> bool: + "True if new state marks the card as a leech." + return self.col._backend.state_is_leech(new_state) + + # Fetching the next card (legacy API) + ########################################################################## + + def reset(self) -> None: + # backend automatically resets queues as operations are performed + pass + def getCard(self) -> Optional[Card]: """Fetch the next card from the queue. None if finished.""" response = self.get_queued_cards() @@ -94,53 +145,46 @@ class Scheduler(SchedulerBaseWithLegacy): def reviewCount(self) -> int: return self.counts()[2] - # Answering a card + def countIdx(self, card: Card) -> int: + if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW): + return QUEUE_TYPE_LRN + return card.queue + + def answerButtons(self, card: Card) -> int: + return 4 + + 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) + return self.col._backend.describe_next_states(states)[ease - 1] + + # Answering a card (legacy API) ########################################################################## - def answerCard(self, card: Card, ease: int) -> None: - assert 1 <= ease <= 4 - assert 0 <= card.queue <= 4 - - self._answerCard(card, ease) - - self.reps += 1 - - def _answerCard(self, card: Card, ease: int) -> _pb.SchedulingState: - states = self.col._backend.get_next_card_states(card.id) + def answerCard(self, card: Card, ease: Literal[1, 2, 3, 4]) -> OpChanges: if ease == BUTTON_ONE: - new_state = states.again - rating = _pb.AnswerCardIn.AGAIN + rating = CardAnswer.AGAIN elif ease == BUTTON_TWO: - new_state = states.hard - rating = _pb.AnswerCardIn.HARD + rating = CardAnswer.HARD elif ease == BUTTON_THREE: - new_state = states.good - rating = _pb.AnswerCardIn.GOOD + rating = CardAnswer.GOOD elif ease == BUTTON_FOUR: - new_state = states.easy - rating = _pb.AnswerCardIn.EASY + rating = CardAnswer.EASY else: assert False, "invalid ease" - self.col._backend.answer_card( - card_id=card.id, - current_state=states.current, - new_state=new_state, - rating=rating, - answered_at_millis=intTime(1000), - milliseconds_taken=card.timeTaken(), + changes = self.answer_card( + self.build_answer( + card=card, states=self.next_states(card_id=card.id), rating=rating + ) ) - # fixme: tests assume card will be mutated, so we need to reload it + # tests assume card will be mutated, so we need to reload it card.load() - return new_state + return changes - def state_is_leech(self, new_state: SchedulingState) -> bool: - "True if new state marks the card as a leech." - return self.col._backend.state_is_leech(new_state) - - # Next times + # Next times (legacy API) ########################################################################## # fixme: move these into tests_schedv2 in the future @@ -195,19 +239,3 @@ class Scheduler(SchedulerBaseWithLegacy): assert False, "invalid ease" return self._interval_for_state(new_state) - - # Review-related UI helpers - ########################################################################## - - def countIdx(self, card: Card) -> int: - if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW): - return QUEUE_TYPE_LRN - return card.queue - - def answerButtons(self, card: Card) -> int: - return 4 - - 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) - return self.col._backend.describe_next_states(states)[ease - 1] diff --git a/qt/.pylintrc b/qt/.pylintrc index 8e8d07dc0..6eae30507 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -12,6 +12,8 @@ ignored-classes= Config, OpChanges, UnburyDeckIn, + CardAnswer, + QueuedCards, [REPORTS] output-format=colorized diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index e59f1d450..6b2eb0353 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -81,7 +81,7 @@ class DeckBrowser: def op_executed( self, changes: OpChanges, handler: Optional[object], focused: bool ) -> bool: - if changes.study_queues and handler is not self: + if changes.reviewer and handler is not self: self._refresh_needed = True if focused: diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py index ec5e27531..087b1475d 100644 --- a/qt/aqt/operations/scheduling.py +++ b/qt/aqt/operations/scheduling.py @@ -9,6 +9,7 @@ import aqt from anki.cards import CardId from anki.collection import ( CARD_TYPE_NEW, + Collection, Config, OpChanges, OpChangesWithCount, @@ -17,6 +18,8 @@ from anki.collection import ( from anki.decks import DeckId from anki.notes import NoteId from anki.scheduler import FilteredDeckForUpdate, UnburyDeck +from anki.scheduler.v3 import CardAnswer +from anki.scheduler.v3 import Scheduler as V3Scheduler from aqt.operations import CollectionOp from aqt.qt import * from aqt.utils import disable_help_button, getText, tooltip, tr @@ -207,3 +210,15 @@ def unbury_deck( return CollectionOp( parent, lambda col: col.sched.unbury_deck(deck_id=deck_id, mode=mode) ) + + +def answer_card( + *, + parent: QWidget, + answer: CardAnswer, +) -> CollectionOp[OpChanges]: + def answer_v3(col: Collection) -> OpChanges: + assert isinstance(col.sched, V3Scheduler) + return col.sched.answer_card(answer) + + return CollectionOp(parent, answer_v3) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index d853f6de0..bda4f5632 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -73,7 +73,7 @@ class Overview: def op_executed( self, changes: OpChanges, handler: Optional[object], focused: bool ) -> bool: - if changes.study_queues: + if changes.reviewer: self._refresh_needed = True if focused: diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 7d5c36a30..be75661b2 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -8,20 +8,25 @@ import html import json import re import unicodedata as ucd +from dataclasses import dataclass from enum import Enum, auto -from typing import Any, Callable, List, Match, Optional, Sequence, Tuple, Union +from typing import Any, Callable, List, Match, Optional, Sequence, Tuple, Union, cast from PyQt5.QtCore import Qt from anki import hooks from anki.cards import Card, CardId from anki.collection import Config, OpChanges, OpChangesWithCount +from anki.scheduler import CongratsInfo +from anki.scheduler.v3 import CardAnswer, NextStates, QueuedCards +from anki.scheduler.v3 import Scheduler as V3Scheduler from anki.tags import MARKED_TAG from anki.utils import stripHTML from aqt import AnkiQt, gui_hooks from aqt.operations.card import set_card_flag from aqt.operations.note import remove_notes from aqt.operations.scheduling import ( + answer_card, bury_cards, bury_notes, set_due_date_dialog, @@ -58,9 +63,55 @@ def replay_audio(card: Card, question_side: bool) -> None: av_player.play_tags(tags) -class Reviewer: - "Manage reviews. Maintains a separate state." +@dataclass +class V3CardInfo: + """2021 test scheduler info. + next_states is copied from the top card on initialization, and can be + mutated to alter the default scheduling. + """ + + queued_cards: QueuedCards + next_states: NextStates + + @staticmethod + def from_queue(queued_cards: QueuedCards) -> V3CardInfo: + return V3CardInfo( + queued_cards=queued_cards, next_states=queued_cards.cards[0].next_states + ) + + def top_card(self) -> QueuedCards.QueuedCard: + return self.queued_cards.cards[0] + + def counts(self) -> Tuple[int, List[int]]: + "Returns (idx, counts)." + counts = [ + self.queued_cards.new_count, + self.queued_cards.learning_count, + self.queued_cards.review_count, + ] + card = self.top_card() + if card.queue == QueuedCards.NEW: + idx = 0 + elif card.queue == QueuedCards.LEARNING: + idx = 1 + else: + idx = 2 + return idx, counts + + @staticmethod + def rating_from_ease(ease: int) -> CardAnswer.Rating.V: + if ease == 1: + return CardAnswer.AGAIN + elif ease == 2: + return CardAnswer.HARD + elif ease == 3: + return CardAnswer.GOOD + else: + return CardAnswer.EASY + + +class Reviewer: def __init__(self, mw: AnkiQt) -> None: self.mw = mw self.web = mw.web @@ -72,6 +123,7 @@ class Reviewer: self.typeCorrect: str = None # web init happens before this is set self.state: Optional[str] = None self._refresh_needed: Optional[RefreshNeeded] = None + self._v3: Optional[V3CardInfo] = None self.bottom = BottomBar(mw, mw.bottomWeb) hooks.card_did_leech.append(self.onLeech) @@ -83,6 +135,7 @@ class Reviewer: self._refresh_needed = RefreshNeeded.QUEUES self.refresh_if_needed() + # this is only used by add-ons def lastCard(self) -> Optional[Card]: if self._answeredIds: if not self.card or self._answeredIds[-1] != self.card.id: @@ -112,7 +165,7 @@ class Reviewer: self, changes: OpChanges, handler: Optional[object], focused: bool ) -> bool: if handler is not self: - if changes.study_queues: + if changes.reviewer: self._refresh_needed = RefreshNeeded.QUEUES elif changes.editor: self._refresh_needed = RefreshNeeded.NOTE_TEXT @@ -133,22 +186,32 @@ class Reviewer: ########################################################################## def nextCard(self) -> None: - elapsed = self.mw.col.timeboxReached() - if elapsed: - assert not isinstance(elapsed, bool) - part1 = tr.studying_card_studied_in(count=elapsed[1]) - mins = int(round(elapsed[0] / 60)) - part2 = tr.studying_minute(count=mins) - fin = tr.studying_finish() - diag = askUserDialog(f"{part1} {part2}", [tr.studying_continue(), fin]) - diag.setIcon(QMessageBox.Information) - if diag.run() == fin: - return self.mw.moveToState("deckBrowser") - self.mw.col.startTimebox() + if self.check_timebox(): + return + + self.card = None + self._v3 = None + + if self.mw.col.sched.version < 3: + self._get_next_v1_v2_card() + else: + self._get_next_v3_card() + + if not self.card: + self.mw.moveToState("overview") + return + + if self._reps is None or self._reps % 100 == 0: + # we recycle the webview periodically so webkit can free memory + self._initWeb() + + self._showQuestion() + + def _get_next_v1_v2_card(self) -> None: if self.cardQueue: # undone/edited cards to show - c = self.cardQueue.pop() - c.startTimer() + card = self.cardQueue.pop() + card.startTimer() self.hadCardQueue = True else: if self.hadCardQueue: @@ -156,15 +219,17 @@ class Reviewer: # need to reset self.mw.col.reset() self.hadCardQueue = False - c = self.mw.col.sched.getCard() - self.card = c - if not c: - self.mw.moveToState("overview") + card = self.mw.col.sched.getCard() + self.card = card + + def _get_next_v3_card(self) -> None: + assert isinstance(self.mw.col.sched, V3Scheduler) + output = self.mw.col.sched.get_queued_cards() + if isinstance(output, CongratsInfo): return - if self._reps is None or self._reps % 100 == 0: - # we recycle the webview periodically so webkit can free memory - self._initWeb() - self._showQuestion() + self._v3 = V3CardInfo.from_queue(output) + self.card = Card(self.mw.col, backend_card=self._v3.top_card().card) + self.card.startTimer() # Audio ########################################################################## @@ -313,7 +378,20 @@ class Reviewer: ) if not proceed: return - self.mw.col.sched.answerCard(self.card, ease) + + if v3 := self._v3: + assert isinstance(self.mw.col.sched, V3Scheduler) + answer = self.mw.col.sched.build_answer( + card=self.card, states=v3.next_states, rating=v3.rating_from_ease(ease) + ) + answer_card(parent=self.mw, answer=answer).success( + lambda _: self._after_answering(ease) + ).run_in_background(initiator=self) + else: + self.mw.col.sched.answerCard(self.card, ease) + self._after_answering(ease) + + def _after_answering(self, ease: int) -> None: gui_hooks.reviewer_did_answer_card(self, self.card, ease) self._answeredIds.append(self.card.id) self.mw.autosave() @@ -648,18 +726,27 @@ time = %(time)d; def _remaining(self) -> str: if not self.mw.col.conf["dueCounts"]: return "" - if self.hadCardQueue: - # if it's come from the undo queue, don't count it separately - counts: List[Union[int, str]] = list(self.mw.col.sched.counts()) + + counts: List[Union[int, str]] + if v3 := self._v3: + idx, counts_ = v3.counts() + counts = cast(List[Union[int, str]], counts_) else: - counts = list(self.mw.col.sched.counts(self.card)) - idx = self.mw.col.sched.countIdx(self.card) + # v1/v2 scheduler + if self.hadCardQueue: + # if it's come from the undo queue, don't count it separately + counts = list(self.mw.col.sched.counts()) + else: + counts = list(self.mw.col.sched.counts(self.card)) + idx = self.mw.col.sched.countIdx(self.card) + counts[idx] = f"{counts[idx]}" - space = " + " - ctxt = f"{counts[0]}" - ctxt += f"{space}{counts[1]}" - ctxt += f"{space}{counts[2]}" - return ctxt + + return f""" +{counts[0]} + +{counts[1]} + +{counts[2]} +""" def _defaultEase(self) -> int: if self.mw.col.sched.answerButtons(self.card) == 4: @@ -695,12 +782,18 @@ time = %(time)d; def _answerButtons(self) -> str: default = self._defaultEase() + if v3 := self._v3: + assert isinstance(self.mw.col.sched, V3Scheduler) + labels = self.mw.col.sched.describe_next_states(v3.next_states) + else: + labels = None + def but(i: int, label: str) -> str: if i == default: extra = """id="defease" class="focus" """ else: extra = "" - due = self._buttonTime(i) + due = self._buttonTime(i, v3_labels=labels) return """ %s""" % ( @@ -718,10 +811,13 @@ time = %(time)d; buf += "" return buf - def _buttonTime(self, i: int) -> str: + def _buttonTime(self, i: int, v3_labels: Optional[Sequence[str]] = None) -> str: if not self.mw.col.conf["estTimes"]: return "
" - txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or " " + if v3_labels: + txt = v3_labels[i - 1] + else: + txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or " " return f"{txt}
" # Leeches @@ -734,6 +830,26 @@ time = %(time)d; s += f" {tr.studying_it_has_been_suspended()}" tooltip(s) + # Timebox + ########################################################################## + + def check_timebox(self) -> bool: + "True if answering should be aborted." + elapsed = self.mw.col.timeboxReached() + if elapsed: + assert not isinstance(elapsed, bool) + part1 = tr.studying_card_studied_in(count=elapsed[1]) + mins = int(round(elapsed[0] / 60)) + part2 = tr.studying_minute(count=mins) + fin = tr.studying_finish() + diag = askUserDialog(f"{part1} {part2}", [tr.studying_continue(), fin]) + diag.setIcon(QMessageBox.Information) + if diag.run() == fin: + self.mw.moveToState("deckBrowser") + return True + self.mw.col.startTimebox() + return False + # Context menu ########################################################################## diff --git a/rslib/backend.proto b/rslib/backend.proto index 0ca84af7b..5f40af92d 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -130,7 +130,7 @@ service SchedulingService { rpc GetNextCardStates(CardId) returns (NextCardStates); rpc DescribeNextStates(NextCardStates) returns (StringList); rpc StateIsLeech(SchedulingState) returns (Bool); - rpc AnswerCard(AnswerCardIn) returns (OpChanges); + rpc AnswerCard(CardAnswer) returns (OpChanges); rpc UpgradeScheduler(Empty) returns (Empty); rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut); } @@ -1489,7 +1489,7 @@ message NextCardStates { SchedulingState easy = 5; } -message AnswerCardIn { +message CardAnswer { enum Rating { AGAIN = 0; HARD = 1; @@ -1510,26 +1510,25 @@ message GetQueuedCardsIn { bool intraday_learning_only = 2; } -message GetQueuedCardsOut { +message QueuedCards { enum Queue { - New = 0; - Learning = 1; - Review = 2; + NEW = 0; + LEARNING = 1; + REVIEW = 2; } - message QueuedCard { Card card = 1; Queue queue = 2; NextCardStates next_states = 3; } - message QueuedCards { - repeated QueuedCard cards = 1; - uint32 new_count = 2; - uint32 learning_count = 3; - uint32 review_count = 4; - } + repeated QueuedCard cards = 1; + uint32 new_count = 2; + uint32 learning_count = 3; + uint32 review_count = 4; +} +message GetQueuedCardsOut { oneof value { QueuedCards queued_cards = 1; CongratsInfoOut congrats_info = 2; @@ -1548,7 +1547,7 @@ message OpChanges { bool browser_table = 7; bool browser_sidebar = 8; bool editor = 9; - bool study_queues = 10; + bool reviewer = 10; } message UndoStatus { diff --git a/rslib/src/backend/ops.rs b/rslib/src/backend/ops.rs index 2d6c7fa2b..41ae739b7 100644 --- a/rslib/src/backend/ops.rs +++ b/rslib/src/backend/ops.rs @@ -21,7 +21,7 @@ impl From for pb::OpChanges { browser_table: c.requires_browser_table_redraw(), browser_sidebar: c.requires_browser_sidebar_redraw(), editor: c.requires_editor_redraw(), - study_queues: c.requires_study_queue_rebuild(), + reviewer: c.requires_reviewer_redraw(), } } } diff --git a/rslib/src/backend/scheduler/answering.rs b/rslib/src/backend/scheduler/answering.rs index dde53ad92..edb4b013a 100644 --- a/rslib/src/backend/scheduler/answering.rs +++ b/rslib/src/backend/scheduler/answering.rs @@ -10,8 +10,8 @@ use crate::{ }, }; -impl From for CardAnswer { - fn from(answer: pb::AnswerCardIn) -> Self { +impl From for CardAnswer { + fn from(answer: pb::CardAnswer) -> Self { CardAnswer { card_id: CardId(answer.card_id), rating: answer.rating().into(), @@ -23,28 +23,34 @@ impl From for CardAnswer { } } -impl From for Rating { - fn from(rating: pb::answer_card_in::Rating) -> Self { +impl From for Rating { + fn from(rating: pb::card_answer::Rating) -> Self { match rating { - pb::answer_card_in::Rating::Again => Rating::Again, - pb::answer_card_in::Rating::Hard => Rating::Hard, - pb::answer_card_in::Rating::Good => Rating::Good, - pb::answer_card_in::Rating::Easy => Rating::Easy, + pb::card_answer::Rating::Again => Rating::Again, + pb::card_answer::Rating::Hard => Rating::Hard, + pb::card_answer::Rating::Good => Rating::Good, + pb::card_answer::Rating::Easy => Rating::Easy, } } } -impl From for pb::get_queued_cards_out::QueuedCard { +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()), - queue: queued_card.kind as i32, + 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, + crate::scheduler::queue::QueueEntryKind::Learning => { + pb::queued_cards::Queue::Learning + } + } as i32, } } } -impl From for pb::get_queued_cards_out::QueuedCards { +impl From for pb::QueuedCards { fn from(queued_cards: QueuedCards) -> Self { Self { cards: queued_cards.cards.into_iter().map(Into::into).collect(), diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs index 020597857..a592975f2 100644 --- a/rslib/src/backend/scheduler/mod.rs +++ b/rslib/src/backend/scheduler/mod.rs @@ -166,7 +166,7 @@ impl SchedulingService for Backend { Ok(state.leeched().into()) } - fn answer_card(&self, input: pb::AnswerCardIn) -> Result { + fn answer_card(&self, input: pb::CardAnswer) -> Result { self.with_col(|col| col.answer_card(&input.into())) .map(Into::into) } diff --git a/rslib/src/ops.rs b/rslib/src/ops.rs index c2396e6c1..47c18ac2c 100644 --- a/rslib/src/ops.rs +++ b/rslib/src/ops.rs @@ -150,7 +150,19 @@ impl OpChanges { c.note || c.notetype } - pub fn requires_study_queue_rebuild(&self) -> bool { + pub fn requires_reviewer_redraw(&self) -> bool { + let c = &self.changes; + c.card + || (c.deck && self.op != Op::ExpandCollapse) + || (c.config && matches!(self.op, Op::SetCurrentDeck)) + || c.deck_config + || c.note + || c.notetype + } + + /// Internal; allows us to avoid rebuilding queues after AnswerCard, + /// and a few other ops as an optimization. + pub(crate) fn requires_study_queue_rebuild(&self) -> bool { let c = &self.changes; if self.op == Op::AnswerCard { return false; diff --git a/rslib/src/scheduler/queue/entry.rs b/rslib/src/scheduler/queue/entry.rs index a17350166..1079505cb 100644 --- a/rslib/src/scheduler/queue/entry.rs +++ b/rslib/src/scheduler/queue/entry.rs @@ -39,8 +39,8 @@ impl QueueEntry { #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) enum QueueEntryKind { New, - Review, Learning, + Review, } impl From<&Card> for QueueEntry {