use std::{sync::LazyLock, time::Duration}; use dioxus::{ desktop::{Config, LogicalSize, WindowBuilder}, prelude::*, }; use futures::StreamExt; use models::{Question, Questions}; use rand::seq::SliceRandom; mod models; const MAIN_CSS: Asset = asset!("/assets/main.css"); 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_min_inner_size(LogicalSize::new(640, 540)) .with_maximized(true) .with_title("Flashcards"), ), ) .launch(App) } #[component] fn App() -> Element { rsx! { document::Link { rel: "stylesheet", href: MAIN_CSS } QuestionForm {} } } #[component] pub fn QuestionForm() -> 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 mut success_animation = use_signal(|| false); use_future(move || async move { loop { tokio::time::sleep(Duration::from_millis(400)).await; if success_animation() { tokio::time::sleep(Duration::from_millis(600)).await; success_animation.set(false); 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)); } } } }); let answer_buttons = current() .answers .into_iter() .enumerate() .map(|(i, _answer)| { rsx! { AnswerCheckbox { current, id: i } } }); rsx! { ResultPopup { is_visible: success_animation }, div { id: "counter", "{left_questions}/{total_questions}" }, form { id: "form", onsubmit: move |_| { if actual_correct() == total_correct() { success_animation.set(true); } else { } }, h1 { class: if success_animation() { "success" }, "{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" }, 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}" }, } } } #[component] pub fn ResultPopup(is_visible: Signal) -> Element { let is_visible = use_memo(move || is_visible()); rsx! { div { id: "result", class: if is_visible() { "visible" }, "✓" } } }