1#![allow(missing_docs)]
2
3use super::ModuleKind;
4use crate::{
5 domain::{audio::AudioId, image::ImageId},
6 media::MediaLibrary,
7};
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9use std::{
10 collections::HashSet,
11 fmt::{self, Debug},
12 hash::Hash,
13};
14use strum_macros::{EnumIs, EnumIter, IntoStaticStr};
15
16pub mod memory;
18
19pub mod poster;
21
22pub mod video;
24
25pub mod embed;
27
28pub mod tapping_board;
30
31pub mod drag_drop;
33
34pub mod cover;
36
37pub mod resource_cover;
39
40pub mod flashcards;
42
43pub mod card_quiz;
45
46pub mod matching;
48
49pub mod find_answer;
51
52pub mod legacy;
54
55pub mod _groups;
57
58#[derive(Clone, Serialize, Deserialize, Debug, strum_macros::EnumTryAs)]
60#[serde(rename_all = "camelCase")]
61#[non_exhaustive]
62pub enum Body {
63 MemoryGame(memory::ModuleData),
65
66 Matching(matching::ModuleData),
68
69 Flashcards(flashcards::ModuleData),
71
72 CardQuiz(card_quiz::ModuleData),
74
75 Poster(poster::ModuleData),
77
78 Video(video::ModuleData),
80
81 Embed(embed::ModuleData),
83
84 TappingBoard(tapping_board::ModuleData),
86
87 DragDrop(drag_drop::ModuleData),
89
90 Cover(cover::ModuleData),
94
95 ResourceCover(resource_cover::ModuleData),
97
98 FindAnswer(find_answer::ModuleData),
100
101 Legacy(legacy::ModuleData),
103}
104
105impl Body {
106 pub fn new(kind: super::ModuleKind) -> Self {
108 match kind {
109 super::ModuleKind::Cover => Self::Cover(Default::default()),
110 super::ModuleKind::ResourceCover => Self::ResourceCover(Default::default()),
111 super::ModuleKind::Memory => Self::MemoryGame(Default::default()),
112 super::ModuleKind::CardQuiz => Self::CardQuiz(Default::default()),
113 super::ModuleKind::Flashcards => Self::Flashcards(Default::default()),
114 super::ModuleKind::Matching => Self::Matching(Default::default()),
115 super::ModuleKind::Poster => Self::Poster(Default::default()),
116 super::ModuleKind::Video => Self::Video(Default::default()),
117 super::ModuleKind::Embed => Self::Embed(Default::default()),
118 super::ModuleKind::TappingBoard => Self::TappingBoard(Default::default()),
119 super::ModuleKind::DragDrop => Self::DragDrop(Default::default()),
120 super::ModuleKind::FindAnswer => Self::FindAnswer(Default::default()),
121 super::ModuleKind::Legacy => Self::Legacy(Default::default()),
122 super::ModuleKind::Tracing => unimplemented!("TODO!"),
123 }
124 }
125
126 pub fn convert_to_body(&self, kind: ModuleKind) -> Result<Self, &'static str> {
128 match self {
129 Self::MemoryGame(data) => data.convert_to_body(kind),
130 Self::Matching(data) => data.convert_to_body(kind),
131 Self::Flashcards(data) => data.convert_to_body(kind),
132 Self::CardQuiz(data) => data.convert_to_body(kind),
133 Self::Poster(data) => data.convert_to_body(kind),
134 Self::Video(data) => data.convert_to_body(kind),
135 Self::Embed(data) => data.convert_to_body(kind),
136 Self::TappingBoard(data) => data.convert_to_body(kind),
137 Self::DragDrop(data) => data.convert_to_body(kind),
138 Self::Cover(data) => data.convert_to_body(kind),
139 Self::ResourceCover(data) => data.convert_to_body(kind),
140 Self::FindAnswer(data) => data.convert_to_body(kind),
141 Self::Legacy(data) => data.convert_to_body(kind),
142 }
143 }
144
145 pub fn is_complete(&self) -> bool {
147 match self {
148 Self::MemoryGame(data) => data.is_complete(),
149 Self::Matching(data) => data.is_complete(),
150 Self::Flashcards(data) => data.is_complete(),
151 Self::CardQuiz(data) => data.is_complete(),
152 Self::Poster(data) => data.is_complete(),
153 Self::Video(data) => data.is_complete(),
154 Self::Embed(data) => data.is_complete(),
155 Self::TappingBoard(data) => data.is_complete(),
156 Self::DragDrop(data) => data.is_complete(),
157 Self::Cover(data) => data.is_complete(),
158 Self::ResourceCover(data) => data.is_complete(),
159 Self::FindAnswer(data) => data.is_complete(),
160 Self::Legacy(data) => data.is_complete(),
161 }
162 }
163}
164
165pub trait BodyExt<Mode: ModeExt, Step: StepExt>:
168 BodyConvert + TryFrom<Body> + Serialize + DeserializeOwned + Clone + Debug
169{
170 fn choose_mode_list() -> Vec<Mode> {
173 Mode::get_list()
174 }
175
176 fn as_body(&self) -> Body;
178
179 fn is_complete(&self) -> bool;
181
182 fn is_legacy() -> bool {
184 false
185 }
186
187 fn has_preload() -> bool {
189 false
190 }
191
192 fn kind() -> super::ModuleKind;
194
195 fn new_with_mode_and_theme(mode: Mode, theme_id: ThemeId) -> Self;
198
199 fn mode(&self) -> Option<Mode>;
201
202 fn requires_choose_mode(&self) -> bool;
204
205 fn get_theme(&self) -> Option<ThemeId>;
207
208 fn set_theme(&mut self, theme_id: ThemeId);
210
211 fn set_editor_state_step(&mut self, step: Step);
213 fn set_editor_state_steps_completed(&mut self, steps_completed: HashSet<Step>);
215 fn get_editor_state_step(&self) -> Option<Step>;
217 fn get_editor_state_steps_completed(&self) -> Option<HashSet<Step>>;
219 fn insert_editor_state_step_completed(&mut self, step: Step) {
221 if let Some(mut steps_completed) = self.get_editor_state_steps_completed() {
222 steps_completed.insert(step);
223 self.set_editor_state_steps_completed(steps_completed);
224 }
225 }
226
227 fn convert_to_body(&self, kind: ModuleKind) -> Result<Body, &'static str> {
229 match kind {
230 ModuleKind::Memory => Ok(Body::MemoryGame(self.convert_to_memory()?)),
231 ModuleKind::Matching => Ok(Body::Matching(self.convert_to_matching()?)),
232 ModuleKind::Flashcards => Ok(Body::Flashcards(self.convert_to_flashcards()?)),
233 ModuleKind::CardQuiz => Ok(Body::CardQuiz(self.convert_to_card_quiz()?)),
234 ModuleKind::Poster => Ok(Body::Poster(self.convert_to_poster()?)),
235 ModuleKind::Video => Ok(Body::Video(self.convert_to_video()?)),
236 ModuleKind::Embed => Ok(Body::Embed(self.convert_to_embed()?)),
237 ModuleKind::TappingBoard => Ok(Body::TappingBoard(self.convert_to_tapping_board()?)),
238 ModuleKind::DragDrop => Ok(Body::DragDrop(self.convert_to_drag_drop()?)),
239 ModuleKind::Cover => Ok(Body::Cover(self.convert_to_cover()?)),
240 ModuleKind::ResourceCover => Ok(Body::ResourceCover(self.convert_to_resource_cover()?)),
241 ModuleKind::FindAnswer => Ok(Body::FindAnswer(self.convert_to_find_answer()?)),
242 ModuleKind::Legacy => Ok(Body::Legacy(self.convert_to_legacy()?)),
243 _ => unimplemented!(
244 "cannot convert from {} to {}",
245 Self::kind().as_str(),
246 kind.as_str()
247 ),
248 }
249 }
250}
251
252pub trait BodyConvert {
256 fn convertable_list() -> Vec<ModuleKind> {
258 Vec::new()
259 }
260 fn convert_to_memory(&self) -> Result<memory::ModuleData, &'static str> {
262 Err("cannot convert to memory game!")
263 }
264 fn convert_to_matching(&self) -> Result<matching::ModuleData, &'static str> {
266 Err("cannot convert to matching!")
267 }
268 fn convert_to_flashcards(&self) -> Result<flashcards::ModuleData, &'static str> {
270 Err("cannot convert to matching!")
271 }
272 fn convert_to_card_quiz(&self) -> Result<card_quiz::ModuleData, &'static str> {
274 Err("cannot convert to quiz game!")
275 }
276 fn convert_to_poster(&self) -> Result<poster::ModuleData, &'static str> {
278 Err("cannot convert to talking poster!")
279 }
280 fn convert_to_tapping_board(&self) -> Result<tapping_board::ModuleData, &'static str> {
282 Err("cannot convert to Listen & Learn!")
283 }
284 fn convert_to_drag_drop(&self) -> Result<drag_drop::ModuleData, &'static str> {
286 Err("cannot convert to drag & drop!")
287 }
288 fn convert_to_cover(&self) -> Result<cover::ModuleData, &'static str> {
290 Err("cannot convert to cover!")
291 }
292 fn convert_to_resource_cover(&self) -> Result<resource_cover::ModuleData, &'static str> {
294 Err("cannot convert to resource cover!")
295 }
296 fn convert_to_find_answer(&self) -> Result<find_answer::ModuleData, &'static str> {
298 Err("cannot convert to answer this!")
299 }
300 fn convert_to_video(&self) -> Result<video::ModuleData, &'static str> {
302 Err("cannot convert to video!")
303 }
304 fn convert_to_embed(&self) -> Result<embed::ModuleData, &'static str> {
306 Err("cannot convert to embed!")
307 }
308 fn convert_to_legacy(&self) -> Result<legacy::ModuleData, &'static str> {
310 Err("cannot convert to legacy!")
311 }
312}
313
314pub trait ModeExt: Copy + Default + PartialEq + Eq + Hash {
316 fn get_list() -> Vec<Self>;
319
320 fn as_str_id(&self) -> &'static str;
322 fn label(&self) -> &'static str;
324
325 fn image_tag_filters(&self) -> Option<Vec<i16>> {
328 None
329 }
330
331 fn image_tag_priorities(&self) -> Option<Vec<i16>> {
334 None
335 }
336}
337
338impl ModeExt for () {
343 fn get_list() -> Vec<Self> {
344 vec![]
345 }
346
347 fn as_str_id(&self) -> &'static str {
348 ""
349 }
350
351 fn label(&self) -> &'static str {
352 ""
353 }
354}
355
356impl Body {
357 pub fn kind(&self) -> super::ModuleKind {
359 match self {
360 Self::Cover(_) => super::ModuleKind::Cover,
361 Self::ResourceCover(_) => super::ModuleKind::ResourceCover,
362 Self::MemoryGame(_) => super::ModuleKind::Memory,
363 Self::Flashcards(_) => super::ModuleKind::Flashcards,
364 Self::CardQuiz(_) => super::ModuleKind::CardQuiz,
365 Self::Matching(_) => super::ModuleKind::Matching,
366 Self::Poster(_) => super::ModuleKind::Poster,
367 Self::Video(_) => super::ModuleKind::Video,
368 Self::Embed(_) => super::ModuleKind::Embed,
369 Self::TappingBoard(_) => super::ModuleKind::TappingBoard,
370 Self::DragDrop(_) => super::ModuleKind::DragDrop,
371 Self::FindAnswer(_) => super::ModuleKind::FindAnswer,
372 Self::Legacy(_) => super::ModuleKind::Legacy,
373 }
374 }
375}
376
377#[derive(Clone, Default, Serialize, Deserialize, Debug)]
382pub struct EditorState<STEP>
383where
384 STEP: StepExt,
385{
386 pub step: STEP,
388
389 pub steps_completed: HashSet<STEP>,
391}
392
393pub trait StepExt: Clone + Copy + Default + PartialEq + Eq + Hash + Debug + Unpin {
396 fn next(&self) -> Option<Self>;
398 fn as_number(&self) -> usize;
400 fn label(&self) -> &'static str;
402 fn get_list() -> Vec<Self>;
404 fn get_preview() -> Self;
408 fn is_preview(&self) -> bool {
410 *self == Self::get_preview()
411 }
412}
413
414impl StepExt for () {
419 fn next(&self) -> Option<Self> {
420 None
421 }
422 fn as_number(&self) -> usize {
423 0
424 }
425 fn label(&self) -> &'static str {
426 ""
427 }
428 fn get_list() -> Vec<Self> {
429 Vec::new()
430 }
431 fn get_preview() -> Self {
432 ()
433 }
434}
435
436#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
437pub struct Audio {
439 pub id: AudioId,
441 pub lib: MediaLibrary,
443}
444
445#[derive(Clone, Default, Serialize, Deserialize, Debug)]
447pub struct ModuleAssist {
448 pub text: Option<String>,
450 pub audio: Option<Audio>,
452 #[serde(default)]
458 #[cfg_attr(feature = "backend", serde(skip))]
459 pub always_show: bool,
460}
461
462impl ModuleAssist {
463 pub fn new(text: Option<String>, audio: Option<Audio>) -> Self {
465 Self {
466 text,
467 audio,
468 ..Default::default()
469 }
470 }
471
472 pub fn always_show(mut self) -> Self {
474 self.always_show = true;
475 self
476 }
477
478 pub fn has_content(&self) -> bool {
480 self.text.is_some() || self.audio.is_some()
481 }
482}
483
484#[derive(Clone, Serialize, Deserialize, Debug)]
487pub enum ModuleAssistType {
488 Instructions,
490 Feedback,
492 InActivity,
494}
495
496impl ModuleAssistType {
497 pub fn is_instructions(&self) -> bool {
499 matches!(self, Self::Instructions)
500 }
501
502 pub fn is_feedback(&self) -> bool {
504 matches!(self, Self::Feedback)
505 }
506
507 pub fn is_in_activity(&self) -> bool {
509 matches!(self, Self::InActivity)
510 }
511}
512
513#[derive(Clone, Serialize, Deserialize, Debug)]
514pub enum Background {
516 Color(Option<rgb::RGBA8>),
518 Image(Image),
520}
521
522#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
523pub struct Image {
525 pub id: ImageId,
527 pub lib: MediaLibrary,
529}
530
531#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
532pub struct Vec2(pub [f64; 2]);
534
535impl From<(f64, f64)> for Vec2 {
536 fn from((x, y): (f64, f64)) -> Self {
537 Self([x, y])
538 }
539}
540
541impl From<Vec2> for (f64, f64) {
542 fn from(v: Vec2) -> Self {
543 (v.0[0], v.0[1])
544 }
545}
546
547#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
548pub struct Vec3(pub [f64; 3]);
550
551impl From<(f64, f64, f64)> for Vec3 {
552 fn from((x, y, z): (f64, f64, f64)) -> Self {
553 Self([x, y, z])
554 }
555}
556
557impl From<Vec3> for (f64, f64, f64) {
558 fn from(v: Vec3) -> Self {
559 (v.0[0], v.0[1], v.0[2])
560 }
561}
562
563#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
564pub struct Vec4(pub [f64; 4]);
566
567impl From<(f64, f64, f64, f64)> for Vec4 {
568 fn from((x, y, z, w): (f64, f64, f64, f64)) -> Self {
569 Self([x, y, z, w])
570 }
571}
572
573impl From<Vec4> for (f64, f64, f64, f64) {
574 fn from(v: Vec4) -> Self {
575 (v.0[0], v.0[1], v.0[2], v.0[3])
576 }
577}
578
579#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
580pub struct Transform {
582 pub translation: Vec3,
584 pub rotation: Vec4,
586 pub scale: Vec3,
588 pub origin: Vec3,
590}
591
592impl Transform {
593 pub fn identity() -> Self {
595 Self {
596 translation: Vec3([0.0, 0.0, 0.0]),
597 rotation: Vec4([0.0, 0.0, 0.0, 1.0]),
598 scale: Vec3([1.0, 1.0, 1.0]),
599 origin: Vec3([0.0, 0.0, 0.0]),
600 }
601 }
602}
603
604impl Default for Transform {
605 fn default() -> Self {
606 Self::identity()
607 }
608}
609
610#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, EnumIs)]
611pub enum HoverAnimation {
612 Grow,
613 Tilt,
614 Buzz,
615}
616
617impl fmt::Display for HoverAnimation {
618 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
619 let str = match self {
620 HoverAnimation::Grow => "Grow",
621 HoverAnimation::Tilt => "Tilt",
622 HoverAnimation::Buzz => "Buzz",
623 };
624 write!(f, "{str}")
625 }
626}
627
628impl HoverAnimation {
629 pub fn as_str(&self) -> &'static str {
630 match self {
631 HoverAnimation::Grow => "grow",
632 HoverAnimation::Tilt => "tilt",
633 HoverAnimation::Buzz => "buzz",
634 }
635 }
636}
637
638#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, EnumIs)]
639pub enum StickerHidden {
640 OnClick(ShowHideAnimation),
641 UntilClick(ShowHideAnimation),
642}
643
644#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, EnumIs, EnumIter, Default)]
645pub enum ShowHideAnimation {
646 #[default]
647 Appear,
648 FadeInTop,
649 FadeInBottom,
650 FadeInLeft,
651 FadeInRight,
652}
653
654impl fmt::Display for ShowHideAnimation {
655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656 write!(
657 f,
658 "{}",
659 match self {
660 ShowHideAnimation::Appear => "Appear",
661 ShowHideAnimation::FadeInTop => "Top",
662 ShowHideAnimation::FadeInBottom => "Bottom",
663 ShowHideAnimation::FadeInLeft => "Left",
664 ShowHideAnimation::FadeInRight => "Right",
665 }
666 )
667 }
668}
669
670impl ShowHideAnimation {
671 pub fn as_str(&self) -> &'static str {
672 match self {
673 ShowHideAnimation::Appear => "Appear",
674 ShowHideAnimation::FadeInTop => "fade-in-top",
675 ShowHideAnimation::FadeInBottom => "fade-in-bottom",
676 ShowHideAnimation::FadeInLeft => "fade-in-left",
677 ShowHideAnimation::FadeInRight => "fade-in-right",
678 }
679 }
680}
681
682#[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Debug, EnumIter, IntoStaticStr)]
685#[repr(i16)]
686#[cfg_attr(feature = "backend", derive(sqlx::Type))]
687#[allow(missing_docs)]
688#[strum(serialize_all = "kebab-case")]
689pub enum ThemeId {
690 Blank,
691 Jigzi,
692 JigziGreen,
693 JigziBlue,
694 JigziRed,
695 Chalkboard,
696 Iml,
697 HebrewReading,
698 MyNotebook,
699 BackToSchool,
700 BackToSchoolYouth,
701 MyWorkspace,
702 Comix,
703 Surreal,
704 Abstract,
705 Denim,
706 HappyBrush,
707 Graffiti,
708 JewishText,
709 NumberGames,
710 WonderLab,
711 ShabbatShalom,
712 RoshHashana,
713 RoshHashanah,
714 AppleWithHoney,
715 Pomegranate,
716 YomKippur,
717 HappySukkot,
718 Sukkot,
719 #[strum(serialize = "tubishvat")]
720 TuBishvat,
721 IlluminatingHanukkah,
722 Chanukah,
723 ChanukahLights,
724 Purim,
725 PurimFeast,
726 PurimSweets,
727 HappyPassover,
728 #[strum(serialize = "passover-matza")]
729 PassoveMatza,
730 PassoverSeder,
731 LagBaOmer,
732 HappyShavuot,
733 ShavuotDishes,
734 ShavuotFields,
735 OurIsrael,
736 Israel,
737 JerusalemCity,
738 JerusalemWall,
739 NoahsArk,
740 GardenOfEden,
741 JewishStories,
742 LovelySpring,
743 Spring,
744 Flowers,
745 Nature,
746 SillyMonsters,
747 DinoGames,
748 WatermelonSummer,
749 SummerPool,
750 ExcitingFall,
751 Autumn,
752 WinterSnow,
753 IceAge,
754 LostInSpace,
755 Space,
756 Camping,
757 HappyBirthday,
758 #[strum(serialize = "valentine_s-day")]
759 Valentine,
760 Jungle,
761 OurPlanet,
762 Theater,
763 Sport,
764 Travel,
765}
766
767impl Default for ThemeId {
768 fn default() -> Self {
769 Self::Blank
770 }
771}