Files
jap_learn/src/pages/repetition.rs
T

349 lines
11 KiB
Rust
Raw Normal View History

2026-08-23 22:05:08 +03:00
use crate::app::AppState;
2026-08-24 11:40:31 +03:00
use crate::core::deck::DeckData;
2026-08-24 11:35:37 +03:00
use crate::core::srs::WordOpenMode;
use crate::data::Id;
2026-08-24 18:16:41 +03:00
use crate::data::entities::{CardStatistics, DeckSettings, WordData};
2026-04-19 22:56:58 +03:00
use crate::data_provider::voice::get_voice;
2026-08-01 17:16:58 +03:00
use crate::navigation::Page::PreviousPage;
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
2026-08-24 23:42:58 +03:00
use crate::notification::{MaybeError, NotificationData};
2026-08-01 17:16:58 +03:00
use crate::styling::*;
2026-08-24 18:16:41 +03:00
use crate::{push_error, push_error_task};
2026-08-06 19:29:58 +03:00
use chrono::Local;
2026-04-14 15:19:46 +03:00
use iced::alignment::Horizontal::Center;
use iced::keyboard::key::Physical::Code;
2026-08-01 17:16:58 +03:00
use iced::widget::text::Style;
2026-08-24 18:16:41 +03:00
use iced::widget::{Column, button, column, container, row, rule, space, text, tooltip};
use iced::{Element, Fill, Task, Theme, alignment, keyboard};
use parking_lot::Mutex;
2026-04-19 22:56:58 +03:00
use rodio::MixerDeviceSink;
2026-05-05 12:02:59 +03:00
use std::collections::HashSet;
2026-08-20 00:07:09 +03:00
use std::sync::Arc;
2026-04-19 22:56:58 +03:00
use tokio::task::spawn_blocking;
2026-04-09 22:45:31 +03:00
pub struct RepetitionState {
2026-08-06 19:12:23 +03:00
state: Arc<Mutex<AppState>>,
2026-08-18 12:14:35 +03:00
pub settings: DeckSettings,
pub set: DeckData,
2026-05-05 12:02:59 +03:00
current_word: WordData,
current_statistic: CardStatistics,
2026-04-14 15:19:46 +03:00
open: bool,
2026-04-19 22:56:58 +03:00
can_play: bool,
2026-08-20 18:53:13 +03:00
sink: Arc<Option<MixerDeviceSink>>,
2026-08-18 12:47:06 +03:00
opened: HashSet<Id>,
2026-04-09 22:45:31 +03:00
}
impl NavigatedPage<RepetitionMessage> for RepetitionState {
2026-08-18 12:14:35 +03:00
fn navigate(&mut self, message: &RepetitionMessage) -> Option<Page> {
2026-04-09 22:45:31 +03:00
if let RepetitionMessage::Back = message {
Some(PreviousPage)
} else {
None
}
}
2026-08-06 23:20:14 +03:00
2026-08-15 13:22:59 +03:00
fn navigated(&mut self) {}
2026-08-13 22:03:47 +03:00
fn update(&mut self, message: RepetitionMessage) -> Task<RootMessage> {
2026-04-14 15:19:46 +03:00
match message {
RepetitionMessage::Back => {}
2026-04-20 10:13:39 +03:00
RepetitionMessage::Next => return self.next(),
RepetitionMessage::Answer(m) => return self.answer(m),
2026-04-19 22:56:58 +03:00
RepetitionMessage::Play => {
if !self.can_play {
2026-05-05 12:02:59 +03:00
return Task::none();
2026-04-19 22:56:58 +03:00
}
self.can_play = false;
let value = self.current_word.key.clone();
2026-04-20 15:21:31 +03:00
if self.settings.require_speech() {
2026-08-23 22:05:08 +03:00
return Task::perform(play_sound(self.sink.clone(), value), |r| match r {
Ok(_) => RootMessage::Repetition(RepetitionMessage::PlayFinished),
2026-08-24 18:16:41 +03:00
Err(err) => push_error!(err),
2026-05-05 12:02:59 +03:00
});
2026-04-20 15:21:31 +03:00
}
2026-05-05 12:02:59 +03:00
}
2026-04-19 22:56:58 +03:00
RepetitionMessage::PlayFinished => {
self.can_play = true;
}
2026-04-14 15:19:46 +03:00
}
2026-04-09 22:45:31 +03:00
Task::none()
}
2026-08-13 22:03:47 +03:00
fn view(&self) -> Element<'_, RepetitionMessage> {
back_overlay(
column![
container(self.draw_forward())
.width(Fill)
.height(Fill)
.align_x(Center)
.align_y(alignment::Vertical::Center),
rule::horizontal(2),
container(self.draw_backward())
.width(Fill)
.height(Fill)
.align_x(Center)
.align_y(alignment::Vertical::Center),
container(self.answer_bar())
.width(Fill)
.align_x(Center)
.height(80),
text!(
"Затронуто слов {}, {}%",
self.opened.len(),
(self.opened.len() as f32 / self.set.len() as f32 * 10000.0).round() / 100.0
)
]
2026-08-15 13:22:59 +03:00
.height(Fill)
.width(Fill)
.into(),
2026-08-13 22:03:47 +03:00
RepetitionMessage::Back,
)
}
}
impl RepetitionState {
2026-08-24 18:16:41 +03:00
pub(crate) fn new(
set: DeckSettings,
state: Arc<Mutex<AppState>>,
) -> Result<RepetitionState, NotificationData> {
let mut card_set = DeckData::new(&set, state.clone())?;
2026-08-13 22:03:47 +03:00
let (word, stat) = card_set.next();
2026-08-20 18:53:13 +03:00
let sink_handle = if set.forward.contains("speech") || set.backward.contains("speech") {
rodio::DeviceSinkBuilder::open_default_sink().ok()
} else {
None
};
2026-08-13 22:03:47 +03:00
2026-08-24 18:16:41 +03:00
Ok(RepetitionState {
2026-08-13 22:03:47 +03:00
state,
settings: set,
set: card_set,
current_word: word,
current_statistic: stat,
open: false,
can_play: true,
sink: Arc::new(sink_handle),
opened: HashSet::new(),
2026-08-24 18:16:41 +03:00
})
2026-08-13 22:03:47 +03:00
}
}
impl RepetitionState {
2026-04-20 10:13:39 +03:00
fn next(&mut self) -> Task<RootMessage> {
2026-04-14 15:19:46 +03:00
if self.open {
2026-04-20 15:21:31 +03:00
self.answer(WordOpenMode::None)
2026-04-14 15:19:46 +03:00
} else {
self.open = true;
2026-05-05 12:06:17 +03:00
Task::none()
2026-04-14 15:19:46 +03:00
}
}
2026-04-20 10:13:39 +03:00
fn answer(&mut self, mode: WordOpenMode) -> Task<RootMessage> {
2026-04-14 15:19:46 +03:00
if !self.open {
2026-04-20 10:13:39 +03:00
return Task::none();
2026-04-14 15:19:46 +03:00
}
2026-08-24 18:16:41 +03:00
if let Err(err) = self.set.open(mode) {
return push_error_task!(err);
}
2026-04-14 15:19:46 +03:00
self.open = false;
2026-04-20 15:21:31 +03:00
self.opened.insert(self.current_word.id);
2026-05-05 12:02:59 +03:00
let next = self.set.next();
self.current_word = next.0;
self.current_statistic = next.1;
2026-04-20 10:13:39 +03:00
2026-08-20 18:53:13 +03:00
if self.settings.require_speech() && self.sink.is_some() {
let sink = self.sink.clone();
let task = play_sound(sink, self.current_word.key.clone());
2026-08-23 22:05:08 +03:00
return Task::perform(task, |r| match r {
Ok(_) => RootMessage::Repetition(RepetitionMessage::PlayFinished),
2026-08-24 18:16:41 +03:00
Err(err) => push_error!(err),
2026-08-20 18:53:13 +03:00
});
2026-04-20 15:21:31 +03:00
}
2026-08-06 19:29:58 +03:00
self.increment_counter();
2026-04-20 15:21:31 +03:00
Task::none()
2026-04-14 15:19:46 +03:00
}
2026-04-09 22:45:31 +03:00
2026-08-06 19:29:58 +03:00
fn increment_counter(&mut self) {
2026-08-20 00:07:09 +03:00
let mut state = self.state.lock();
2026-08-06 19:29:58 +03:00
let id = self.settings.id;
let activity = state.activity.get_mut(&id);
let now = Local::now().date_naive();
if let Some(activity) = activity {
let (last_day, counter) = activity.last_mut().unwrap();
if *last_day == now {
*counter += 1;
} else {
activity.push((now, 1));
}
} else {
state.activity.insert(id, vec![(now, 1)]);
}
}
2026-08-13 22:03:47 +03:00
2026-04-14 15:19:46 +03:00
fn draw_forward(&self) -> Element<'_, RepetitionMessage> {
2026-05-05 12:02:59 +03:00
self.draw_card_view(self.settings.forward.as_str())
2026-04-14 15:19:46 +03:00
}
fn draw_backward(&self) -> Element<'_, RepetitionMessage> {
if !self.open {
2026-08-06 19:29:58 +03:00
return container(text!("?").size(72))
.width(Fill)
.height(Fill)
.align_y(alignment::Vertical::Center)
.align_x(Center)
.into();
2026-04-14 15:19:46 +03:00
}
2026-05-05 12:02:59 +03:00
self.draw_card_view(self.settings.backward.as_str())
}
fn draw_card_view(&self, properties: &str) -> Element<'_, RepetitionMessage> {
2026-04-14 15:19:46 +03:00
let word = &self.current_word;
2026-05-05 12:02:59 +03:00
let mut col = Column::new();
for view_type in properties.split(" ") {
2026-08-01 17:16:58 +03:00
col = col.push(match view_type {
2026-05-05 12:02:59 +03:00
"key" => self.draw_key(word),
"value" => self.draw_value(word),
"speech" => self.draw_voice(),
"reading" => self.draw_reading(word),
2026-05-25 12:23:32 +03:00
"context" => self.draw_context(word),
"description" => self.draw_description(word),
2026-05-05 12:02:59 +03:00
_ => space().into(),
})
2026-04-14 15:19:46 +03:00
}
2026-05-05 12:02:59 +03:00
2026-05-25 12:23:32 +03:00
col.spacing(DEFAULT_SPACING).align_x(Center).into()
2026-04-14 15:19:46 +03:00
}
fn answer_bar(&self) -> Element<'_, RepetitionMessage> {
if !self.open {
2026-08-01 17:16:58 +03:00
return text!("Нажмите Пробел для открытия карточки")
.style(|theme: &Theme| Style {
color: Some(theme.palette().text.scale_alpha(0.3)),
})
.into();
2026-04-14 15:19:46 +03:00
}
2026-05-05 12:02:59 +03:00
column![
2026-08-01 17:16:58 +03:00
text!(
"{} очков",
self.current_statistic.calculated_score().round() as i32
),
2026-05-05 12:02:59 +03:00
row![
2026-08-01 17:16:58 +03:00
tooltip(
button("Не получилось").on_press(RepetitionMessage::Answer(WordOpenMode::None)),
"Клавиша 1",
tooltip::Position::Top
),
tooltip(
button("Трудно").on_press(RepetitionMessage::Answer(WordOpenMode::Hard)),
"Клавиша 2",
tooltip::Position::Top
),
tooltip(
button("Нормально").on_press(RepetitionMessage::Answer(WordOpenMode::Ok)),
"Клавиша 3",
tooltip::Position::Top
),
tooltip(
button("Легко").on_press(RepetitionMessage::Answer(WordOpenMode::Easy)),
"Клавиша 4",
tooltip::Position::Top
),
2026-05-05 12:02:59 +03:00
]
2026-08-04 14:09:40 +03:00
.spacing(QUARTER_SPACING)
2026-04-14 15:19:46 +03:00
]
2026-05-05 12:02:59 +03:00
.align_x(Center)
2026-04-16 09:56:57 +03:00
.spacing(DEFAULT_SPACING)
2026-04-14 15:19:46 +03:00
.into()
}
2026-05-05 12:02:59 +03:00
fn draw_key(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
2026-08-01 01:22:26 +03:00
text!("{}", word.key).size(36).into()
2026-04-14 15:19:46 +03:00
}
2026-05-05 12:02:59 +03:00
fn draw_value(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
2026-08-01 01:22:26 +03:00
text!("{}", word.value).size(24).into()
2026-04-09 22:45:31 +03:00
}
2026-04-19 22:56:58 +03:00
2026-04-20 10:13:39 +03:00
fn draw_voice(&self) -> Element<'_, RepetitionMessage> {
2026-04-19 22:56:58 +03:00
button("Воспроизвести")
2026-08-01 17:16:58 +03:00
.style(jl_button)
2026-04-19 22:56:58 +03:00
.on_press(RepetitionMessage::Play)
.into()
}
2026-05-05 12:02:59 +03:00
fn draw_reading(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("reading") {
None => space().into(),
2026-08-01 01:22:26 +03:00
Some(reading) => text!("{}", reading).size(24).into(),
2026-05-05 12:02:59 +03:00
}
}
2026-05-25 12:23:32 +03:00
fn draw_context(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("context") {
None => space().into(),
2026-08-01 01:22:26 +03:00
Some(context) => text!("{}", context).size(24).into(),
2026-05-25 12:23:32 +03:00
}
}
fn draw_description(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("description") {
None => space().into(),
Some(context) => text!("{}", context).size(24).into(),
}
}
2026-04-09 22:45:31 +03:00
}
2026-04-14 15:19:46 +03:00
impl KeyPressedPage for RepetitionState {
2026-05-05 12:06:17 +03:00
fn press(&mut self, message: &keyboard::Event) -> Task<RootMessage> {
2026-04-14 15:19:46 +03:00
if let keyboard::Event::KeyPressed {
key: _,
modified_key: _,
physical_key: pk,
location: _,
modifiers: _,
text: _,
repeat: _,
} = message
2026-08-15 13:22:59 +03:00
&& let Code(code) = pk
2026-04-14 15:19:46 +03:00
{
2026-08-15 13:22:59 +03:00
return match code {
keyboard::key::Code::Space => self.next(),
keyboard::key::Code::Digit1 => self.answer(WordOpenMode::None),
keyboard::key::Code::Digit2 => self.answer(WordOpenMode::Hard),
keyboard::key::Code::Digit3 => self.answer(WordOpenMode::Ok),
keyboard::key::Code::Digit4 => self.answer(WordOpenMode::Easy),
_ => Task::none(),
};
2026-04-14 15:19:46 +03:00
}
2026-08-15 13:22:59 +03:00
2026-04-20 10:13:39 +03:00
Task::none()
2026-04-14 15:19:46 +03:00
}
}
#[derive(Clone)]
2026-04-09 22:45:31 +03:00
pub enum RepetitionMessage {
Next,
Back,
2026-04-14 15:19:46 +03:00
Answer(WordOpenMode),
2026-04-19 22:56:58 +03:00
Play,
PlayFinished,
2026-04-09 22:45:31 +03:00
}
2026-04-19 22:56:58 +03:00
2026-08-25 12:04:07 +03:00
async fn play_sound(sink: Arc<Option<MixerDeviceSink>>, text: String) -> MaybeError {
2026-08-23 22:05:08 +03:00
let data = get_voice(text.as_str()).await?;
spawn_blocking(move || {
if let Some(sink) = sink.as_ref()
&& let Ok(play) = rodio::play(sink.mixer(), data)
{
play.sleep_until_end()
}
})
.await
2026-08-24 23:42:58 +03:00
.expect("Playing sound failed");
2026-08-23 22:05:08 +03:00
Ok(())
2026-05-05 12:02:59 +03:00
}