use std::{sync::LazyLock, time::Duration}; use dioxus::{ desktop::{Config, LogicalSize, WindowBuilder}, prelude::*, }; 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 || { let current_lock = current.read(); let incorrectly_checked = current_lock .answers .iter() .filter(|a| !a.is_correct.unwrap_or_default() && a.checked) .count(); let correctly_checked = current_lock .answers .iter() .filter(|a| a.is_correct.unwrap_or_default() && a.checked) .count(); correctly_checked.saturating_sub(incorrectly_checked) }); 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 correct_questions = use_signal(|| 0); let mut wrong_questions = use_signal(|| 0); let mut correct_animation = use_signal(|| false); let mut wrong_animation = use_signal(|| false); use_future(move || async move { loop { tokio::time::sleep(Duration::from_millis(400)).await; let is_correct = correct_animation(); let is_wrong = wrong_animation(); if is_correct || is_wrong { tokio::time::sleep(Duration::from_millis(600)).await; // Uncheck all the answers current().answers.iter_mut().for_each(|a| a.checked = false); let mut correct_questions_lock = correct_questions.write(); let mut wrong_questions_lock = wrong_questions.write(); // Get a new question or reroll the list if !questions().is_empty() { current.set(questions.remove(0)); // Update the correct/wrong counters if is_correct { *correct_questions_lock += 1; } else if is_wrong { *wrong_questions_lock += 1; } } else { questions.set(get_questions()); current.set(questions.remove(0)); *correct_questions_lock = 0; *wrong_questions_lock = 0; } // Reset animation flags correct_animation.set(false); wrong_animation.set(false); } } }); let answer_buttons = current() .answers .into_iter() .enumerate() .map(|(i, _answer)| { rsx! { AnswerCheckbox { current, id: i } } }); rsx! { ResultPopup { is_correct: correct_animation, is_wrong: wrong_animation }, div { id: "counter", div { "{left_questions}" } div { class: "correct", "{correct_questions}" } div { class: "wrong", "{wrong_questions}" } div { "{total_questions}" } }, form { id: "form", onsubmit: move |_| { if actual_correct() == total_correct() { correct_animation.set(true); } else { wrong_animation.set(true); } }, h1 { class: if correct_animation() { "correct" }, class: if wrong_animation() { "wrong" }, "{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_correct: Signal, is_wrong: Signal) -> Element { let is_correct = use_memo(move || is_correct()); let is_wrong = use_memo(move || is_wrong()); let is_visible = use_memo(move || is_correct() || is_wrong()); rsx! { div { id: "result", class: if is_visible() { "visible" }, class: if is_correct() { "correct" }, class: if is_wrong() { "wrong" }, if is_correct() { "✓" } else { "X" } } } }