update the card layout screen

- changes are now committed in bulk when closing the dialog,
and can be canceled
- it's not necessary to save the note to the database to preview it
- duplicate fields are now shown as duplicates in the top list
- redraw preview more quickly
- use + instead of _ when deduplicating names, as the latter is a
glob character
This commit is contained in:
Damien Elmes 2020-05-13 17:24:49 +10:00
parent 12b8fe6147
commit 6680cdf1d3
11 changed files with 251 additions and 184 deletions

View file

@ -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;

View file

@ -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)

View file

@ -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
##########################################################################

View file

@ -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)

View file

@ -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)

View file

@ -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)(.+)<hr id=answer>(.+)", 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<hr id=answer>\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")

View file

@ -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()

View file

@ -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):

View file

@ -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
##########################################################################

View file

@ -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<pb::RenderCardOut> {
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(&note, &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

View file

@ -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('+');
}
}
}