Exam page works
This commit is contained in:
1 parent
0b23da9eda
commit
7ad44110c3
5 files changed
+253
-38
No files matched your search
@@ -146,7 +146,7 @@ impl DeckSettings {
|
|||||||
ast.is_ok()
|
ast.is_ok()
|
||||||
}
|
}
|
||||||
pub fn require_speech(&self) -> bool {
|
pub fn require_speech(&self) -> bool {
|
||||||
self.forward == "speech" || self.backward == "speech"
|
self.forward.contains("speech") || self.backward.contains("speech")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -33,6 +33,7 @@ use iced::{Element, Task};
|
|||||||
use iced_core::Length::Fill;
|
use iced_core::Length::Fill;
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
use crate::pages::exam::{ExamMessage, ExamState};
|
||||||
|
|
||||||
impl Default for ScreenState {
|
impl Default for ScreenState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@@ -61,6 +62,7 @@ pub enum RootMessage {
|
|||||||
RepetitionSettings(RepetitionSettingsMessage),
|
RepetitionSettings(RepetitionSettingsMessage),
|
||||||
Import(ImportMessage),
|
Import(ImportMessage),
|
||||||
Exams(ExamsMessage),
|
Exams(ExamsMessage),
|
||||||
|
Exam(ExamMessage),
|
||||||
Keyboard(Event),
|
Keyboard(Event),
|
||||||
PushNotification(NotificationRequest),
|
PushNotification(NotificationRequest),
|
||||||
Notification(NotificationMessage),
|
Notification(NotificationMessage),
|
||||||
@@ -83,6 +85,7 @@ pub enum Page {
|
|||||||
RepetitionSettings(RepetitionSettingsState),
|
RepetitionSettings(RepetitionSettingsState),
|
||||||
Import(ImportState),
|
Import(ImportState),
|
||||||
Exams(ExamsState),
|
Exams(ExamsState),
|
||||||
|
Exam(ExamState),
|
||||||
PreviousPage,
|
PreviousPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +189,8 @@ impl ScreenState {
|
|||||||
History,
|
History,
|
||||||
RepetitionSettings,
|
RepetitionSettings,
|
||||||
Import,
|
Import,
|
||||||
Exams
|
Exams,
|
||||||
|
Exam
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
debug!("Update took {:?}", time.elapsed());
|
debug!("Update took {:?}", time.elapsed());
|
||||||
@@ -210,7 +214,8 @@ impl ScreenState {
|
|||||||
History,
|
History,
|
||||||
RepetitionSettings,
|
RepetitionSettings,
|
||||||
Import,
|
Import,
|
||||||
Exams
|
Exams,
|
||||||
|
Exam
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +236,8 @@ impl ScreenState {
|
|||||||
History,
|
History,
|
||||||
RepetitionSettings,
|
RepetitionSettings,
|
||||||
Import,
|
Import,
|
||||||
Exams
|
Exams,
|
||||||
|
Exam
|
||||||
);
|
);
|
||||||
|
|
||||||
let notifications = self.notification_manager.draw().map(Notification);
|
let notifications = self.notification_manager.draw().map(Notification);
|
||||||
@@ -245,6 +251,7 @@ impl ScreenState {
|
|||||||
return match page {
|
return match page {
|
||||||
Repetition(page) => page.press(&message),
|
Repetition(page) => page.press(&message),
|
||||||
Dictionary(page) => page.press(&message),
|
Dictionary(page) => page.press(&message),
|
||||||
|
Exam(page) => page.press(&message),
|
||||||
_ => Task::none(),
|
_ => Task::none(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+225
-18
@@ -1,48 +1,255 @@
|
|||||||
use iced::{Element, Task};
|
|
||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
use crate::data::entities::{EntityHandle, Exam, ExamEntry};
|
use crate::core::deck::DeckData;
|
||||||
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
use crate::core::srs::WordOpenMode;
|
||||||
|
use crate::data::Id;
|
||||||
|
use crate::data::entities::{CustomField, DeckSettings, EntityHandle, Exam, ExamEntry, WordData};
|
||||||
|
use crate::data::repositories::Repository;
|
||||||
use crate::navigation::Page::PreviousPage;
|
use crate::navigation::Page::PreviousPage;
|
||||||
use crate::styling::back_overlay;
|
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
|
||||||
use iced::widget::{button, row, column, text};
|
|
||||||
use crate::pages::exam::ExamMessage::*;
|
use crate::pages::exam::ExamMessage::*;
|
||||||
|
use crate::pages::repetition::{RepetitionMessage, play_sound};
|
||||||
|
use crate::styling::*;
|
||||||
|
use crate::{push_error, push_error_task};
|
||||||
|
use iced::widget::space::horizontal;
|
||||||
|
use iced::widget::{button, column, container, progress_bar, row, text, tooltip};
|
||||||
|
use iced::{Element, Task};
|
||||||
|
use iced_core::Alignment::Center;
|
||||||
|
use iced_core::Length::Fill;
|
||||||
|
use iced_core::keyboard::Event;
|
||||||
|
use iced_core::keyboard::key::Physical::Code;
|
||||||
|
use iced_core::widget::text::Style;
|
||||||
|
use iced_core::{Theme, keyboard};
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
|
use rand::rng;
|
||||||
|
use rodio::MixerDeviceSink;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct ExamPage
|
pub struct ExamState {
|
||||||
{
|
id: Id,
|
||||||
pub id: EntityHandle<Exam>,
|
state: AppState,
|
||||||
pub state: AppState,
|
entries: Vec<ExamEntry>,
|
||||||
pub entries : Vec<ExamEntry>,
|
total_count: u32,
|
||||||
|
open: bool,
|
||||||
|
custom_fields_cache: Vec<CustomField>,
|
||||||
|
current_word: WordData,
|
||||||
|
current_exam_entry_handle: EntityHandle<ExamEntry>,
|
||||||
|
deck: DeckSettings,
|
||||||
|
can_play: bool,
|
||||||
|
sink: Arc<Option<MixerDeviceSink>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ExamMessage {
|
pub enum ExamMessage {
|
||||||
Back,
|
Back,
|
||||||
Open,
|
Open,
|
||||||
Answer(bool)
|
Answer(bool),
|
||||||
|
Play,
|
||||||
|
PlayFinished,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NavigatedPage<ExamMessage> for ExamPage {
|
impl NavigatedPage<ExamMessage> for ExamState {
|
||||||
fn navigate(&mut self, message: &ExamMessage) -> Option<Page> {
|
fn navigate(&mut self, message: &ExamMessage) -> Option<Page> {
|
||||||
if let Back = message {
|
if let Back = message {
|
||||||
return Some(PreviousPage)
|
return Some(PreviousPage);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn navigated(&mut self) {
|
fn navigated(&mut self) {}
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, message: ExamMessage) -> Task<RootMessage> {
|
fn update(&mut self, message: ExamMessage) -> Task<RootMessage> {
|
||||||
match message {
|
match message {
|
||||||
Back => {}
|
Back => {}
|
||||||
Open => {}
|
Open => {
|
||||||
Answer(answer) => {}
|
if self.open {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.open = true;
|
||||||
|
}
|
||||||
|
Answer(answer) => {
|
||||||
|
if !self.open {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let id = self.current_exam_entry_handle.id;
|
||||||
|
let mut repo = self.state.exam_entries.write();
|
||||||
|
repo.set(
|
||||||
|
&mut self.current_exam_entry_handle,
|
||||||
|
ExamEntry {
|
||||||
|
id,
|
||||||
|
exam_id: self.id,
|
||||||
|
word_id: self.current_word.id,
|
||||||
|
status: Some(answer),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if let Err(err) = repo.save(&mut self.current_exam_entry_handle) {
|
||||||
|
return push_error_task!(err);
|
||||||
|
}
|
||||||
|
drop(repo);
|
||||||
|
self.open = false;
|
||||||
|
return self.next();
|
||||||
|
}
|
||||||
|
Play => {
|
||||||
|
if !self.can_play {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.can_play = false;
|
||||||
|
let value = self.current_word.key.clone();
|
||||||
|
if self.deck.require_speech() {
|
||||||
|
return Task::perform(play_sound(self.sink.clone(), value), |r| match r {
|
||||||
|
Ok(_) => RootMessage::Exam(PlayFinished),
|
||||||
|
Err(err) => push_error!(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlayFinished => {
|
||||||
|
self.can_play = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) -> Element<'_, ExamMessage> {
|
fn view(&self) -> Element<'_, ExamMessage> {
|
||||||
back_overlay(column![].into(), Back)
|
back_overlay(
|
||||||
|
column![
|
||||||
|
column![
|
||||||
|
progress_bar(
|
||||||
|
0.0..=(self.total_count as f32),
|
||||||
|
(self.total_count - self.entries.len() as u32) as f32
|
||||||
|
),
|
||||||
|
row![
|
||||||
|
text!("Решено: {}", self.total_count - self.entries.len() as u32),
|
||||||
|
horizontal().width(Fill),
|
||||||
|
text!("Осталось: {}", self.entries.len())
|
||||||
|
],
|
||||||
|
],
|
||||||
|
crate::widgets::card_display::word_display(
|
||||||
|
&self.deck,
|
||||||
|
&self.current_word,
|
||||||
|
self.open,
|
||||||
|
Play,
|
||||||
|
self.custom_fields_cache.as_slice()
|
||||||
|
),
|
||||||
|
container(self.answer_bar())
|
||||||
|
.width(Fill)
|
||||||
|
.align_x(Center)
|
||||||
|
.height(80),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
Back,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExamState {
|
||||||
|
fn answer_bar(&self) -> Element<'_, ExamMessage> {
|
||||||
|
if !self.open {
|
||||||
|
return text!("Нажмите Пробел для открытия карточки")
|
||||||
|
.style(|theme: &Theme| Style {
|
||||||
|
color: Some(theme.palette().text.scale_alpha(0.3)),
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
row![
|
||||||
|
tooltip(
|
||||||
|
button("Не знаю").on_press(Answer(false)),
|
||||||
|
"Клавиша 1",
|
||||||
|
tooltip::Position::Top
|
||||||
|
),
|
||||||
|
tooltip(
|
||||||
|
button("Знаю").on_press(Answer(true)),
|
||||||
|
"Клавиша 2",
|
||||||
|
tooltip::Position::Top
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(QUARTER_SPACING)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExamState {
|
||||||
|
pub fn new(exam_id: Id, deck: DeckSettings, state: &AppState) -> Self {
|
||||||
|
let custom_fields_cache = state.custom_fields.read().iter().cloned().collect();
|
||||||
|
let sink_handle = if deck.require_speech() {
|
||||||
|
rodio::DeviceSinkBuilder::open_default_sink().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut entries: Vec<_> = state
|
||||||
|
.exam_entries
|
||||||
|
.read()
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.exam_id == exam_id)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
let total_count = entries.len() as u32;
|
||||||
|
entries.retain(|e| e.status.is_none());
|
||||||
|
entries.shuffle(&mut rng());
|
||||||
|
let word = state
|
||||||
|
.dictionary
|
||||||
|
.read()
|
||||||
|
.get_static(EntityHandle::from_id(entries.last().unwrap().word_id))
|
||||||
|
.unwrap();
|
||||||
|
Self {
|
||||||
|
id: exam_id,
|
||||||
|
state: state.clone(),
|
||||||
|
current_word: word,
|
||||||
|
current_exam_entry_handle: EntityHandle::from_id(entries.last().unwrap().id),
|
||||||
|
entries,
|
||||||
|
total_count,
|
||||||
|
open: false,
|
||||||
|
custom_fields_cache,
|
||||||
|
deck,
|
||||||
|
can_play: true,
|
||||||
|
sink: Arc::new(sink_handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&mut self) -> Task<RootMessage> {
|
||||||
|
self.entries.pop().unwrap();
|
||||||
|
if self.entries.is_empty() {
|
||||||
|
return Task::done(RootMessage::Exam(Back));
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = self.entries.last().unwrap();
|
||||||
|
let handle: EntityHandle<ExamEntry> = EntityHandle::from_id(entry.id);
|
||||||
|
let word = self
|
||||||
|
.state
|
||||||
|
.dictionary
|
||||||
|
.read()
|
||||||
|
.get_static(EntityHandle::from_id(entry.word_id))
|
||||||
|
.unwrap();
|
||||||
|
self.current_word = word;
|
||||||
|
self.current_exam_entry_handle = handle;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyPressedPage for ExamState {
|
||||||
|
fn press(&mut self, message: &Event) -> Task<RootMessage> {
|
||||||
|
if let Event::KeyPressed {
|
||||||
|
key: _,
|
||||||
|
modified_key: _,
|
||||||
|
physical_key: pk,
|
||||||
|
location: _,
|
||||||
|
modifiers: _,
|
||||||
|
text: _,
|
||||||
|
repeat: _,
|
||||||
|
} = message
|
||||||
|
&& let Code(code) = pk
|
||||||
|
{
|
||||||
|
return match code {
|
||||||
|
keyboard::key::Code::Space => Task::done(RootMessage::Exam(Open)),
|
||||||
|
keyboard::key::Code::Digit1 => Task::done(RootMessage::Exam(Answer(false))),
|
||||||
|
keyboard::key::Code::Digit2 => Task::done(RootMessage::Exam(Answer(true))),
|
||||||
|
_ => Task::none(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+15
-14
@@ -1,19 +1,19 @@
|
|||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
|
use crate::data::Id;
|
||||||
use crate::data::entities::{DeckSettings, EntityHandle, Exam, ExamEntry};
|
use crate::data::entities::{DeckSettings, EntityHandle, Exam, ExamEntry};
|
||||||
use crate::data::repositories::{BatchedRepository, Repository};
|
use crate::data::repositories::{BatchedRepository, Repository};
|
||||||
use crate::data::Id;
|
|
||||||
use crate::navigation::Page::PreviousPage;
|
use crate::navigation::Page::PreviousPage;
|
||||||
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::notification::MaybeError;
|
use crate::notification::MaybeError;
|
||||||
|
use crate::pages::exam::ExamState;
|
||||||
use crate::pages::exams::ExamsMessage::*;
|
use crate::pages::exams::ExamsMessage::*;
|
||||||
use crate::pages::repetitions::Style;
|
use crate::pages::repetitions::Style;
|
||||||
use crate::push_error;
|
use crate::push_error;
|
||||||
use crate::styling::{back_overlay, cta_button, BUTTON_RADIUS, DEFAULT_SPACING, HALF_SPACING};
|
use crate::styling::{BUTTON_RADIUS, DEFAULT_SPACING, HALF_SPACING, back_overlay, cta_button};
|
||||||
use crate::{jl_button, push_error_task};
|
use crate::{jl_button, push_error_task};
|
||||||
use chrono::Utc;
|
use chrono::{Local, Utc};
|
||||||
use iced::widget::button::Status;
|
use iced::widget::button::Status;
|
||||||
use iced::widget::rule::horizontal;
|
use iced::widget::{Column, button, center_x, column, container, row, rule, scrollable, space, text, tooltip};
|
||||||
use iced::widget::{button, center_x, column, container, row, rule, scrollable, space, text, tooltip, Column};
|
|
||||||
use iced::{Element, Task};
|
use iced::{Element, Task};
|
||||||
use iced_core::Alignment::*;
|
use iced_core::Alignment::*;
|
||||||
use iced_core::Length::{Fill, FillPortion};
|
use iced_core::Length::{Fill, FillPortion};
|
||||||
@@ -46,12 +46,14 @@ impl NavigatedPage<ExamsMessage> for ExamsState {
|
|||||||
if let Some(selected_exam_id) = self.selected_exam_id
|
if let Some(selected_exam_id) = self.selected_exam_id
|
||||||
&& GotoExam == *message
|
&& GotoExam == *message
|
||||||
{
|
{
|
||||||
return Some(PreviousPage);
|
return Some(Page::Exam(ExamState::new(selected_exam_id.id, self.deck.clone(), &self.state)));
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn navigated(&mut self) {}
|
fn navigated(&mut self) {
|
||||||
|
self.exams = Self::get_exams_list(&self.state, self.deck_id.id);
|
||||||
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: ExamsMessage) -> Task<RootMessage> {
|
fn update(&mut self, message: ExamsMessage) -> Task<RootMessage> {
|
||||||
match message {
|
match message {
|
||||||
@@ -170,7 +172,7 @@ impl ExamsState {
|
|||||||
|
|
||||||
for (handle, deck) in self.exams.iter() {
|
for (handle, deck) in self.exams.iter() {
|
||||||
column = column.push(
|
column = column.push(
|
||||||
jl_button!(text!("{}", &deck.timestamp.format("%d/%m/%Y %H:%M")))
|
jl_button!(text!("{}", &deck.timestamp.with_timezone(&Local).format("%d/%m/%Y %H:%M")))
|
||||||
.on_press_with(move || SelectExam(*handle))
|
.on_press_with(move || SelectExam(*handle))
|
||||||
.style(move |_x: &Theme, status: Status| Style {
|
.style(move |_x: &Theme, status: Status| Style {
|
||||||
background: if status == Status::Hovered {
|
background: if status == Status::Hovered {
|
||||||
@@ -178,7 +180,7 @@ impl ExamsState {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
text_color: _x.palette().primary,
|
text_color: if deck.finished() { _x.palette().success } else { _x.palette().primary},
|
||||||
border: Border {
|
border: Border {
|
||||||
color: Default::default(),
|
color: Default::default(),
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
@@ -197,7 +199,7 @@ impl ExamsState {
|
|||||||
return column![
|
return column![
|
||||||
center_x(text!(
|
center_x(text!(
|
||||||
"Экзамен от {}",
|
"Экзамен от {}",
|
||||||
exam.timestamp.format("%d/%m/%Y %H:%M")
|
exam.timestamp.with_timezone(&Local).format("%d/%m/%Y %H:%M")
|
||||||
)),
|
)),
|
||||||
row![text!("{} карточек", exam.total), Self::exam_bar(exam)]
|
row![text!("{} карточек", exam.total), Self::exam_bar(exam)]
|
||||||
.height(30)
|
.height(30)
|
||||||
@@ -217,10 +219,9 @@ impl ExamsState {
|
|||||||
|
|
||||||
fn exam_bar(exam: &ExamExtended) -> Element<'_, ExamsMessage> {
|
fn exam_bar(exam: &ExamExtended) -> Element<'_, ExamsMessage> {
|
||||||
let total = 1000.0;
|
let total = 1000.0;
|
||||||
let answered = exam.answered as f64 / exam.total as f64 * total;
|
let answered = (exam.answered as f64 / exam.total as f64 * total).round();
|
||||||
let unknown = exam.unknown as f64 / exam.total as f64 * total;
|
let unknown = (exam.unknown as f64 / exam.total as f64 * total).round();
|
||||||
let remaining = exam.remaining() as f64 / exam.total as f64 * total;
|
let remaining = (exam.remaining() as f64 / exam.total as f64 * total).round();
|
||||||
println!("{} {} {}", remaining, answered, unknown);
|
|
||||||
let mut row = row![];
|
let mut row = row![];
|
||||||
if answered > 0.0 {
|
if answered > 0.0 {
|
||||||
row = row.push(tooltip(
|
row = row.push(tooltip(
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ impl NavigatedPage<RepetitionMessage> for RepetitionState {
|
|||||||
RepetitionMessage::Back => {}
|
RepetitionMessage::Back => {}
|
||||||
RepetitionMessage::Next => return self.next(),
|
RepetitionMessage::Next => return self.next(),
|
||||||
RepetitionMessage::Answer(m) => return self.answer(m),
|
RepetitionMessage::Answer(m) => return self.answer(m),
|
||||||
RepetitionMessage::Play => {
|
Play => {
|
||||||
if !self.can_play {
|
if !self.can_play {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
@@ -242,7 +242,7 @@ pub enum RepetitionMessage {
|
|||||||
PlayFinished,
|
PlayFinished,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn play_sound(sink: Arc<Option<MixerDeviceSink>>, text: String) -> MaybeError {
|
pub async fn play_sound(sink: Arc<Option<MixerDeviceSink>>, text: String) -> MaybeError {
|
||||||
let data = get_voice(text.as_str()).await?;
|
let data = get_voice(text.as_str()).await?;
|
||||||
spawn_blocking(move || {
|
spawn_blocking(move || {
|
||||||
if let Some(sink) = sink.as_ref()
|
if let Some(sink) = sink.as_ref()
|
||||||
|
|||||||
Reference in new issue
Block a user