shared/domain/module/body/_groups/
cards.rs1use crate::{
6 config,
7 domain::module::body::{Audio, Background, Image, ModeExt, ModuleAssist, StepExt, ThemeId},
8};
9use serde::{de, Deserialize, Serialize};
10use std::collections::HashSet;
11use std::fmt;
12use unicode_segmentation::UnicodeSegmentation;
13
14#[derive(Default, Clone, Serialize, Deserialize, Debug)]
16pub struct BaseContent {
17 pub editor_state: EditorState,
19
20 pub instructions: ModuleAssist,
22
23 #[serde(default)]
25 pub feedback: ModuleAssist,
26
27 pub mode: Mode,
29
30 pub pairs: Vec<CardPair>,
32
33 pub theme: ThemeId,
35
36 pub background: Option<Background>,
38}
39
40impl BaseContent {
41 pub fn new(mode: Mode) -> Self {
43 Self {
44 mode,
45 ..Self::default()
46 }
47 }
48
49 pub fn is_valid(&self) -> bool {
51 let pair_len = self.pairs.len();
52 pair_len >= config::MIN_LIST_WORDS && self.mode.pairs_valid(&self.pairs)
53 }
54}
55
56#[derive(Default, Clone, Serialize, Deserialize, Debug)]
58pub struct EditorState {
59 pub step: Step,
61
62 pub steps_completed: HashSet<Step>,
64}
65
66#[derive(Clone, Serialize, Deserialize, Debug)]
68pub struct CardPair(pub Card, pub Card);
69
70#[derive(Clone, Serialize, Debug)]
72pub struct Card {
73 pub audio: Option<Audio>,
75
76 pub card_content: CardContent,
78}
79
80pub fn get_card_text_length(card: &Card) -> usize {
82 match &card.card_content {
83 CardContent::Text(text) => text.graphemes(true).count(),
84 _ => 0,
85 }
86}
87
88pub fn get_longest_card_text_length<'c>(cards: impl Iterator<Item = &'c Card>) -> usize {
90 cards.fold(0, |acc, card| {
91 let longest_current = match &card.card_content {
92 CardContent::Text(a) => a.graphemes(true).count(),
93 _ => 0,
94 };
95
96 acc.max(longest_current)
97 })
98}
99
100impl<'de> de::Deserialize<'de> for Card {
120 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
121 where
122 D: de::Deserializer<'de>,
123 {
124 #[derive(Debug, Deserialize)]
125 #[serde(field_identifier)]
126 enum CardField {
127 #[serde(rename = "audio")]
128 Audio,
129 #[serde(rename = "card_content")]
130 CardContent,
131 #[serde(rename = "Text")]
132 Text,
133 #[serde(rename = "Image")]
134 Image,
135 }
136
137 struct CardVisitor;
138
139 impl<'de> de::Visitor<'de> for CardVisitor {
140 type Value = Card;
141
142 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
143 formatter.write_str("A CardContent or Card map")
144 }
145
146 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
147 where
148 M: de::MapAccess<'de>,
149 {
150 let mut audio: Option<Option<Audio>> = None;
151 let mut card_content: Option<CardContent> = None;
152
153 while let Some(key) = access.next_key()? {
154 match key {
155 CardField::Text => {
156 if card_content.is_some() {
157 return Err(de::Error::duplicate_field("card_content"));
158 }
159 card_content = Some(CardContent::Text(access.next_value()?));
160 break;
161 }
162 CardField::Image => {
163 if card_content.is_some() {
164 return Err(de::Error::duplicate_field("card_content"));
165 }
166 card_content = Some(CardContent::Image(access.next_value()?));
167 break;
168 }
169 CardField::Audio => {
170 if audio.is_some() {
171 return Err(de::Error::duplicate_field("audio"));
172 }
173 audio = Some(access.next_value()?);
174 }
175 CardField::CardContent => {
176 if card_content.is_some() {
177 return Err(de::Error::duplicate_field("card_content"));
178 }
179 card_content = Some(access.next_value()?);
180 }
181 }
182 }
183
184 let audio = audio.map_or(None, |audio| audio);
185 let card_content =
186 card_content.ok_or_else(|| de::Error::missing_field("card_content"))?;
187
188 Ok(Card {
189 audio,
190 card_content,
191 })
192 }
193 }
194
195 deserializer.deserialize_map(CardVisitor)
196 }
197}
198
199#[derive(Clone, Serialize, Deserialize, Debug)]
201pub enum CardContent {
202 #[allow(missing_docs)]
204 Text(String),
205
206 #[allow(missing_docs)]
208 Image(Option<Image>),
209}
210
211impl Card {
212 pub fn is_empty(&self) -> bool {
214 match &self.card_content {
215 CardContent::Text(value) if value.trim().len() == 0 => true,
216 CardContent::Image(None) => true,
217 _ => false,
218 }
219 }
220}
221
222#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
224#[repr(i16)]
225#[cfg_attr(feature = "backend", derive(sqlx::Type))]
226pub enum Mode {
227 #[allow(missing_docs)]
229 Duplicate = 0,
230
231 #[allow(missing_docs)]
233 WordsAndImages = 1,
234
235 #[allow(missing_docs)]
237 BeginsWith = 2,
238
239 #[allow(missing_docs)]
241 Lettering = 3,
242
243 #[allow(missing_docs)]
245 Riddles = 4,
246
247 #[allow(missing_docs)]
249 Opposites = 5,
250
251 #[allow(missing_docs)]
253 Synonyms = 6,
254
255 Translate = 7,
257
258 Images = 8,
260}
261
262impl Mode {
263 pub fn pairs_valid(&self, pairs: &Vec<CardPair>) -> bool {
265 match self {
266 Self::WordsAndImages => {
268 pairs
269 .iter()
270 .find(|pair| {
271 pair.0.is_empty()
274 || pair.1.is_empty()
275 || !matches!(pair.0.card_content, CardContent::Text(_))
276 || !matches!(pair.1.card_content, CardContent::Image(_))
277 })
278 .is_none()
279 }
280 Self::Images => {
282 pairs
283 .iter()
284 .find(|pair| {
285 pair.0.is_empty()
287 || pair.1.is_empty()
288 || !matches!(pair.0.card_content, CardContent::Image(_))
289 || !matches!(pair.1.card_content, CardContent::Image(_))
290 })
291 .is_none()
292 }
293 _ => {
295 pairs
296 .iter()
297 .find(|pair| {
298 pair.0.is_empty()
300 || pair.1.is_empty()
301 || !matches!(pair.0.card_content, CardContent::Text(_))
302 || !matches!(pair.1.card_content, CardContent::Text(_))
303 })
304 .is_none()
305 }
306 }
307 }
308}
309
310impl Default for Mode {
311 fn default() -> Self {
312 Self::Duplicate
313 }
314}
315
316impl ModeExt for Mode {
317 fn get_list() -> Vec<Self> {
318 vec![
319 Self::Duplicate,
320 Self::WordsAndImages,
321 Self::Lettering,
322 Self::Images,
323 Self::BeginsWith,
324 Self::Riddles,
325 Self::Opposites,
326 Self::Synonyms,
327 Self::Translate,
328 ]
329 }
330
331 fn as_str_id(&self) -> &'static str {
332 match self {
333 Self::Duplicate => "duplicate",
334 Self::WordsAndImages => "words-images",
335 Self::BeginsWith => "begins-with",
336 Self::Lettering => "lettering",
337 Self::Riddles => "riddles",
338 Self::Opposites => "opposites",
339 Self::Synonyms => "synonyms",
340 Self::Translate => "translate",
341 Self::Images => "images",
342 }
343 }
344
345 fn label(&self) -> &'static str {
346 const STR_DUPLICATE: &'static str = "Duplicate";
347 const STR_WORDS_IMAGES: &'static str = "Words & Images";
348 const STR_BEGINS_WITH: &'static str = "Begins with...";
349 const STR_LETTERING: &'static str = "Script & Print";
350 const STR_RIDDLES: &'static str = "Riddles";
351 const STR_OPPOSITES: &'static str = "Opposites";
352 const STR_SYNONYMS: &'static str = "Synonyms";
353 const STR_TRANSLATE: &'static str = "Translation";
354 const STR_IMAGES: &'static str = "Images";
355
356 match self {
357 Self::Duplicate => STR_DUPLICATE,
358 Self::WordsAndImages => STR_WORDS_IMAGES,
359 Self::BeginsWith => STR_BEGINS_WITH,
360 Self::Lettering => STR_LETTERING,
361 Self::Riddles => STR_RIDDLES,
362 Self::Opposites => STR_OPPOSITES,
363 Self::Synonyms => STR_SYNONYMS,
364 Self::Translate => STR_TRANSLATE,
365 Self::Images => STR_IMAGES,
366 }
367 }
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
372pub enum Step {
373 One,
375 Two,
377 Three,
379 Four,
381}
382
383impl Default for Step {
384 fn default() -> Self {
385 Self::One
386 }
387}
388
389impl StepExt for Step {
390 fn next(&self) -> Option<Self> {
391 match self {
392 Self::One => Some(Self::Two),
393 Self::Two => Some(Self::Three),
394 Self::Three => Some(Self::Four),
395 Self::Four => None,
396 }
397 }
398
399 fn as_number(&self) -> usize {
400 match self {
401 Self::One => 1,
402 Self::Two => 2,
403 Self::Three => 3,
404 Self::Four => 4,
405 }
406 }
407
408 fn label(&self) -> &'static str {
409 const STR_CONTENT: &'static str = "Content";
411 const STR_DESIGN: &'static str = "Design";
412 const STR_SETTINGS: &'static str = "Settings";
413 const STR_PREVIEW: &'static str = "Preview";
414
415 match self {
416 Self::One => STR_CONTENT,
417 Self::Two => STR_DESIGN,
418 Self::Three => STR_SETTINGS,
419 Self::Four => STR_PREVIEW,
420 }
421 }
422
423 fn get_list() -> Vec<Self> {
424 vec![Self::One, Self::Two, Self::Three, Self::Four]
425 }
426 fn get_preview() -> Self {
427 Self::Four
428 }
429}