diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 1fdda74..ac31015 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,10 +1,6 @@ -use std::collections::HashMap; -use chrono::{DateTime, Utc}; -use iced::time::now; -use rusqlite::{Connection, ToSql}; -use rusqlite::types::ToSqlOutput; -use crate::lang::{CardSet, CardStatistics, DictionaryElement}; +use crate::lang::CardStatistics; use crate::repetitions::CardSetSettings; +use rusqlite::Connection; pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec { let mut stmt = connection.prepare("SELECT id, word_id, score, last_opened FROM card_stats WHERE set_id = ?1").unwrap(); @@ -12,8 +8,9 @@ pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec< Ok(CardStatistics { id: row.get(0)?, word_id: row.get(1)?, + set_id: set.id, score: row.get(2)?, - last_open: row.get(2)?, + last_open: row.get(3)?, }) }).unwrap(); @@ -25,50 +22,64 @@ pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec< buffer } -pub fn add_set(set: &mut CardSetSettings, connection: &Connection) { +pub fn add_stat(stat: &mut CardStatistics, connection: &Connection) { let index = connection .query_row( - "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", + "INSERT INTO card_stats (word_id, set_id, score, last_opened) VALUES (?1, ?2, ?3, ?4) RETURNING id", ( - &set.name, - &set.forward, - &set.backward, - &set.filter, + &stat.word_id, + &stat.set_id, + &stat.score, + &stat.last_open, ), |row| row.get(0) ) .unwrap_or_else(|e| {println!("{}", e); 0}); - set.id = index; + stat.id = index; } -pub fn update_card_set(set: &mut CardSetSettings, connection: &Connection){ - if set.id == 0 { - add_set(set, &connection); +pub fn update_stat(stat: &mut CardStatistics, connection: &Connection){ + if stat.id == 0 { + add_stat(stat, &connection); } else { connection .execute( - "UPDATE card_set SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5", + "UPDATE card_stats SET word_id = ?1, set_id = ?2, score = ?3, last_opened = ?4 WHERE id = ?5", ( - &set.name, - &set.forward, - &set.backward, - &set.filter, - &set.id + &stat.word_id, + &stat.set_id, + &stat.score, + &stat.last_open, + &stat.id ), ) .unwrap_or_else(|e| {println!("{}", e); 0}); } } + +pub fn update_stat_score(stat: &CardStatistics, connection: &Connection){ + connection + .execute( + "UPDATE card_stats SET score = ?1, last_opened = ?2 WHERE id = ?3", + ( + &stat.score, + &stat.last_open, + &stat.id + ), + ) + .unwrap_or_else(|e| {println!("{}", e); 0}); +} + pub fn delete_set(set: &CardSetSettings, connection: &Connection) { if set.id == 0 { return; } connection - .execute("DELETE FROM card_set WHERE id = ?1", (&set.id,)) + .execute("DELETE FROM card_stats WHERE id = ?1", (&set.id,)) .unwrap_or_else(|e| { println!("{}", e); 0 diff --git a/src/lang.rs b/src/lang.rs index b9034ed..e810ca1 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,10 +1,14 @@ +use crate::data_provider::card_stats::{add_stat, load_stats_of_set, update_stat_score}; use crate::repetitions::CardSetSettings; use crate::AppState; use chrono::{DateTime, Utc}; +use rand::distr::weighted::WeightedIndex; +use rand::distr::Distribution; +use rand::rng; +use rand::rngs::ThreadRng; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use crate::data_provider::card_stats::load_stats_of_set; const MAX_SCORE: u8 = 25_u8; const FADE_PER_DAY: f32 = 0.95; @@ -242,29 +246,30 @@ impl DictionaryElement { } } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct CardStatistics { pub id: u32, pub word_id: u32, + pub set_id: u32, pub last_open: DateTime, pub score: u8, } impl CardStatistics { - pub fn open(&mut self, status: WordOpenMode) { + pub fn update(&mut self, status: WordOpenMode) { self.last_open = Utc::now(); match status { WordOpenMode::Easy => { - self.score += 5; + self.score = self.calculated_score().round() as u8 + 5; } WordOpenMode::Ok => { - self.score += 3; + self.score += self.calculated_score().round() as u8 + 3; } WordOpenMode::Hard => { - self.score -= 1; + self.score = self.calculated_score().round() as u8 - 1; } WordOpenMode::None => { - self.score /= 2; + self.score = (self.calculated_score() * 0.5) as u8; } } @@ -279,10 +284,11 @@ impl CardStatistics { let time = Utc::now() - self.last_open; let days = time.num_days(); let multiplier = FADE_PER_DAY.powi(days as i32); - self.score as f32 * multiplier + (1.0 / self.score as f32) * multiplier } } +#[derive(Clone)] pub enum WordOpenMode { Easy, Ok, @@ -292,22 +298,67 @@ pub enum WordOpenMode { #[derive(Clone)] pub struct CardSet { + words: Vec, set: Vec, - last_weights: Vec, + last_weights: WeightedIndex, + current_word_index: Option, + generator: ThreadRng, + state: Arc> } impl CardSet { - pub fn new(settings: CardSetSettings, state: Arc>) -> Self { + pub fn new(settings: &CardSetSettings, state: Arc>) -> Self { + let state_for = state.clone(); let state_locked = state.lock().unwrap(); - let current_set = load_stats_of_set(&settings, &state_locked.connection); + let mut current_set = load_stats_of_set(&settings, &state_locked.connection); let last_list = settings.get_word_list(&state_locked); + let saved_ids = current_set.iter().map(|l| l.word_id).collect::>(); + for word in &last_list { + if !saved_ids.contains(&word.id) { + let mut new_statistic = CardStatistics { + id: 0, + word_id: word.id.clone(), + last_open: Utc::now(), + score: 1, + set_id: settings.id.clone(), + }; - let weights = current_set.iter().map(|x| x.calculated_score()).collect(); + add_stat(&mut new_statistic, &state_locked.connection); + + current_set.push(new_statistic); + } + } + + let indexes = WeightedIndex::new(current_set.iter().map(|s| 1.0 / s.score as f32)).unwrap(); Self { set: current_set, - last_weights: weights, + words: last_list, + last_weights: indexes, + current_word_index: None, + generator: rng(), + state: state_for + } + } + + pub fn next(&mut self) -> DictionaryElement { + let index = self.last_weights.sample(&mut self.generator); + self.current_word_index = Some(index); + self.words[index].clone() + } + + pub fn open(&mut self, status: WordOpenMode) { + if let None = self.current_word_index { + return; + } + + let word = &mut self.set[self.current_word_index.unwrap()]; + word.update(status); + let new_weight = word.calculated_score(); + self.last_weights.update_weights(&[(self.current_word_index.unwrap(), &new_weight)]).unwrap(); + { + update_stat_score(word, &self.state.lock().unwrap().connection) } } } diff --git a/src/main.rs b/src/main.rs index 2167bda..80d3d5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,13 +24,15 @@ use crate::Page::{ Dictionary, DictionaryQuiz, Quiz, Randomizer, Repetition, Repetitions, Selector, Writing, }; use iced::widget::text; -use iced::Element; +use iced::{keyboard, Element, Program, Subscription}; use iced::{Font, Task}; use rusqlite::Connection; use std::sync::{Arc, Mutex}; +use iced::keyboard::Event; +use crate::RootMessage::Keyboard; fn main() -> iced::Result { - iced::application(ScreenState::boot, ScreenState::update, ScreenState::view) + iced::application(ScreenState::boot, ScreenState::update, ScreenState::view).subscription(subscription) .title("Kana learn app") .font(include_bytes!("../noto.ttf")) .default_font(Font::with_name("Noto Sans JP")) @@ -38,7 +40,11 @@ fn main() -> iced::Result { run() } -#[derive(Debug, Clone)] +fn subscription(_state: &ScreenState) -> Subscription { + keyboard::listen().map(|e| Keyboard(e)) +} + +#[derive(Clone)] pub enum RootMessage { Selector(SelectorMessage), Quiz(QuizMessage), @@ -48,6 +54,7 @@ pub enum RootMessage { Randomizer(RandomizerMessage), Repetitions(RepetitionsMessage), Repetition(RepetitionMessage), + Keyboard(keyboard::Event), } enum Page { @@ -93,6 +100,10 @@ impl ScreenState { (ScreenState::default(), Task::none()) } pub fn update(&mut self, message: RootMessage) -> Task { + if let Keyboard(e) = message { + return self.handle_keyboard(e); + } + state_update!( message, self.stack, @@ -107,7 +118,6 @@ impl ScreenState { ); Task::none() } - pub fn view(&self) -> Element<'_, RootMessage> { view_navigation!( self.stack, @@ -121,8 +131,19 @@ impl ScreenState { Repetition ) } + + fn handle_keyboard(&mut self, message: Event) -> Task { + let page = self.stack.last_mut().unwrap(); + match page { + Repetition(page) => page.press(&message), + _ => {} + } + Task::none() + } } + + #[macro_export] macro_rules! view_navigation { ($stack:expr, $($e:ident), *) => { @@ -146,6 +167,7 @@ macro_rules! state_update { } } )* + _ => {} } } } @@ -168,3 +190,7 @@ macro_rules! message_navigation { trait NavigatedPage { fn navigate(&self, message: &T) -> Option; } + +pub trait KeyPressedPage { + fn press(&mut self, message: &keyboard::Event); +} \ No newline at end of file diff --git a/src/repetition.rs b/src/repetition.rs index 7f8c394..684232f 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -1,15 +1,22 @@ -use std::sync::{Arc, Mutex}; -use crate::Page::PreviousPage; -use crate::{AppState, NavigatedPage, Page, RootMessage}; -use iced::widget::{button, container}; -use iced::{Element, Fill, Left, Task}; -use crate::lang::CardSet; +use crate::lang::{CardSet, DictionaryElement, WordOpenMode}; use crate::repetitions::CardSetSettings; +use crate::Page::PreviousPage; +use crate::{AppState, KeyPressedPage, NavigatedPage, Page, RootMessage}; +use iced::alignment::Horizontal::Center; +use iced::keyboard::key::Physical::Code; +use iced::widget::container::rounded_box; +use iced::widget::space::vertical; +use iced::widget::{button, column, container, row, space, text}; +use iced::{alignment, keyboard, Element, Fill, Left, Task}; +use rusqlite::fallible_iterator::FallibleIterator; +use std::sync::{Arc, Mutex}; -#[derive(Clone)] pub struct RepetitionState { - pub set: CardSetSettings, + pub settings: CardSetSettings, + pub set: CardSet, pub state: Arc>, + current_word: DictionaryElement, + open: bool, } impl NavigatedPage for RepetitionState { @@ -24,34 +31,155 @@ impl NavigatedPage for RepetitionState { impl RepetitionState { pub(crate) fn new(set: CardSetSettings, state: Arc>) -> RepetitionState { + let mut card_set = CardSet::new(&set, state.clone()); + let word = card_set.next(); RepetitionState { - set, - state + settings: set, + set: card_set, + state, + current_word: word, + open: false, } } } impl RepetitionState { - pub fn update(&mut self, message: RepetitionMessage) -> Task { + pub fn update(&mut self, message: RepetitionMessage) -> Task { + match message { + RepetitionMessage::Back => {} + RepetitionMessage::Next => {self.next()} + RepetitionMessage::Answer(m) => {self.answer(m)} + } + Task::none() } + fn next(&mut self) { + if self.open { + self.set.open(WordOpenMode::None); + self.current_word = self.set.next(); + return; + } else { + self.open = true; + } + } + + fn answer(&mut self, mode: WordOpenMode) { + if !self.open { + return; + } + + self.set.open(mode); + self.open = false; + self.current_word = self.set.next(); + } pub fn view(&self) -> Element<'_, RepetitionMessage> { container( iced::widget::column![ button("Назад").on_press(RepetitionMessage::Back), - - ].align_x(Left).width(Fill) + column![ + container(self.draw_forward()) + .width(Fill) + .height(Fill) + .align_x(Center) + .align_y(alignment::Vertical::Center), + container(vertical().height(5)).width(Fill).padding(5).style(rounded_box), + 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(30) + ] + .height(Fill) + .width(Fill) + ] + .align_x(Left) + .width(Fill), ) - .center_x(Fill) - .padding(10) - .into() + .center_x(Fill) + .padding(10) + .into() + } + + fn draw_forward(&self) -> Element<'_, RepetitionMessage> { + let word = &self.current_word; + match self.settings.forward.as_str() { + "key" => self.draw_key(word), + "value" => self.draw_value(word), + _ => space().into(), + } + + } + + fn draw_backward(&self) -> Element<'_, RepetitionMessage> { + if !self.open { + return space().into(); + } + + let word = &self.current_word; + match self.settings.backward.as_str() { + "key" => self.draw_key(word), + "value" => self.draw_value(word), + _ => space().into(), + } + } + + fn answer_bar(&self) -> Element<'_, RepetitionMessage> { + if !self.open { + return space().into(); + } + + row![ + 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(10) + .into() + } + + fn draw_key(&self, word: &DictionaryElement) -> Element<'_, RepetitionMessage> { + text!("{}", word.key).size(36).into() + } + fn draw_value(&self, word: &DictionaryElement) -> Element<'_, RepetitionMessage> { + text!("{}", word.value).size(24).into() } } -#[derive(Debug, Clone)] + +impl KeyPressedPage for RepetitionState { + fn press(&mut self, message: &keyboard::Event) { + if let keyboard::Event::KeyPressed { + key: _, + modified_key: _, + physical_key: pk, + location: _, + modifiers: _, + text: _, + repeat: _, + } = message + { + if let Code(code) = pk { + 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), + _ => {} + } + } + } + } +} + +#[derive(Clone)] pub enum RepetitionMessage { Next, Back, - SwitchShowMode(bool), + Answer(WordOpenMode), } diff --git a/src/repetitions.rs b/src/repetitions.rs index 8f39ef1..950e129 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -24,7 +24,11 @@ impl NavigatedPage for RepetitionsState { } else if let RepetitionsMessage::GoToRepetition = message { let clone = self.state.clone(); - Some(Repetition(RepetitionState::new(self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].clone(), clone) )) + let card_set; + { + card_set = self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].clone(); + } + Some(Repetition(RepetitionState::new(card_set, clone) )) } else { None } @@ -212,6 +216,7 @@ impl RepetitionsState { column } } + #[derive(Debug, Clone)] pub enum RepetitionsMessage { Next,