360 lines
11 KiB
Rust
360 lines
11 KiB
Rust
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<DeckSettings>,
|
|
deck: DeckSettings,
|
|
selected_exam_id: Option<EntityHandle<Exam>>,
|
|
exams: BTreeMap<EntityHandle<Exam>, ExamExtended>,
|
|
}
|
|
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub enum ExamsMessage {
|
|
Back,
|
|
SelectExam(EntityHandle<Exam>),
|
|
DeleteExam(EntityHandle<Exam>),
|
|
CreateExam,
|
|
GotoExam,
|
|
}
|
|
|
|
impl NavigatedPage<ExamsMessage> for ExamsState {
|
|
fn navigate(&mut self, message: &ExamsMessage) -> Option<Page> {
|
|
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<RootMessage> {
|
|
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<DeckSettings>, 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<Exam>) -> 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::<Vec<_>>();
|
|
|
|
self.state
|
|
.exam_entries
|
|
.write()
|
|
.create_now_many(entries.as_mut_slice())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_exams_list(state: &AppState, deck_id: Id) -> BTreeMap<EntityHandle<Exam>, 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
|
|
}
|
|
}
|