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:
parent
e361bb9514
commit
dd13e78eca
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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!(
|
||||
"<div style=\"display: none\">{{{{cloze:{}}}}}</div>
|
||||
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!(
|
||||
"<div style=\"display: none\">{{{{cloze:{}}}}}</div>
|
||||
<div id=container>
|
||||
{{{{{}}}}}
|
||||
<canvas id=\"canvas\" class=\"image-occlusion-canvas\"></canvas>
|
||||
@ -222,25 +224,24 @@ try {{
|
||||
}}
|
||||
</script>
|
||||
",
|
||||
occlusion,
|
||||
image,
|
||||
tr.notetypes_error_loading_image_occlusion(),
|
||||
);
|
||||
let afmt = format!(
|
||||
"{{{{{}}}}}
|
||||
occlusion,
|
||||
image,
|
||||
tr.notetypes_error_loading_image_occlusion(),
|
||||
);
|
||||
let afmt = format!(
|
||||
"{{{{{}}}}}
|
||||
{}
|
||||
<button id=\"toggle\">{}</button>
|
||||
<br>
|
||||
{{{{{}}}}}
|
||||
<br>
|
||||
{{{{{}}}}}",
|
||||
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
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ mod emptycards;
|
||||
mod fields;
|
||||
mod notetypechange;
|
||||
mod render;
|
||||
mod restore;
|
||||
mod schema11;
|
||||
mod schemachange;
|
||||
pub(crate) mod stock;
|
||||
|
93
rslib/src/notetype/restore.rs
Normal file
93
rslib/src/notetype/restore.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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>,
|
||||
|
Loading…
Reference in New Issue
Block a user