diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 2b2791b..13aa4db 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,5 +1,6 @@ use crate::repetitions::CardSetSettings; use rusqlite::Connection; +use crate::lang::SetOrderMode; pub fn load_sets(connection: &Connection) -> Vec { let mut stmt = connection.prepare("SELECT id, name, forward, backward, filter FROM card_set").unwrap(); @@ -10,7 +11,9 @@ pub fn load_sets(connection: &Connection) -> Vec { forward: row.get(2)?, backward: row.get(3)?, filter: row.get(4)?, - count: None + count: None, + worst_words_list: None, + open_mode: SetOrderMode::Default, }) }).unwrap(); diff --git a/src/dictionary.rs b/src/dictionary.rs index 341ca2c..196d10b 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -35,6 +35,7 @@ pub struct DictionaryState { selected_group_index: usize, reverse_list: bool, auto_save_queue: HashMap>, + total_tags_list: Vec, } #[derive(Debug, Clone)] @@ -116,6 +117,7 @@ impl DictionaryState { no_typing: true, reverse_list: true, auto_save_queue: HashMap::new(), + total_tags_list: vec![], }; result.update_tags(); @@ -149,9 +151,22 @@ impl DictionaryState { } return self.launch_auto_save_offset(i); } - DictionaryMessage::SetTags(i, v) => { + DictionaryMessage::SetTags(i, mut v) => { { let dict = &mut self.state.lock().unwrap().dictionary; + + let current_tags_value = dict[i].tags.clone(); + + if v.ends_with(", ") && v.len() < current_tags_value.len() { + v = v[..v.len() - 2].to_string() + } + + + while v.contains(",,") { + let index = v.find(",,").unwrap(); + v.remove(index); + } + dict.get_mut(i).unwrap().tags = v; } @@ -289,10 +304,10 @@ impl DictionaryState { .spacing(5), self.filters() ] - .spacing(DEFAULT_SPACING), + .spacing(DEFAULT_SPACING), ) - .padding(10) - .into() + .padding(10) + .into() } fn words_list(&self) -> iced::Element<'_, DictionaryMessage> { @@ -402,9 +417,9 @@ impl DictionaryState { .on_press(Test) .width(Length::Fill), ] - .width(250) - .spacing(DEFAULT_SPACING) - .into() + .width(250) + .spacing(DEFAULT_SPACING) + .into() } fn tags_selector(&self) -> iced::Element<'_, DictionaryMessage> { @@ -445,6 +460,7 @@ impl DictionaryState { }); }); + let current_tags = self .tag_map .keys() @@ -479,14 +495,15 @@ impl DictionaryState { let dict = &self.state.lock().unwrap().dictionary; let time = Instant::now(); - self.include_map = dict.iter() + self.include_map = dict + .iter() .map(|word| (split_with_coma(word.tags.as_str()), word.group_id)) .map(|(tags, word_group_id)| { tags.iter().all(|t| include_tags.contains(t)) && word_group_id == group_id - }).collect(); + }) + .collect(); println!("Time {}", time.elapsed().as_micros()); - } fn groups_panel(&self) -> iced::Element<'_, DictionaryMessage> { @@ -525,7 +542,7 @@ impl DictionaryState { ] .spacing(DEFAULT_SPACING) ] - .into() + .into() } } diff --git a/src/lang.rs b/src/lang.rs index 7ecea0c..a17489c 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -6,6 +6,7 @@ use crate::AppState; use chrono::{DateTime, Utc}; use rand::distr::weighted::WeightedIndex; use rand::distr::Distribution; +use rand::prelude::SliceRandom; use rand::rng; use rand::rngs::ThreadRng; use serde::{Deserialize, Serialize}; @@ -299,7 +300,7 @@ impl CardStatistics { } } -#[derive(Clone)] +#[derive(Clone, Copy)] pub enum WordOpenMode { Easy, Ok, @@ -311,11 +312,9 @@ pub enum WordOpenMode { pub struct CardSet { words: Vec, set: Vec, - last_weights: WeightedIndex, current_word_index: Option, - generator: ThreadRng, state: Arc>, - history: Vec, + order_module: OrderModule, } impl CardSet { @@ -331,14 +330,12 @@ impl CardSet { last_list .iter() .filter(|word| !saved_ids.contains(&word.id)) - .map(|word| { - CardStatistics { - id: 0, - word_id: word.id.clone(), - last_open: Utc::now(), - score: 1, - set_id: settings.id.clone(), - } + .map(|word| CardStatistics { + id: 0, + word_id: word.id.clone(), + last_open: Utc::now(), + score: 1, + set_id: settings.id.clone(), }) .for_each(|mut new_statistic| { add_stat(&mut new_statistic, &state_locked.connection); @@ -355,34 +352,49 @@ impl CardSet { } } - let weights = current_set - .iter() - .map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0) - .collect::>(); - let indexes = WeightedIndex::new(weights).unwrap(); - Self { set: current_set, words: last_list, - last_weights: indexes, current_word_index: None, - generator: rng(), state: state_for, - history: vec![], + order_module: match settings.open_mode { + SetOrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()), + SetOrderMode::TrainWorstFirst => { + OrderModule::WorstWordsSRS(WorstWordsSRSModule::new()) + } + SetOrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), + }, } } pub fn next(&mut self) -> (WordData, CardStatistics) { - let index = self.last_weights.sample(&mut self.generator); + let index = match self.order_module.clone() { + OrderModule::SemiRandomSRS(mut module) => { + if module.initializated == false { + module.init(self) + } + let index = module.next(self); + self.order_module = OrderModule::SemiRandomSRS(module); + index + } + OrderModule::RandomSRS(mut module) => { + if module.initializated == false { + module.init(self) + } + let index = module.next(self); + self.order_module = OrderModule::RandomSRS(module); + index + } + OrderModule::WorstWordsSRS(mut module) => { + if module.initializated == false { + module.init(self) + } + let index = module.next(self); + self.order_module = OrderModule::WorstWordsSRS(module); + index + } + }; - if self.history.contains(&index) { - return self.next(); - } - - if self.history.len() == self.history_len() { - self.history.remove(0); - } - self.history.push(index); self.current_word_index = Some(index); (self.words[index].clone(), self.set[index].clone()) } @@ -391,24 +403,172 @@ impl CardSet { if let None = self.current_word_index { return; } + let index = self.current_word_index.unwrap(); - let word = &mut self.set[self.current_word_index.unwrap()]; + let word = &mut self.set[index]; word.update(status); - let new_weight = (100.0 / word.calculated_score()).powf(2.0); - self.last_weights - .update_weights(&[(self.current_word_index.unwrap(), &new_weight)]) - .unwrap(); - { update_stat_score(word, &self.state.lock().unwrap().connection) } - } - fn history_len(&self) -> usize { - min( - MAX_HISTORY_LEN, - (self.set.len() as f32 * MAX_HISTORY_LEN_PART) as usize, - ) + match self.order_module.clone() { + OrderModule::SemiRandomSRS(mut module) => { + module.open(status, index, word.clone()); + self.order_module = OrderModule::SemiRandomSRS(module); + } + OrderModule::RandomSRS(mut module) => { + module.open(status, index, word.clone()); + self.order_module = OrderModule::RandomSRS(module); + } + OrderModule::WorstWordsSRS(mut module) => { + module.open(status, index, word.clone()); + self.order_module = OrderModule::WorstWordsSRS(module); + } + } + update_stat_score(word, &self.state.lock().unwrap().connection) } pub fn len(&self) -> usize { self.set.len() } } + +#[derive(Clone, PartialEq, Copy, Eq)] +pub(crate) enum SetOrderMode { + Default, + TrainWorstFirst, + FullRandom, +} + +#[derive(Clone)] +enum OrderModule { + SemiRandomSRS(SemiRandomSRSModule), + RandomSRS(RandomSRSModule), + WorstWordsSRS(WorstWordsSRSModule), +} + +trait SRSModule { + fn next(&mut self, set: &mut CardSet) -> usize; + fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics); + fn init(&mut self, set: &mut CardSet); +} + +#[derive(Clone)] +struct RandomSRSModule { + backet: Vec, + initializated: bool, +} + +impl RandomSRSModule { + fn new() -> RandomSRSModule { + Self{ + backet: vec![], + initializated: false, + } + } +} + +impl SRSModule for RandomSRSModule { + fn next(&mut self, set: &mut CardSet) -> usize { + if self.backet.is_empty() { + self.backet = (0..set.words.len()).collect::>(); + self.backet.shuffle(&mut rand::rng()) + } + + self.backet.pop().unwrap() + } + + fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {} + + fn init(&mut self, set: &mut CardSet) { + self.initializated = true; + self.backet = (0..set.words.len()).collect::>(); + self.backet.shuffle(&mut rand::rng()) + } +} + +#[derive(Clone)] +struct SemiRandomSRSModule { + history: Vec, + last_weights: WeightedIndex, + generator: ThreadRng, + initializated: bool, +} + +impl SemiRandomSRSModule { + fn new() -> SemiRandomSRSModule { + SemiRandomSRSModule { + history: vec![], + last_weights: WeightedIndex::new([1.0]).unwrap(), + generator: rng(), + initializated: false, + } + } +} + +impl SRSModule for SemiRandomSRSModule { + fn next(&mut self, set: &mut CardSet) -> usize { + let index = self.last_weights.sample(&mut self.generator); + + if self.history.contains(&index) { + return self.next(set); + } + + if self.history.len() == self.history_len(set) { + self.history.remove(0); + } + self.history.push(index); + + index + } + + fn open(&mut self, status: WordOpenMode, index: usize, word: CardStatistics) { + let new_weight = (100.0 / word.calculated_score()).powf(2.0); + self.last_weights + .update_weights(&[(index, &new_weight)]) + .unwrap(); + } + + fn init(&mut self, set: &mut CardSet) { + self.initializated = true; + let weights = set + .set + .iter() + .map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0) + .collect::>(); + self.last_weights = WeightedIndex::new(weights).unwrap(); + } +} + +impl SemiRandomSRSModule { + fn history_len(&self, set: &CardSet) -> usize { + min( + MAX_HISTORY_LEN, + (set.len() as f32 * MAX_HISTORY_LEN_PART) as usize, + ) + } +} + +#[derive(Clone)] +struct WorstWordsSRSModule { + initializated: bool, +} + +impl SRSModule for WorstWordsSRSModule { + fn next(&mut self, set: &mut CardSet) -> usize { + todo!() + } + + fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics) { + todo!() + } + + fn init(&mut self, set: &mut CardSet) { + todo!() + } +} + +impl WorstWordsSRSModule { + fn new() -> WorstWordsSRSModule { + WorstWordsSRSModule { + initializated: false, + } + } +} diff --git a/src/repetition.rs b/src/repetition.rs index 8ee5e9f..5f78643 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -172,11 +172,12 @@ impl RepetitionState { "value" => self.draw_value(word), "speech" => self.draw_voice(), "reading" => self.draw_reading(word), + "context" => self.draw_context(word), _ => space().into(), }) } - col.spacing(DEFAULT_SPACING).into() + col.spacing(DEFAULT_SPACING).align_x(Center).into() } fn answer_bar(&self) -> Element<'_, RepetitionMessage> { @@ -218,6 +219,12 @@ impl RepetitionState { Some(reading) => text!("{}", reading).size(24).into(), } } + fn draw_context(&self, word: &WordData) -> Element<'_, RepetitionMessage> { + match word.additional.get("context") { + None => space().into(), + Some(context) => text!("{}", context).size(24).into(), + } + } } impl KeyPressedPage for RepetitionState { diff --git a/src/repetitions.rs b/src/repetitions.rs index 9615c8c..ce39855 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,11 +1,13 @@ use crate::data_provider::card_sets::{delete_set, update_card_set}; -use crate::lang::WordData; +use crate::data_provider::card_stats::load_stats_of_set; +use crate::lang::{SetOrderMode, WordData}; use crate::repetition::RepetitionState; use crate::Page::{PreviousPage, Repetition}; use crate::{AppState, NavigatedPage, Page, RootMessage, DEFAULT_SPACING}; use iced::widget::button::danger; pub use iced::widget::button::{Catalog, Style}; -use iced::widget::{button, column, container, row, scrollable, space, text, text_input, Column}; +use iced::widget::container::bordered_box; +use iced::widget::{button, column, container, radio, row, scrollable, space, text, text_input, Column}; use iced::{Border, Center, Element, Fill, Left, Length, Shadow, Task, Theme}; use rhai::{Engine, Scope}; use std::sync::{Arc, Mutex}; @@ -14,6 +16,7 @@ use std::sync::{Arc, Mutex}; pub struct RepetitionsState { selected_set: Option, correct_filters: Vec, + pub state: Arc>, } @@ -21,14 +24,13 @@ impl NavigatedPage for RepetitionsState { fn navigate(&self, message: &RepetitionsMessage) -> Option { if let RepetitionsMessage::Back = message { Some(PreviousPage) - } - else if let RepetitionsMessage::GoToRepetition = message { + } else if let RepetitionsMessage::GoToRepetition = message { let clone = self.state.clone(); let card_set; { card_set = self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].clone(); } - Some(Repetition(RepetitionState::new(card_set, clone) )) + Some(Repetition(RepetitionState::new(card_set, clone))) } else { None } @@ -68,6 +70,9 @@ impl RepetitionsState { } RepetitionsMessage::SelectSet(index) => { self.selected_set = Some(index); + let mut set = state.card_sets.get(index).unwrap().clone(); + set.update_worst_words(&state); + state.card_sets[index] = set; } RepetitionsMessage::SetName(new) => { state.card_sets[self.selected_set.unwrap()].name = new; @@ -96,6 +101,9 @@ impl RepetitionsState { let set = state.card_sets.get(self.selected_set.unwrap()).unwrap(); let count = set.get_word_list(&state).len(); state.card_sets[self.selected_set.unwrap()].count = Some(count); + }, + RepetitionsMessage::SetOpenMode(mode) => { + state.card_sets.get_mut(self.selected_set.unwrap()).unwrap().open_mode = mode; } } Task::none() @@ -143,21 +151,25 @@ impl RepetitionsState { fn selected_set_view(&self) -> Element<'_, RepetitionsMessage> { if let Some(index) = self.selected_set { - let sets = &self.state.lock().unwrap().card_sets; - + let set; + { + set = self.state.lock().unwrap().card_sets[index].clone(); + } return column![ scrollable( column![ - text_input("Название набора", &sets[index].name) + text_input("Название набора", &set.name) .on_input(RepetitionsMessage::SetName), - text_input("Передняя сторона", &sets[index].forward) + text_input("Передняя сторона", &set.forward) .on_input(RepetitionsMessage::SetForward), - text_input("Задняя сторона", &sets[index].backward) + text_input("Задняя сторона", &set.backward) .on_input(RepetitionsMessage::SetBackward), text!("Фильтр"), - text_input("", &sets[index].filter).on_input(RepetitionsMessage::SetFilter), + text_input("", &set.filter).on_input(RepetitionsMessage::SetFilter), button("Проверить фильтр").on_press(RepetitionsMessage::TryFilter), - self.count_view(&sets[index]) + self.count_view(&set), + radio("Обычный режим", SetOrderMode::Default, Some(set.open_mode), RepetitionsMessage::SetOpenMode), + self.words_words_view(&set) ] .spacing(DEFAULT_SPACING) ) @@ -177,6 +189,28 @@ impl RepetitionsState { space().width(Length::FillPortion(2)).into() } + fn words_words_view(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { + column![ + text!("Худшие слова"), + container(scrollable(self.worst_words_list(&set)).height(200)).style(bordered_box), + radio("Начать с плохих слов", SetOrderMode::TrainWorstFirst, Some(set.open_mode), RepetitionsMessage::SetOpenMode), + radio("Полностью случайно", SetOrderMode::FullRandom, Some(set.open_mode), RepetitionsMessage::SetOpenMode) + ] + .spacing(DEFAULT_SPACING) + .into() + } + + fn worst_words_list(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { + + + let mut column = Column::new(); + + for word in set.worst_words_list.clone().unwrap() { + column = column.push(text!("{} | {}", &word.key, &word.value)); + } + column.into() + } + fn count_view(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { if let Some(count) = set.count { return text!("Колличество слов: {}", count).into(); @@ -209,9 +243,10 @@ impl RepetitionsState { column } + } -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum RepetitionsMessage { Next, Back, @@ -225,9 +260,10 @@ pub enum RepetitionsMessage { SetBackward(String), SetFilter(String), TryFilter, + SetOpenMode(SetOrderMode) } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct CardSetSettings { pub id: u32, pub name: String, @@ -235,6 +271,8 @@ pub struct CardSetSettings { pub backward: String, pub filter: String, pub count: Option, + pub worst_words_list: Option>, + pub open_mode: SetOrderMode } impl CardSetSettings { @@ -246,6 +284,8 @@ impl CardSetSettings { backward: "".to_string(), filter: "true".to_string(), count: None, + worst_words_list: None, + open_mode: SetOrderMode::Default, } } @@ -278,7 +318,15 @@ impl CardSetSettings { .push_constant("value", word.value.clone()) .push_constant("tags", word.tags.clone()) .push_constant("more", more) - .push_constant("group", groups.iter().find(|g| g.id == word.group_id).cloned().unwrap().name); + .push_constant( + "group", + groups + .iter() + .find(|g| g.id == word.group_id) + .cloned() + .unwrap() + .name, + ); let result = engine.eval_ast_with_scope::(&mut scope, &ast); if result.is_ok() && result.unwrap() { @@ -290,6 +338,32 @@ impl CardSetSettings { } pub fn require_speech(&self) -> bool { - self.forward == "speech" || self.backward == "speech" + self.forward == "speech" || self.backward == "speech" } + + fn update_worst_words(&mut self, state: &AppState){ + if let Some(_) = self.worst_words_list { + return; + } + + let connection = &state.connection; + let mut stats = load_stats_of_set(self, connection); + stats.sort_by_key(|s| s.calculated_score() as i32); + let avg = stats.iter().map(|s| s.calculated_score()).sum::() / stats.len() as f32; + let avg = avg * 0.7; + let bad: Vec = stats + .iter() + .take_while(|word| word.calculated_score() < avg) + .map(|stat| { + state.dictionary[ state + .dictionary + .binary_search_by_key(&stat.word_id, |x| x.id) + .unwrap()].clone() + }) + .collect(); + + self.worst_words_list = Some(bad.clone()); + } + + } diff --git a/src/word.rs b/src/word.rs index 91ffcf3..08fea52 100644 --- a/src/word.rs +++ b/src/word.rs @@ -25,11 +25,7 @@ impl NavigatedPage for WordState { } impl WordState { - pub(crate) fn new( - word: WordData, - index: usize, - state: Arc>, - ) -> WordState { + pub(crate) fn new(word: WordData, index: usize, state: Arc>) -> WordState { WordState { state, index, word } } } @@ -42,7 +38,7 @@ impl WordState { let mut state = self.state.lock().unwrap(); state.dictionary[self.index] = self.word.clone(); update_word(&mut self.word, &state.connection); - return Task::done(RootMessage::Word(WordMessage::Back)) + return Task::done(RootMessage::Word(WordMessage::Back)); } WordMessage::Delete => { let mut state = self.state.lock().unwrap(); @@ -57,15 +53,14 @@ impl WordState { WordMessage::SetValue(n) => { self.word.value = n; } - WordMessage::SetAdditional(key, value) => { - match key.as_str() { - _ => { self.word.additional.insert(key, value.clone());} - + WordMessage::SetAdditional(key, value) => match key.as_str() { + _ => { + self.word.additional.insert(key, value.clone()); } }, WordMessage::AddAdditional(key) => { self.word.additional.insert(key, "".to_string()); - }, + } } Task::none() } @@ -73,11 +68,28 @@ impl WordState { pub fn view(&self) -> Element<'_, WordMessage> { let mut fast_add = row![]; if !self.word.additional.contains_key("reading") { - fast_add = fast_add.push(button("Чтение").style(button::text).on_press(WordMessage::AddAdditional("reading".to_string()))); + fast_add = fast_add.push( + button("Чтение") + .style(button::text) + .on_press(WordMessage::AddAdditional("reading".to_string())), + ); } if !self.word.additional.contains_key("description") { - fast_add = fast_add.push(button("Описание").style(button::text).on_press(WordMessage::AddAdditional("description".to_string()))); + fast_add = fast_add.push( + button("Описание") + .style(button::text) + .on_press(WordMessage::AddAdditional("description".to_string())), + ); } + + if !self.word.additional.contains_key("context") { + fast_add = fast_add.push( + button("В контексте") + .style(button::text) + .on_press(WordMessage::AddAdditional("context".to_string())), + ); + } + let mut col = iced::widget::column![ button("Назад").on_press(WordMessage::Back), text!("Ключ"), @@ -114,26 +126,32 @@ impl WordState { match value.0.as_str() { "reading" => self.reading_field(value), "description" => self.description_field(value), + "context" => self.context_field(value), _ => space().into(), } } fn reading_field(&self, value: (&String, &String)) -> Element<'_, WordMessage> { - column![ - text!("Чтение слова"), - text_input("reading", &value.1) - .on_input(|string| WordMessage::SetAdditional("reading".to_string(), string)) - ].spacing(DEFAULT_SPACING) - .into() + self.additional_field(value, "Чтение слова".to_string(), "reading".to_string()) } fn description_field(&self, value: (&String, &String)) -> Element<'_, WordMessage> { + self.additional_field(value, "Описание".to_string(), "description".to_string()) + } + + fn context_field(&self, value: (&String, &String)) -> Element<'_, WordMessage> { + self.additional_field(value, "В контексте".to_string(), "context".to_string()) + } + + fn additional_field(&self, value: (&String, &String), name: String, id: String) -> Element<'_, WordMessage> { column![ - text!("Описание"), - text_input("description", &value.1) - .on_input(|string| WordMessage::SetAdditional("description".to_string(), string)) - ].spacing(DEFAULT_SPACING) - .into() } + text!("{}", name), + text_input(id.clone().as_str(), &value.1) + .on_input(move |string| WordMessage::SetAdditional(id.clone(), string)) + ] + .spacing(DEFAULT_SPACING) + .into() + } } #[derive(Debug, Clone)] pub enum WordMessage {