use crate::app::AppState; use crate::data::entities::{DeckSettings, EntityHandle, Exam, ExamEntry}; use crate::data::repositories::{BatchedRepository, Repository}; use crate::data::Id; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::MaybeError; use crate::pages::exams::ExamsMessage::*; use crate::pages::repetitions::Style; use crate::push_error; use crate::styling::{back_overlay, BUTTON_RADIUS, DEFAULT_SPACING, HALF_SPACING}; use crate::{jl_button, push_error_task}; use chrono::Utc; use iced::widget::button::Status; use iced::widget::{ button, center_x, column, container, row, scrollable, space, text, tooltip, Column, }; use iced::{Element, Task}; use iced_core::Alignment::*; use iced_core::Length::{Fill, FillPortion}; use iced_core::{Background, Border, Color, Shadow, Theme}; use std::collections::BTreeMap; use std::ops::{Deref, DerefMut}; use std::sync::Arc; pub struct ExamsState { state: AppState, deck_id: EntityHandle, deck: DeckSettings, selected_exam_id: Option>, exams: BTreeMap, ExamExtended>, } #[derive(Clone, Eq, PartialEq)] pub enum ExamsMessage { Back, SelectExam(EntityHandle), DeleteExam(EntityHandle), CreateExam, GotoExam, } impl NavigatedPage for ExamsState { fn navigate(&mut self, message: &ExamsMessage) -> Option { if let Back = message { return Some(PreviousPage); } if let Some(selected_exam_id) = self.selected_exam_id && GotoExam == *message { return Some(PreviousPage); } None } fn navigated(&mut self) {} fn update(&mut self, message: ExamsMessage) -> Task { match message { Back => {} GotoExam => {} SelectExam(id) => { self.selected_exam_id = Some(id); } DeleteExam(id) => { self.selected_exam_id = None; } CreateExam => { let exam = Exam { id: Default::default(), timestamp: Utc::now(), deck_id: self.deck_id.id, }; let mut exam_repo = self.state.exam.write(); let handle = exam_repo.create_now(exam); if let Ok(handle) = handle { drop(exam_repo); if let Err(err) = self.append_exam_entries(handle) { return push_error_task!(err); } self.exams = Self::get_exams_list(&self.state, self.deck_id.id); self.selected_exam_id = Some(handle); } else { return push_error_task!(handle.err().unwrap()); } } } Task::none() } fn view(&self) -> Element<'_, ExamsMessage> { back_overlay( column![ text(format!("Экзамены для колоды {}", self.deck.name)), row![row![ column![ scrollable(self.exams_list()).height(Fill), jl_button!("Добавить").width(Fill).on_press(CreateExam), ] .spacing(DEFAULT_SPACING) .width(FillPortion(1)), self.selected_exam_view(), ]] ] .into(), Back, ) } } impl ExamsState { pub fn new(deck_id: EntityHandle, state: AppState) -> Self { let deck = state.decks.read().get_static(deck_id).unwrap(); Self { exams: Self::get_exams_list(&state, deck_id.id), state, deck_id, deck, selected_exam_id: None, } } fn selected_exam(&self) -> Option<&ExamExtended> { self.exams.get(&self.selected_exam_id?) } fn selected_exam_mut(&mut self) -> Option<&mut ExamExtended> { self.exams.get_mut(&self.selected_exam_id?) } fn append_exam_entries(&mut self, handle: EntityHandle) -> MaybeError { let deck_id = self.deck_id.id; let words: Vec<_> = self .state .card_states .read() .iter() .filter(|state| state.deck_id == deck_id) .map(|state| state.word_id) .collect(); let mut entries = words .iter() .map(|word_id| ExamEntry { id: Default::default(), exam_id: handle.id, word_id: *word_id, status: None, }) .collect::>(); self.state .exam_entries .write() .create_now_many(entries.as_mut_slice())?; Ok(()) } fn get_exams_list(state: &AppState, deck_id: Id) -> BTreeMap, ExamExtended> { let mut query = state .exam .read() .query(Arc::new(move |exam| exam.deck_id == deck_id)); let handles: Vec<_> = query.handles().collect(); query .iter(&state.exam.read()) .cloned() .zip(handles) .map(|(e, h)| (h, ExamExtended::from(e, &state))) .collect() } } impl ExamsState { pub fn exams_list(&self) -> Column<'_, ExamsMessage> { let mut column = Column::new(); for (handle, deck) in self.exams.iter() { column = column.push( button(text!("{}", &deck.timestamp.format("%d/%m/%Y %H:%M"))) .on_press_with(move || SelectExam(*handle)) .style(move |_x: &Theme, status: Status| Style { background: if status == Status::Hovered { Some(Background::Color(Color::WHITE.scale_alpha(0.2))) } else { None }, text_color: _x.palette().primary, border: Border { color: Default::default(), width: 0.0, radius: BUTTON_RADIUS.into(), }, shadow: Shadow::default(), snap: false, }), ); } column } pub fn selected_exam_view(&self) -> Element<'_, ExamsMessage> { if let Some(exam) = self.selected_exam() { return column![ center_x(text!( "Экзамен от {}", exam.timestamp.format("%d/%m/%Y %H:%M") )), row![text!("{} карточек", exam.total), Self::exam_bar(exam)] .height(30) .align_y(Center) .width(Fill).spacing(HALF_SPACING), self.exam_variable_view(exam), ] .spacing(DEFAULT_SPACING) .width(FillPortion(2)) .into(); } space().width(FillPortion(3)).into() } fn exam_bar(exam: &ExamExtended) -> Element<'_, ExamsMessage> { let total = 1000.0; let answered = exam.answered as f64 / exam.total as f64 * total; let unknown = exam.unknown as f64 / exam.total as f64 * total; let remaining = exam.remaining() as f64 / exam.total as f64 * total; row![ tooltip( container(space()) .height(Fill) .width(FillPortion(answered as u16)) .style(|theme: &Theme| { container::Style { background: Some(Background::Color(theme.palette().success)), text_color: Default::default(), border: Default::default(), shadow: Default::default(), snap: false, } }), text!("Правильно: {}%", answered / 10.0), tooltip::Position::Bottom ), tooltip( container(space()) .height(Fill) .width(FillPortion(unknown as u16)) .style(|theme: &Theme| { container::Style { background: Some(Background::Color(theme.palette().danger)), text_color: Default::default(), border: Default::default(), shadow: Default::default(), snap: false, } }), text!("Не получилось: {}%", unknown / 10.0), tooltip::Position::Bottom ), tooltip( container(space()) .height(Fill) .width(FillPortion(remaining as u16)) .style(|theme: &Theme| { container::Style { background: Some(Background::Color(theme.palette().primary)), text_color: Default::default(), border: Default::default(), shadow: Default::default(), snap: false, } }), text!("Осталось {}%", remaining / 10.0), tooltip::Position::Bottom ) ] .width(Fill) .height(Fill) .into() } fn exam_variable_view(&self, exam: &ExamExtended) -> Element<'_, ExamsMessage> { if exam.finished() { self.exam_finished_view(exam) } else { self.exam_ongoing_view(exam) } } fn exam_finished_view(&self, exam: &ExamExtended) -> Element<'_, ExamsMessage> { column![].spacing(DEFAULT_SPACING).into() } fn exam_ongoing_view(&self, exam: &ExamExtended) -> Element<'_, ExamsMessage> { center_x(button("Продолжить экзамен").on_press(GotoExam)).into() } } struct ExamExtended { exam: Exam, total: u32, answered: u32, unknown: u32, } impl ExamExtended { fn from(exam: Exam, state: &AppState) -> Self { let entries = state.exam_entries.read(); let mut total = 0; let mut answered = 0; let mut unknown = 0; entries.iter().filter(|e| e.exam_id == e.id).for_each(|e| { total += 1; match e.status { None => {} Some(s) => { if s { answered += 1; } else { unknown += 1; } } } }); Self { exam, total, answered, unknown, } } fn solved(&self) -> u32 { self.answered + self.unknown } fn finished(&self) -> bool { self.solved() == self.total } fn remaining(&self) -> u32 { self.total - self.solved() } } impl Deref for ExamExtended { type Target = Exam; fn deref(&self) -> &Self::Target { &self.exam } } impl DerefMut for ExamExtended { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.exam } }