2026-04-19 22:56:58 +03:00
|
|
|
use crate::data_provider::voice::get_voice;
|
2026-05-05 12:02:59 +03:00
|
|
|
use crate::lang::{CardSet, CardStatistics, WordData, WordOpenMode};
|
2026-04-13 22:25:58 +03:00
|
|
|
use crate::repetitions::CardSetSettings;
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::{AppState, RootMessage};
|
2026-04-14 15:19:46 +03:00
|
|
|
use iced::alignment::Horizontal::Center;
|
|
|
|
|
use iced::keyboard::key::Physical::Code;
|
2026-05-05 12:02:59 +03:00
|
|
|
use iced::widget::{button, column, container, row, rule, space, text, Column};
|
2026-08-01 13:04:18 +03:00
|
|
|
use iced::{alignment, keyboard, Element, Fill, Left, Task, Theme};
|
2026-04-19 22:56:58 +03:00
|
|
|
use rodio::MixerDeviceSink;
|
2026-05-05 12:02:59 +03:00
|
|
|
use std::collections::HashSet;
|
2026-04-14 15:19:46 +03:00
|
|
|
use std::sync::{Arc, Mutex};
|
2026-08-01 13:04:18 +03:00
|
|
|
use iced::widget::text::Style;
|
2026-04-19 22:56:58 +03:00
|
|
|
use tokio::task::spawn_blocking;
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::navigation::{KeyPressedPage, NavigatedPage, Page};
|
|
|
|
|
use crate::navigation::Page::PreviousPage;
|
|
|
|
|
use crate::styling::*;
|
2026-04-09 22:45:31 +03:00
|
|
|
|
|
|
|
|
pub struct RepetitionState {
|
2026-04-14 15:19:46 +03:00
|
|
|
pub settings: CardSetSettings,
|
|
|
|
|
pub set: CardSet,
|
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,
|
|
|
|
|
sink: Arc<MixerDeviceSink>,
|
2026-04-20 15:21:31 +03:00
|
|
|
opened: HashSet<u32>,
|
2026-04-09 22:45:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl NavigatedPage<RepetitionMessage> for RepetitionState {
|
|
|
|
|
fn navigate(&self, message: &RepetitionMessage) -> Option<Page> {
|
|
|
|
|
if let RepetitionMessage::Back = message {
|
|
|
|
|
Some(PreviousPage)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RepetitionState {
|
2026-04-13 22:25:58 +03:00
|
|
|
pub(crate) fn new(set: CardSetSettings, state: Arc<Mutex<AppState>>) -> RepetitionState {
|
2026-04-14 15:19:46 +03:00
|
|
|
let mut card_set = CardSet::new(&set, state.clone());
|
2026-05-05 12:02:59 +03:00
|
|
|
let (word, stat) = card_set.next();
|
2026-04-19 22:56:58 +03:00
|
|
|
let sink_handle = rodio::DeviceSinkBuilder::open_default_sink().unwrap();
|
|
|
|
|
|
2026-04-09 22:45:31 +03:00
|
|
|
RepetitionState {
|
2026-04-14 15:19:46 +03:00
|
|
|
settings: set,
|
|
|
|
|
set: card_set,
|
|
|
|
|
current_word: word,
|
2026-05-05 12:02:59 +03:00
|
|
|
current_statistic: stat,
|
2026-04-14 15:19:46 +03:00
|
|
|
open: false,
|
2026-04-19 22:56:58 +03:00
|
|
|
can_play: true,
|
2026-04-20 15:21:31 +03:00
|
|
|
sink: Arc::new(sink_handle),
|
|
|
|
|
opened: HashSet::new(),
|
2026-04-09 22:45:31 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RepetitionState {
|
2026-04-14 15:19:46 +03:00
|
|
|
pub fn update(&mut self, message: RepetitionMessage) -> Task<RootMessage> {
|
|
|
|
|
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-05-05 12:02:59 +03:00
|
|
|
return Task::perform(play_sound(self.sink.clone(), value), |_| {
|
|
|
|
|
RootMessage::Repetition(RepetitionMessage::PlayFinished)
|
|
|
|
|
});
|
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-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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.set.open(mode);
|
|
|
|
|
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-04-20 15:21:31 +03:00
|
|
|
if self.settings.require_speech() {
|
2026-05-05 12:02:59 +03:00
|
|
|
return Task::perform(
|
|
|
|
|
play_sound(self.sink.clone(), self.current_word.key.clone()),
|
|
|
|
|
|_| RootMessage::Repetition(RepetitionMessage::PlayFinished),
|
|
|
|
|
);
|
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
|
|
|
|
|
|
|
|
pub fn view(&self) -> Element<'_, RepetitionMessage> {
|
|
|
|
|
container(
|
|
|
|
|
iced::widget::column![
|
|
|
|
|
button("Назад").on_press(RepetitionMessage::Back),
|
2026-04-14 15:19:46 +03:00
|
|
|
column![
|
|
|
|
|
container(self.draw_forward())
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.align_x(Center)
|
|
|
|
|
.align_y(alignment::Vertical::Center),
|
2026-04-19 20:22:35 +03:00
|
|
|
rule::horizontal(2),
|
2026-04-14 15:19:46 +03:00
|
|
|
container(self.draw_backward())
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.align_x(Center)
|
|
|
|
|
.align_y(alignment::Vertical::Center),
|
2026-04-19 20:22:35 +03:00
|
|
|
container(self.answer_bar())
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.align_x(Center)
|
2026-08-01 13:04:18 +03:00
|
|
|
.height(80),
|
2026-05-05 12:02:59 +03:00
|
|
|
text!(
|
|
|
|
|
"Затронуто слов {}, {}%",
|
|
|
|
|
self.opened.len(),
|
|
|
|
|
(self.opened.len() as f32 / self.set.len() as f32 * 10000.0).round()
|
|
|
|
|
/ 100.0
|
|
|
|
|
)
|
2026-04-14 15:19:46 +03:00
|
|
|
]
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.width(Fill)
|
|
|
|
|
]
|
|
|
|
|
.align_x(Left)
|
|
|
|
|
.width(Fill),
|
2026-04-09 22:45:31 +03:00
|
|
|
)
|
2026-04-14 15:19:46 +03:00
|
|
|
.center_x(Fill)
|
|
|
|
|
.padding(10)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
return space().into();
|
|
|
|
|
}
|
|
|
|
|
|
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(" ") {
|
|
|
|
|
col = col.push( match view_type {
|
|
|
|
|
"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),
|
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 13:04:18 +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![
|
|
|
|
|
text!("{} очков", self.current_statistic.calculated_score().round() as i32),
|
|
|
|
|
row![
|
2026-08-01 13:04:18 +03:00
|
|
|
|
2026-05-05 12:02:59 +03:00
|
|
|
button("Не получилось").on_press(RepetitionMessage::Answer(WordOpenMode::None)),
|
|
|
|
|
button("Трудно").on_press(RepetitionMessage::Answer(WordOpenMode::Hard)),
|
|
|
|
|
button("Нормально").on_press(RepetitionMessage::Answer(WordOpenMode::Ok)),
|
|
|
|
|
button("Легко").on_press(RepetitionMessage::Answer(WordOpenMode::Easy)),
|
|
|
|
|
]
|
|
|
|
|
.spacing(DEFAULT_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("Воспроизвести")
|
|
|
|
|
.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
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
|
|
{
|
|
|
|
|
if let Code(code) = pk {
|
2026-04-20 10:13:39 +03:00
|
|
|
return match code {
|
2026-04-14 15:19:46 +03:00
|
|
|
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),
|
2026-04-20 10:13:39 +03:00
|
|
|
_ => Task::none(),
|
2026-05-05 12:02:59 +03:00
|
|
|
};
|
2026-04-14 15:19:46 +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
|
|
|
|
|
|
|
|
async fn play_sound(sink: Arc<MixerDeviceSink>, text: String) {
|
|
|
|
|
let data = get_voice(text.as_str()).await;
|
|
|
|
|
spawn_blocking(move || {
|
|
|
|
|
rodio::play(&sink.mixer(), data).unwrap().sleep_until_end();
|
2026-05-05 12:02:59 +03:00
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|