// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use std::{collections::HashMap, sync::Arc}; use crate::{ import_export::{ text::{ForeignCard, ForeignData, ForeignNote, ForeignNotetype, ForeignTemplate}, LogNote, NoteLog, }, notetype::{CardGenContext, CardTemplate, NoteField, NotetypeConfig}, prelude::*, }; impl ForeignData { pub fn import(self, col: &mut Collection) -> Result> { col.transact(Op::Import, |col| { let mut ctx = Context::new(&self, col)?; ctx.import_foreign_notetypes(self.notetypes)?; ctx.import_foreign_notes(self.notes) }) } } struct Context<'a> { col: &'a mut Collection, /// Notetypes by their name or id as string. The empty string yields the /// default notetype (which may be [None]). notetypes: HashMap>>, /// Deck ids by their decks' name or id as string. The empty string yields /// the default deck (which may be [None]). deck_ids: HashMap>, usn: Usn, normalize_notes: bool, card_gen_ctxs: HashMap<(NotetypeId, DeckId), CardGenContext>>, //progress: IncrementableProgress, } impl<'a> Context<'a> { fn new(data: &ForeignData, col: &'a mut Collection) -> Result { let usn = col.usn()?; let normalize_notes = col.get_config_bool(BoolKey::NormalizeNoteText); let mut notetypes = HashMap::new(); notetypes.insert( String::new(), col.notetype_for_string(&data.default_notetype)?, ); let mut deck_ids = HashMap::new(); deck_ids.insert(String::new(), col.deck_id_for_string(&data.default_deck)?); Ok(Self { col, usn, normalize_notes, notetypes, deck_ids, card_gen_ctxs: HashMap::new(), }) } fn import_foreign_notetypes(&mut self, notetypes: Vec) -> Result<()> { for foreign in notetypes { let mut notetype = foreign.into_native(); notetype.usn = self.usn; self.col .add_notetype_inner(&mut notetype, self.usn, false)?; } Ok(()) } fn notetype_for_note(&mut self, note: &ForeignNote) -> Result>> { Ok(if let Some(nt) = self.notetypes.get(¬e.notetype) { nt.clone() } else { let nt = self.col.notetype_for_string(¬e.notetype)?; self.notetypes.insert(note.notetype.clone(), nt.clone()); nt }) } fn deck_id_for_note(&mut self, note: &ForeignNote) -> Result> { Ok(if let Some(did) = self.deck_ids.get(¬e.deck) { *did } else { let did = self.col.deck_id_for_string(¬e.deck)?; self.deck_ids.insert(note.deck.clone(), did); did }) } fn import_foreign_notes(&mut self, notes: Vec) -> Result { let mut log = NoteLog::default(); for foreign in notes { if let Some(notetype) = self.notetype_for_note(&foreign)? { if let Some(deck_id) = self.deck_id_for_note(&foreign)? { let log_note = self.import_foreign_note(foreign, notetype, deck_id)?; log.new.push(log_note); } } } Ok(log) } fn import_foreign_note( &mut self, foreign: ForeignNote, notetype: Arc, deck_id: DeckId, ) -> Result { let today = self.col.timing_today()?.days_elapsed; let (mut note, mut cards) = foreign.into_native(¬etype, deck_id, today); self.import_note(&mut note, ¬etype)?; self.import_cards(&mut cards, note.id)?; self.generate_missing_cards(notetype, deck_id, ¬e)?; Ok(note.into_log_note()) } fn import_note(&mut self, note: &mut Note, notetype: &Notetype) -> Result<()> { self.col.canonify_note_tags(note, self.usn)?; note.prepare_for_update(notetype, self.normalize_notes)?; note.usn = self.usn; self.col.add_note_only_undoable(note) } fn import_cards(&mut self, cards: &mut [Card], note_id: NoteId) -> Result<()> { for card in cards { card.note_id = note_id; self.col.add_card(card)?; } Ok(()) } fn generate_missing_cards( &mut self, notetype: Arc, deck_id: DeckId, note: &Note, ) -> Result<()> { let card_gen_context = self .card_gen_ctxs .entry((notetype.id, deck_id)) .or_insert_with(|| CardGenContext::new(notetype, Some(deck_id), self.usn)); self.col .generate_cards_for_existing_note(card_gen_context, note) } } pub(super) trait NotetypeForString { fn notetype_for_string(&mut self, name_or_id: &str) -> Result>>; } impl NotetypeForString for Collection { fn notetype_for_string(&mut self, name_or_id: &str) -> Result>> { if let Some(nt) = self.get_notetype_for_id_string(name_or_id)? { Ok(Some(nt)) } else { self.get_notetype_by_name(name_or_id) } } } impl Collection { pub(super) fn deck_id_for_string(&mut self, deck: &str) -> Result> { if let Ok(did) = deck.parse::() { if self.get_deck(did)?.is_some() { return Ok(Some(did)); } } self.get_deck_id(deck) } fn get_notetype_for_id_string(&mut self, notetype: &str) -> Result>> { notetype .parse::() .ok() .map(|ntid| self.get_notetype(ntid)) .unwrap_or(Ok(None)) } } impl ForeignNote { fn into_native(self, notetype: &Notetype, deck_id: DeckId, today: u32) -> (Note, Vec) { // TODO: Handle new and learning cards let mut note = Note::new(notetype); note.tags = self.tags; note.fields_mut() .iter_mut() .zip(self.fields.into_iter()) .for_each(|(field, value)| *field = value); let cards = self .cards .into_iter() .enumerate() .map(|(idx, c)| c.into_native(NoteId(0), idx as u16, deck_id, today)) .collect(); (note, cards) } } impl ForeignCard { fn into_native(self, note_id: NoteId, template_idx: u16, deck_id: DeckId, today: u32) -> Card { let mut card = Card::new(note_id, template_idx, deck_id, self.native_due(today)); card.interval = self.interval; card.ease_factor = (self.ease_factor * 1000.).round() as u16; card.reps = self.reps; card.lapses = self.lapses; card } fn native_due(self, today: u32) -> i32 { let remaining_secs = self.interval as i64 - TimestampSecs::now().0; let remaining_days = remaining_secs / (60 * 60 * 24); 0.max(remaining_days as i32 + today as i32) } } impl ForeignNotetype { fn into_native(self) -> Notetype { Notetype { name: self.name, fields: self.fields.into_iter().map(NoteField::new).collect(), templates: self .templates .into_iter() .map(ForeignTemplate::into_native) .collect(), config: if self.is_cloze { NotetypeConfig::new_cloze() } else { NotetypeConfig::new() }, ..Notetype::default() } } } impl ForeignTemplate { fn into_native(self) -> CardTemplate { CardTemplate::new(self.name, self.qfmt, self.afmt) } }