use new API for test scheduler

Avoids duplicate work, and is a step towards allowing the next
states to be modified by third-party code.

Also:

- fixed incorrect underlined count, due to reviews being labeled as
learning cards
- fixed reviewer not refreshing when undoing a test review, by splitting
up backend queue rebuilding from frontend reviewer refresh
- moved answering into a CollectionOp
This commit is contained in:
Damien Elmes 2021-05-11 11:37:08 +10:00
parent 63437f5cde
commit 49a1580566
15 changed files with 320 additions and 130 deletions

View File

@ -7,7 +7,8 @@ ignored-classes=
BrowserColumns,
BrowserRow,
FormatTimespanIn,
AnswerCardIn,
CardAnswer,
QueuedCards,
UnburyDeckIn,
BuryOrSuspendCardsIn,
NoteIsDuplicateOrEmptyOut,

View File

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

View File

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

View File

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

View File

@ -12,6 +12,8 @@ ignored-classes=
Config,
OpChanges,
UnburyDeckIn,
CardAnswer,
QueuedCards,
[REPORTS]
output-format=colorized

View File

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

View File

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

View File

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

View File

@ -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"<u>{counts[idx]}</u>"
space = " + "
ctxt = f"<span class=new-count>{counts[0]}</span>"
ctxt += f"{space}<span class=learn-count>{counts[1]}</span>"
ctxt += f"{space}<span class=review-count>{counts[2]}</span>"
return ctxt
return f"""
<span class=new-count>{counts[0]}</span> +
<span class=learn-count>{counts[1]}</span> +
<span class=review-count>{counts[2]}</span>
"""
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 """
<td align=center>%s<button %s title="%s" data-ease="%s" onclick='pycmd("ease%d");'>\
%s</button></td>""" % (
@ -718,10 +811,13 @@ time = %(time)d;
buf += "</tr></table>"
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 "<div class=spacer></div>"
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or "&nbsp;"
if v3_labels:
txt = v3_labels[i - 1]
else:
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or "&nbsp;"
return f"<span class=nobold>{txt}</span><br>"
# 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
##########################################################################

View File

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

View File

@ -21,7 +21,7 @@ impl From<OpChanges> 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(),
}
}
}

View File

@ -10,8 +10,8 @@ use crate::{
},
};
impl From<pb::AnswerCardIn> for CardAnswer {
fn from(answer: pb::AnswerCardIn) -> Self {
impl From<pb::CardAnswer> 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<pb::AnswerCardIn> for CardAnswer {
}
}
impl From<pb::answer_card_in::Rating> for Rating {
fn from(rating: pb::answer_card_in::Rating) -> Self {
impl From<pb::card_answer::Rating> 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<QueuedCard> for pb::get_queued_cards_out::QueuedCard {
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()),
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<QueuedCards> for pb::get_queued_cards_out::QueuedCards {
impl From<QueuedCards> for pb::QueuedCards {
fn from(queued_cards: QueuedCards) -> Self {
Self {
cards: queued_cards.cards.into_iter().map(Into::into).collect(),

View File

@ -166,7 +166,7 @@ impl SchedulingService for Backend {
Ok(state.leeched().into())
}
fn answer_card(&self, input: pb::AnswerCardIn) -> Result<pb::OpChanges> {
fn answer_card(&self, input: pb::CardAnswer) -> Result<pb::OpChanges> {
self.with_col(|col| col.answer_card(&input.into()))
.map(Into::into)
}

View File

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

View File

@ -39,8 +39,8 @@ impl QueueEntry {
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum QueueEntryKind {
New,
Review,
Learning,
Review,
}
impl From<&Card> for QueueEntry {