use std::{ ops::{Deref, DerefMut}, sync::LazyLock, }; use dioxus::{ desktop::{Config, WindowBuilder}, prelude::*, }; use rand::seq::SliceRandom; use serde::Deserialize; // const FAVICON: Asset = asset!("/assets/favicon.ico"); const MAIN_CSS: Asset = asset!("/assets/main.css"); // const HEADER_SVG: Asset = asset!("/assets/header.svg"); #[derive(Debug, Deserialize)] pub struct Questions { questions: Vec, } impl Deref for Questions { type Target = Vec; fn deref(&self) -> &Self::Target { &self.questions } } impl DerefMut for Questions { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.questions } } #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Question { message: String, answers: Vec, } #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Answer { message: String, #[serde(default)] is_correct: Option, #[serde(default, skip)] checked: bool, } #[derive(Debug, Props, PartialEq, Clone)] struct QuestionProps { current: Question, } pub static QUESTIONS: LazyLock = LazyLock::new(|| { let questions_str = std::fs::read_to_string("questions.toml").unwrap(); let questions: Questions = toml::from_str(&questions_str).unwrap(); questions }); fn get_questions() -> Vec { let mut questions = QUESTIONS.questions.clone(); let mut rng = rand::rng(); // Randomize the answers questions .iter_mut() .for_each(|q| q.answers.shuffle(&mut rng)); // Randomize the questions list questions.shuffle(&mut rng); questions } fn main() { dioxus::LaunchBuilder::new() .with_cfg( Config::default().with_menu(None).with_window( WindowBuilder::new() .with_maximized(true) .with_title("Flashcards"), ), ) .launch(App) } #[component] fn App() -> Element { rsx! { // document::Link { rel: "icon", href: FAVICON } document::Link { rel: "stylesheet", href: MAIN_CSS } QuestionPrompt {} } } #[component] pub fn QuestionPrompt() -> Element { let mut questions = use_signal(get_questions); let mut current = use_signal(move || questions.remove(0)); let total_correct = use_memo(move || { current .read() .answers .iter() .filter(|a| a.is_correct.unwrap_or_default()) .count() }); let actual_correct = use_memo(move || { current .read() .answers .iter() .filter(|a| a.is_correct.unwrap_or_default() && a.checked) .count() }); let total_questions = QUESTIONS.len(); let current_question = use_memo(move || questions().len()); let left_questions = use_memo(move || total_questions - current_question()); let answer_buttons = current() .answers .into_iter() .enumerate() .map(|(i, _answer)| { rsx! { AnswerCheckbox { current, id: i } } }); rsx! { div { "{left_questions}/{total_questions}" }, form { id: "form", onsubmit: move |_| { if actual_correct() == total_correct() { current().answers.iter_mut().for_each(|a| a.checked = false); if !questions().is_empty() { current.set(questions.remove(0)); } else { questions.set(get_questions()); current.set(questions.remove(0)); } } }, h1 { "{current().message}" } { answer_buttons } input { type: "submit", } } } } #[component] pub fn AnswerCheckbox(current: Signal, id: usize) -> Element { let message = use_memo(move || { current .read() .answers .get(id) .map(|a| a.message.clone()) .unwrap_or("???".to_string()) }); let checked = use_memo(move || { current .read() .answers .get(id) .map(|a| a.checked) .unwrap_or(false) }); rsx! { div { class: if checked() { "selected" } else { "" }, onclick: move |_| { let mut write_lock = current.write(); if let Some(answer) = write_lock.answers.get_mut(id) { answer.checked = !answer.checked; } }, input { id: "a{id}", name: "a{id}", type: "checkbox", value: "{id}", checked: "{checked}", onclick: move |event| event.prevent_default(), } label { for: "a{id}", onclick: move |event| event.prevent_default(), "{message}" }, } } }