diff --git a/proto/backend.proto b/proto/backend.proto index 806929e66..43dcea93e 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -33,7 +33,7 @@ message BackendInput { DeckTreeIn deck_tree = 18; SearchCardsIn search_cards = 19; SearchNotesIn search_notes = 20; -// RenderCardIn render_card = 21; + RenderUncommittedCardIn render_uncommitted_card = 21; int64 local_minutes_west = 22; string strip_av_tags = 23; ExtractAVTagsIn extract_av_tags = 24; @@ -120,7 +120,7 @@ message BackendOutput { DeckTreeNode deck_tree = 18; SearchCardsOut search_cards = 19; SearchNotesOut search_notes = 20; -// RenderCardOut render_card = 21; + RenderCardOut render_uncommitted_card = 21; string add_media_file = 26; Empty sync_media = 27; MediaCheckOut check_media = 28; @@ -269,6 +269,12 @@ message RenderExistingCardIn { bool browser = 2; } +message RenderUncommittedCardIn { + Note note = 1; + uint32 card_ord = 2; + bytes template = 3; +} + message RenderCardOut { repeated RenderedTemplateNode question_nodes = 1; repeated RenderedTemplateNode answer_nodes = 2; diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 75ff244f5..29248df2f 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -14,7 +14,6 @@ from anki.models import NoteType, Template from anki.notes import Note from anki.rsbackend import BackendCard from anki.sound import AVTag -from anki.utils import joinFields # Cards ########################################################################## @@ -133,6 +132,9 @@ class Card: ).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.getNote(self.nid) diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 5f3a31a7f..2739ee9cd 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -408,24 +408,27 @@ class ModelManager: t["name"] = name return t - def addTemplate(self, m: NoteType, template: Template) -> None: + def addTemplate(self, m: NoteType, template: Template, save=True) -> None: if m["id"]: self.col.modSchema(check=True) m["tmpls"].append(template) - if m["id"]: + if save and m["id"]: self.save(m) - def remTemplate(self, m: NoteType, template: Template) -> None: + def remTemplate(self, m: NoteType, template: Template, save=True) -> None: assert len(m["tmpls"]) > 1 self.col.modSchema(check=True) m["tmpls"].remove(template) - self.save(m) + if save: + self.save(m) - def moveTemplate(self, m: NoteType, template: Template, idx: int) -> None: + def moveTemplate( + self, m: NoteType, template: Template, idx: int, save=True + ) -> None: self.col.modSchema(check=True) oldidx = m["tmpls"].index(template) @@ -435,7 +438,8 @@ class ModelManager: m["tmpls"].remove(template) m["tmpls"].insert(idx, template) - self.save(m) + if save: + self.save(m) # Model changing ########################################################################## diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index dc09b6ab0..de3287105 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -312,6 +312,23 @@ class RustBackend: return PartiallyRenderedCard(qnodes, anodes) + def render_uncommitted_card( + self, note: BackendNote, card_ord: int, template: Dict + ) -> PartiallyRenderedCard: + template_json = orjson.dumps(template) + out = self._run_command( + pb.BackendInput( + render_uncommitted_card=pb.RenderUncommittedCardIn( + note=note, template=template_json, card_ord=card_ord + ) + ) + ).render_uncommitted_card + + qnodes = proto_replacement_list_to_native(out.question_nodes) # type: ignore + anodes = proto_replacement_list_to_native(out.answer_nodes) # type: ignore + + return PartiallyRenderedCard(qnodes, anodes) + def local_minutes_west(self, stamp: int) -> int: return self._run_command( pb.BackendInput(local_minutes_west=stamp) diff --git a/pylib/anki/template.py b/pylib/anki/template.py index 26124ecfa..298f4db8f 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -56,15 +56,10 @@ class TemplateRenderContext: return TemplateRenderContext(card.col, card, card.note(), browser) @classmethod - def from_card_layout(cls, note: Note, card_ord: int) -> TemplateRenderContext: - card = cls.synthesized_card(note.col, card_ord) - return TemplateRenderContext(note.col, card, note) - - @classmethod - def synthesized_card(cls, col: anki.storage._Collection, ord: int): - c = Card(col) - c.ord = ord - return c + def from_card_layout( + cls, note: Note, card: Card, template: Dict + ) -> TemplateRenderContext: + return TemplateRenderContext(note.col, card, note, template=template) def __init__( self, @@ -72,7 +67,7 @@ class TemplateRenderContext: card: Card, note: Note, browser: bool = False, - template: Optional[Any] = None, + template: Optional[Dict] = None, ) -> None: self._col = col.weakref() self._card = card @@ -146,7 +141,9 @@ class TemplateRenderContext: def _partially_render(self) -> PartiallyRenderedCard: if self._template: # card layout screen - raise Exception("nyi") + return self._col.backend.render_uncommitted_card( + self._note.to_backend_note(), self._card.ord, self._template + ) else: # existing card (eg study mode) return self._col.backend.render_existing_card(self._card.id, self._browser) diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 150f5ddaa..463030000 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -2,10 +2,9 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import collections import json import re -from typing import Optional +from typing import List, Optional import aqt from anki.cards import Card @@ -13,6 +12,7 @@ from anki.consts import * from anki.lang import _, ngettext from anki.notes import Note from anki.rsbackend import TemplateError +from anki.template import TemplateRenderContext from anki.utils import isMac, isWin, joinFields from aqt import AnkiQt, gui_hooks from aqt.qt import * @@ -27,41 +27,34 @@ from aqt.utils import ( saveGeom, showInfo, showWarning, + tooltip, ) from aqt.webview import AnkiWebView +# fixme: removeColons() +# fixme: previewing with empty fields +# fixme: deck name on new cards +# fixme: card count when removing +# fixme: i18n +# fixme: change tracking and tooltip in fields + class CardLayout(QDialog): - card: Optional[Card] - def __init__( - self, - mw: AnkiQt, - note: Note, - ord=0, - parent: Optional[QWidget] = None, - addMode=False, + self, mw: AnkiQt, note: Note, ord=0, parent: Optional[QWidget] = None, ): QDialog.__init__(self, parent or mw, Qt.Window) mw.setupDialogGC(self) self.mw = aqt.mw self.note = note self.ord = ord - self.col = self.mw.col + self.col = self.mw.col.weakref() self.mm = self.mw.col.models self.model = note.model() + self.templates = self.model["tmpls"] + self.mm._remove_from_cache(self.model["id"]) self.mw.checkpoint(_("Card Types")) - self.addMode = addMode - if addMode: - # save it to DB temporarily - self.emptyFields = [] - for name, val in list(note.items()): - if val.strip(): - continue - self.emptyFields.append(name) - note[name] = "(%s)" % name - note.flush() - self.removeColons() + self.changed = False self.setupTopArea() self.setupMainArea() self.setupButtons() @@ -74,7 +67,7 @@ class CardLayout(QDialog): v1.setContentsMargins(12, 12, 12, 12) self.setLayout(v1) gui_hooks.card_layout_will_show(self) - self.redraw() + self.redraw_everything() restoreGeom(self, "CardLayout") self.setWindowModality(Qt.ApplicationModal) self.show() @@ -82,32 +75,26 @@ class CardLayout(QDialog): # as users tend to accidentally type into the template self.setFocus() - def redraw(self): - did = None - if hasattr(self.parent(), "deckChooser"): - did = self.parent().deckChooser.selectedId() - self.cards = self.col.previewCards(self.note, 2, did=did) - idx = self.ord - if idx >= len(self.cards): - self.ord = len(self.cards) - 1 - - self.redrawing = True + def redraw_everything(self): + self.ignore_change_signals = True self.updateTopArea() self.updateMainArea() - self.redrawing = False - self.onCardSelected(self.ord) + self.ignore_change_signals = False + self.update_current_ordinal_and_redraw(self.ord) - def setupShortcuts(self): - for i in range(1, 9): - QShortcut( - QKeySequence("Ctrl+%d" % i), - self, - activated=lambda i=i: self.selectCard(i), - ) # type: ignore + def update_current_ordinal_and_redraw(self, idx): + if self.ignore_change_signals: + return + self.ord = idx + self.playedAudio = {} + self.fill_fields_from_template() + self.renderPreview() - def selectCard(self, n): - self.ord = n - 1 - self.redraw() + def _isCloze(self): + return self.model["type"] == MODEL_CLOZE + + # Top area + ########################################################################## def setupTopArea(self): self.topArea = QWidget() @@ -115,7 +102,10 @@ class CardLayout(QDialog): self.topAreaForm.setupUi(self.topArea) self.topAreaForm.templateOptions.setText(_("Options") + " " + downArrow()) qconnect(self.topAreaForm.templateOptions.clicked, self.onMore) - qconnect(self.topAreaForm.templatesBox.currentIndexChanged, self.onCardSelected) + qconnect( + self.topAreaForm.templatesBox.currentIndexChanged, + self.update_current_ordinal_and_redraw, + ) def updateTopArea(self): cnt = self.mw.col.models.useCount(self.model) @@ -130,17 +120,19 @@ class CardLayout(QDialog): self.updateCardNames() def updateCardNames(self): - self.redrawing = True + self.ignore_change_signals = True combo = self.topAreaForm.templatesBox combo.clear() - combo.addItems(self._summarizedName(t) for t in self.model["tmpls"]) + combo.addItems( + self._summarizedName(idx, tmpl) for (idx, tmpl) in enumerate(self.templates) + ) combo.setCurrentIndex(self.ord) combo.setEnabled(not self._isCloze()) - self.redrawing = False + self.ignore_change_signals = False - def _summarizedName(self, tmpl): + def _summarizedName(self, idx: int, tmpl: Dict): return "{}: {}: {} -> {}".format( - tmpl["ord"] + 1, + idx + 1, tmpl["name"], self._fieldsOnTemplate(tmpl["qfmt"]), self._fieldsOnTemplate(tmpl["afmt"]), @@ -148,8 +140,8 @@ class CardLayout(QDialog): def _fieldsOnTemplate(self, fmt): matches = re.findall("{{[^#/}]+?}}", fmt) - charsAllowed = 30 - result: Dict[str, bool] = collections.OrderedDict() + chars_allowed = 30 + field_names: List[str] = [] for m in matches: # strip off mustache m = re.sub(r"[{}]", "", m) @@ -159,19 +151,30 @@ class CardLayout(QDialog): if m == "FrontSide": continue - if m not in result: - result[m] = True - charsAllowed -= len(m) - if charsAllowed <= 0: - break + field_names.append(m) + chars_allowed -= len(m) + if chars_allowed <= 0: + break - s = "+".join(result.keys()) - if charsAllowed <= 0: + s = "+".join(field_names) + if chars_allowed <= 0: s += "+..." return s - def _isCloze(self): - return self.model["type"] == MODEL_CLOZE + def setupShortcuts(self): + for i in range(1, 9): + QShortcut( + QKeySequence("Ctrl+%d" % i), + self, + activated=lambda i=i: self.selectCard(i), + ) # type: ignore + + def selectCard(self, n): + self.ord = n - 1 + self.redraw_everything() + + # Main area + ########################################################################## def setupMainArea(self): w = self.mainArea = QWidget() @@ -192,9 +195,9 @@ class CardLayout(QDialog): tform.tlayout2.setContentsMargins(0, 11, 0, 0) tform.tlayout3.setContentsMargins(0, 11, 0, 0) tform.groupBox_3.setTitle(_("Styling (shared between cards)")) - qconnect(tform.front.textChanged, self.saveCard) - qconnect(tform.css.textChanged, self.saveCard) - qconnect(tform.back.textChanged, self.saveCard) + qconnect(tform.front.textChanged, self.write_edits_to_template_and_redraw) + qconnect(tform.css.textChanged, self.write_edits_to_template_and_redraw) + qconnect(tform.back.textChanged, self.write_edits_to_template_and_redraw) l.addWidget(left, 5) # preview area right = QWidget() @@ -235,7 +238,7 @@ class CardLayout(QDialog): def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): - play_clicked_audio(cmd, self.card) + play_clicked_audio(cmd, self.rendered_card) def updateMainArea(self): if self._isCloze(): @@ -243,23 +246,14 @@ class CardLayout(QDialog): for g in self.pform.groupBox, self.pform.groupBox_2: g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1)) - def onRemove(self): - if len(self.model["tmpls"]) < 2: - return showInfo(_("At least one card type is required.")) - idx = self.ord - msg = _("Delete the '%(a)s' card type, and its %(b)s?") % dict( - a=self.model["tmpls"][idx]["name"], b=_("cards") - ) - if not askUser(msg): - return - self.mm.remTemplate(self.model, self.cards[idx].template()) - self.redraw() - - def removeColons(self): - # colons in field names conflict with the template language - for fld in self.model["flds"]: - if ":" in fld["name"]: - self.mm.renameField(self.model, fld, fld["name"]) + def ephemeral_card_for_rendering(self) -> Card: + card = Card(self.col) + card.ord = self.ord + output = TemplateRenderContext.from_card_layout( + self.note, card, template=self.current_template() + ).render() + card.set_render_output(output) + return card # Buttons ########################################################################## @@ -291,21 +285,15 @@ class CardLayout(QDialog): l.addWidget(close) qconnect(close.clicked, self.reject) - # Cards + # Reading/writing question/answer/css ########################################################################## - def onCardSelected(self, idx): - if self.redrawing: - return - self.card = self.cards[idx] - self.ord = idx - self.playedAudio = {} - self.readCard() - self.renderPreview() + def current_template(self) -> Dict: + return self.templates[self.ord] - def readCard(self): - t = self.card.template() - self.redrawing = True + def fill_fields_from_template(self): + t = self.current_template() + self.ignore_change_signals = True self.tform.front.setPlainText(t["qfmt"]) self.tform.css.setPlainText(self.model["css"]) self.tform.back.setPlainText(t["afmt"]) @@ -321,17 +309,16 @@ class CardLayout(QDialog): self.tform.front.setTabStopDistance(tab_width) self.tform.css.setTabStopDistance(tab_width) self.tform.back.setTabStopDistance(tab_width) - self.redrawing = False + self.ignore_change_signals = False - def saveCard(self): - if self.redrawing: + def write_edits_to_template_and_redraw(self): + if self.ignore_change_signals: return - text = self.tform.front.toPlainText() - self.card.template()["qfmt"] = text - text = self.tform.css.toPlainText() - self.card.model()["css"] = text - text = self.tform.back.toPlainText() - self.card.template()["afmt"] = text + self.changed = True + t = self.current_template() + t["qfmt"] = self.tform.front.toPlainText() + t["afmt"] = self.tform.back.toPlainText() + self.model["css"] = self.tform.css.toPlainText() self.renderPreview() # Preview @@ -342,7 +329,7 @@ class CardLayout(QDialog): def renderPreview(self): # schedule a preview when timing stops self.cancelPreviewTimer() - self._previewTimer = self.mw.progress.timer(500, self._renderPreview, False) + self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False) def cancelPreviewTimer(self): if self._previewTimer: @@ -352,12 +339,13 @@ class CardLayout(QDialog): def _renderPreview(self) -> None: self.cancelPreviewTimer() - c = self.card + c = self.rendered_card = self.ephemeral_card_for_rendering() + ti = self.maybeTextInput bodyclass = theme_manager.body_classes_for_card_ord(c.ord) - q = ti(self.mw.prepare_card_text_for_display(c.q(reload=True))) + q = ti(self.mw.prepare_card_text_for_display(c.q())) q = gui_hooks.card_will_show(q, c, "clayoutQuestion") a = ti(self.mw.prepare_card_text_for_display(c.a()), type="a") @@ -398,21 +386,43 @@ class CardLayout(QDialog): # Card operations ###################################################################### - def onRename(self): - name = getOnlyText(_("New name:"), default=self.card.template()["name"]) - if not name: + def onRemove(self): + if len(self.templates) < 2: + return showInfo(_("At least one card type is required.")) + template = self.current_template() + msg = _("Delete the '%(a)s' card type, and its %(b)s?") % dict( + a=template["name"], b=_("cards") + ) + if not askUser(msg): return - if name in [ - c.template()["name"] for c in self.cards if c.template()["ord"] != self.ord - ]: - return showWarning(_("That name is already used.")) - self.card.template()["name"] = name - self.redraw() + + self.changed = True + self.mm.remTemplate(self.model, template, save=False) + + # ensure current ordinal is within bounds + idx = self.ord + if idx >= len(self.templates): + self.ord = len(self.templates) - 1 + + self.redraw_everything() + + def onRename(self): + template = self.current_template() + name = getOnlyText(_("New name:"), default=template["name"]) + if not name.strip(): + return + + self.changed = True + template["name"] = name + self.redraw_everything() def onReorder(self): - n = len(self.cards) - cur = self.card.template()["ord"] + 1 # type: ignore - pos = getOnlyText(_("Enter new card position (1...%s):") % n, default=str(cur)) + n = len(self.templates) + template = self.current_template() + current_pos = self.templates.index(template) + 1 + pos = getOnlyText( + _("Enter new card position (1...%s):") % n, default=str(current_pos) + ) if not pos: return try: @@ -421,18 +431,19 @@ class CardLayout(QDialog): return if pos < 1 or pos > n: return - if pos == cur: + if pos == current_pos: return - pos -= 1 - self.mm.moveTemplate(self.model, self.card.template(), pos) - self.ord = pos - self.redraw() + new_idx = pos - 1 + self.changed = True + self.mm.moveTemplate(self.model, template, new_idx, save=False) + self.ord = new_idx + self.redraw_everything() def _newCardName(self): - n = len(self.cards) + 1 + n = len(self.templates) + 1 while 1: name = _("Card %d") % n - if name not in [c.template()["name"] for c in self.cards]: + if name not in [t["name"] for t in self.templates]: break n += 1 return name @@ -449,19 +460,20 @@ class CardLayout(QDialog): ) if not askUser(txt): return + self.changed = True name = self._newCardName() t = self.mm.newTemplate(name) - old = self.card.template() + old = self.current_template() t["qfmt"] = old["qfmt"] t["afmt"] = old["afmt"] - self.mm.addTemplate(self.model, t) - self.ord = len(self.cards) - self.redraw() + self.mm.addTemplate(self.model, t, save=False) + self.ord = len(self.templates) - 1 + self.redraw_everything() def onFlip(self): - old = self.card.template() + old = self.current_template() self._flipQA(old, old) - self.redraw() + self.redraw_everything() def _flipQA(self, src, dst): m = re.match("(?s)(.+)
(.+)", src["afmt"]) @@ -474,6 +486,7 @@ adjust the template manually to switch the question and answer.""" ) ) return + self.changed = True dst["afmt"] = "{{FrontSide}}\n\n
\n\n%s" % src["qfmt"] dst["qfmt"] = m.group(2).strip() return True @@ -496,7 +509,7 @@ adjust the template manually to switch the question and answer.""" m.addSeparator() - t = self.card.template() + t = self.current_template() if t["did"]: s = _(" (on)") else: @@ -513,7 +526,7 @@ adjust the template manually to switch the question and answer.""" d = QDialog() f = aqt.forms.browserdisp.Ui_Dialog() f.setupUi(d) - t = self.card.template() + t = self.current_template() f.qfmt.setText(t.get("bqfmt", "")) f.afmt.setText(t.get("bafmt", "")) if t.get("bfont"): @@ -524,7 +537,8 @@ adjust the template manually to switch the question and answer.""" d.exec_() def onBrowserDisplayOk(self, f): - t = self.card.template() + t = self.current_template() + self.changed = True t["bqfmt"] = f.qfmt.text().strip() t["bafmt"] = f.afmt.text().strip() if f.overrideFont.isChecked(): @@ -538,7 +552,7 @@ adjust the template manually to switch the question and answer.""" def onTargetDeck(self): from aqt.tagedit import TagEdit - t = self.card.template() + t = self.current_template() d = QDialog(self) d.setWindowTitle("Anki") d.setMinimumWidth(400) @@ -548,7 +562,7 @@ adjust the template manually to switch the question and answer.""" """\ Enter deck to place new %s cards in, or leave blank:""" ) - % self.card.template()["name"] + % self.current_template()["name"] ) lab.setWordWrap(True) l.addWidget(lab) @@ -563,6 +577,7 @@ Enter deck to place new %s cards in, or leave blank:""" l.addWidget(bb) d.setLayout(l) d.exec_() + self.changed = True if not te.text().strip(): t["did"] = None else: @@ -606,42 +621,45 @@ Enter deck to place new %s cards in, or leave blank:""" field, ) widg.setPlainText(t) - self.saveCard() + self.changed = True + self.write_edits_to_template_and_redraw() # Closing & Help ###################################################################### def accept(self) -> None: - try: + def save(): self.mm.save(self.model) - except TemplateError as e: - # fixme: i18n - showWarning("Unable to save changes: " + str(e)) - return - self.mw.reset() - self.cleanup() - return QDialog.accept(self) + def on_done(fut): + try: + fut.result() + except TemplateError as e: + showWarning("Unable to save changes: " + str(e)) + return + self.mw.reset() + tooltip("Changes saved.", parent=self.mw) + self.cleanup() + return QDialog.accept(self) + + self.mw.taskman.with_progress(save, on_done) def reject(self) -> None: - # discard mutations - in the future we should load a fresh - # copy at the start instead - self.mm._remove_from_cache(self.model["id"]) + if self.changed: + if not askUser("Discard changes?"): + return self.cleanup() return QDialog.reject(self) def cleanup(self) -> None: self.cancelPreviewTimer() av_player.stop_and_clear_queue() - # fixme - if self.addMode: - # remove the filler fields we added - for name in self.emptyFields: - self.note[name] = "" - self.mw.col.db.execute("delete from notes where id = ?", self.note.id) saveGeom(self, "CardLayout") self.pform.frontWeb = None self.pform.backWeb = None + self.model = None + self.rendered_card = None + self.mw = None def onHelp(self): openHelp("templates") diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index ef4c26047..d8d71913f 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -353,7 +353,7 @@ class Editor: else: ord = 0 CardLayout( - self.mw, self.note, ord=ord, parent=self.parentWindow, addMode=self.addMode + self.mw, self.note, ord=ord, parent=self.parentWindow, ) if isWin: self.parentWindow.activateWindow() diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index 8c226cfd7..4bade27a2 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -18,6 +18,7 @@ class FieldDialog(QDialog): self.col = self.mw.col self.mm = self.mw.col.models self.model = nt + self.mm._remove_from_cache(self.model["id"]) self.mw.checkpoint(_("Fields")) self.form = aqt.forms.fields.Ui_Dialog() self.form.setupUi(self) @@ -163,7 +164,6 @@ class FieldDialog(QDialog): fld["rtl"] = f.rtl.isChecked() def reject(self): - self.mm._remove_from_cache(self.model["id"]) QDialog.reject(self) def accept(self): diff --git a/qt/aqt/models.py b/qt/aqt/models.py index 1b934bd18..4f1270cc5 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -188,7 +188,7 @@ class Models(QDialog): from aqt.clayout import CardLayout n = self._tmpNote() - CardLayout(self.mw, n, ord=0, parent=self, addMode=True) + CardLayout(self.mw, n, ord=0, parent=self) # Cleanup ########################################################################## diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 0405ea500..a32f1164e 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -21,7 +21,10 @@ use crate::{ media::sync::MediaSyncProgress, media::MediaManager, notes::{Note, NoteID}, - notetype::{all_stock_notetypes, NoteType, NoteTypeID, NoteTypeSchema11, RenderCardOutput}, + notetype::{ + all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeID, NoteTypeSchema11, + RenderCardOutput, + }, sched::cutoff::local_minutes_west_for_stamp, sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span}, search::SortMode, @@ -216,9 +219,6 @@ impl Backend { Ok(match ival { Value::SchedTimingToday(_) => OValue::SchedTimingToday(self.sched_timing_today()?), Value::DeckTree(input) => OValue::DeckTree(self.deck_tree(input)?), - Value::RenderExistingCard(input) => { - OValue::RenderExistingCard(self.render_existing_card(input)?) - } Value::LocalMinutesWest(stamp) => { OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp)) } @@ -375,6 +375,12 @@ impl Backend { self.set_preferences(prefs)?; pb::Empty {} }), + Value::RenderExistingCard(input) => { + OValue::RenderExistingCard(self.render_existing_card(input)?) + } + Value::RenderUncommittedCard(input) => { + OValue::RenderUncommittedCard(self.render_uncommitted_card(input)?) + } }) } @@ -456,6 +462,23 @@ impl Backend { }) } + fn render_uncommitted_card( + &self, + input: pb::RenderUncommittedCardIn, + ) -> Result { + let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?; + let template = schema11.into(); + let note = input + .note + .ok_or_else(|| AnkiError::invalid_input("missing note"))? + .into(); + let ord = input.card_ord as u16; + self.with_col(|col| { + col.render_uncommitted_card(¬e, &template, ord) + .map(Into::into) + }) + } + fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> pb::ExtractAvTagsOut { let (text, tags) = extract_av_tags(&input.text, input.question_side); let pt_tags = tags diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 9e659aeb2..2b9ab5e46 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -84,7 +84,7 @@ impl NoteType { names.insert(name); break; } - t.name.push('_'); + t.name.push('+'); } } names.clear(); @@ -95,7 +95,7 @@ impl NoteType { names.insert(name); break; } - t.name.push('_'); + t.name.push('+'); } } }