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:
parent
63437f5cde
commit
49a1580566
@ -7,7 +7,8 @@ ignored-classes=
|
||||
BrowserColumns,
|
||||
BrowserRow,
|
||||
FormatTimespanIn,
|
||||
AnswerCardIn,
|
||||
CardAnswer,
|
||||
QueuedCards,
|
||||
UnburyDeckIn,
|
||||
BuryOrSuspendCardsIn,
|
||||
NoteIsDuplicateOrEmptyOut,
|
||||
|
@ -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"}
|
||||
|
@ -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())
|
||||
|
@ -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]
|
||||
|
@ -12,6 +12,8 @@ ignored-classes=
|
||||
Config,
|
||||
OpChanges,
|
||||
UnburyDeckIn,
|
||||
CardAnswer,
|
||||
QueuedCards,
|
||||
|
||||
[REPORTS]
|
||||
output-format=colorized
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
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 ""
|
||||
|
||||
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 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:
|
||||
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,9 +811,12 @@ 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>"
|
||||
if v3_labels:
|
||||
txt = v3_labels[i - 1]
|
||||
else:
|
||||
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or " "
|
||||
return f"<span class=nobold>{txt}</span><br>"
|
||||
|
||||
@ -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
|
||||
##########################################################################
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -39,8 +39,8 @@ impl QueueEntry {
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) enum QueueEntryKind {
|
||||
New,
|
||||
Review,
|
||||
Learning,
|
||||
Review,
|
||||
}
|
||||
|
||||
impl From<&Card> for QueueEntry {
|
||||
|
Loading…
Reference in New Issue
Block a user