shared/domain/module/
body.rs

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
16/// Memory Game Body.
17pub mod memory;
18
19/// Talking Poster Body.
20pub mod poster;
21
22/// Video Body.
23pub mod video;
24
25/// Embed Body.
26pub mod embed;
27
28/// Listen & Learn Body.
29pub mod tapping_board;
30
31/// Drag & Drop Body.
32pub mod drag_drop;
33
34/// Cover Body.
35pub mod cover;
36
37/// Resource Cover Body.
38pub mod resource_cover;
39
40/// Flashcards .
41pub mod flashcards;
42
43/// Quiz Game
44pub mod card_quiz;
45
46/// Matching
47pub mod matching;
48
49/// Answer This (Previously Find the Answer)
50pub mod find_answer;
51
52/// Legacy
53pub mod legacy;
54
55/// Groups that share types
56pub mod _groups;
57
58/// Body kinds for Modules.
59#[derive(Clone, Serialize, Deserialize, Debug, strum_macros::EnumTryAs)]
60#[serde(rename_all = "camelCase")]
61#[non_exhaustive]
62pub enum Body {
63    /// Module is a memory game, and has a memory game's body.
64    MemoryGame(memory::ModuleData),
65
66    /// Module is matching game, and has a matching game's body.
67    Matching(matching::ModuleData),
68
69    /// Module is flashcards, and has a flashcard's body.
70    Flashcards(flashcards::ModuleData),
71
72    /// Module is a quiz game, and has a quiz game's body.
73    CardQuiz(card_quiz::ModuleData),
74
75    /// Module is a poster, and has a talking poster's body.
76    Poster(poster::ModuleData),
77
78    /// Module is a Video, and has a video body.
79    Video(video::ModuleData),
80
81    /// Module is a Embed, and has a embed body.
82    Embed(embed::ModuleData),
83
84    /// Module is a Listen & Learn, and has a Listen & Learn's body.
85    TappingBoard(tapping_board::ModuleData),
86
87    /// Module is a drag & drop, and has a drag & drop's body.
88    DragDrop(drag_drop::ModuleData),
89
90    /// Module is a [`Cover`](super::ModuleKind::Cover).
91    ///
92    /// Cover for Module type
93    Cover(cover::ModuleData),
94
95    /// Module is a Resource Cover.
96    ResourceCover(resource_cover::ModuleData),
97
98    /// Module is a Answer This (Find the Answer), and has Answer This's (Find the Answer)'s body.
99    FindAnswer(find_answer::ModuleData),
100
101    /// Module is a legacy, and has a legacy's body.
102    Legacy(legacy::ModuleData),
103}
104
105impl Body {
106    /// create a new Body for a given ModuleKind
107    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    /// Convert this container to a Body wrapper of a specific kind
127    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    /// Helper to check whether the inner data is complete
146    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
165/// Extension trait for interop
166/// impl on inner body data
167pub trait BodyExt<Mode: ModeExt, Step: StepExt>:
168    BodyConvert + TryFrom<Body> + Serialize + DeserializeOwned + Clone + Debug
169{
170    /// get choose mode list. By default it's the full list
171    /// but that can be overridden to re-order or hide some modes
172    fn choose_mode_list() -> Vec<Mode> {
173        Mode::get_list()
174    }
175
176    /// get self as a Body
177    fn as_body(&self) -> Body;
178
179    /// is complete
180    fn is_complete(&self) -> bool;
181
182    /// is legacy
183    fn is_legacy() -> bool {
184        false
185    }
186
187    /// should wait for manual phase change to Init
188    fn has_preload() -> bool {
189        false
190    }
191
192    /// get the kind from the type itself
193    fn kind() -> super::ModuleKind;
194
195    /// given a Mode, get a new Self
196    /// will usually populate an inner .content
197    fn new_with_mode_and_theme(mode: Mode, theme_id: ThemeId) -> Self;
198
199    /// Fetch the mode for this module
200    fn mode(&self) -> Option<Mode>;
201
202    /// requires an additional step of choosing the mode
203    fn requires_choose_mode(&self) -> bool;
204
205    /// Get the current theme
206    fn get_theme(&self) -> Option<ThemeId>;
207
208    /// Set the current theme
209    fn set_theme(&mut self, theme_id: ThemeId);
210
211    /// Set editor state step
212    fn set_editor_state_step(&mut self, step: Step);
213    /// Set editor state steps completed
214    fn set_editor_state_steps_completed(&mut self, steps_completed: HashSet<Step>);
215    /// Get editor state step
216    fn get_editor_state_step(&self) -> Option<Step>;
217    /// Get editor state steps completed
218    fn get_editor_state_steps_completed(&self) -> Option<HashSet<Step>>;
219    /// Insert a completed step
220    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    /// Convert this inner data to a Body wrapper of a specific kind
228    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
252/// These will all error by default.
253/// Modules that can be converted between eachother must override
254/// The relevant methods
255pub trait BodyConvert {
256    /// Get a list of valid conversion targets
257    fn convertable_list() -> Vec<ModuleKind> {
258        Vec::new()
259    }
260    /// Memory game
261    fn convert_to_memory(&self) -> Result<memory::ModuleData, &'static str> {
262        Err("cannot convert to memory game!")
263    }
264    /// Matching
265    fn convert_to_matching(&self) -> Result<matching::ModuleData, &'static str> {
266        Err("cannot convert to matching!")
267    }
268    /// Flashcards
269    fn convert_to_flashcards(&self) -> Result<flashcards::ModuleData, &'static str> {
270        Err("cannot convert to matching!")
271    }
272    /// Quiz game
273    fn convert_to_card_quiz(&self) -> Result<card_quiz::ModuleData, &'static str> {
274        Err("cannot convert to quiz game!")
275    }
276    /// Talking Poster
277    fn convert_to_poster(&self) -> Result<poster::ModuleData, &'static str> {
278        Err("cannot convert to talking poster!")
279    }
280    /// Listen & Learn
281    fn convert_to_tapping_board(&self) -> Result<tapping_board::ModuleData, &'static str> {
282        Err("cannot convert to Listen & Learn!")
283    }
284    /// Drag & Drop
285    fn convert_to_drag_drop(&self) -> Result<drag_drop::ModuleData, &'static str> {
286        Err("cannot convert to drag & drop!")
287    }
288    /// Cover
289    fn convert_to_cover(&self) -> Result<cover::ModuleData, &'static str> {
290        Err("cannot convert to cover!")
291    }
292    /// Resource Cover
293    fn convert_to_resource_cover(&self) -> Result<resource_cover::ModuleData, &'static str> {
294        Err("cannot convert to resource cover!")
295    }
296    /// Resource Cover
297    fn convert_to_find_answer(&self) -> Result<find_answer::ModuleData, &'static str> {
298        Err("cannot convert to answer this!")
299    }
300    /// Video
301    fn convert_to_video(&self) -> Result<video::ModuleData, &'static str> {
302        Err("cannot convert to video!")
303    }
304    /// Embed
305    fn convert_to_embed(&self) -> Result<embed::ModuleData, &'static str> {
306        Err("cannot convert to embed!")
307    }
308    /// Legacy
309    fn convert_to_legacy(&self) -> Result<legacy::ModuleData, &'static str> {
310        Err("cannot convert to legacy!")
311    }
312}
313
314/// Extenstion trait for modes
315pub trait ModeExt: Copy + Default + PartialEq + Eq + Hash {
316    /// get a list of all the modes
317    /// (becomes the default in Choose page, which can be overriden in BodyExt)
318    fn get_list() -> Vec<Self>;
319
320    /// get the mode itself as a string id
321    fn as_str_id(&self) -> &'static str;
322    /// for headers, labels, etc.
323    fn label(&self) -> &'static str;
324
325    /// Image tag filters for search
326    /// The actual ImageTag enum variants are in components
327    fn image_tag_filters(&self) -> Option<Vec<i16>> {
328        None
329    }
330
331    /// Image tag priorities for search
332    /// The actual ImageTag enum variants are in components
333    fn image_tag_priorities(&self) -> Option<Vec<i16>> {
334        None
335    }
336}
337
338/// impl ModeExt for empty modes
339/// this is a special case and should only be used
340/// where the module genuinely ignores the mode
341/// one example is the Cover module
342impl 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    /// Gets this body's related [`ModuleKind`](super::ModuleKind)
358    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/* The following are things which are often used by multiple modules */
378
379/// Generic editor state which must be preserved between sessions
380/// Although these are saved to the db, they aren't relevant for playback
381#[derive(Clone, Default, Serialize, Deserialize, Debug)]
382pub struct EditorState<STEP>
383where
384    STEP: StepExt,
385{
386    /// the current step
387    pub step: STEP,
388
389    /// the completed steps
390    pub steps_completed: HashSet<STEP>,
391}
392
393/// This extension trait makes it possible to keep the Step
394/// functionality generic and at a higher level than the module itself
395pub trait StepExt: Clone + Copy + Default + PartialEq + Eq + Hash + Debug + Unpin {
396    /// Get the next step from current step
397    fn next(&self) -> Option<Self>;
398    /// Get the step as a number
399    fn as_number(&self) -> usize;
400    /// Label to display (will be localized)
401    fn label(&self) -> &'static str;
402    /// List of all available steps
403    fn get_list() -> Vec<Self>;
404    /// Get the step which is synonymous with "preview"
405    /// TODO: this could probably be derived as a combo
406    /// of get_list() and next() (i.e. the first step to return None)
407    fn get_preview() -> Self;
408    /// Auto-implemented, check whether current step is "preview"
409    fn is_preview(&self) -> bool {
410        *self == Self::get_preview()
411    }
412}
413
414/// impl StepExt for empty steps
415/// this is a special case and should only be used
416/// where the module genuinely ignores the step
417/// one example is the Legacy module
418impl 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)]
437/// Audio
438pub struct Audio {
439    /// The Audio Id
440    pub id: AudioId,
441    /// The Media Library
442    pub lib: MediaLibrary,
443}
444
445/// Module-specific assistance during play.
446#[derive(Clone, Default, Serialize, Deserialize, Debug)]
447pub struct ModuleAssist {
448    /// Text displayed in banner
449    pub text: Option<String>,
450    /// Audio played on module start
451    pub audio: Option<Audio>,
452    /// Whether to always show module assistance
453    ///
454    /// This will override the default module assist behavior.
455    ///
456    /// Note: This value will never be persisted in the backend.
457    #[serde(default)]
458    #[cfg_attr(feature = "backend", serde(skip))]
459    pub always_show: bool,
460}
461
462impl ModuleAssist {
463    /// Create a new ModuleAssist instance which doesn't override module assist behavior
464    pub fn new(text: Option<String>, audio: Option<Audio>) -> Self {
465        Self {
466            text,
467            audio,
468            ..Default::default()
469        }
470    }
471
472    /// Override default module assist behavior
473    pub fn always_show(mut self) -> Self {
474        self.always_show = true;
475        self
476    }
477
478    /// Whether the instructions actually have either text or audio content set
479    pub fn has_content(&self) -> bool {
480        self.text.is_some() || self.audio.is_some()
481    }
482}
483
484/// Type of assistance to be shown. This is only set during play and should never be
485/// persisted to the database.
486#[derive(Clone, Serialize, Deserialize, Debug)]
487pub enum ModuleAssistType {
488    /// Instructions to be shown when an activity starts
489    Instructions,
490    /// Feedback to be shown at the end of an activity
491    Feedback,
492    /// Replayable activity-specific assistance.
493    InActivity,
494}
495
496impl ModuleAssistType {
497    /// Whether this variant is `Instructions`
498    pub fn is_instructions(&self) -> bool {
499        matches!(self, Self::Instructions)
500    }
501
502    /// Whether this variant is `Feedback`
503    pub fn is_feedback(&self) -> bool {
504        matches!(self, Self::Feedback)
505    }
506
507    /// Whether this variant is `InActivity`
508    pub fn is_in_activity(&self) -> bool {
509        matches!(self, Self::InActivity)
510    }
511}
512
513#[derive(Clone, Serialize, Deserialize, Debug)]
514/// Background
515pub enum Background {
516    /// Color
517    Color(Option<rgb::RGBA8>),
518    /// Any other image
519    Image(Image),
520}
521
522#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
523/// Images need id and lib
524pub struct Image {
525    /// The Image Id
526    pub id: ImageId,
527    /// The MediaLibrary
528    pub lib: MediaLibrary,
529}
530
531#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
532/// Vector of 2 floats
533pub 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)]
548/// Vector of 3 floats
549pub 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)]
564/// Vector of 4 floats, also used as a Quaternion
565pub 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)]
580/// Visual Transform
581pub struct Transform {
582    /// Translation
583    pub translation: Vec3,
584    /// Rotation Quaternion
585    pub rotation: Vec4,
586    /// Scale for each axis
587    pub scale: Vec3,
588    /// Origin
589    pub origin: Vec3,
590}
591
592impl Transform {
593    /// Create a new Transform
594    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/// Theme Ids. Used in various modules
683/// See the frontend extension trait for more info
684#[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}