empty card handling

This commit is contained in:
Damien Elmes 2020-04-25 18:25:56 +10:00
parent e0de3d6b8c
commit bee0eb1264
6 changed files with 182 additions and 6 deletions

View file

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

View file

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

View file

@ -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<pb::EmptyCardsReport> {
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 {

View file

@ -84,13 +84,14 @@ impl CardGenContext<'_> {
&self,
note: &Note,
existing: &[AlreadyGeneratedCardInfo],
ensure_not_empty: bool,
) -> Vec<CardToGenerate> {
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<AlreadyGeneratedCardInfo>,
) -> Vec<(NoteID, Vec<AlreadyGeneratedCardInfo>)> {
let mut out = vec![];
@ -225,7 +226,7 @@ impl Collection {
existing: &[AlreadyGeneratedCardInfo],
target_deck_id: Option<DeckID>,
) -> Result<()> {
let cards = ctx.new_cards_required(note, &existing);
let cards = ctx.new_cards_required(note, &existing, true);
if cards.is_empty() {
return Ok(());
}

View file

@ -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<Vec<EmptyCardsForNote>> {
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(&note, &[], 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<Vec<(NoteTypeID, Vec<EmptyCardsForNote>)>> {
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<EmptyCardsForNote>)],
) -> Result<String> {
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,
"<div><b>{}</b></div><ol>",
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::<Vec<_>>()
.join(", "),
// "Cloze 1, 3"
NoteTypeKind::Cloze => format!(
"{} {}",
self.i18n.tr(TR::NotetypesClozeName),
note.empty
.iter()
.map(|(ord, _)| (ord + 1).to_string())
.collect::<Vec<_>>()
.join(", ")
),
};
let class = if note.current_count == note.empty.len() {
"allempty"
} else {
""
};
write!(
buf,
"<li class={}>[anki:nid:{}] {}</li>",
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("</ol>");
}
}
Ok(buf)
}
}

View file

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