use crate::data_provider::words::{delete_group, delete_word, update_group, update_word}; use crate::dictionary::DictionaryMessage::*; use crate::dictionary_test::DictionaryQuizState; use crate::import::ImportState; use crate::lang::{WordData, WordGroup}; use crate::navigation::Page::{Import, Word}; use crate::navigation::{NavigatedPage, Page}; use crate::styling::*; use crate::word::WordState; use crate::{AppState, RootMessage}; use chrono::{DateTime, TimeDelta, Utc}; use hashbrown::{HashMap, HashSet}; use iced::alignment::Vertical::Center; use iced::widget::button::Style; use iced::widget::button::{danger, text}; use iced::widget::space::horizontal; use iced::widget::text_input::default; use iced::widget::*; use iced::{Border, Color, Shadow, Task}; use iced_core::Length::Fill; use rand::random_range; use std::fs; use std::ops::Add; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; #[derive(Clone)] pub struct DictionaryState { state: Arc>, include_map: Vec, tag_map: HashMap, reverse: bool, search: String, no_typing: bool, selected_group_index: usize, reverse_list: bool, auto_save_queue: HashMap>, } #[derive(Debug, Clone)] pub enum DictionaryMessage { Back, SetTags(usize, String), SetKey(usize, String), SetValue(usize, String), SubmitWord(usize), WordAction(usize), NewWord, Include(usize, bool), IncludeTag(String, bool), Test, ResetTags, SetReverse(bool), Search(String), SetTyping(bool), CreateGroup, EditGroup(String), SaveGroup, SelectGroup(usize), DeleteGroup, ChangeDirection, TrySave(usize), ToImport, } impl NavigatedPage for DictionaryState { fn navigate(&mut self, message: &DictionaryMessage) -> Option { if let Back = message { return Some(Page::PreviousPage); } if let Test = message && self.include_map.iter().any(|x| *x) { let mut words = vec![]; let dict = &self.state.lock().unwrap().dictionary; words = self .include_map .iter() .zip(0..self.include_map.len()) .filter(|(flag, _)| **flag) .map(|(_, index)| dict[index].clone()) .collect(); return Some(Page::DictionaryQuiz(DictionaryQuizState::new( words, self.reverse, self.no_typing, ))); } if let WordAction(index) = message { let word: WordData; { let state = self.state.lock().unwrap(); let dict = &state.dictionary; word = dict[*index].clone(); } if word.id.is_valid() { return Some(Word(WordState::new(word, *index, self.state.clone()))); } } if let ToImport = message { return Some(Import(ImportState::new(self.state.clone()))); } None } fn navigated(&mut self) { let len = self.state.lock().unwrap().dictionary.len(); self.include_map = vec![false; len]; self.tag_map = Default::default(); self.update_tags(); } fn update(&mut self, message: DictionaryMessage) -> Task { match message { NewWord => { let mut state = self.state.lock().unwrap(); let mut word = WordData::new(); word.group_id = state.word_groups[self.selected_group_index].id; let dict = &mut state.dictionary; dict.push(word); self.include_map.push(false); } SetKey(i, v) => { { let dict = &mut self.state.lock().unwrap().dictionary; dict[i].key = v; } return self.launch_auto_save_offset(i); } SetValue(i, v) => { { let dict = &mut self.state.lock().unwrap().dictionary; dict[i].value = v } return self.launch_auto_save_offset(i); } 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; } self.update_tags(); return self.launch_auto_save_offset(i); } WordAction(i) => { let state = &mut self.state.lock().unwrap(); let dict = &mut state.dictionary; let word = dict.remove(i); self.include_map.remove(i); self.auto_save_queue.remove(&i); delete_word(&word, &state.connection) } Include(i, b) => self.include_map[i] = b, IncludeTag(t, v) => { let index; { let state = self.state.lock().unwrap(); index = state.word_groups[self.selected_group_index].id; } self.tag_map.insert(t, v); self.update_words_include(index) } ResetTags => { self.tag_map.iter_mut().for_each(|(_, v)| *v = false); self.include_map.iter_mut().for_each(|x| *x = false) } SetReverse(v) => self.reverse = v, Search(s) => { self.search = s; } SetTyping(b) => self.no_typing = b, SubmitWord(i) => self.save_word(i), Back => {} Test => {} CreateGroup => { let state = &mut self.state.lock().unwrap(); state.word_groups.push(WordGroup { id: 0.into(), name: format!("Группа слов {}", random_range(100..1000)), }); } EditGroup(new) => { let state = &mut self.state.lock().unwrap(); let group = state.word_groups.get_mut(self.selected_group_index); if let Some(group) = group { group.name = new.clone(); } } SaveGroup => { let state = &mut self.state.lock().unwrap(); let connection = &state.connection; let group = &mut state .word_groups .get(self.selected_group_index) .unwrap() .clone(); update_group(group, connection); state.word_groups[self.selected_group_index] = group.clone(); } SelectGroup(i) => { self.selected_group_index = i; let index; { let state = self.state.lock().unwrap(); index = state.word_groups[i].id; } self.update_words_include(index) } DeleteGroup => { if self.selected_group_index == 0 { return Task::none(); } { let state = &mut self.state.lock().unwrap(); if let Some(group) = state.word_groups.get(self.selected_group_index) { let remove_group_id = group.id; let connection = &state.connection; delete_group(group, connection); state.word_groups.remove(self.selected_group_index); state .dictionary .retain(|word| word.group_id != remove_group_id); self.selected_group_index = 0; } } self.update_tags() } ChangeDirection => { self.reverse_list = !self.reverse_list; } TrySave(word_index) => { let now = Utc::now(); if !self.auto_save_queue.contains_key(&word_index) { return Task::none(); } if self.auto_save_queue[&word_index] <= now { self.auto_save_queue.remove(&word_index); self.save_word(word_index); } } ToImport => {} } Task::none() } fn view(&self) -> iced::Element<'_, DictionaryMessage> { back_overlay( row![ iced::widget::column![ self.groups_panel(), row![horizontal().width(8), self.words_list(),], row![ button("Добавить слово").style(jl_button).on_press(NewWord), horizontal().width(Fill), button("Импорт").style(text).on_press(ToImport), ] ] .spacing(5), self.filters(), ] .spacing(DEFAULT_SPACING) .into(), Back, ) } } impl DictionaryState { pub fn new(state: Arc>) -> Self { let len = state.lock().unwrap().dictionary.len(); let mut result = DictionaryState { include_map: vec![false; len], selected_group_index: 0, state, tag_map: HashMap::new(), reverse: false, search: "".to_string(), no_typing: true, reverse_list: true, auto_save_queue: HashMap::new(), }; result.update_tags(); result } fn save_word(&mut self, i: usize) { let state = &mut self.state.lock().unwrap(); let connection = &state.connection; let word = &mut state.dictionary.get(i).unwrap().clone(); update_word(word, connection); state.dictionary[i] = word.clone(); } fn launch_auto_save_offset(&mut self, index: usize) -> Task { let save_time = Utc::now().add(TimeDelta::milliseconds(900)); self.auto_save_queue.insert(index, save_time); let message = RootMessage::Dictionary(TrySave(index)); Task::perform( async { tokio::time::sleep(Duration::from_secs(1)).await }, |_| message, ) } fn words_list(&self) -> iced::Element<'_, DictionaryMessage> { let time = Instant::now(); let mut col = Column::new().width(Fill); let state = self.state.lock().unwrap(); let group_id = state.word_groups[self.selected_group_index].id; let dict = &state.dictionary; let mut index = 0; for access_index in 0..dict.len() { let mut i = access_index; if self.reverse_list { i = dict.len() - access_index - 1; } let word = &dict[i]; if word.group_id != group_id { continue; } if !self.search.is_empty() && !word.key.contains(&self.search) && !word.value.contains(&self.search) && !word.tags.contains(&self.search) { continue; } let word_line_data = WordLineState { is_included: self.include_map[i], key: word.key.clone(), value: word.value.clone(), tags: word.tags.clone(), id: word.id, index: i, }; index += 1; let lazy_line = lazy(word_line_data, move |data| { let index = data.index; let mut line = Row::new().width(Fill).align_y(Center); line = line.push( checkbox(data.is_included) .label("") .spacing(15) .on_toggle(move |b| Include(index, b)), ); line = line.push( text_input("Слово", &data.key) .size(ACCENT_FONT_SIZE) .width(Fill) .on_input(move |string| SetKey(index, string)) .on_submit(SubmitWord(index)) .style(|x, status| { let mut default_style = default(x, status); default_style.border.radius = 0.0.into(); default_style }), ); line = line.push( text_input("Перевод", &data.value) .size(ACCENT_FONT_SIZE) .width(Fill) .on_input(move |string| SetValue(index, string)) .on_submit(SubmitWord(index)) .style(|x, status| { let mut default_style = default(x, status); default_style.border.radius = 0.0.into(); default_style }), ); line = line.push( text_input("Теги", &data.tags) .size(ACCENT_FONT_SIZE) .width(Fill) .on_input(move |string| SetTags(index, string)) .on_submit(SubmitWord(index)) .style(|x, status| { let mut default_style = default(x, status); default_style.border.radius = 0.0.into(); default_style }), ); let line_button = || { let action = WordAction(index); if !data.id.is_valid() { return button("-").on_press(action).style(|_x, _status| Style { background: None, text_color: Color::BLACK, border: Border::default(), shadow: Shadow::default(), snap: false, }); } button("").on_press(WordAction(index)).width(15) }; line = line.push(line_button()).push(space().width(10)); line }); col = col.push(lazy_line); } println!("Drawing {index} lines"); println!("Words rendering time: {:?}", time.elapsed()); scrollable(col).height(Fill).into() } fn filters(&self) -> iced::Element<'_, DictionaryMessage> { let dict = &self.state.lock().unwrap().dictionary; iced::widget::column![ text_input("Поиск", &self.search) .on_input(Search) .width(Fill), text!("Всего слов: {}", dict.len()), text!( "Выбрано слов: {}", self.include_map.iter().filter(|i| **i).count() ), self.tags_selector(), toggler(self.no_typing) .label("Без набора") .on_toggle(SetTyping), toggler(self.reverse) .label("Обратный тест") .on_toggle(SetReverse), button(text!("Тест").center().width(Fill)) .style(cta_button) .on_press(Test) .width(Fill), ] .width(250) .spacing(DEFAULT_SPACING) .into() } fn tags_selector(&self) -> iced::Element<'_, DictionaryMessage> { let mut col = Column::new().width(Fill); col = col.push( button("Сбросить") .on_press(ResetTags) .style(|x: &Theme, _status| Style { background: None, text_color: x.palette().primary, border: Border::default(), shadow: Shadow::default(), snap: false, }), ); let mut sorted_tags = self.tag_map.iter().collect::>(); sorted_tags.sort(); for tag in sorted_tags { col = col.push( checkbox(*tag.1) .label(tag.0) .on_toggle(|x1| IncludeTag(tag.0.clone(), x1)), ) } container(scrollable(col)).height(Fill).into() } fn update_tags(&mut self) { let mut tags_list: HashSet = HashSet::new(); let dict = &self.state.lock().unwrap().dictionary; dict.iter().for_each(|element| { split_with_coma(element.tags.as_str()) .iter() .for_each(|tag| { tags_list.insert(tag.clone()); }); }); let current_tags = self .tag_map .keys() .map(|k| k.clone().to_string()) .collect::>(); current_tags .iter() .filter(|tag| !tags_list.contains(*tag)) .for_each(|tag| { self.tag_map.remove(tag); }); tags_list.iter().for_each(|tag| { self.tag_map.insert(tag.clone(), false); }); } fn update_words_include(&mut self, group_id: crate::lang::Id) { let include_tags = self .tag_map .iter() .filter(|(_, value)| **value) .map(|(t, _)| t.clone()) .collect::>(); if include_tags.is_empty() { self.include_map.iter_mut().for_each(|x| *x = false); return; } let dict = &self.state.lock().unwrap().dictionary; let time = Instant::now(); self.include_map = dict .iter() .map(|word| (split_with_coma(word.tags.as_str()), word.group_id)) .map(|(tags, word_group_id)| { !tags.is_empty() && tags.iter().all(|t| include_tags.contains(t)) && word_group_id == group_id }) .collect(); println!("Time {}", time.elapsed().as_micros()); } fn groups_panel(&self) -> iced::Element<'_, DictionaryMessage> { let mut row = Row::new(); row = row.push(button("+").style(text).on_press(CreateGroup)); let state = &self.state.lock().unwrap(); let groups = &state.word_groups; for (index, group) in groups.iter().enumerate() { row = row.push( button(text!("{}", group.name.clone())) .style(text) .on_press(SelectGroup(index)), ); } let group = state.word_groups[self.selected_group_index].clone(); iced::widget::column![ scrollable(row).width(Fill).horizontal(), row![ button("⇳").on_press(ChangeDirection).style(jl_button), text_input("Название группы слов", &group.name) .on_input(EditGroup) .width(250) .on_submit(SaveGroup), horizontal(), self.group_delete_button(), ] .spacing(DEFAULT_SPACING / 2.0) ] .into() } fn group_delete_button(&self) -> iced::Element<'_, DictionaryMessage> { if self.selected_group_index != 0 { button("Удалить").style(danger).on_press(DeleteGroup).into() } else { space().into() } } } pub fn split_with_coma(ts: &str) -> Vec { ts.split(',') .map(|ts| ts.to_lowercase().trim().to_string()) .filter(|t| !t.is_empty()) .collect::>() } pub fn app_data_dir() -> PathBuf { let mut dir = dirs::data_dir().unwrap(); dir.push("jap_learn"); if !dir.exists() { fs::create_dir(dir.clone()).unwrap(); } dir } #[derive(Hash, PartialEq, Eq)] struct WordLineState { is_included: bool, key: String, value: String, tags: String, id: crate::lang::Id, index: usize, }