From ce2f4136ea6efe65a800376efb68fc252ad04ce4 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 29 Aug 2024 15:06:41 +0200 Subject: [PATCH] Empty cards become undoable (#3386) * Empty cards is undoable If there was a reason for this operation not to be undoable, I can't easily guess it. My main hyposhesis was that the number of deleted card may be too big. But I realized that deleting a deck is undoable and may delete as many note. As you may know, I realized that only the undoable operations triggered notification in AnkiDroid that we may have to update the UI. And while I just wanted to trigger more notifications, some reviewers thought it would be nicer if the operation were returning a OpChanges. So here it's done. If you would please consider merging it. I decided to introduce a new string because the closest strings I could find currently are "Empty cards..." and the trailing commas don't seem nice in "undo". And the title, which we may not be able to reuse in all language * Don't count cards that have already been removed (dae) --- ftl/core/actions.ftl | 1 + proto/anki/cards.proto | 2 +- pylib/anki/collection.py | 6 ++++-- rslib/src/card/mod.rs | 6 ++++-- rslib/src/card/service.rs | 12 ++++++++---- rslib/src/ops.rs | 2 ++ 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/ftl/core/actions.ftl b/ftl/core/actions.ftl index fa70bb24f..82c31e1ed 100644 --- a/ftl/core/actions.ftl +++ b/ftl/core/actions.ftl @@ -12,6 +12,7 @@ actions-decks = Decks actions-decrement-value = Decrement value actions-delete = Delete actions-export = Export +actions-empty-cards = Empty Cards actions-filter = Filter actions-help = Help actions-increment-value = Increment value diff --git a/proto/anki/cards.proto b/proto/anki/cards.proto index 17e5c3412..82d29dcff 100644 --- a/proto/anki/cards.proto +++ b/proto/anki/cards.proto @@ -13,7 +13,7 @@ import "anki/collection.proto"; service CardsService { rpc GetCard(CardId) returns (Card); rpc UpdateCards(UpdateCardsRequest) returns (collection.OpChanges); - rpc RemoveCards(RemoveCardsRequest) returns (generic.Empty); + rpc RemoveCards(RemoveCardsRequest) returns (collection.OpChangesWithCount); rpc SetDeck(SetDeckRequest) returns (collection.OpChangesWithCount); rpc SetFlag(SetFlagRequest) returns (collection.OpChangesWithCount); } diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index b3bad9922..ce1c7135a 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -604,9 +604,11 @@ class Collection(DeprecatedNamesMixin): def card_count(self) -> Any: return self.db.scalar("select count() from cards") - def remove_cards_and_orphaned_notes(self, card_ids: Sequence[CardId]) -> None: + def remove_cards_and_orphaned_notes( + self, card_ids: Sequence[CardId] + ) -> OpChangesWithCount: "You probably want .remove_notes_by_card() instead." - self._backend.remove_cards(card_ids=card_ids) + return self._backend.remove_cards(card_ids=card_ids) def set_deck(self, card_ids: Sequence[CardId], deck_id: int) -> OpChangesWithCount: return self._backend.set_deck(card_ids=card_ids, deck_id=deck_id) diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index 06b1a86a3..f943ee3b5 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -327,13 +327,15 @@ impl Collection { /// Remove cards and any resulting orphaned notes. /// Expects a transaction. - pub(crate) fn remove_cards_and_orphaned_notes(&mut self, cids: &[CardId]) -> Result<()> { + pub(crate) fn remove_cards_and_orphaned_notes(&mut self, cids: &[CardId]) -> Result { let usn = self.usn()?; let mut nids = HashSet::new(); + let mut card_count = 0; for cid in cids { if let Some(card) = self.storage.get_card(*cid)? { nids.insert(card.note_id); self.remove_card_and_add_grave_undoable(card, usn)?; + card_count += 1; } } for nid in nids { @@ -342,7 +344,7 @@ impl Collection { } } - Ok(()) + Ok(card_count) } pub fn set_deck(&mut self, cards: &[CardId], deck_id: DeckId) -> Result> { diff --git a/rslib/src/card/service.rs b/rslib/src/card/service.rs index 6a43d5312..950cf8528 100644 --- a/rslib/src/card/service.rs +++ b/rslib/src/card/service.rs @@ -14,6 +14,7 @@ use crate::error::OrNotFound; use crate::notes::NoteId; use crate::prelude::TimestampSecs; use crate::prelude::Usn; +use crate::undo::Op; impl crate::services::CardsService for Collection { fn get_card( @@ -44,17 +45,20 @@ impl crate::services::CardsService for Collection { .map(Into::into) } - fn remove_cards(&mut self, input: anki_proto::cards::RemoveCardsRequest) -> error::Result<()> { - self.transact_no_undo(|col| { + fn remove_cards( + &mut self, + input: anki_proto::cards::RemoveCardsRequest, + ) -> error::Result { + self.transact(Op::EmptyCards, |col| { col.remove_cards_and_orphaned_notes( &input .card_ids .into_iter() .map(Into::into) .collect::>(), - )?; - Ok(()) + ) }) + .map(Into::into) } fn set_deck( diff --git a/rslib/src/ops.rs b/rslib/src/ops.rs index 4ea1c005a..54780300d 100644 --- a/rslib/src/ops.rs +++ b/rslib/src/ops.rs @@ -15,6 +15,7 @@ pub enum Op { ChangeNotetype, ClearUnusedTags, CreateCustomStudy, + EmptyCards, EmptyFilteredDeck, FindAndReplace, ImageOcclusion, @@ -57,6 +58,7 @@ impl Op { Op::AnswerCard => tr.actions_answer_card(), Op::Bury => tr.studying_bury(), Op::CreateCustomStudy => tr.actions_custom_study(), + Op::EmptyCards => tr.actions_empty_cards(), Op::Import => tr.actions_import(), Op::RemoveDeck => tr.decks_delete_deck(), Op::RemoveNote => tr.studying_delete_note(),