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, BrowserColumns,
BrowserRow, BrowserRow,
FormatTimespanIn, FormatTimespanIn,
AnswerCardIn, CardAnswer,
QueuedCards,
UnburyDeckIn, UnburyDeckIn,
BuryOrSuspendCardsIn, BuryOrSuspendCardsIn,
NoteIsDuplicateOrEmptyOut, NoteIsDuplicateOrEmptyOut,

View File

@ -34,7 +34,12 @@ LABEL_REQUIRED = 2
LABEL_REPEATED = 3 LABEL_REPEATED = 3
# messages we don't want to unroll in codegen # 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_UNROLL_OUTPUT = {"GetPreferences"}
SKIP_DECODE = {"Graphs", "GetGraphPreferences"} SKIP_DECODE = {"Graphs", "GetGraphPreferences"}

View File

@ -28,6 +28,7 @@ from anki.sound import AVTag
# types # types
CardId = NewType("CardId", int) CardId = NewType("CardId", int)
BackendCard = _pb.Card
class Card: class Card:
@ -43,7 +44,10 @@ class Card:
type: CardType type: CardType
def __init__( 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: ) -> None:
self.col = col.weakref() self.col = col.weakref()
self.timerStarted = None self.timerStarted = None
@ -52,6 +56,8 @@ class Card:
# existing card # existing card
self.id = id self.id = id
self.load() self.load()
elif backend_card:
self._load_from_backend_card(backend_card)
else: else:
# new card with defaults # new card with defaults
self._load_from_backend_card(_pb.Card()) self._load_from_backend_card(_pb.Card())

View File

@ -11,18 +11,21 @@ as '2' internally.
from __future__ import annotations from __future__ import annotations
from typing import Tuple, Union from typing import Literal, Sequence, Tuple, Union
import anki._backend.backend_pb2 as _pb 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.consts import *
from anki.scheduler.base import CongratsInfo from anki.scheduler.base import CongratsInfo
from anki.scheduler.legacy import SchedulerBaseWithLegacy from anki.scheduler.legacy import SchedulerBaseWithLegacy
from anki.types import assert_exhaustive from anki.types import assert_exhaustive
from anki.utils import intTime from anki.utils import intTime
QueuedCards = _pb.GetQueuedCardsOut.QueuedCards QueuedCards = _pb.QueuedCards
SchedulingState = _pb.SchedulingState SchedulingState = _pb.SchedulingState
NextStates = _pb.NextCardStates
CardAnswer = _pb.CardAnswer
class Scheduler(SchedulerBaseWithLegacy): class Scheduler(SchedulerBaseWithLegacy):
@ -34,16 +37,13 @@ class Scheduler(SchedulerBaseWithLegacy):
# Fetching the next card # Fetching the next card
########################################################################## ##########################################################################
def reset(self) -> None:
# backend automatically resets queues as operations are performed
pass
def get_queued_cards( def get_queued_cards(
self, self,
*, *,
fetch_limit: int = 1, fetch_limit: int = 1,
intraday_learning_only: bool = False, intraday_learning_only: bool = False,
) -> Union[QueuedCards, CongratsInfo]: ) -> Union[QueuedCards, CongratsInfo]:
"Returns one or more card ids, or the congratulations screen info."
info = self.col._backend.get_queued_cards( info = self.col._backend.get_queued_cards(
fetch_limit=fetch_limit, intraday_learning_only=intraday_learning_only fetch_limit=fetch_limit, intraday_learning_only=intraday_learning_only
) )
@ -56,6 +56,57 @@ class Scheduler(SchedulerBaseWithLegacy):
assert_exhaustive(kind) assert_exhaustive(kind)
assert False 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]: def getCard(self) -> Optional[Card]:
"""Fetch the next card from the queue. None if finished.""" """Fetch the next card from the queue. None if finished."""
response = self.get_queued_cards() response = self.get_queued_cards()
@ -94,53 +145,46 @@ class Scheduler(SchedulerBaseWithLegacy):
def reviewCount(self) -> int: def reviewCount(self) -> int:
return self.counts()[2] 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: def answerCard(self, card: Card, ease: Literal[1, 2, 3, 4]) -> OpChanges:
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)
if ease == BUTTON_ONE: if ease == BUTTON_ONE:
new_state = states.again rating = CardAnswer.AGAIN
rating = _pb.AnswerCardIn.AGAIN
elif ease == BUTTON_TWO: elif ease == BUTTON_TWO:
new_state = states.hard rating = CardAnswer.HARD
rating = _pb.AnswerCardIn.HARD
elif ease == BUTTON_THREE: elif ease == BUTTON_THREE:
new_state = states.good rating = CardAnswer.GOOD
rating = _pb.AnswerCardIn.GOOD
elif ease == BUTTON_FOUR: elif ease == BUTTON_FOUR:
new_state = states.easy rating = CardAnswer.EASY
rating = _pb.AnswerCardIn.EASY
else: else:
assert False, "invalid ease" assert False, "invalid ease"
self.col._backend.answer_card( changes = self.answer_card(
card_id=card.id, self.build_answer(
current_state=states.current, card=card, states=self.next_states(card_id=card.id), rating=rating
new_state=new_state, )
rating=rating,
answered_at_millis=intTime(1000),
milliseconds_taken=card.timeTaken(),
) )
# 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() card.load()
return new_state return changes
def state_is_leech(self, new_state: SchedulingState) -> bool: # Next times (legacy API)
"True if new state marks the card as a leech."
return self.col._backend.state_is_leech(new_state)
# Next times
########################################################################## ##########################################################################
# fixme: move these into tests_schedv2 in the future # fixme: move these into tests_schedv2 in the future
@ -195,19 +239,3 @@ class Scheduler(SchedulerBaseWithLegacy):
assert False, "invalid ease" assert False, "invalid ease"
return self._interval_for_state(new_state) 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, Config,
OpChanges, OpChanges,
UnburyDeckIn, UnburyDeckIn,
CardAnswer,
QueuedCards,
[REPORTS] [REPORTS]
output-format=colorized output-format=colorized

View File

@ -81,7 +81,7 @@ class DeckBrowser:
def op_executed( def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool self, changes: OpChanges, handler: Optional[object], focused: bool
) -> bool: ) -> bool:
if changes.study_queues and handler is not self: if changes.reviewer and handler is not self:
self._refresh_needed = True self._refresh_needed = True
if focused: if focused:

View File

@ -9,6 +9,7 @@ import aqt
from anki.cards import CardId from anki.cards import CardId
from anki.collection import ( from anki.collection import (
CARD_TYPE_NEW, CARD_TYPE_NEW,
Collection,
Config, Config,
OpChanges, OpChanges,
OpChangesWithCount, OpChangesWithCount,
@ -17,6 +18,8 @@ from anki.collection import (
from anki.decks import DeckId from anki.decks import DeckId
from anki.notes import NoteId from anki.notes import NoteId
from anki.scheduler import FilteredDeckForUpdate, UnburyDeck 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.operations import CollectionOp
from aqt.qt import * from aqt.qt import *
from aqt.utils import disable_help_button, getText, tooltip, tr from aqt.utils import disable_help_button, getText, tooltip, tr
@ -207,3 +210,15 @@ def unbury_deck(
return CollectionOp( return CollectionOp(
parent, lambda col: col.sched.unbury_deck(deck_id=deck_id, mode=mode) 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( def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool self, changes: OpChanges, handler: Optional[object], focused: bool
) -> bool: ) -> bool:
if changes.study_queues: if changes.reviewer:
self._refresh_needed = True self._refresh_needed = True
if focused: if focused:

View File

@ -8,20 +8,25 @@ import html
import json import json
import re import re
import unicodedata as ucd import unicodedata as ucd
from dataclasses import dataclass
from enum import Enum, auto 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 PyQt5.QtCore import Qt
from anki import hooks from anki import hooks
from anki.cards import Card, CardId from anki.cards import Card, CardId
from anki.collection import Config, OpChanges, OpChangesWithCount 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.tags import MARKED_TAG
from anki.utils import stripHTML from anki.utils import stripHTML
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.operations.card import set_card_flag from aqt.operations.card import set_card_flag
from aqt.operations.note import remove_notes from aqt.operations.note import remove_notes
from aqt.operations.scheduling import ( from aqt.operations.scheduling import (
answer_card,
bury_cards, bury_cards,
bury_notes, bury_notes,
set_due_date_dialog, set_due_date_dialog,
@ -58,9 +63,55 @@ def replay_audio(card: Card, question_side: bool) -> None:
av_player.play_tags(tags) av_player.play_tags(tags)
class Reviewer: @dataclass
"Manage reviews. Maintains a separate state." 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: def __init__(self, mw: AnkiQt) -> None:
self.mw = mw self.mw = mw
self.web = mw.web self.web = mw.web
@ -72,6 +123,7 @@ class Reviewer:
self.typeCorrect: str = None # web init happens before this is set self.typeCorrect: str = None # web init happens before this is set
self.state: Optional[str] = None self.state: Optional[str] = None
self._refresh_needed: Optional[RefreshNeeded] = None self._refresh_needed: Optional[RefreshNeeded] = None
self._v3: Optional[V3CardInfo] = None
self.bottom = BottomBar(mw, mw.bottomWeb) self.bottom = BottomBar(mw, mw.bottomWeb)
hooks.card_did_leech.append(self.onLeech) hooks.card_did_leech.append(self.onLeech)
@ -83,6 +135,7 @@ class Reviewer:
self._refresh_needed = RefreshNeeded.QUEUES self._refresh_needed = RefreshNeeded.QUEUES
self.refresh_if_needed() self.refresh_if_needed()
# this is only used by add-ons
def lastCard(self) -> Optional[Card]: def lastCard(self) -> Optional[Card]:
if self._answeredIds: if self._answeredIds:
if not self.card or self._answeredIds[-1] != self.card.id: 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 self, changes: OpChanges, handler: Optional[object], focused: bool
) -> bool: ) -> bool:
if handler is not self: if handler is not self:
if changes.study_queues: if changes.reviewer:
self._refresh_needed = RefreshNeeded.QUEUES self._refresh_needed = RefreshNeeded.QUEUES
elif changes.editor: elif changes.editor:
self._refresh_needed = RefreshNeeded.NOTE_TEXT self._refresh_needed = RefreshNeeded.NOTE_TEXT
@ -133,22 +186,32 @@ class Reviewer:
########################################################################## ##########################################################################
def nextCard(self) -> None: def nextCard(self) -> None:
elapsed = self.mw.col.timeboxReached() if self.check_timebox():
if elapsed: return
assert not isinstance(elapsed, bool)
part1 = tr.studying_card_studied_in(count=elapsed[1]) self.card = None
mins = int(round(elapsed[0] / 60)) self._v3 = None
part2 = tr.studying_minute(count=mins)
fin = tr.studying_finish() if self.mw.col.sched.version < 3:
diag = askUserDialog(f"{part1} {part2}", [tr.studying_continue(), fin]) self._get_next_v1_v2_card()
diag.setIcon(QMessageBox.Information) else:
if diag.run() == fin: self._get_next_v3_card()
return self.mw.moveToState("deckBrowser")
self.mw.col.startTimebox() 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: if self.cardQueue:
# undone/edited cards to show # undone/edited cards to show
c = self.cardQueue.pop() card = self.cardQueue.pop()
c.startTimer() card.startTimer()
self.hadCardQueue = True self.hadCardQueue = True
else: else:
if self.hadCardQueue: if self.hadCardQueue:
@ -156,15 +219,17 @@ class Reviewer:
# need to reset # need to reset
self.mw.col.reset() self.mw.col.reset()
self.hadCardQueue = False self.hadCardQueue = False
c = self.mw.col.sched.getCard() card = self.mw.col.sched.getCard()
self.card = c self.card = card
if not c:
self.mw.moveToState("overview") 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 return
if self._reps is None or self._reps % 100 == 0: self._v3 = V3CardInfo.from_queue(output)
# we recycle the webview periodically so webkit can free memory self.card = Card(self.mw.col, backend_card=self._v3.top_card().card)
self._initWeb() self.card.startTimer()
self._showQuestion()
# Audio # Audio
########################################################################## ##########################################################################
@ -313,7 +378,20 @@ class Reviewer:
) )
if not proceed: if not proceed:
return return
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.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) gui_hooks.reviewer_did_answer_card(self, self.card, ease)
self._answeredIds.append(self.card.id) self._answeredIds.append(self.card.id)
self.mw.autosave() self.mw.autosave()
@ -648,18 +726,27 @@ time = %(time)d;
def _remaining(self) -> str: def _remaining(self) -> str:
if not self.mw.col.conf["dueCounts"]: if not self.mw.col.conf["dueCounts"]:
return "" return ""
counts: List[Union[int, str]]
if v3 := self._v3:
idx, counts_ = v3.counts()
counts = cast(List[Union[int, str]], counts_)
else:
# v1/v2 scheduler
if self.hadCardQueue: if self.hadCardQueue:
# if it's come from the undo queue, don't count it separately # 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(self.mw.col.sched.counts())
else: else:
counts = list(self.mw.col.sched.counts(self.card)) counts = list(self.mw.col.sched.counts(self.card))
idx = self.mw.col.sched.countIdx(self.card) idx = self.mw.col.sched.countIdx(self.card)
counts[idx] = f"<u>{counts[idx]}</u>" counts[idx] = f"<u>{counts[idx]}</u>"
space = " + "
ctxt = f"<span class=new-count>{counts[0]}</span>" return f"""
ctxt += f"{space}<span class=learn-count>{counts[1]}</span>" <span class=new-count>{counts[0]}</span> +
ctxt += f"{space}<span class=review-count>{counts[2]}</span>" <span class=learn-count>{counts[1]}</span> +
return ctxt <span class=review-count>{counts[2]}</span>
"""
def _defaultEase(self) -> int: def _defaultEase(self) -> int:
if self.mw.col.sched.answerButtons(self.card) == 4: if self.mw.col.sched.answerButtons(self.card) == 4:
@ -695,12 +782,18 @@ time = %(time)d;
def _answerButtons(self) -> str: def _answerButtons(self) -> str:
default = self._defaultEase() 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: def but(i: int, label: str) -> str:
if i == default: if i == default:
extra = """id="defease" class="focus" """ extra = """id="defease" class="focus" """
else: else:
extra = "" extra = ""
due = self._buttonTime(i) due = self._buttonTime(i, v3_labels=labels)
return """ return """
<td align=center>%s<button %s title="%s" data-ease="%s" onclick='pycmd("ease%d");'>\ <td align=center>%s<button %s title="%s" data-ease="%s" onclick='pycmd("ease%d");'>\
%s</button></td>""" % ( %s</button></td>""" % (
@ -718,9 +811,12 @@ time = %(time)d;
buf += "</tr></table>" buf += "</tr></table>"
return 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"]: if not self.mw.col.conf["estTimes"]:
return "<div class=spacer></div>" return "<div class=spacer></div>"
if v3_labels:
txt = v3_labels[i - 1]
else:
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or "&nbsp;" txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or "&nbsp;"
return f"<span class=nobold>{txt}</span><br>" return f"<span class=nobold>{txt}</span><br>"
@ -734,6 +830,26 @@ time = %(time)d;
s += f" {tr.studying_it_has_been_suspended()}" s += f" {tr.studying_it_has_been_suspended()}"
tooltip(s) 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 # Context menu
########################################################################## ##########################################################################

View File

@ -130,7 +130,7 @@ service SchedulingService {
rpc GetNextCardStates(CardId) returns (NextCardStates); rpc GetNextCardStates(CardId) returns (NextCardStates);
rpc DescribeNextStates(NextCardStates) returns (StringList); rpc DescribeNextStates(NextCardStates) returns (StringList);
rpc StateIsLeech(SchedulingState) returns (Bool); rpc StateIsLeech(SchedulingState) returns (Bool);
rpc AnswerCard(AnswerCardIn) returns (OpChanges); rpc AnswerCard(CardAnswer) returns (OpChanges);
rpc UpgradeScheduler(Empty) returns (Empty); rpc UpgradeScheduler(Empty) returns (Empty);
rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut); rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut);
} }
@ -1489,7 +1489,7 @@ message NextCardStates {
SchedulingState easy = 5; SchedulingState easy = 5;
} }
message AnswerCardIn { message CardAnswer {
enum Rating { enum Rating {
AGAIN = 0; AGAIN = 0;
HARD = 1; HARD = 1;
@ -1510,26 +1510,25 @@ message GetQueuedCardsIn {
bool intraday_learning_only = 2; bool intraday_learning_only = 2;
} }
message GetQueuedCardsOut { message QueuedCards {
enum Queue { enum Queue {
New = 0; NEW = 0;
Learning = 1; LEARNING = 1;
Review = 2; REVIEW = 2;
} }
message QueuedCard { message QueuedCard {
Card card = 1; Card card = 1;
Queue queue = 2; Queue queue = 2;
NextCardStates next_states = 3; NextCardStates next_states = 3;
} }
message QueuedCards {
repeated QueuedCard cards = 1; repeated QueuedCard cards = 1;
uint32 new_count = 2; uint32 new_count = 2;
uint32 learning_count = 3; uint32 learning_count = 3;
uint32 review_count = 4; uint32 review_count = 4;
} }
message GetQueuedCardsOut {
oneof value { oneof value {
QueuedCards queued_cards = 1; QueuedCards queued_cards = 1;
CongratsInfoOut congrats_info = 2; CongratsInfoOut congrats_info = 2;
@ -1548,7 +1547,7 @@ message OpChanges {
bool browser_table = 7; bool browser_table = 7;
bool browser_sidebar = 8; bool browser_sidebar = 8;
bool editor = 9; bool editor = 9;
bool study_queues = 10; bool reviewer = 10;
} }
message UndoStatus { message UndoStatus {

View File

@ -21,7 +21,7 @@ impl From<OpChanges> for pb::OpChanges {
browser_table: c.requires_browser_table_redraw(), browser_table: c.requires_browser_table_redraw(),
browser_sidebar: c.requires_browser_sidebar_redraw(), browser_sidebar: c.requires_browser_sidebar_redraw(),
editor: c.requires_editor_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 { impl From<pb::CardAnswer> for CardAnswer {
fn from(answer: pb::AnswerCardIn) -> Self { fn from(answer: pb::CardAnswer) -> Self {
CardAnswer { CardAnswer {
card_id: CardId(answer.card_id), card_id: CardId(answer.card_id),
rating: answer.rating().into(), rating: answer.rating().into(),
@ -23,28 +23,34 @@ impl From<pb::AnswerCardIn> for CardAnswer {
} }
} }
impl From<pb::answer_card_in::Rating> for Rating { impl From<pb::card_answer::Rating> for Rating {
fn from(rating: pb::answer_card_in::Rating) -> Self { fn from(rating: pb::card_answer::Rating) -> Self {
match rating { match rating {
pb::answer_card_in::Rating::Again => Rating::Again, pb::card_answer::Rating::Again => Rating::Again,
pb::answer_card_in::Rating::Hard => Rating::Hard, pb::card_answer::Rating::Hard => Rating::Hard,
pb::answer_card_in::Rating::Good => Rating::Good, pb::card_answer::Rating::Good => Rating::Good,
pb::answer_card_in::Rating::Easy => Rating::Easy, 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 { fn from(queued_card: QueuedCard) -> Self {
Self { Self {
card: Some(queued_card.card.into()), card: Some(queued_card.card.into()),
next_states: Some(queued_card.next_states.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 { fn from(queued_cards: QueuedCards) -> Self {
Self { Self {
cards: queued_cards.cards.into_iter().map(Into::into).collect(), cards: queued_cards.cards.into_iter().map(Into::into).collect(),

View File

@ -166,7 +166,7 @@ impl SchedulingService for Backend {
Ok(state.leeched().into()) 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())) self.with_col(|col| col.answer_card(&input.into()))
.map(Into::into) .map(Into::into)
} }

View File

@ -150,7 +150,19 @@ impl OpChanges {
c.note || c.notetype 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; let c = &self.changes;
if self.op == Op::AnswerCard { if self.op == Op::AnswerCard {
return false; return false;

View File

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