224 lines
6.9 KiB
Python
224 lines
6.9 KiB
Python
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
# pylint: enable=invalid-name
|
|
|
|
from __future__ import annotations
|
|
|
|
import pprint
|
|
import time
|
|
from typing import List, NewType, Optional
|
|
|
|
import anki # pylint: disable=unused-import
|
|
import anki._backend.backend_pb2 as _pb
|
|
from anki import hooks
|
|
from anki._legacy import DeprecatedNamesMixin, deprecated
|
|
from anki.consts import *
|
|
from anki.models import NotetypeDict, TemplateDict
|
|
from anki.notes import Note
|
|
from anki.sound import AVTag
|
|
|
|
# Cards
|
|
##########################################################################
|
|
|
|
# Type: 0=new, 1=learning, 2=due
|
|
# Queue: same as above, and:
|
|
# -1=suspended, -2=user buried, -3=sched buried
|
|
# Due is used differently for different queues.
|
|
# - new queue: position
|
|
# - rev queue: integer day
|
|
# - lrn queue: integer timestamp
|
|
|
|
# types
|
|
CardId = NewType("CardId", int)
|
|
BackendCard = _pb.Card
|
|
|
|
|
|
class Card(DeprecatedNamesMixin):
|
|
_note: Optional[Note]
|
|
lastIvl: int
|
|
ord: int
|
|
nid: anki.notes.NoteId
|
|
id: CardId
|
|
did: anki.decks.DeckId
|
|
odid: anki.decks.DeckId
|
|
queue: CardQueue
|
|
type: CardType
|
|
|
|
def __init__(
|
|
self,
|
|
col: anki.collection.Collection,
|
|
id: Optional[CardId] = None,
|
|
backend_card: Optional[BackendCard] = None,
|
|
) -> None:
|
|
self.col = col.weakref()
|
|
self.timer_started: Optional[float] = None
|
|
self._render_output: Optional[anki.template.TemplateRenderOutput] = None
|
|
if id:
|
|
# 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())
|
|
|
|
def load(self) -> None:
|
|
card = self.col._backend.get_card(self.id)
|
|
assert card
|
|
self._load_from_backend_card(card)
|
|
|
|
def _load_from_backend_card(self, card: _pb.Card) -> None:
|
|
self._render_output = None
|
|
self._note = None
|
|
self.id = CardId(card.id)
|
|
self.nid = anki.notes.NoteId(card.note_id)
|
|
self.did = anki.decks.DeckId(card.deck_id)
|
|
self.ord = card.template_idx
|
|
self.mod = card.mtime_secs
|
|
self.usn = card.usn
|
|
self.type = CardType(card.ctype)
|
|
self.queue = CardQueue(card.queue)
|
|
self.due = card.due
|
|
self.ivl = card.interval
|
|
self.factor = card.ease_factor
|
|
self.reps = card.reps
|
|
self.lapses = card.lapses
|
|
self.left = card.remaining_steps
|
|
self.odue = card.original_due
|
|
self.odid = anki.decks.DeckId(card.original_deck_id)
|
|
self.flags = card.flags
|
|
self.data = card.data
|
|
|
|
def _to_backend_card(self) -> _pb.Card:
|
|
# mtime & usn are set by backend
|
|
return _pb.Card(
|
|
id=self.id,
|
|
note_id=self.nid,
|
|
deck_id=self.did,
|
|
template_idx=self.ord,
|
|
ctype=self.type,
|
|
queue=self.queue,
|
|
due=self.due,
|
|
interval=self.ivl,
|
|
ease_factor=self.factor,
|
|
reps=self.reps,
|
|
lapses=self.lapses,
|
|
remaining_steps=self.left,
|
|
original_due=self.odue,
|
|
original_deck_id=self.odid,
|
|
flags=self.flags,
|
|
data=self.data,
|
|
)
|
|
|
|
def flush(self) -> None:
|
|
hooks.card_will_flush(self)
|
|
if self.id != 0:
|
|
self.col._backend.update_card(
|
|
card=self._to_backend_card(), skip_undo_entry=True
|
|
)
|
|
else:
|
|
raise Exception("card.flush() expects an existing card")
|
|
|
|
def question(self, reload: bool = False, browser: bool = False) -> str:
|
|
return self.render_output(reload, browser).question_and_style()
|
|
|
|
def answer(self) -> str:
|
|
return self.render_output().answer_and_style()
|
|
|
|
def question_av_tags(self) -> List[AVTag]:
|
|
return self.render_output().question_av_tags
|
|
|
|
def answer_av_tags(self) -> List[AVTag]:
|
|
return self.render_output().answer_av_tags
|
|
|
|
def render_output(
|
|
self, reload: bool = False, browser: bool = False
|
|
) -> anki.template.TemplateRenderOutput:
|
|
if not self._render_output or reload:
|
|
self._render_output = (
|
|
anki.template.TemplateRenderContext.from_existing_card(
|
|
self, browser
|
|
).render()
|
|
)
|
|
return self._render_output
|
|
|
|
def set_render_output(self, output: anki.template.TemplateRenderOutput) -> None:
|
|
self._render_output = output
|
|
|
|
def note(self, reload: bool = False) -> Note:
|
|
if not self._note or reload:
|
|
self._note = self.col.get_note(self.nid)
|
|
return self._note
|
|
|
|
def note_type(self) -> NotetypeDict:
|
|
return self.col.models.get(self.note().mid)
|
|
|
|
def template(self) -> TemplateDict:
|
|
notetype = self.note_type()
|
|
if notetype["type"] == MODEL_STD:
|
|
return self.note_type()["tmpls"][self.ord]
|
|
else:
|
|
return self.note_type()["tmpls"][0]
|
|
|
|
def start_timer(self) -> None:
|
|
self.timer_started = time.time()
|
|
|
|
def current_deck_id(self) -> anki.decks.DeckId:
|
|
return anki.decks.DeckId(self.odid or self.did)
|
|
|
|
def time_limit(self) -> int:
|
|
"Time limit for answering in milliseconds."
|
|
conf = self.col.decks.confForDid(self.current_deck_id())
|
|
return conf["maxTaken"] * 1000
|
|
|
|
def should_show_timer(self) -> bool:
|
|
conf = self.col.decks.confForDid(self.current_deck_id())
|
|
return conf["timer"]
|
|
|
|
def replay_question_audio_on_answer_side(self) -> bool:
|
|
conf = self.col.decks.confForDid(self.current_deck_id())
|
|
return conf.get("replayq", True)
|
|
|
|
def autoplay(self) -> bool:
|
|
return self.col.decks.confForDid(self.current_deck_id())["autoplay"]
|
|
|
|
def time_taken(self) -> int:
|
|
"Time taken to answer card, in integer MS."
|
|
total = int((time.time() - self.timer_started) * 1000)
|
|
return min(total, self.time_limit())
|
|
|
|
def description(self) -> str:
|
|
dict_copy = dict(self.__dict__)
|
|
# remove non-useful elements
|
|
del dict_copy["_note"]
|
|
del dict_copy["_render_output"]
|
|
del dict_copy["col"]
|
|
del dict_copy["timerStarted"]
|
|
return f"{super().__repr__()} {pprint.pformat(dict_copy, width=300)}"
|
|
|
|
def user_flag(self) -> int:
|
|
return self.flags & 0b111
|
|
|
|
def set_user_flag(self, flag: int) -> None:
|
|
print("use col.set_user_flag_for_cards() instead")
|
|
assert 0 <= flag <= 7
|
|
self.flags = (self.flags & ~0b111) | flag
|
|
|
|
@deprecated(info="use card.render_output() directly")
|
|
def css(self) -> str:
|
|
return f"<style>{self.render_output().css}</style>"
|
|
|
|
@deprecated(info="handled by template rendering")
|
|
def is_empty(self) -> bool:
|
|
return False
|
|
|
|
|
|
Card.register_deprecated_aliases(
|
|
flushSched=Card.flush,
|
|
q=Card.question,
|
|
a=Card.answer,
|
|
model=Card.note_type,
|
|
)
|