diff --git a/.pylintrc b/.pylintrc index ceb890365..2413cc6c4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -45,4 +45,4 @@ good-names = ip, [IMPORTS] -ignored-modules = anki.*_pb2, anki.sync_pb2, win32file,pywintypes,socket,win32pipe,pyaudio,anki.scheduler_pb2 +ignored-modules = anki.*_pb2, anki.sync_pb2, win32file,pywintypes,socket,win32pipe,pyaudio,anki.scheduler_pb2,anki.notetypes_pb2 diff --git a/ftl/core/card-templates.ftl b/ftl/core/card-templates.ftl index 248a34a06..6763cbff1 100644 --- a/ftl/core/card-templates.ftl +++ b/ftl/core/card-templates.ftl @@ -57,3 +57,8 @@ card-templates-this-will-create-card-proceed = *[other] This will create { $count } cards. Proceed? } card-templates-type-boxes-warning = Only one typing box per card template is supported. +card-templates-restore-to-default = Restore to Default +card-templates-restore-to-default-confirmation = This will reset all fields and templates in this notetype to their default + values, and remove any custom styling. Do you wish to proceed? +card-templates-restored-to-default = Notetype has been restored to its original state. + diff --git a/proto/anki/notetypes.proto b/proto/anki/notetypes.proto index 33259c3e1..68816efea 100644 --- a/proto/anki/notetypes.proto +++ b/proto/anki/notetypes.proto @@ -30,6 +30,8 @@ service NotetypesService { returns (ChangeNotetypeInfo); rpc ChangeNotetype(ChangeNotetypeRequest) returns (collection.OpChanges); rpc GetFieldNames(NotetypeId) returns (generic.StringList); + rpc RestoreNotetypeToStock(RestoreNotetypeToStockRequest) + returns (collection.OpChanges); } message NotetypeId { @@ -56,12 +58,14 @@ message Notetype { Kind kind = 1; uint32 sort_field_idx = 2; string css = 3; - /// This is now stored separately; retrieve with DefaultsForAdding() + // This is now stored separately; retrieve with DefaultsForAdding() int64 target_deck_id_unused = 4; string latex_pre = 5; string latex_post = 6; bool latex_svg = 7; repeated CardRequirement reqs = 8; + // Only set on notetypes created with Anki 2.1.62+. + StockNotetype.OriginalStockKind original_stock_kind = 9; bytes other = 255; } @@ -119,12 +123,24 @@ message AddOrUpdateNotetypeRequest { message StockNotetype { enum Kind { - BASIC = 0; - BASIC_AND_REVERSED = 1; - BASIC_OPTIONAL_REVERSED = 2; - BASIC_TYPING = 3; - CLOZE = 4; - IMAGE_OCCLUSION = 5; + KIND_BASIC = 0; + KIND_BASIC_AND_REVERSED = 1; + KIND_BASIC_OPTIONAL_REVERSED = 2; + KIND_BASIC_TYPING = 3; + KIND_CLOZE = 4; + } + // This is decoupled from Kind to allow us to evolve notetypes over time + // (eg an older notetype might require different JS), and allow us to store + // a type even for notetypes that we don't add by default. Code should not + // assume that the entries here are always +1 from Kind. + enum OriginalStockKind { + ORIGINAL_STOCK_KIND_UNKNOWN = 0; + ORIGINAL_STOCK_KIND_BASIC = 1; + ORIGINAL_STOCK_KIND_BASIC_AND_REVERSED = 2; + ORIGINAL_STOCK_KIND_BASIC_OPTIONAL_REVERSED = 3; + ORIGINAL_STOCK_KIND_BASIC_TYPING = 4; + ORIGINAL_STOCK_KIND_CLOZE = 5; + ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION = 6; } Kind kind = 1; @@ -185,3 +201,10 @@ message ChangeNotetypeInfo { ChangeNotetypeRequest input = 5; string old_notetype_name = 6; } + +message RestoreNotetypeToStockRequest { + NotetypeId notetype_id = 1; + // Older notetypes did not store their original stock kind, so we allow the UI + // to pass in an override to use when missing, or for tests. + optional StockNotetype.Kind force_kind = 2; +} \ No newline at end of file diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 7cc3b0c54..7fe27dba3 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -181,7 +181,7 @@ class ModelManager(DeprecatedNamesMixin): "Create a new model, and return it." # caller should call save() after modifying notetype = from_json_bytes( - self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.BASIC) + self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.KIND_BASIC) ) notetype["flds"] = [] notetype["tmpls"] = [] @@ -277,7 +277,7 @@ class ModelManager(DeprecatedNamesMixin): def new_field(self, name: str) -> FieldDict: assert isinstance(name, str) notetype = from_json_bytes( - self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.BASIC) + self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.KIND_BASIC) ) field = notetype["flds"][0] field["name"] = name @@ -321,7 +321,7 @@ class ModelManager(DeprecatedNamesMixin): def new_template(self, name: str) -> TemplateDict: notetype = from_json_bytes( - self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.BASIC) + self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.KIND_BASIC) ) template = notetype["tmpls"][0] template["name"] = name @@ -393,6 +393,16 @@ and notes.mid = ? and cards.ord = ?""", op_bytes = self.col._backend.change_notetype_raw(input.SerializeToString()) return OpChanges.FromString(op_bytes) + def restore_notetype_to_stock( + self, notetype_id: NotetypeId, force_kind: StockNotetypeKind.V | None + ) -> OpChanges: + msg = notetypes_pb2.RestoreNotetypeToStockRequest( + notetype_id=notetypes_pb2.NotetypeId(ntid=notetype_id), + ) + if force_kind is not None: + msg.force_kind = force_kind + return self.col._backend.restore_notetype_to_stock(msg) + # legacy API - used by unit tests and add-ons def change( # pylint: disable=invalid-name diff --git a/pylib/anki/stdmodels.py b/pylib/anki/stdmodels.py index a207bc274..ed91e7815 100644 --- a/pylib/anki/stdmodels.py +++ b/pylib/anki/stdmodels.py @@ -31,13 +31,13 @@ def get_stock_notetypes( out: list[ tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]] ] = [] - # add standard + # add standard - this order should match the one in notetypes.proto for kind in [ - StockNotetypeKind.BASIC, - StockNotetypeKind.BASIC_TYPING, - StockNotetypeKind.BASIC_AND_REVERSED, - StockNotetypeKind.BASIC_OPTIONAL_REVERSED, - StockNotetypeKind.CLOZE, + StockNotetypeKind.KIND_BASIC, + StockNotetypeKind.KIND_BASIC_AND_REVERSED, + StockNotetypeKind.KIND_BASIC_OPTIONAL_REVERSED, + StockNotetypeKind.KIND_BASIC_TYPING, + StockNotetypeKind.KIND_CLOZE, ]: note_type = from_json_bytes(col._backend.get_stock_notetype_legacy(kind)) @@ -65,7 +65,7 @@ def get_stock_notetypes( def _legacy_add_basic_model( col: anki.collection.Collection, ) -> anki.models.NotetypeDict: - note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC) + note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_BASIC) col.models.add(note_type) return note_type @@ -73,7 +73,7 @@ def _legacy_add_basic_model( def _legacy_add_basic_typing_model( col: anki.collection.Collection, ) -> anki.models.NotetypeDict: - note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_TYPING) + note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_BASIC_TYPING) col.models.add(note_type) return note_type @@ -81,7 +81,7 @@ def _legacy_add_basic_typing_model( def _legacy_add_forward_reverse( col: anki.collection.Collection, ) -> anki.models.NotetypeDict: - note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_AND_REVERSED) + note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_BASIC_AND_REVERSED) col.models.add(note_type) return note_type @@ -89,7 +89,7 @@ def _legacy_add_forward_reverse( def _legacy_add_forward_optional_reverse( col: anki.collection.Collection, ) -> anki.models.NotetypeDict: - note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED) + note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_BASIC_OPTIONAL_REVERSED) col.models.add(note_type) return note_type @@ -97,7 +97,7 @@ def _legacy_add_forward_optional_reverse( def _legacy_add_cloze_model( col: anki.collection.Collection, ) -> anki.models.NotetypeDict: - note_type = _get_stock_notetype(col, StockNotetypeKind.CLOZE) + note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_CLOZE) col.models.add(note_type) return note_type diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 52d3fe5fc..1f6e6d4c6 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -1,20 +1,25 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + import json import re from concurrent.futures import Future -from typing import Any, Match, Optional +from typing import Any, Match, Optional, cast import aqt import aqt.forms import aqt.operations +from anki import stdmodels from anki.collection import OpChanges from anki.consts import * from anki.lang import without_unicode_isolation from anki.notes import Note +from anki.notetypes_pb2 import StockNotetype from aqt import AnkiQt, gui_hooks from aqt.forms import browserdisp -from aqt.operations.notetype import update_notetype_legacy +from aqt.operations.notetype import restore_notetype_to_stock, update_notetype_legacy from aqt.qt import * from aqt.schema_change_tracker import ChangeTracker from aqt.sound import av_player, play_clicked_audio @@ -696,6 +701,31 @@ class CardLayout(QDialog): self.ord = len(self.templates) - 1 self.redraw_everything() + def on_restore_to_default( + self, force_kind: StockNotetype.Kind.V | None = None + ) -> None: + if force_kind is None and not self.model.get("originalStockKind", 0): + SelectStockNotetype( + mw=self.mw, + on_success=lambda kind: self.on_restore_to_default(force_kind=kind), + parent=self, + ) + return + + if not askUser( + tr.card_templates_restore_to_default_confirmation(), defaultno=True + ): + return + + def on_success(changes: OpChanges) -> None: + self.change_tracker.set_unchanged() + self.close() + showInfo(tr.card_templates_restored_to_default(), parent=self.mw) + + restore_notetype_to_stock( + parent=self, notetype_id=self.model["id"], force_kind=force_kind + ).success(on_success).run_in_background() + def onFlip(self) -> None: old = self.current_template() self._flipQA(old, old) @@ -717,6 +747,14 @@ class CardLayout(QDialog): a = m.addAction(tr.card_templates_add_card_type()) qconnect(a.triggered, self.onAddCard) + a = m.addAction( + tr.actions_with_ellipsis(action=tr.card_templates_restore_to_default()) + ) + qconnect( + a.triggered, + lambda: self.on_restore_to_default(), # pylint: disable=unnecessary-lambda + ) + a = m.addAction(tr.card_templates_remove_card_type()) qconnect(a.triggered, self.onRemove) @@ -869,3 +907,42 @@ class CardLayout(QDialog): def onHelp(self) -> None: openHelp(HelpPage.TEMPLATES) + + +class SelectStockNotetype(QDialog): + def __init__( + self, + mw: AnkiQt, + on_success: Callable[[StockNotetype.Kind.V], None], + parent: QWidget, + ) -> None: + self.mw = mw + QDialog.__init__(self, parent, Qt.WindowType.Window) + self.dialog = aqt.forms.addmodel.Ui_Dialog() + self.dialog.setupUi(self) + self.setWindowTitle("Anki") + self.setWindowModality(Qt.WindowModality.ApplicationModal) + disable_help_button(self) + stock_types = stdmodels.get_stock_notetypes(mw.col) + + for name, func in stock_types: + item = QListWidgetItem(name) + self.dialog.models.addItem(item) + self.dialog.models.setCurrentRow(0) + # the list widget will swallow the enter key + s = QShortcut(QKeySequence("Return"), self) + qconnect(s.activated, self.accept) + # help + # self.dialog.buttonBox.standardButton(QDialogButtonBox.StandardButton.Help). + self.on_success = on_success + self.show() + + def reject(self) -> None: + QDialog.reject(self) + + def accept(self) -> None: + kind = cast(StockNotetype.Kind.V, self.dialog.models.currentRow()) + QDialog.accept(self) + # On Mac, we need to allow time for the existing modal to close or + # Qt gets confused. + self.mw.progress.single_shot(100, lambda: self.on_success(kind), True) diff --git a/qt/aqt/operations/notetype.py b/qt/aqt/operations/notetype.py index 22231c919..909ec0a98 100644 --- a/qt/aqt/operations/notetype.py +++ b/qt/aqt/operations/notetype.py @@ -5,6 +5,7 @@ from __future__ import annotations from anki.collection import OpChanges, OpChangesWithId from anki.models import ChangeNotetypeRequest, NotetypeDict, NotetypeId +from anki.stdmodels import StockNotetypeKind from aqt.operations import CollectionOp from aqt.qt import QWidget @@ -37,3 +38,12 @@ def change_notetype_of_notes( *, parent: QWidget, input: ChangeNotetypeRequest ) -> CollectionOp[OpChanges]: return CollectionOp(parent, lambda col: col.models.change_notetype_of_notes(input)) + + +def restore_notetype_to_stock( + *, parent: QWidget, notetype_id: NotetypeId, force_kind: StockNotetypeKind.V | None +) -> CollectionOp[OpChanges]: + return CollectionOp( + parent, + lambda col: col.models.restore_notetype_to_stock(notetype_id, force_kind), + ) diff --git a/qt/aqt/schema_change_tracker.py b/qt/aqt/schema_change_tracker.py index 3d61fe516..5b4e203b0 100644 --- a/qt/aqt/schema_change_tracker.py +++ b/qt/aqt/schema_change_tracker.py @@ -34,3 +34,6 @@ class ChangeTracker: def changed(self) -> bool: return self._changed != Change.NO_CHANGE + + def set_unchanged(self) -> None: + self._changed = Change.NO_CHANGE diff --git a/rslib/src/backend/notetypes.rs b/rslib/src/backend/notetypes.rs index a5837d531..854d05611 100644 --- a/rslib/src/backend/notetypes.rs +++ b/rslib/src/backend/notetypes.rs @@ -3,7 +3,8 @@ use super::Backend; use crate::config::get_aux_notetype_config_key; -use crate::notetype::all_stock_notetypes; +use crate::notetype::stock::get_stock_notetype; +use crate::notetype::stock::StockKind; use crate::notetype::ChangeNotetypeInput; use crate::notetype::Notetype; use crate::notetype::NotetypeChangeInfo; @@ -81,10 +82,7 @@ impl NotetypesService for Backend { &self, input: pb::notetypes::StockNotetype, ) -> Result { - // fixme: use individual functions instead of full vec - let mut all = all_stock_notetypes(&self.tr); - let idx = (input.kind as usize).min(all.len() - 1); - let nt = all.swap_remove(idx); + let nt = get_stock_notetype(input.kind(), &self.tr); let schema11: NotetypeSchema11 = nt.into(); serde_json::to_vec(&schema11) .map_err(Into::into) @@ -208,6 +206,20 @@ impl NotetypesService for Backend { self.with_col(|col| col.storage.get_field_names(input.into())) .map(Into::into) } + + fn restore_notetype_to_stock( + &self, + input: pb::notetypes::RestoreNotetypeToStockRequest, + ) -> Result { + let force_kind = input.force_kind.and_then(StockKind::from_i32); + self.with_col(|col| { + col.restore_notetype_to_stock( + input.notetype_id.or_invalid("missing notetype id")?.into(), + force_kind, + ) + .map(Into::into) + }) + } } impl From for Notetype { diff --git a/rslib/src/image_occlusion/imagedata.rs b/rslib/src/image_occlusion/imagedata.rs index 6e4878d9a..ad7594196 100644 --- a/rslib/src/image_occlusion/imagedata.rs +++ b/rslib/src/image_occlusion/imagedata.rs @@ -10,13 +10,15 @@ use regex::Regex; use crate::io::metadata; use crate::io::read_file; use crate::media::MediaManager; +use crate::notetype::stock::empty_stock; use crate::notetype::CardGenContext; use crate::notetype::Notetype; -use crate::notetype::NotetypeConfig; +use crate::notetype::NotetypeKind; use crate::pb::image_occlusion::image_cloze_note_response::Value; use crate::pb::image_occlusion::ImageClozeNote; use crate::pb::image_occlusion::ImageClozeNoteResponse; pub use crate::pb::image_occlusion::ImageData; +use crate::pb::notetypes::stock_notetype::OriginalStockKind; use crate::prelude::*; impl Collection { @@ -101,7 +103,7 @@ impl Collection { fn add_io_notetype(&mut self) -> Result<()> { let usn = self.usn()?; - let mut nt = self.image_occlusion_notetype(); + let mut nt = image_occlusion_notetype(&self.tr); self.add_notetype_inner(&mut nt, usn, false)?; Ok(()) } @@ -187,28 +189,28 @@ impl Collection { Ok(false) } +} - fn image_occlusion_notetype(&mut self) -> Notetype { - let tr = &self.tr; - const IMAGE_CLOZE_CSS: &str = include_str!("image_occlusion_styling.css"); - let mut nt = Notetype { - name: tr.notetypes_image_occlusion_name().into(), - config: NotetypeConfig::new_cloze(), - ..Default::default() - }; - nt.config.css = IMAGE_CLOZE_CSS.to_string(); - let occlusion = tr.notetypes_occlusion(); - nt.add_field(occlusion.as_ref()); - let image = tr.notetypes_image(); - nt.add_field(image.as_ref()); - let header = tr.notetypes_header(); - nt.add_field(header.as_ref()); - let back_extra = tr.notetypes_back_extra_field(); - nt.add_field(back_extra.as_ref()); - let comments = tr.notetypes_comments_field(); - nt.add_field(comments.as_ref()); - let qfmt = format!( - "
{{{{cloze:{}}}}}
+pub(crate) fn image_occlusion_notetype(tr: &I18n) -> Notetype { + const IMAGE_CLOZE_CSS: &str = include_str!("image_occlusion_styling.css"); + let mut nt = empty_stock( + NotetypeKind::Cloze, + OriginalStockKind::ImageOcclusion, + tr.notetypes_image_occlusion_name(), + ); + nt.config.css = IMAGE_CLOZE_CSS.to_string(); + let occlusion = tr.notetypes_occlusion(); + nt.add_field(occlusion.as_ref()); + let image = tr.notetypes_image(); + nt.add_field(image.as_ref()); + let header = tr.notetypes_header(); + nt.add_field(header.as_ref()); + let back_extra = tr.notetypes_back_extra_field(); + nt.add_field(back_extra.as_ref()); + let comments = tr.notetypes_comments_field(); + nt.add_field(comments.as_ref()); + let qfmt = format!( + "
{{{{cloze:{}}}}}
{{{{{}}}}} @@ -222,25 +224,24 @@ try {{ }} ", - occlusion, - image, - tr.notetypes_error_loading_image_occlusion(), - ); - let afmt = format!( - "{{{{{}}}}} + occlusion, + image, + tr.notetypes_error_loading_image_occlusion(), + ); + let afmt = format!( + "{{{{{}}}}} {}
{{{{{}}}}}
{{{{{}}}}}", - header, - qfmt, - tr.notetypes_toggle_masks(), - back_extra, - comments, - ); - nt.add_template(nt.name.clone(), qfmt, afmt); - nt - } + header, + qfmt, + tr.notetypes_toggle_masks(), + back_extra, + comments, + ); + nt.add_template(nt.name.clone(), qfmt, afmt); + nt } diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index aa5fd7985..5034e5f7b 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -7,6 +7,7 @@ mod emptycards; mod fields; mod notetypechange; mod render; +mod restore; mod schema11; mod schemachange; pub(crate) mod stock; diff --git a/rslib/src/notetype/restore.rs b/rslib/src/notetype/restore.rs new file mode 100644 index 000000000..25234c5d5 --- /dev/null +++ b/rslib/src/notetype/restore.rs @@ -0,0 +1,93 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::notetype::stock::get_original_stock_notetype; +use crate::notetype::stock::StockKind; +use crate::pb::notetypes::stock_notetype::Kind; +use crate::pb::notetypes::stock_notetype::OriginalStockKind; +use crate::prelude::*; + +impl Collection { + /// If force_kind is not Unknown, it will be used in preference to the kind + /// stored in the notetype. If Unknown, and the kind stored in the + /// notetype is also Unknown, an error will be returned. + pub(crate) fn restore_notetype_to_stock( + &mut self, + notetype_id: NotetypeId, + force_kind: Option, + ) -> Result> { + let mut nt = self + .storage + .get_notetype(notetype_id)? + .or_not_found(notetype_id)?; + let stock_kind = match (nt.config.original_stock_kind(), force_kind) { + (_, Some(force_kind)) => match force_kind { + Kind::Basic => OriginalStockKind::Basic, + Kind::BasicAndReversed => OriginalStockKind::BasicAndReversed, + Kind::BasicOptionalReversed => OriginalStockKind::BasicOptionalReversed, + Kind::BasicTyping => OriginalStockKind::BasicTyping, + Kind::Cloze => OriginalStockKind::Cloze, + }, + (stock, _) => stock, + }; + if stock_kind == OriginalStockKind::Unknown { + invalid_input!("unknown original notetype kind"); + } + + let mut stock_nt = get_original_stock_notetype(stock_kind, &self.tr)?; + for (idx, item) in stock_nt.templates.iter_mut().enumerate() { + item.ord = Some(idx as u32); + } + nt.templates = stock_nt.templates; + for (idx, item) in stock_nt.fields.iter_mut().enumerate() { + item.ord = Some(idx as u32); + } + nt.fields = stock_nt.fields; + nt.config.css = stock_nt.config.css; + if force_kind.is_some() { + nt.config.original_stock_kind = stock_kind as i32; + nt.config.kind = stock_nt.config.kind; + } + self.update_notetype(&mut nt, false) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn adding_and_removing_fields_and_templates() -> Result<()> { + let mut col = Collection::new(); + let nt = col.get_notetype_by_name("Basic")?.unwrap(); + let note = NoteAdder::basic(&mut col) + .fields(&["front", "back"]) + .add(&mut col); + + col.restore_notetype_to_stock(nt.id, Some(StockKind::BasicOptionalReversed))?; + let note = col.storage.get_note(note.id)?.unwrap(); + assert_eq!(note.fields(), &["front", "back", ""]); + assert_eq!( + col.storage.db_scalar::("select count(*) from cards")?, + 1 + ); + + col.restore_notetype_to_stock(nt.id, Some(StockKind::BasicAndReversed))?; + let note = col.storage.get_note(note.id)?.unwrap(); + assert_eq!(note.fields(), &["front", "back"]); + assert_eq!( + col.storage.db_scalar::("select count(*) from cards")?, + 2 + ); + + col.restore_notetype_to_stock(nt.id, Some(StockKind::Cloze))?; + let note = col.storage.get_note(note.id)?.unwrap(); + assert_eq!(note.fields(), &["front", "back"]); + assert_eq!( + col.storage.db_scalar::("select count(*) from cards")?, + 1 + ); + + Ok(()) + } +} diff --git a/rslib/src/notetype/schema11.rs b/rslib/src/notetype/schema11.rs index 47d4fd2d7..174cbd20a 100644 --- a/rslib/src/notetype/schema11.rs +++ b/rslib/src/notetype/schema11.rs @@ -23,6 +23,7 @@ use crate::notetype::NotetypeConfig; use crate::serde::default_on_invalid; use crate::serde::deserialize_bool_from_anything; use crate::serde::deserialize_number_from_string; +use crate::serde::is_default; use crate::timestamp::TimestampSecs; use crate::types::Usn; @@ -59,6 +60,8 @@ pub struct NotetypeSchema11 { pub latexsvg: bool, #[serde(default, deserialize_with = "default_on_invalid")] pub(crate) req: CardRequirementsSchema11, + #[serde(default, skip_serializing_if = "is_default")] + pub(crate) original_stock_kind: i32, #[serde(flatten)] pub(crate) other: HashMap, } @@ -103,6 +106,7 @@ impl From for Notetype { latex_post: nt.latex_post, latex_svg: nt.latexsvg, reqs: nt.req.0.into_iter().map(Into::into).collect(), + original_stock_kind: nt.original_stock_kind, other: other_to_bytes(&nt.other), }, fields: nt.flds.into_iter().map(Into::into).collect(), @@ -160,6 +164,7 @@ impl From for NotetypeSchema11 { latex_post: c.latex_post, latexsvg: c.latex_svg, req: CardRequirementsSchema11(c.reqs.into_iter().map(Into::into).collect()), + original_stock_kind: c.original_stock_kind, other: bytes_to_other(&c.other), } } @@ -167,7 +172,13 @@ impl From for NotetypeSchema11 { /// See [crate::deckconfig::schema11::clear_other_duplicates()]. fn clear_other_field_duplicates(other: &mut HashMap) { - for key in &["description", "plainText", "collapsed", "excludeFromSearch"] { + for key in &[ + "description", + "plainText", + "collapsed", + "excludeFromSearch", + "originalStockKind", + ] { other.remove(*key); } } diff --git a/rslib/src/notetype/stock.rs b/rslib/src/notetype/stock.rs index 5abc689e5..1769895d1 100644 --- a/rslib/src/notetype/stock.rs +++ b/rslib/src/notetype/stock.rs @@ -6,8 +6,13 @@ use crate::config::ConfigEntry; use crate::config::ConfigKey; use crate::error::Result; use crate::i18n::I18n; +use crate::image_occlusion::imagedata::image_occlusion_notetype; +use crate::invalid_input; use crate::notetype::Notetype; +use crate::pb::notetypes::notetype::config::Kind as NotetypeKind; use crate::pb::notetypes::stock_notetype::Kind; +pub(crate) use crate::pb::notetypes::stock_notetype::Kind as StockKind; +use crate::pb::notetypes::stock_notetype::OriginalStockKind; use crate::storage::SqliteStorage; use crate::timestamp::TimestampSecs; @@ -16,7 +21,7 @@ impl SqliteStorage { for (idx, mut nt) in all_stock_notetypes(tr).into_iter().enumerate() { nt.prepare_for_update(None, true)?; self.add_notetype(&mut nt)?; - if idx == Kind::Basic as usize { + if idx == 0 { self.set_config_entry(&ConfigEntry::boxed( ConfigKey::CurrentNotetypeId.into(), serde_json::to_vec(&nt.id)?, @@ -29,7 +34,8 @@ impl SqliteStorage { } } -// if changing this, make sure to update StockNotetype enum +// If changing this, make sure to update StockNotetype enum. Other parts of the +// code expect the order here to be the same as the enum. pub fn all_stock_notetypes(tr: &I18n) -> Vec { vec![ basic(tr), @@ -45,11 +51,55 @@ fn fieldref>(name: S) -> String { format!("{{{{{}}}}}", name.as_ref()) } -pub(crate) fn basic(tr: &I18n) -> Notetype { - let mut nt = Notetype { - name: tr.notetypes_basic_name().into(), +/// Create an empty notetype with a given name and stock kind. +pub(crate) fn empty_stock( + nt_kind: NotetypeKind, + original_stock_kind: OriginalStockKind, + name: impl Into, +) -> Notetype { + Notetype { + name: name.into(), + config: NotetypeConfig { + kind: nt_kind as i32, + original_stock_kind: original_stock_kind as i32, + ..if nt_kind == NotetypeKind::Cloze { + NotetypeConfig::new_cloze() + } else { + NotetypeConfig::new() + } + }, ..Default::default() - }; + } +} + +pub(crate) fn get_stock_notetype(kind: StockKind, tr: &I18n) -> Notetype { + match kind { + Kind::Basic => basic(tr), + Kind::BasicAndReversed => basic_forward_reverse(tr), + Kind::BasicOptionalReversed => basic_optional_reverse(tr), + Kind::BasicTyping => basic_typing(tr), + Kind::Cloze => cloze(tr), + } +} + +pub(crate) fn get_original_stock_notetype(kind: OriginalStockKind, tr: &I18n) -> Result { + Ok(match kind { + OriginalStockKind::Unknown => invalid_input!("original stock kind not provided"), + OriginalStockKind::Basic => basic(tr), + OriginalStockKind::BasicAndReversed => basic_forward_reverse(tr), + OriginalStockKind::BasicOptionalReversed => basic_optional_reverse(tr), + OriginalStockKind::BasicTyping => basic_typing(tr), + OriginalStockKind::Cloze => cloze(tr), + OriginalStockKind::ImageOcclusion => image_occlusion_notetype(tr), + }) +} + +pub(crate) fn basic(tr: &I18n) -> Notetype { + let mut nt = empty_stock( + NotetypeKind::Normal, + OriginalStockKind::Basic, + tr.notetypes_basic_name(), + ); let front = tr.notetypes_front_field(); let back = tr.notetypes_back_field(); nt.add_field(front.as_ref()); @@ -68,6 +118,7 @@ pub(crate) fn basic(tr: &I18n) -> Notetype { pub(crate) fn basic_typing(tr: &I18n) -> Notetype { let mut nt = basic(tr); + nt.config.original_stock_kind = StockKind::BasicTyping as i32; nt.name = tr.notetypes_basic_type_answer_name().into(); let front = tr.notetypes_front_field(); let back = tr.notetypes_back_field(); @@ -83,6 +134,7 @@ pub(crate) fn basic_typing(tr: &I18n) -> Notetype { pub(crate) fn basic_forward_reverse(tr: &I18n) -> Notetype { let mut nt = basic(tr); + nt.config.original_stock_kind = StockKind::BasicAndReversed as i32; nt.name = tr.notetypes_basic_reversed_name().into(); let front = tr.notetypes_front_field(); let back = tr.notetypes_back_field(); @@ -100,6 +152,7 @@ pub(crate) fn basic_forward_reverse(tr: &I18n) -> Notetype { pub(crate) fn basic_optional_reverse(tr: &I18n) -> Notetype { let mut nt = basic_forward_reverse(tr); + nt.config.original_stock_kind = StockKind::BasicOptionalReversed as i32; nt.name = tr.notetypes_basic_optional_reversed_name().into(); let addrev = tr.notetypes_add_reverse_field(); nt.add_field(addrev.as_ref()); @@ -109,11 +162,11 @@ pub(crate) fn basic_optional_reverse(tr: &I18n) -> Notetype { } pub(crate) fn cloze(tr: &I18n) -> Notetype { - let mut nt = Notetype { - name: tr.notetypes_cloze_name().into(), - config: NotetypeConfig::new_cloze(), - ..Default::default() - }; + let mut nt = empty_stock( + NotetypeKind::Cloze, + OriginalStockKind::Cloze, + tr.notetypes_cloze_name(), + ); let text = tr.notetypes_text_field(); nt.add_field(text.as_ref()); let back_extra = tr.notetypes_back_extra_field(); diff --git a/rslib/src/serde.rs b/rslib/src/serde.rs index 62746a67c..b0bcfc695 100644 --- a/rslib/src/serde.rs +++ b/rslib/src/serde.rs @@ -20,6 +20,10 @@ where Ok(T::deserialize(v).unwrap_or_default()) } +pub(crate) fn is_default(t: &T) -> bool { + *t == Default::default() +} + pub(crate) fn deserialize_int_from_number<'de, T, D>(deserializer: D) -> Result where D: Deserializer<'de>,