Exam page works
This commit is contained in:
1 parent
0b23da9eda
commit
7ad44110c3
5 files changed
+254
-39
No files matched your search
@@ -146,7 +146,7 @@ impl DeckSettings {
|
||||
ast.is_ok()
|
||||
}
|
||||
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 log::{debug, info};
|
||||
use std::time::Instant;
|
||||
use crate::pages::exam::{ExamMessage, ExamState};
|
||||
|
||||
impl Default for ScreenState {
|
||||
fn default() -> Self {
|
||||
@@ -61,6 +62,7 @@ pub enum RootMessage {
|
||||
RepetitionSettings(RepetitionSettingsMessage),
|
||||
Import(ImportMessage),
|
||||
Exams(ExamsMessage),
|
||||
Exam(ExamMessage),
|
||||
Keyboard(Event),
|
||||
PushNotification(NotificationRequest),
|
||||
Notification(NotificationMessage),
|
||||
@@ -83,6 +85,7 @@ pub enum Page {
|
||||
RepetitionSettings(RepetitionSettingsState),
|
||||
Import(ImportState),
|
||||
Exams(ExamsState),
|
||||
Exam(ExamState),
|
||||
PreviousPage,
|
||||
}
|
||||
|
||||
@@ -186,7 +189,8 @@ impl ScreenState {
|
||||
History,
|
||||
RepetitionSettings,
|
||||
Import,
|
||||
Exams
|
||||
Exams,
|
||||
Exam
|
||||
);
|
||||
}
|
||||
debug!("Update took {:?}", time.elapsed());
|
||||
@@ -210,7 +214,8 @@ impl ScreenState {
|
||||
History,
|
||||
RepetitionSettings,
|
||||
Import,
|
||||
Exams
|
||||
Exams,
|
||||
Exam
|
||||
)
|
||||
}
|
||||
|
||||
@@ -231,7 +236,8 @@ impl ScreenState {
|
||||
History,
|
||||
RepetitionSettings,
|
||||
Import,
|
||||
Exams
|
||||
Exams,
|
||||
Exam
|
||||
);
|
||||
|
||||
let notifications = self.notification_manager.draw().map(Notification);
|
||||
@@ -245,6 +251,7 @@ impl ScreenState {
|
||||
return match page {
|
||||
Repetition(page) => page.press(&message),
|
||||
Dictionary(page) => page.press(&message),
|
||||
Exam(page) => page.press(&message),
|
||||
_ => Task::none(),
|
||||
};
|
||||
}
|
||||
|
||||
+226
-19
@@ -1,48 +1,255 @@
|
||||
use iced::{Element, Task};
|
||||
use crate::app::AppState;
|
||||
use crate::data::entities::{EntityHandle, Exam, ExamEntry};
|
||||
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||
use crate::core::deck::DeckData;
|
||||
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::styling::back_overlay;
|
||||
use iced::widget::{button, row, column, text};
|
||||
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
|
||||
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 id: EntityHandle<Exam>,
|
||||
pub state: AppState,
|
||||
pub entries : Vec<ExamEntry>,
|
||||
pub struct ExamState {
|
||||
id: Id,
|
||||
state: AppState,
|
||||
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)]
|
||||
pub enum ExamMessage {
|
||||
Back,
|
||||
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> {
|
||||
if let Back = message {
|
||||
return Some(PreviousPage)
|
||||
return Some(PreviousPage);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn navigated(&mut self) {
|
||||
}
|
||||
fn navigated(&mut self) {}
|
||||
|
||||
fn update(&mut self, message: ExamMessage) -> Task<RootMessage> {
|
||||
match message {
|
||||
Back => {}
|
||||
Open => {}
|
||||
Answer(answer) => {}
|
||||
Open => {
|
||||
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()
|
||||
}
|
||||
|
||||
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::data::Id;
|
||||
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::exam::ExamState;
|
||||
use crate::pages::exams::ExamsMessage::*;
|
||||
use crate::pages::repetitions::Style;
|
||||
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 chrono::Utc;
|
||||
use chrono::{Local, Utc};
|
||||
use iced::widget::button::Status;
|
||||
use iced::widget::rule::horizontal;
|
||||
use iced::widget::{button, center_x, column, container, row, rule, scrollable, space, text, tooltip, Column};
|
||||
use iced::widget::{Column, button, center_x, column, container, row, rule, scrollable, space, text, tooltip};
|
||||
use iced::{Element, Task};
|
||||
use iced_core::Alignment::*;
|
||||
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
|
||||
&& GotoExam == *message
|
||||
{
|
||||
return Some(PreviousPage);
|
||||
return Some(Page::Exam(ExamState::new(selected_exam_id.id, self.deck.clone(), &self.state)));
|
||||
}
|
||||
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> {
|
||||
match message {
|
||||
@@ -170,7 +172,7 @@ impl ExamsState {
|
||||
|
||||
for (handle, deck) in self.exams.iter() {
|
||||
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))
|
||||
.style(move |_x: &Theme, status: Status| Style {
|
||||
background: if status == Status::Hovered {
|
||||
@@ -178,7 +180,7 @@ impl ExamsState {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
text_color: _x.palette().primary,
|
||||
text_color: if deck.finished() { _x.palette().success } else { _x.palette().primary},
|
||||
border: Border {
|
||||
color: Default::default(),
|
||||
width: 0.0,
|
||||
@@ -197,7 +199,7 @@ impl ExamsState {
|
||||
return column![
|
||||
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)]
|
||||
.height(30)
|
||||
@@ -217,10 +219,9 @@ impl ExamsState {
|
||||
|
||||
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;
|
||||
println!("{} {} {}", remaining, answered, unknown);
|
||||
let answered = (exam.answered as f64 / exam.total as f64 * total).round();
|
||||
let unknown = (exam.unknown as f64 / exam.total as f64 * total).round();
|
||||
let remaining = (exam.remaining() as f64 / exam.total as f64 * total).round();
|
||||
let mut row = row![];
|
||||
if answered > 0.0 {
|
||||
row = row.push(tooltip(
|
||||
|
||||
@@ -50,7 +50,7 @@ impl NavigatedPage<RepetitionMessage> for RepetitionState {
|
||||
RepetitionMessage::Back => {}
|
||||
RepetitionMessage::Next => return self.next(),
|
||||
RepetitionMessage::Answer(m) => return self.answer(m),
|
||||
RepetitionMessage::Play => {
|
||||
Play => {
|
||||
if !self.can_play {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -242,7 +242,7 @@ pub enum RepetitionMessage {
|
||||
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?;
|
||||
spawn_blocking(move || {
|
||||
if let Some(sink) = sink.as_ref()
|
||||
|
||||
Reference in new issue
Block a user