mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
empty card handling
This commit is contained in:
parent
e0de3d6b8c
commit
bee0eb1264
6 changed files with 182 additions and 6 deletions
|
@ -83,6 +83,7 @@ message BackendInput {
|
||||||
AddNoteIn add_note = 67;
|
AddNoteIn add_note = 67;
|
||||||
Note update_note = 68;
|
Note update_note = 68;
|
||||||
int64 get_note = 69;
|
int64 get_note = 69;
|
||||||
|
Empty get_empty_cards = 70;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +146,7 @@ message BackendOutput {
|
||||||
int64 add_note = 67;
|
int64 add_note = 67;
|
||||||
Empty update_note = 68;
|
Empty update_note = 68;
|
||||||
Note get_note = 69;
|
Note get_note = 69;
|
||||||
|
EmptyCardsReport get_empty_cards = 70;
|
||||||
|
|
||||||
BackendError error = 2047;
|
BackendError error = 2047;
|
||||||
}
|
}
|
||||||
|
@ -628,3 +630,14 @@ message AddNoteIn {
|
||||||
Note note = 1;
|
Note note = 1;
|
||||||
int64 deck_id = 2;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
empty-cards-card-line =
|
empty-cards-for-note-type = Empty cards for { $notetype }:
|
||||||
Empty card numbers: {$card-numbers}
|
empty-cards-count-line =
|
||||||
Fields: {$fields}
|
{ $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
|
||||||
|
|
|
@ -344,6 +344,7 @@ impl Backend {
|
||||||
OValue::UpdateNote(pb::Empty {})
|
OValue::UpdateNote(pb::Empty {})
|
||||||
}
|
}
|
||||||
Value::GetNote(nid) => OValue::GetNote(self.get_note(nid)?),
|
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)
|
.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 {
|
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
||||||
|
|
|
@ -84,13 +84,14 @@ impl CardGenContext<'_> {
|
||||||
&self,
|
&self,
|
||||||
note: &Note,
|
note: &Note,
|
||||||
existing: &[AlreadyGeneratedCardInfo],
|
existing: &[AlreadyGeneratedCardInfo],
|
||||||
|
ensure_not_empty: bool,
|
||||||
) -> Vec<CardToGenerate> {
|
) -> Vec<CardToGenerate> {
|
||||||
let extracted = extract_data_from_existing_cards(existing);
|
let extracted = extract_data_from_existing_cards(existing);
|
||||||
let cards = match self.notetype.config.kind() {
|
let cards = match self.notetype.config.kind() {
|
||||||
NoteTypeKind::Normal => self.new_cards_required_normal(note, &extracted),
|
NoteTypeKind::Normal => self.new_cards_required_normal(note, &extracted),
|
||||||
NoteTypeKind::Cloze => self.new_cards_required_cloze(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,
|
// 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
|
// we add card 0 to ensure the note always has at least one card
|
||||||
vec![CardToGenerate {
|
vec![CardToGenerate {
|
||||||
|
@ -157,7 +158,7 @@ impl CardGenContext<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// this could be reworked in the future to avoid the extra vec allocation
|
// 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>,
|
items: Vec<AlreadyGeneratedCardInfo>,
|
||||||
) -> Vec<(NoteID, Vec<AlreadyGeneratedCardInfo>)> {
|
) -> Vec<(NoteID, Vec<AlreadyGeneratedCardInfo>)> {
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
|
@ -225,7 +226,7 @@ impl Collection {
|
||||||
existing: &[AlreadyGeneratedCardInfo],
|
existing: &[AlreadyGeneratedCardInfo],
|
||||||
target_deck_id: Option<DeckID>,
|
target_deck_id: Option<DeckID>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let cards = ctx.new_cards_required(note, &existing);
|
let cards = ctx.new_cards_required(note, &existing, true);
|
||||||
if cards.is_empty() {
|
if cards.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
137
rslib/src/notetype/emptycards.rs
Normal file
137
rslib/src/notetype/emptycards.rs
Normal 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(¬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<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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
mod cardgen;
|
mod cardgen;
|
||||||
|
mod emptycards;
|
||||||
mod fields;
|
mod fields;
|
||||||
mod schema11;
|
mod schema11;
|
||||||
mod schemachange;
|
mod schemachange;
|
||||||
|
|
Loading…
Reference in a new issue