Add ability to restore a notetype to its original configuration (#2472)

* Store the original stock notetype kind in the notetype

Will allow us to provide a command to restore a notetype to its default
settings/templates.

* Add a new action to restore a notetype to its original state
This commit is contained in:
Damien Elmes 2023-04-18 14:07:51 +10:00 committed by GitHub
parent e361bb9514
commit dd13e78eca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 382 additions and 79 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<pb::generic::Json> {
// 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<pb::collection::OpChanges> {
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<pb::notetypes::Notetype> for Notetype {

View File

@ -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,15 +189,15 @@ impl Collection {
Ok(false)
}
}
fn image_occlusion_notetype(&mut self) -> Notetype {
let tr = &self.tr;
pub(crate) fn image_occlusion_notetype(tr: &I18n) -> Notetype {
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()
};
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());
@ -242,5 +244,4 @@ try {{
);
nt.add_template(nt.name.clone(), qfmt, afmt);
nt
}
}

View File

@ -7,6 +7,7 @@ mod emptycards;
mod fields;
mod notetypechange;
mod render;
mod restore;
mod schema11;
mod schemachange;
pub(crate) mod stock;

View File

@ -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<StockKind>,
) -> Result<OpOutput<()>> {
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::<u32>("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::<u32>("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::<u32>("select count(*) from cards")?,
1
);
Ok(())
}
}

View File

@ -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<String, Value>,
}
@ -103,6 +106,7 @@ impl From<NotetypeSchema11> 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<Notetype> 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<Notetype> for NotetypeSchema11 {
/// See [crate::deckconfig::schema11::clear_other_duplicates()].
fn clear_other_field_duplicates(other: &mut HashMap<String, Value>) {
for key in &["description", "plainText", "collapsed", "excludeFromSearch"] {
for key in &[
"description",
"plainText",
"collapsed",
"excludeFromSearch",
"originalStockKind",
] {
other.remove(*key);
}
}

View File

@ -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<Notetype> {
vec![
basic(tr),
@ -45,11 +51,55 @@ fn fieldref<S: AsRef<str>>(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<String>,
) -> 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<Notetype> {
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();

View File

@ -20,6 +20,10 @@ where
Ok(T::deserialize(v).unwrap_or_default())
}
pub(crate) fn is_default<T: Default + PartialEq>(t: &T) -> bool {
*t == Default::default()
}
pub(crate) fn deserialize_int_from_number<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,