From bee0eb126426ad5cef9ca508ed1c9eecb9024fb8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 25 Apr 2020 18:25:56 +1000 Subject: [PATCH] empty card handling --- proto/backend.proto | 13 +++ rslib/ftl/empty-cards.ftl | 9 +- rslib/src/backend/mod.rs | 21 +++++ rslib/src/notetype/cardgen.rs | 7 +- rslib/src/notetype/emptycards.rs | 137 +++++++++++++++++++++++++++++++ rslib/src/notetype/mod.rs | 1 + 6 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 rslib/src/notetype/emptycards.rs diff --git a/proto/backend.proto b/proto/backend.proto index c637d9603..4f631e917 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -83,6 +83,7 @@ message BackendInput { AddNoteIn add_note = 67; Note update_note = 68; int64 get_note = 69; + Empty get_empty_cards = 70; } } @@ -145,6 +146,7 @@ message BackendOutput { int64 add_note = 67; Empty update_note = 68; Note get_note = 69; + EmptyCardsReport get_empty_cards = 70; BackendError error = 2047; } @@ -628,3 +630,14 @@ message AddNoteIn { Note note = 1; int64 deck_id = 2; } + +message EmptyCardsReport { + string report = 1; + repeated NoteWithEmptyCards notes = 2; +} + +message NoteWithEmptyCards { + int64 note_id = 1; + repeated int64 card_ids = 2; + bool will_delete_note = 3; +} diff --git a/rslib/ftl/empty-cards.ftl b/rslib/ftl/empty-cards.ftl index 2cc9758ac..f4227e72d 100644 --- a/rslib/ftl/empty-cards.ftl +++ b/rslib/ftl/empty-cards.ftl @@ -1,3 +1,6 @@ -empty-cards-card-line = - Empty card numbers: {$card-numbers} - Fields: {$fields} +empty-cards-for-note-type = Empty cards for { $notetype }: +empty-cards-count-line = + { $empty_count } of { $existing_count } cards empty ({ $template_names }). +empty-cards-window-title = Empty Cards +empty-cards-preserve-notes-checkbox = Keep notes with no valid cards +empty-cards-delete-button = Delete diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index c02c4179b..82844c7a6 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -344,6 +344,7 @@ impl Backend { OValue::UpdateNote(pb::Empty {}) } Value::GetNote(nid) => OValue::GetNote(self.get_note(nid)?), + Value::GetEmptyCards(_) => OValue::GetEmptyCards(self.get_empty_cards()?), }) } @@ -996,6 +997,26 @@ impl Backend { .map(Into::into) }) } + + fn get_empty_cards(&self) -> Result { + self.with_col(|col| { + let mut empty = col.empty_cards()?; + let report = col.empty_cards_report(&mut empty)?; + + let mut outnotes = vec![]; + for (_ntid, notes) in empty { + outnotes.extend(notes.into_iter().map(|e| pb::NoteWithEmptyCards { + note_id: e.nid.0, + will_delete_note: e.empty.len() == e.current_count, + card_ids: e.empty.into_iter().map(|(_ord, id)| id.0).collect(), + })) + } + Ok(pb::EmptyCardsReport { + report, + notes: outnotes, + }) + }) + } } fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { diff --git a/rslib/src/notetype/cardgen.rs b/rslib/src/notetype/cardgen.rs index fb456727c..29c1b293e 100644 --- a/rslib/src/notetype/cardgen.rs +++ b/rslib/src/notetype/cardgen.rs @@ -84,13 +84,14 @@ impl CardGenContext<'_> { &self, note: &Note, existing: &[AlreadyGeneratedCardInfo], + ensure_not_empty: bool, ) -> Vec { let extracted = extract_data_from_existing_cards(existing); let cards = match self.notetype.config.kind() { NoteTypeKind::Normal => self.new_cards_required_normal(note, &extracted), NoteTypeKind::Cloze => self.new_cards_required_cloze(note, &extracted), }; - if extracted.existing_ords.is_empty() && cards.is_empty() { + if extracted.existing_ords.is_empty() && cards.is_empty() && ensure_not_empty { // if there are no existing cards and no cards will be generated, // we add card 0 to ensure the note always has at least one card vec![CardToGenerate { @@ -157,7 +158,7 @@ impl CardGenContext<'_> { } // this could be reworked in the future to avoid the extra vec allocation -fn group_generated_cards_by_note( +pub(super) fn group_generated_cards_by_note( items: Vec, ) -> Vec<(NoteID, Vec)> { let mut out = vec![]; @@ -225,7 +226,7 @@ impl Collection { existing: &[AlreadyGeneratedCardInfo], target_deck_id: Option, ) -> Result<()> { - let cards = ctx.new_cards_required(note, &existing); + let cards = ctx.new_cards_required(note, &existing, true); if cards.is_empty() { return Ok(()); } diff --git a/rslib/src/notetype/emptycards.rs b/rslib/src/notetype/emptycards.rs new file mode 100644 index 000000000..f579f9a1d --- /dev/null +++ b/rslib/src/notetype/emptycards.rs @@ -0,0 +1,137 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{ + cardgen::group_generated_cards_by_note, CardGenContext, NoteType, NoteTypeID, NoteTypeKind, +}; +use crate::{ + card::CardID, + collection::Collection, + err::Result, + i18n::{tr_args, TR}, + notes::NoteID, +}; +use std::collections::HashSet; +use std::fmt::Write; + +pub struct EmptyCardsForNote { + pub nid: NoteID, + // (ordinal, card id) + pub empty: Vec<(u32, CardID)>, + pub current_count: usize, +} + +impl Collection { + fn empty_cards_for_notetype(&self, nt: &NoteType) -> Result> { + let ctx = CardGenContext::new(nt, self.usn()?); + let existing_cards = self.storage.existing_cards_for_notetype(nt.id)?; + let by_note = group_generated_cards_by_note(existing_cards); + let mut out = Vec::with_capacity(by_note.len()); + + for (nid, existing) in by_note { + let note = self.storage.get_note(nid)?.unwrap(); + let cards = ctx.new_cards_required(¬e, &[], false); + let nonempty_ords: HashSet<_> = cards.into_iter().map(|c| c.ord).collect(); + let current_count = existing.len(); + let empty: Vec<_> = existing + .into_iter() + .filter_map(|e| { + if !nonempty_ords.contains(&e.ord) { + Some((e.ord, e.id)) + } else { + None + } + }) + .collect(); + out.push(EmptyCardsForNote { + nid, + empty, + current_count, + }) + } + + Ok(out) + } + + pub fn empty_cards(&mut self) -> Result)>> { + self.get_all_notetypes()? + .into_iter() + .map(|(id, nt)| self.empty_cards_for_notetype(&nt).map(|v| (id, v))) + .collect() + } + + /// Create a report on empty cards. Mutates the provided data to sort ordinals. + pub fn empty_cards_report( + &mut self, + empty: &mut [(NoteTypeID, Vec)], + ) -> Result { + let nts = self.get_all_notetypes()?; + let mut buf = String::new(); + for (ntid, notes) in empty { + if !notes.is_empty() { + let nt = nts.get(ntid).unwrap(); + write!( + buf, + "
{}
    ", + self.i18n.trn( + TR::EmptyCardsForNoteType, + tr_args!["notetype"=>nt.name.clone()], + ) + ) + .unwrap(); + + for note in notes { + note.empty.sort_unstable(); + let templates = match nt.config.kind() { + // "Front, Back" + NoteTypeKind::Normal => note + .empty + .iter() + .map(|(ord, _)| { + nt.templates + .get(*ord as usize) + .map(|t| t.name.clone()) + .unwrap_or_default() + }) + .collect::>() + .join(", "), + // "Cloze 1, 3" + NoteTypeKind::Cloze => format!( + "{} {}", + self.i18n.tr(TR::NotetypesClozeName), + note.empty + .iter() + .map(|(ord, _)| (ord + 1).to_string()) + .collect::>() + .join(", ") + ), + }; + let class = if note.current_count == note.empty.len() { + "allempty" + } else { + "" + }; + write!( + buf, + "
  1. [anki:nid:{}] {}
  2. ", + class, + note.nid, + self.i18n.trn( + TR::EmptyCardsCountLine, + tr_args![ + "empty_count"=>note.empty.len(), + "existing_count"=>note.current_count, + "template_names"=>templates + ], + ) + ) + .unwrap(); + } + + buf.push_str("
"); + } + } + + Ok(buf) + } +} diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 3fc48ca1d..2996fb671 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -2,6 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod cardgen; +mod emptycards; mod fields; mod schema11; mod schemachange;