make mark toggling undoable
- note.flush() behaves like before, as otherwise actions or add-ons that perform bulk flushing would end up creating an undo entry for each note - added col.update_note() to opt in to the new behaviour - tidy up the names of some related routines
This commit is contained in:
parent
bec77fd420
commit
f1a1b0891e
@ -13,3 +13,4 @@ undo-answer-card = Answer Card
|
|||||||
undo-unbury-unsuspend = Unbury/Unsuspend
|
undo-unbury-unsuspend = Unbury/Unsuspend
|
||||||
undo-add-note = Add Note
|
undo-add-note = Add Note
|
||||||
undo-update-tag = Update Tag
|
undo-update-tag = Update Tag
|
||||||
|
undo-update-note = Update Note
|
||||||
|
@ -141,7 +141,7 @@ class Card:
|
|||||||
|
|
||||||
def note(self, reload: bool = False) -> Note:
|
def note(self, reload: bool = False) -> Note:
|
||||||
if not self._note or reload:
|
if not self._note or reload:
|
||||||
self._note = self.col.getNote(self.nid)
|
self._note = self.col.get_note(self.nid)
|
||||||
return self._note
|
return self._note
|
||||||
|
|
||||||
def note_type(self) -> NoteType:
|
def note_type(self) -> NoteType:
|
||||||
|
@ -322,9 +322,16 @@ class Collection:
|
|||||||
def getCard(self, id: int) -> Card:
|
def getCard(self, id: int) -> Card:
|
||||||
return Card(self, id)
|
return Card(self, id)
|
||||||
|
|
||||||
def getNote(self, id: int) -> Note:
|
def get_note(self, id: int) -> Note:
|
||||||
return Note(self, id=id)
|
return Note(self, id=id)
|
||||||
|
|
||||||
|
def update_note(self, note: Note) -> None:
|
||||||
|
"""Save note changes to database, and add an undo entry.
|
||||||
|
Unlike note.flush(), this will invalidate any current checkpoint."""
|
||||||
|
self._backend.update_note(note=note.to_backend_note(), skip_undo_entry=False)
|
||||||
|
|
||||||
|
getNote = get_note
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
@ -789,7 +796,7 @@ table.review-log {{ {revlog_style} }}
|
|||||||
if not isinstance(self._undo, _ReviewsUndo):
|
if not isinstance(self._undo, _ReviewsUndo):
|
||||||
self._undo = _ReviewsUndo()
|
self._undo = _ReviewsUndo()
|
||||||
|
|
||||||
was_leech = card.note().hasTag("leech")
|
was_leech = card.note().has_tag("leech")
|
||||||
entry = ReviewUndo(card=copy.copy(card), was_leech=was_leech)
|
entry = ReviewUndo(card=copy.copy(card), was_leech=was_leech)
|
||||||
self._undo.entries.append(entry)
|
self._undo.entries.append(entry)
|
||||||
|
|
||||||
@ -824,8 +831,8 @@ table.review-log {{ {revlog_style} }}
|
|||||||
card = entry.card
|
card = entry.card
|
||||||
|
|
||||||
# remove leech tag if it didn't have it before
|
# remove leech tag if it didn't have it before
|
||||||
if not entry.was_leech and card.note().hasTag("leech"):
|
if not entry.was_leech and card.note().has_tag("leech"):
|
||||||
card.note().delTag("leech")
|
card.note().remove_tag("leech")
|
||||||
card.note().flush()
|
card.note().flush()
|
||||||
|
|
||||||
# write old data
|
# write old data
|
||||||
|
@ -66,8 +66,10 @@ class Note:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def flush(self) -> None:
|
def flush(self) -> None:
|
||||||
|
"""This preserves any current checkpoint.
|
||||||
|
For an undo entry, use col.update_note() instead."""
|
||||||
assert self.id != 0
|
assert self.id != 0
|
||||||
self.col._backend.update_note(self.to_backend_note())
|
self.col._backend.update_note(note=self.to_backend_note(), skip_undo_entry=True)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
@ -154,16 +156,10 @@ class Note:
|
|||||||
# Tags
|
# Tags
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def hasTag(self, tag: str) -> bool:
|
def has_tag(self, tag: str) -> bool:
|
||||||
return self.col.tags.inList(tag, self.tags)
|
return self.col.tags.inList(tag, self.tags)
|
||||||
|
|
||||||
def stringTags(self) -> Any:
|
def remove_tag(self, tag: str) -> None:
|
||||||
return self.col.tags.join(self.col.tags.canonify(self.tags))
|
|
||||||
|
|
||||||
def setTagsFromStr(self, tags: str) -> None:
|
|
||||||
self.tags = self.col.tags.split(tags)
|
|
||||||
|
|
||||||
def delTag(self, tag: str) -> None:
|
|
||||||
rem = []
|
rem = []
|
||||||
for t in self.tags:
|
for t in self.tags:
|
||||||
if t.lower() == tag.lower():
|
if t.lower() == tag.lower():
|
||||||
@ -171,10 +167,20 @@ class Note:
|
|||||||
for r in rem:
|
for r in rem:
|
||||||
self.tags.remove(r)
|
self.tags.remove(r)
|
||||||
|
|
||||||
def addTag(self, tag: str) -> None:
|
def add_tag(self, tag: str) -> None:
|
||||||
# duplicates will be stripped on save
|
"Add tag. Duplicates will be stripped on save."
|
||||||
self.tags.append(tag)
|
self.tags.append(tag)
|
||||||
|
|
||||||
|
def stringTags(self) -> Any:
|
||||||
|
return self.col.tags.join(self.col.tags.canonify(self.tags))
|
||||||
|
|
||||||
|
def setTagsFromStr(self, tags: str) -> None:
|
||||||
|
self.tags = self.col.tags.split(tags)
|
||||||
|
|
||||||
|
hasTag = has_tag
|
||||||
|
addTag = add_tag
|
||||||
|
delTag = remove_tag
|
||||||
|
|
||||||
# Unique/duplicate check
|
# Unique/duplicate check
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
@ -606,7 +606,7 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
|
|||||||
if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0:
|
if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0:
|
||||||
# add a leech tag
|
# add a leech tag
|
||||||
f = card.note()
|
f = card.note()
|
||||||
f.addTag("leech")
|
f.add_tag("leech")
|
||||||
f.flush()
|
f.flush()
|
||||||
# handle
|
# handle
|
||||||
a = conf["leechAction"]
|
a = conf["leechAction"]
|
||||||
|
@ -411,7 +411,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
|||||||
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
||||||
|
|
||||||
def buryNote(self, nid: int) -> None:
|
def buryNote(self, nid: int) -> None:
|
||||||
note = self.col.getNote(nid)
|
note = self.col.get_note(nid)
|
||||||
self.bury_cards(note.card_ids())
|
self.bury_cards(note.card_ids())
|
||||||
|
|
||||||
def unburyCards(self) -> None:
|
def unburyCards(self) -> None:
|
||||||
|
@ -1078,7 +1078,7 @@ limit ?"""
|
|||||||
if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0:
|
if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0:
|
||||||
# add a leech tag
|
# add a leech tag
|
||||||
f = card.note()
|
f = card.note()
|
||||||
f.addTag("leech")
|
f.add_tag("leech")
|
||||||
f.flush()
|
f.flush()
|
||||||
# handle
|
# handle
|
||||||
a = conf["leechAction"]
|
a = conf["leechAction"]
|
||||||
@ -1323,7 +1323,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
|||||||
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
||||||
|
|
||||||
def buryNote(self, nid: int) -> None:
|
def buryNote(self, nid: int) -> None:
|
||||||
note = self.col.getNote(nid)
|
note = self.col.get_note(nid)
|
||||||
self.bury_cards(note.card_ids())
|
self.bury_cards(note.card_ids())
|
||||||
|
|
||||||
def unburyCards(self) -> None:
|
def unburyCards(self) -> None:
|
||||||
|
@ -51,7 +51,7 @@ def test_anki2_mediadupes():
|
|||||||
imp = Anki2Importer(empty, col.path)
|
imp = Anki2Importer(empty, col.path)
|
||||||
imp.run()
|
imp.run()
|
||||||
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
||||||
n = empty.getNote(empty.db.scalar("select id from notes"))
|
n = empty.get_note(empty.db.scalar("select id from notes"))
|
||||||
assert "foo.mp3" in n.fields[0]
|
assert "foo.mp3" in n.fields[0]
|
||||||
# if the local file content is different, and import should trigger a
|
# if the local file content is different, and import should trigger a
|
||||||
# rename
|
# rename
|
||||||
@ -61,7 +61,7 @@ def test_anki2_mediadupes():
|
|||||||
imp = Anki2Importer(empty, col.path)
|
imp = Anki2Importer(empty, col.path)
|
||||||
imp.run()
|
imp.run()
|
||||||
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
||||||
n = empty.getNote(empty.db.scalar("select id from notes"))
|
n = empty.get_note(empty.db.scalar("select id from notes"))
|
||||||
assert "_" in n.fields[0]
|
assert "_" in n.fields[0]
|
||||||
# if the localized media file already exists, we rewrite the note and
|
# if the localized media file already exists, we rewrite the note and
|
||||||
# media
|
# media
|
||||||
@ -72,7 +72,7 @@ def test_anki2_mediadupes():
|
|||||||
imp.run()
|
imp.run()
|
||||||
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
||||||
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
||||||
n = empty.getNote(empty.db.scalar("select id from notes"))
|
n = empty.get_note(empty.db.scalar("select id from notes"))
|
||||||
assert "_" in n.fields[0]
|
assert "_" in n.fields[0]
|
||||||
|
|
||||||
|
|
||||||
@ -162,8 +162,8 @@ def test_csv():
|
|||||||
assert len(i.log) == 10
|
assert len(i.log) == 10
|
||||||
assert i.total == 5
|
assert i.total == 5
|
||||||
# but importing should not clobber tags if they're unmapped
|
# but importing should not clobber tags if they're unmapped
|
||||||
n = col.getNote(col.db.scalar("select id from notes"))
|
n = col.get_note(col.db.scalar("select id from notes"))
|
||||||
n.addTag("test")
|
n.add_tag("test")
|
||||||
n.flush()
|
n.flush()
|
||||||
i.run()
|
i.run()
|
||||||
n.load()
|
n.load()
|
||||||
@ -217,7 +217,7 @@ def test_tsv_tag_modified():
|
|||||||
n["Front"] = "1"
|
n["Front"] = "1"
|
||||||
n["Back"] = "2"
|
n["Back"] = "2"
|
||||||
n["Top"] = "3"
|
n["Top"] = "3"
|
||||||
n.addTag("four")
|
n.add_tag("four")
|
||||||
col.addNote(n)
|
col.addNote(n)
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
|
# https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
|
||||||
@ -253,8 +253,8 @@ def test_tsv_tag_multiple_tags():
|
|||||||
n["Front"] = "1"
|
n["Front"] = "1"
|
||||||
n["Back"] = "2"
|
n["Back"] = "2"
|
||||||
n["Top"] = "3"
|
n["Top"] = "3"
|
||||||
n.addTag("four")
|
n.add_tag("four")
|
||||||
n.addTag("five")
|
n.add_tag("five")
|
||||||
col.addNote(n)
|
col.addNote(n)
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
|
# https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
|
||||||
|
@ -46,37 +46,37 @@ def test_fields():
|
|||||||
# add a field
|
# add a field
|
||||||
field = col.models.newField("foo")
|
field = col.models.newField("foo")
|
||||||
col.models.addField(m, field)
|
col.models.addField(m, field)
|
||||||
assert col.getNote(col.models.nids(m)[0]).fields == ["1", "2", ""]
|
assert col.get_note(col.models.nids(m)[0]).fields == ["1", "2", ""]
|
||||||
assert col.models.scmhash(m) != h
|
assert col.models.scmhash(m) != h
|
||||||
# rename it
|
# rename it
|
||||||
field = m["flds"][2]
|
field = m["flds"][2]
|
||||||
col.models.renameField(m, field, "bar")
|
col.models.renameField(m, field, "bar")
|
||||||
assert col.getNote(col.models.nids(m)[0])["bar"] == ""
|
assert col.get_note(col.models.nids(m)[0])["bar"] == ""
|
||||||
# delete back
|
# delete back
|
||||||
col.models.remField(m, m["flds"][1])
|
col.models.remField(m, m["flds"][1])
|
||||||
assert col.getNote(col.models.nids(m)[0]).fields == ["1", ""]
|
assert col.get_note(col.models.nids(m)[0]).fields == ["1", ""]
|
||||||
# move 0 -> 1
|
# move 0 -> 1
|
||||||
col.models.moveField(m, m["flds"][0], 1)
|
col.models.moveField(m, m["flds"][0], 1)
|
||||||
assert col.getNote(col.models.nids(m)[0]).fields == ["", "1"]
|
assert col.get_note(col.models.nids(m)[0]).fields == ["", "1"]
|
||||||
# move 1 -> 0
|
# move 1 -> 0
|
||||||
col.models.moveField(m, m["flds"][1], 0)
|
col.models.moveField(m, m["flds"][1], 0)
|
||||||
assert col.getNote(col.models.nids(m)[0]).fields == ["1", ""]
|
assert col.get_note(col.models.nids(m)[0]).fields == ["1", ""]
|
||||||
# add another and put in middle
|
# add another and put in middle
|
||||||
field = col.models.newField("baz")
|
field = col.models.newField("baz")
|
||||||
col.models.addField(m, field)
|
col.models.addField(m, field)
|
||||||
note = col.getNote(col.models.nids(m)[0])
|
note = col.get_note(col.models.nids(m)[0])
|
||||||
note["baz"] = "2"
|
note["baz"] = "2"
|
||||||
note.flush()
|
note.flush()
|
||||||
assert col.getNote(col.models.nids(m)[0]).fields == ["1", "", "2"]
|
assert col.get_note(col.models.nids(m)[0]).fields == ["1", "", "2"]
|
||||||
# move 2 -> 1
|
# move 2 -> 1
|
||||||
col.models.moveField(m, m["flds"][2], 1)
|
col.models.moveField(m, m["flds"][2], 1)
|
||||||
assert col.getNote(col.models.nids(m)[0]).fields == ["1", "2", ""]
|
assert col.get_note(col.models.nids(m)[0]).fields == ["1", "2", ""]
|
||||||
# move 0 -> 2
|
# move 0 -> 2
|
||||||
col.models.moveField(m, m["flds"][0], 2)
|
col.models.moveField(m, m["flds"][0], 2)
|
||||||
assert col.getNote(col.models.nids(m)[0]).fields == ["2", "", "1"]
|
assert col.get_note(col.models.nids(m)[0]).fields == ["2", "", "1"]
|
||||||
# move 0 -> 1
|
# move 0 -> 1
|
||||||
col.models.moveField(m, m["flds"][0], 1)
|
col.models.moveField(m, m["flds"][0], 1)
|
||||||
assert col.getNote(col.models.nids(m)[0]).fields == ["", "2", "1"]
|
assert col.get_note(col.models.nids(m)[0]).fields == ["", "2", "1"]
|
||||||
|
|
||||||
|
|
||||||
def test_templates():
|
def test_templates():
|
||||||
|
@ -145,7 +145,7 @@ class AddCards(QDialog):
|
|||||||
m = QMenu(self)
|
m = QMenu(self)
|
||||||
for nid in self.history:
|
for nid in self.history:
|
||||||
if self.mw.col.findNotes(SearchNode(nid=nid)):
|
if self.mw.col.findNotes(SearchNode(nid=nid)):
|
||||||
note = self.mw.col.getNote(nid)
|
note = self.mw.col.get_note(nid)
|
||||||
fields = note.fields
|
fields = note.fields
|
||||||
txt = htmlToTextLine(", ".join(fields))
|
txt = htmlToTextLine(", ".join(fields))
|
||||||
if len(txt) > 30:
|
if len(txt) > 30:
|
||||||
|
@ -413,7 +413,7 @@ class StatusDelegate(QItemDelegate):
|
|||||||
col = None
|
col = None
|
||||||
if c.userFlag() > 0:
|
if c.userFlag() > 0:
|
||||||
col = getattr(colors, f"FLAG{c.userFlag()}_BG")
|
col = getattr(colors, f"FLAG{c.userFlag()}_BG")
|
||||||
elif c.note().hasTag("Marked"):
|
elif c.note().has_tag("Marked"):
|
||||||
col = colors.MARKED_BG
|
col = colors.MARKED_BG
|
||||||
elif c.queue == QUEUE_TYPE_SUSPENDED:
|
elif c.queue == QUEUE_TYPE_SUSPENDED:
|
||||||
col = colors.SUSPENDED_BG
|
col = colors.SUSPENDED_BG
|
||||||
@ -1317,7 +1317,7 @@ where id in %s"""
|
|||||||
self.remove_tags_from_selected_notes(tags="marked")
|
self.remove_tags_from_selected_notes(tags="marked")
|
||||||
|
|
||||||
def isMarked(self) -> bool:
|
def isMarked(self) -> bool:
|
||||||
return bool(self.card and self.card.note().hasTag("Marked"))
|
return bool(self.card and self.card.note().has_tag("Marked"))
|
||||||
|
|
||||||
# Repositioning
|
# Repositioning
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -506,7 +506,7 @@ class AnkiQt(QMainWindow):
|
|||||||
# make sure we don't get into an inconsistent state if an add-on
|
# make sure we don't get into an inconsistent state if an add-on
|
||||||
# has broken the deck browser or the did_load hook
|
# has broken the deck browser or the did_load hook
|
||||||
try:
|
try:
|
||||||
self.maybeEnableUndo()
|
self.update_undo_actions()
|
||||||
gui_hooks.collection_did_load(self.col)
|
gui_hooks.collection_did_load(self.col)
|
||||||
self.moveToState("deckBrowser")
|
self.moveToState("deckBrowser")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -688,7 +688,7 @@ class AnkiQt(QMainWindow):
|
|||||||
if not guiOnly:
|
if not guiOnly:
|
||||||
self.col.reset()
|
self.col.reset()
|
||||||
gui_hooks.state_did_reset()
|
gui_hooks.state_did_reset()
|
||||||
self.maybeEnableUndo()
|
self.update_undo_actions()
|
||||||
self.moveToState(self.state)
|
self.moveToState(self.state)
|
||||||
|
|
||||||
def requireReset(
|
def requireReset(
|
||||||
@ -1032,7 +1032,7 @@ title="%s" %s>%s</button>""" % (
|
|||||||
if result is None:
|
if result is None:
|
||||||
# should not happen
|
# should not happen
|
||||||
showInfo("nothing to undo")
|
showInfo("nothing to undo")
|
||||||
self.maybeEnableUndo()
|
self.update_undo_actions()
|
||||||
return
|
return
|
||||||
|
|
||||||
elif isinstance(result, ReviewUndo):
|
elif isinstance(result, ReviewUndo):
|
||||||
@ -1070,9 +1070,11 @@ title="%s" %s>%s</button>""" % (
|
|||||||
|
|
||||||
tooltip(tr(TR.UNDO_ACTION_UNDONE, action=name))
|
tooltip(tr(TR.UNDO_ACTION_UNDONE, action=name))
|
||||||
gui_hooks.state_did_revert(name)
|
gui_hooks.state_did_revert(name)
|
||||||
self.maybeEnableUndo()
|
self.update_undo_actions()
|
||||||
|
|
||||||
def maybeEnableUndo(self) -> None:
|
def update_undo_actions(self) -> None:
|
||||||
|
"""Update menu text and enable/disable menu item as appropriate.
|
||||||
|
Plural as this may handle redo in the future too."""
|
||||||
if self.col:
|
if self.col:
|
||||||
status = self.col.undo_status()
|
status = self.col.undo_status()
|
||||||
undo_action = status.undo or None
|
undo_action = status.undo or None
|
||||||
@ -1091,11 +1093,13 @@ title="%s" %s>%s</button>""" % (
|
|||||||
|
|
||||||
def checkpoint(self, name: str) -> None:
|
def checkpoint(self, name: str) -> None:
|
||||||
self.col.save(name)
|
self.col.save(name)
|
||||||
self.maybeEnableUndo()
|
self.update_undo_actions()
|
||||||
|
|
||||||
def autosave(self) -> None:
|
def autosave(self) -> None:
|
||||||
self.col.autosave()
|
self.col.autosave()
|
||||||
self.maybeEnableUndo()
|
self.update_undo_actions()
|
||||||
|
|
||||||
|
maybeEnableUndo = update_undo_actions
|
||||||
|
|
||||||
# Other menu operations
|
# Other menu operations
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -204,8 +204,8 @@ class Reviewer:
|
|||||||
bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
|
bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
|
||||||
|
|
||||||
self.web.eval(f"_showQuestion({json.dumps(q)},'{bodyclass}');")
|
self.web.eval(f"_showQuestion({json.dumps(q)},'{bodyclass}');")
|
||||||
self._drawFlag()
|
self._update_flag_icon()
|
||||||
self._drawMark()
|
self._update_mark_icon()
|
||||||
self._showAnswerButton()
|
self._showAnswerButton()
|
||||||
self.mw.web.setFocus()
|
self.mw.web.setFocus()
|
||||||
# user hook
|
# user hook
|
||||||
@ -215,11 +215,14 @@ class Reviewer:
|
|||||||
print("use card.autoplay() instead of reviewer.autoplay(card)")
|
print("use card.autoplay() instead of reviewer.autoplay(card)")
|
||||||
return card.autoplay()
|
return card.autoplay()
|
||||||
|
|
||||||
def _drawFlag(self) -> None:
|
def _update_flag_icon(self) -> None:
|
||||||
self.web.eval(f"_drawFlag({self.card.userFlag()});")
|
self.web.eval(f"_drawFlag({self.card.userFlag()});")
|
||||||
|
|
||||||
def _drawMark(self) -> None:
|
def _update_mark_icon(self) -> None:
|
||||||
self.web.eval(f"_drawMark({json.dumps(self.card.note().hasTag('marked'))});")
|
self.web.eval(f"_drawMark({json.dumps(self.card.note().has_tag('marked'))});")
|
||||||
|
|
||||||
|
_drawMark = _update_mark_icon
|
||||||
|
_drawFlag = _update_flag_icon
|
||||||
|
|
||||||
# Showing the answer
|
# Showing the answer
|
||||||
##########################################################################
|
##########################################################################
|
||||||
@ -291,7 +294,7 @@ class Reviewer:
|
|||||||
("Ctrl+2", lambda: self.setFlag(2)),
|
("Ctrl+2", lambda: self.setFlag(2)),
|
||||||
("Ctrl+3", lambda: self.setFlag(3)),
|
("Ctrl+3", lambda: self.setFlag(3)),
|
||||||
("Ctrl+4", lambda: self.setFlag(4)),
|
("Ctrl+4", lambda: self.setFlag(4)),
|
||||||
("*", self.onMark),
|
("*", self.toggle_mark_on_current_note),
|
||||||
("=", self.bury_current_note),
|
("=", self.bury_current_note),
|
||||||
("-", self.bury_current_card),
|
("-", self.bury_current_card),
|
||||||
("!", self.suspend_current_note),
|
("!", self.suspend_current_note),
|
||||||
@ -726,7 +729,7 @@ time = %(time)d;
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[tr(TR.STUDYING_MARK_NOTE), "*", self.onMark],
|
[tr(TR.STUDYING_MARK_NOTE), "*", self.toggle_mark_on_current_note],
|
||||||
[tr(TR.STUDYING_BURY_CARD), "-", self.bury_current_card],
|
[tr(TR.STUDYING_BURY_CARD), "-", self.bury_current_card],
|
||||||
[tr(TR.STUDYING_BURY_NOTE), "=", self.bury_current_note],
|
[tr(TR.STUDYING_BURY_NOTE), "=", self.bury_current_note],
|
||||||
[tr(TR.ACTIONS_SET_DUE_DATE), "Ctrl+Shift+D", self.on_set_due],
|
[tr(TR.ACTIONS_SET_DUE_DATE), "Ctrl+Shift+D", self.on_set_due],
|
||||||
@ -785,16 +788,17 @@ time = %(time)d;
|
|||||||
flag = 0
|
flag = 0
|
||||||
self.card.setUserFlag(flag)
|
self.card.setUserFlag(flag)
|
||||||
self.card.flush()
|
self.card.flush()
|
||||||
self._drawFlag()
|
self._update_flag_icon()
|
||||||
|
|
||||||
def onMark(self) -> None:
|
def toggle_mark_on_current_note(self) -> None:
|
||||||
f = self.card.note()
|
note = self.card.note()
|
||||||
if f.hasTag("marked"):
|
if note.has_tag("marked"):
|
||||||
f.delTag("marked")
|
note.remove_tag("marked")
|
||||||
else:
|
else:
|
||||||
f.addTag("marked")
|
note.add_tag("marked")
|
||||||
f.flush()
|
self.mw.col.update_note(note)
|
||||||
self._drawMark()
|
self._update_mark_icon()
|
||||||
|
self.mw.update_undo_actions()
|
||||||
|
|
||||||
def on_set_due(self) -> None:
|
def on_set_due(self) -> None:
|
||||||
if self.mw.state != "review" or not self.card:
|
if self.mw.state != "review" or not self.card:
|
||||||
@ -858,3 +862,4 @@ time = %(time)d;
|
|||||||
onSuspend = suspend_current_note
|
onSuspend = suspend_current_note
|
||||||
onSuspendCard = suspend_current_card
|
onSuspendCard = suspend_current_card
|
||||||
onDelete = delete_current_note
|
onDelete = delete_current_note
|
||||||
|
onMark = toggle_mark_on_current_note
|
||||||
|
@ -171,7 +171,7 @@ service BackendService {
|
|||||||
|
|
||||||
rpc NewNote(NoteTypeID) returns (Note);
|
rpc NewNote(NoteTypeID) returns (Note);
|
||||||
rpc AddNote(AddNoteIn) returns (NoteID);
|
rpc AddNote(AddNoteIn) returns (NoteID);
|
||||||
rpc UpdateNote(Note) returns (Empty);
|
rpc UpdateNote(UpdateNoteIn) returns (Empty);
|
||||||
rpc GetNote(NoteID) returns (Note);
|
rpc GetNote(NoteID) returns (Note);
|
||||||
rpc RemoveNotes(RemoveNotesIn) returns (Empty);
|
rpc RemoveNotes(RemoveNotesIn) returns (Empty);
|
||||||
rpc AddNoteTags(AddNoteTagsIn) returns (UInt32);
|
rpc AddNoteTags(AddNoteTagsIn) returns (UInt32);
|
||||||
@ -948,6 +948,11 @@ message AddNoteIn {
|
|||||||
int64 deck_id = 2;
|
int64 deck_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UpdateNoteIn {
|
||||||
|
Note note = 1;
|
||||||
|
bool skip_undo_entry = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message EmptyCardsReport {
|
message EmptyCardsReport {
|
||||||
message NoteWithEmptyCards {
|
message NoteWithEmptyCards {
|
||||||
int64 note_id = 1;
|
int64 note_id = 1;
|
||||||
|
@ -50,6 +50,7 @@ use crate::{
|
|||||||
text::{escape_anki_wildcards, extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag},
|
text::{escape_anki_wildcards, extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag},
|
||||||
timestamp::TimestampSecs,
|
timestamp::TimestampSecs,
|
||||||
types::Usn,
|
types::Usn,
|
||||||
|
undo::UndoableOp,
|
||||||
};
|
};
|
||||||
use fluent::FluentValue;
|
use fluent::FluentValue;
|
||||||
use futures::future::{AbortHandle, AbortRegistration, Abortable};
|
use futures::future::{AbortHandle, AbortRegistration, Abortable};
|
||||||
@ -1043,10 +1044,15 @@ impl BackendService for Backend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_note(&self, input: pb::Note) -> BackendResult<Empty> {
|
fn update_note(&self, input: pb::UpdateNoteIn) -> BackendResult<Empty> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let mut note: Note = input.into();
|
let op = if input.skip_undo_entry {
|
||||||
col.update_note(&mut note)
|
None
|
||||||
|
} else {
|
||||||
|
Some(UndoableOp::UpdateNote)
|
||||||
|
};
|
||||||
|
let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into();
|
||||||
|
col.update_note_with_op(&mut note, op)
|
||||||
})
|
})
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
@ -330,13 +330,21 @@ impl Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
|
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
|
||||||
|
self.update_note_with_op(note, Some(UndoableOp::UpdateNote))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_note_with_op(
|
||||||
|
&mut self,
|
||||||
|
note: &mut Note,
|
||||||
|
op: Option<UndoableOp>,
|
||||||
|
) -> Result<()> {
|
||||||
let mut existing_note = self.storage.get_note(note.id)?.ok_or(AnkiError::NotFound)?;
|
let mut existing_note = self.storage.get_note(note.id)?.ok_or(AnkiError::NotFound)?;
|
||||||
if !note_modified(&mut existing_note, note) {
|
if !note_modified(&mut existing_note, note) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transact(None, |col| {
|
self.transact(op, |col| {
|
||||||
let nt = col
|
let nt = col
|
||||||
.get_notetype(note.notetype_id)?
|
.get_notetype(note.notetype_id)?
|
||||||
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
||||||
|
@ -16,6 +16,7 @@ pub enum UndoableOp {
|
|||||||
AddNote,
|
AddNote,
|
||||||
RemoveNote,
|
RemoveNote,
|
||||||
UpdateTag,
|
UpdateTag,
|
||||||
|
UpdateNote,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UndoableOp {
|
impl UndoableOp {
|
||||||
@ -35,6 +36,7 @@ impl Collection {
|
|||||||
UndoableOp::AddNote => TR::UndoAddNote,
|
UndoableOp::AddNote => TR::UndoAddNote,
|
||||||
UndoableOp::RemoveNote => TR::StudyingDeleteNote,
|
UndoableOp::RemoveNote => TR::StudyingDeleteNote,
|
||||||
UndoableOp::UpdateTag => TR::UndoUpdateTag,
|
UndoableOp::UpdateTag => TR::UndoUpdateTag,
|
||||||
|
UndoableOp::UpdateNote => TR::UndoUpdateNote,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.i18n.tr(key).to_string()
|
self.i18n.tr(key).to_string()
|
||||||
|
Loading…
Reference in New Issue
Block a user