use std::borrow::Cow;
use derive_builder::Builder;
use indexmap::IndexMap;
use num_traits::PrimInt;
use serde::{Deserialize, Serialize};
use crate::{
error::AnkiError,
generic::{GenericRequest, GenericRequestBuilder},
ModelsProxy, Number,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum NoteType {
Regular(RegNoteType),
Cloze(ClozeNoteType),
}
#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
pub struct RegNoteType {
#[builder(setter(custom))]
pub id: Number,
pub name: String,
pub fields: Vec<String>,
pub card_templates: Vec<CardTemplate>,
}
impl RegNoteTypeBuilder {
pub fn id(&mut self, val: impl PrimInt) -> &mut Self {
self.id = Some(Number::new(val));
self
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
pub struct ClozeNoteType {
#[builder(setter(custom))]
pub id: Number,
pub name: String,
pub fields: Vec<String>,
pub card_template: CardTemplate,
}
impl ClozeNoteTypeBuilder {
pub fn id(&mut self, val: impl PrimInt) -> &mut Self {
self.id = Some(Number::new(val));
self
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Builder, Default)]
pub struct CardTemplate {
pub name: String,
pub front: String,
pub back: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct ModelTemplates {
#[serde(flatten)]
pub inner: IndexMap<String, TemplateInfo>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct TemplateInfo {
#[serde(rename = "Front")]
pub front: String,
#[serde(rename = "Back")]
pub back: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ModelStyling {
pub css: String,
#[serde(default)]
pub is_cloze: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ModelDetails {
Less(LessModelDetails),
Full(FullModelDetails),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct LessModelDetails {
pub name: String,
pub fields: Vec<String>,
}
impl From<FullModelDetails> for LessModelDetails {
fn from(details: FullModelDetails) -> Self {
let FullModelDetails { name, fields, .. } = details;
LessModelDetails { name, fields }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FullModelDetails {
pub name: String,
pub fields: Vec<String>,
pub templates: ModelTemplates,
pub styling: ModelStyling,
}
impl FullModelDetails {
pub fn is_cloze(&self) -> bool {
self.styling.is_cloze
}
pub fn mock() -> Self {
FullModelDetails {
name: "JapaneseVocab".to_string(),
fields: vec![
"Sentence".to_string(),
"Reading".to_string(),
"Definition".to_string(),
"WordAudio".to_string(),
"Picture".to_string(),
],
templates: ModelTemplates {
inner: IndexMap::from([(
"Recognition".to_string(),
TemplateInfo {
front: "<div class='sentence'>{{Sentence}}</div>".to_string(),
back: r#"
{{FrontSide}}
<hr id="answer">
<div class="picture">{{Picture}}</div>
<div class="reading">{{Reading}}</div>
<div class="definition">{{Definition}}</div>
<div class="audio">{{WordAudio}}</div>
"#
.to_string(),
},
)]),
},
styling: ModelStyling {
css: r#"
.card {
font-family: "Noto Sans JP", sans-serif;
background-color: #121511;
color: white;
text-align: center;
border-radius: 10px;
padding: 1em;
}
.sentence {
font-size: 2rem;
margin-bottom: 1rem;
}
.reading {
font-size: 1.5rem;
color: #89daff; /* a color from your real css */
}
.definition {
margin-top: 1rem;
font-size: 1.2rem;
}
img {
max-width: 200px;
border-radius: 5px;
}
"#
.to_string(),
is_cloze: false,
},
}
}
pub fn find_templates_by_names<'a>(
&'a self,
template_names: &'a [&str],
) -> impl Iterator<Item = (&'a str, &'a TemplateInfo)> {
template_names.iter().filter_map(move |name| {
self.templates
.inner
.get_key_value(*name)
.map(|(k, v)| (k.as_str(), v))
})
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ModelNameParams {
model_name: String,
}
impl ModelsProxy {
fn get_all_model_names_and_ids(&self) -> Result<IndexMap<String, Number>, AnkiError> {
type ModelsResult = IndexMap<String, Number>;
let payload: GenericRequest<()> = GenericRequestBuilder::default()
.action("modelNamesAndIds".into())
.version(self.version)
.build()
.unwrap();
self.post_generic_request::<ModelsResult>(payload)
}
pub fn get_all_models_less(
&self,
) -> Result<IndexMap<String, LessModelDetails>, AnkiError> {
let indexmap = self.get_all_model_names_and_ids()?;
let details: Result<Vec<LessModelDetails>, AnkiError> = indexmap
.into_iter()
.map(move |(name, _id)| self.get_less_model_details_by_name(name.into()))
.collect();
let res: IndexMap<String, LessModelDetails> =
details?.into_iter().map(|d| (d.name.clone(), d)).collect();
Ok(res)
}
pub fn get_all_models_full(
&self,
) -> Result<IndexMap<String, FullModelDetails>, AnkiError> {
let indexmap = self.get_all_model_names_and_ids()?;
let details: Result<Vec<FullModelDetails>, AnkiError> = indexmap
.into_iter()
.map(move |(name, _id)| self.get_full_model_details_by_name(name.into()))
.collect();
let res: IndexMap<String, FullModelDetails> =
details?.into_iter().map(|d| (d.name.clone(), d)).collect();
Ok(res)
}
pub fn get_model_field_names(&self, model_name: &str) -> Result<Vec<String>, AnkiError> {
let params = ModelNameParams {
model_name: model_name.to_string(),
};
let payload: GenericRequest<_> = GenericRequestBuilder::default()
.action("modelFieldNames".into())
.version(self.version)
.params(Some(params))
.build()
.unwrap();
self.post_generic_request(payload)
}
pub fn get_model_templates(&self, model_name: &str) -> Result<ModelTemplates, AnkiError> {
let params = ModelNameParams {
model_name: model_name.to_string(),
};
let payload: GenericRequest<_> = GenericRequestBuilder::default()
.action("modelTemplates".into())
.version(self.version)
.params(Some(params))
.build()
.unwrap();
self.post_generic_request(payload)
}
pub fn get_model_styling(&self, model_name: &str) -> Result<ModelStyling, AnkiError> {
let params = ModelNameParams {
model_name: model_name.to_string(),
};
let payload: GenericRequest<_> = GenericRequestBuilder::default()
.action("modelStyling".into())
.version(self.version)
.params(Some(params))
.build()
.unwrap();
self.post_generic_request(payload)
}
pub fn get_full_model_details_by_name(
&self,
model_name: Cow<'_, str>,
) -> Result<FullModelDetails, AnkiError> {
let model_name = model_name.into_owned();
let fields = self.get_model_field_names(&model_name)?;
let templates = self.get_model_templates(&model_name)?;
let styling = self.get_model_styling(&model_name)?;
Ok(FullModelDetails {
name: model_name.to_string(),
fields,
templates,
styling,
})
}
pub fn get_less_model_details_by_name(
&self,
model_name: Cow<'_, str>,
) -> Result<LessModelDetails, AnkiError> {
let fields = self.get_model_field_names(&model_name)?;
Ok(LessModelDetails {
name: model_name.into_owned(),
fields,
})
}
}
#[cfg(test)]
mod modeltests {
use crate::{error::AnkiResult, test_utils::ANKICLIENT};
#[test]
fn get_all_model_names_and_ids() {
let res = ANKICLIENT
.models()
.get_all_model_names_and_ids()
.unwrap();
assert!(!res.is_empty());
dbg!(res);
}
#[test]
fn get_full_model_details() {
let res = ANKICLIENT
.models()
.get_all_model_names_and_ids()
.unwrap();
assert!(!res.is_empty());
let first = res.first().unwrap();
let res = ANKICLIENT
.models()
.get_full_model_details_by_name(first.0.into())
.map_err(|e| e.pretty_panic())
.unwrap();
dbg!(res);
}
#[test]
fn get_less_model_details() -> AnkiResult<()> {
let res = ANKICLIENT.models().get_all_model_names_and_ids()?;
assert!(!res.is_empty());
let first = res.first().unwrap();
let res = ANKICLIENT
.models()
.get_less_model_details_by_name(first.0.into())?;
dbg!(res);
Ok(())
}
#[test]
fn get_all_models_less() {
let res = ANKICLIENT.models().get_all_models_less().unwrap();
assert!(!res.is_empty());
dbg!(res);
}
}