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,
|
BrowserColumns,
|
||||||
BrowserRow,
|
BrowserRow,
|
||||||
FormatTimespanIn,
|
FormatTimespanIn,
|
||||||
AnswerCardIn,
|
CardAnswer,
|
||||||
|
QueuedCards,
|
||||||
UnburyDeckIn,
|
UnburyDeckIn,
|
||||||
BuryOrSuspendCardsIn,
|
BuryOrSuspendCardsIn,
|
||||||
NoteIsDuplicateOrEmptyOut,
|
NoteIsDuplicateOrEmptyOut,
|
||||||
|
@ -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"}
|
||||||
|
@ -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())
|
||||||
|
@ -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]
|
|
||||||
|
@ -12,6 +12,8 @@ ignored-classes=
|
|||||||
Config,
|
Config,
|
||||||
OpChanges,
|
OpChanges,
|
||||||
UnburyDeckIn,
|
UnburyDeckIn,
|
||||||
|
CardAnswer,
|
||||||
|
QueuedCards,
|
||||||
|
|
||||||
[REPORTS]
|
[REPORTS]
|
||||||
output-format=colorized
|
output-format=colorized
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
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)
|
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 ""
|
||||||
if self.hadCardQueue:
|
|
||||||
# if it's come from the undo queue, don't count it separately
|
counts: List[Union[int, str]]
|
||||||
counts: List[Union[int, str]] = list(self.mw.col.sched.counts())
|
if v3 := self._v3:
|
||||||
|
idx, counts_ = v3.counts()
|
||||||
|
counts = cast(List[Union[int, str]], counts_)
|
||||||
else:
|
else:
|
||||||
counts = list(self.mw.col.sched.counts(self.card))
|
# v1/v2 scheduler
|
||||||
idx = self.mw.col.sched.countIdx(self.card)
|
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>"
|
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,10 +811,13 @@ 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>"
|
||||||
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"<span class=nobold>{txt}</span><br>"
|
return f"<span class=nobold>{txt}</span><br>"
|
||||||
|
|
||||||
# Leeches
|
# Leeches
|
||||||
@ -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
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user