use crate::app::AppState; use crate::data::Id; use crate::data::entities::{EntityHandle, WordData, WordGroup}; use crate::data::repositories::{Query, Repository}; use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner}; use crate::data_provider::words::WordDataLayer; use crate::data_provider::words::WordRepositoryInner; use crate::navigation::Page::{Import, Word}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::pages::dictionary::DictionaryMessage::*; use crate::pages::dictionary_quiz::DictionaryQuizState; use crate::pages::import::ImportState; use crate::pages::word::WordState; use crate::push_error_task; use crate::styling::*; use crate::widgets::hard_mouse_area::hard_mouse_area; use crate::{jl_button, push_error}; 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::operation::focus; use iced::widget::space::horizontal; use iced::widget::text_input::Status::Active; use iced::widget::text_input::default; use iced::widget::*; use iced::{Border, Color, Shadow, Task}; use iced_core::Length::Fill; use log::info; use rand::random_range; use std::ops::Add; use std::sync::Arc; use std::time::{Duration, Instant}; pub struct DictionaryState { state: AppState, included: HashSet>, tag_map: HashMap, words_tags: Vec<(EntityHandle, Vec)>, reverse: bool, search: String, no_typing: bool, selected_group: EntityHandle, reverse_list: bool, auto_save_queue: HashMap, DateTime>, displayed_words_query: Query, groups_query: Query, selected_cell: Option<(EntityHandle, BasicWordFields)>, hovered_cell: Option<(EntityHandle, BasicWordFields)>, } #[derive(Clone)] pub enum DictionaryMessage { Back, SetTags(EntityHandle, String), SetKey(EntityHandle, String), SetValue(EntityHandle, String), WordAction(EntityHandle), NewWord, Include(EntityHandle, bool), IncludeTag(String, bool), GotoTest, ResetTags, SetReverse(bool), Search(String), SetTyping(bool), CreateGroup, EditGroup(String), SaveGroup, SelectGroup(EntityHandle), DeleteGroup, ChangeDirection, TrySave(EntityHandle), GotoImport, SelectField(EntityHandle, BasicWordFields), EditField(String), } #[derive(Clone, PartialEq, Eq, Hash, Copy)] pub enum BasicWordFields { Key, Value, Tags, } impl NavigatedPage for DictionaryState { fn navigate(&mut self, message: &DictionaryMessage) -> Option { if let Back = message { return Some(Page::PreviousPage); } if let GotoTest = message && !self.included.is_empty() { let mut words = vec![]; let dict = &self.state.dictionary.read(); let query = dict.query(Arc::new(|_| true)); let handles = query.handles(); words = handles.filter(|h| self.included.contains(h)).collect(); return Some(Page::DictionaryQuiz(DictionaryQuizState::new( words, self.reverse, self.no_typing, self.state.clone(), ))); } if let WordAction(handle) = message && handle.real() { return Some(Word(WordState::new(*handle, self.state.clone()))); } if let GotoImport = message { return Some(Import(ImportState::new(self.state.clone()))); } None } fn navigated(&mut self) { self.included = Default::default(); self.tag_map = Default::default(); let groups = self.state.word_groups.read(); self.groups_query.load(&groups); drop(groups); self.update_tags(); } fn update(&mut self, message: DictionaryMessage) -> Task { match message { NewWord => { let mut dictionary = self.state.dictionary.write(); let mut word = WordData::new(); word.group_id = self.selected_group.id; dictionary.create(word); self.displayed_words_query.load(&dictionary); } SetKey(handle, v) => { let mut dictionary = self.state.dictionary.write(); dictionary.get_mut_static(handle).unwrap().key = v; drop(dictionary); return self.launch_auto_save_offset(handle); } SetValue(handle, v) => { let mut dictionary = self.state.dictionary.write(); dictionary.get_mut_static(handle).unwrap().value = v; drop(dictionary); return self.launch_auto_save_offset(handle); } SetTags(i, mut v) => { { let mut dictionary = self.state.dictionary.write(); let word = dictionary.get_mut_static(i).unwrap(); let current_tags_value = word.tags.clone(); if v.ends_with(", ") && v.len() < current_tags_value.len() { v = v[..v.len() - 2].to_string() } while let Some(index) = v.find(",,") { v.remove(index); } word.tags = v; } self.update_tags(); return self.launch_auto_save_offset(i); } WordAction(i) => { let mut dictionary = self.state.dictionary.write(); if let Err(err) = dictionary.delete(i) { return push_error_task!(err); } self.displayed_words_query.load(&dictionary); } Include(i, b) => { if b { self.included.insert(i); } else { self.included.remove(&i); } } IncludeTag(t, v) => { self.tag_map.insert(t, v); self.update_words_include(self.selected_group.id) } ResetTags => { self.tag_map.iter_mut().for_each(|(_, v)| *v = false); self.included.clear(); } SetReverse(v) => self.reverse = v, Search(s) => { self.search = s; } SetTyping(b) => self.no_typing = b, Back => {} GotoTest => {} CreateGroup => { let mut groups = self.state.word_groups.write(); groups.create(WordGroup { id: 0.into(), name: format!("Группа слов {}", random_range(100..1000)), }); self.groups_query.load(&groups); } EditGroup(new) => { let mut groups = self.state.word_groups.write(); let group = groups.get_mut(&mut self.selected_group); if let Some(group) = group { group.name = new; } } SaveGroup => { { let mut groups = self.state.word_groups.write(); if let Err(err) = groups.save(&mut self.selected_group) { return push_error_task!(err); } self.groups_query.load(&groups); } self.setup_words_query(); } SelectGroup(i) => { self.selected_group = i; self.setup_words_query(); self.update_words_include(i.id) } DeleteGroup => { if self.selected_group.id.0 == 1 { return Task::none(); } { let delete_group = self.selected_group; self.selected_group = EntityHandle::from_id(1.into()); self.setup_words_query(); let mut groups = self.state.word_groups.write(); if let Err(err) = groups.delete(delete_group) { return push_error_task!(err); } if let Err(err) = self.state.dictionary.write().reload() { return push_error_task!(err); } self.groups_query.load(&groups); } self.update_tags() } ChangeDirection => { self.reverse_list = !self.reverse_list; } TrySave(mut handle) => { let now = Utc::now(); if !self.auto_save_queue.contains_key(&handle) { return Task::none(); } if self.auto_save_queue[&handle] <= now { self.auto_save_queue.remove(&handle); let mut dictionary = self.state.dictionary.write(); let reload_query = !handle.real(); let _ = dictionary.save(&mut handle); if reload_query { self.displayed_words_query.load(&dictionary); } info!("Saved word"); return Task::none(); } } GotoImport => {} SelectField(e, f) => { info!("Editing word {}", e.id); self.selected_cell = Some((e, f)); return focus(iced::widget::Id::new("editor")); } EditField(value) => { if let Some(selected) = self.selected_cell.as_ref() { { let mut dictionary = self.state.dictionary.write(); let word = dictionary.get_mut_static(selected.0).unwrap(); match selected.1 { BasicWordFields::Key => { word.key = value; } BasicWordFields::Value => { word.value = value; } BasicWordFields::Tags => { word.key = value; } } } return self.launch_auto_save_offset(selected.0); } } } Task::none() } fn view(&self) -> iced::Element<'_, DictionaryMessage> { back_overlay( row![ iced::widget::column![ self.groups_panel(), row![horizontal().width(8), self.words_list(),], { let mut display = "".to_string(); if let Some(selected) = self.selected_cell.as_ref() { let mut dictionary = self.state.dictionary.write(); let word = dictionary.get_mut_static(selected.0).unwrap(); match selected.1 { BasicWordFields::Key => { display = word.key.clone(); } BasicWordFields::Value => { display = word.value.clone(); } BasicWordFields::Tags => { display = word.tags.clone(); } } } text_input("", display.as_str()) .width(Fill) .id(iced::widget::Id::new("editor")) .on_input(EditField) }, row![ self.add_word_button(), horizontal().width(Fill), button("Импорт").style(text).on_press(GotoImport), ] ] .spacing(5), self.filters(), ] .spacing(DEFAULT_SPACING) .into(), Back, ) } } impl DictionaryState { pub fn new(state: AppState) -> Self { let words_query = state.dictionary.read().empty_query_par(); let groups_query = state.word_groups.read().query(Arc::new(|_| true)); let mut result = DictionaryState { included: Default::default(), selected_group: EntityHandle::new(Id(1), 0, true), state, tag_map: HashMap::new(), words_tags: vec![], reverse: false, search: "".to_string(), no_typing: true, reverse_list: true, auto_save_queue: HashMap::new(), displayed_words_query: words_query, groups_query, selected_cell: None, hovered_cell: None, }; result.init(); result.update_tags(); result } fn init(&mut self) { self.displayed_words_query = self .state .dictionary .read() .query(Arc::new(|word| word.group_id == Id(1))); } fn setup_words_query(&mut self) { let id = self.selected_group.id; self.displayed_words_query = self .state .dictionary .read() .query(Arc::new(move |word| word.group_id == id)); } fn launch_auto_save_offset(&mut self, handle: EntityHandle) -> Task { let save_time = Utc::now().add(TimeDelta::milliseconds(500)); self.auto_save_queue.insert(handle, save_time); let message = RootMessage::Dictionary(TrySave(handle)); Task::perform( async { tokio::time::sleep(Duration::from_millis(520)).await }, |_| message, ) } fn words_list(&self) -> iced::Element<'_, DictionaryMessage> { let time = Instant::now(); let mut col = Column::new().width(Fill); let query = &self.displayed_words_query; let dictionary = self.state.dictionary.read(); for access_index in 0..query.len() { let mut i = access_index; if self.reverse_list { i = query.len() - access_index - 1; } let word_handle = &query[i]; let word = dictionary.get_ref_static(*word_handle).unwrap(); if !self.search.is_empty() && !word.key.contains(&self.search) && !word.value.contains(&self.search) && !word.tags.contains(&self.search) { continue; } let mut selection = None; let mut hovered = None; if let Some(cell) = self.selected_cell.as_ref() { if cell.0 == *word_handle { selection = Some(cell.1); } } if let Some(cell) = self.hovered_cell.as_ref() { if cell.0 == *word_handle { hovered = Some(cell.1); } } let word_line_data = WordLineState { is_included: self.included.contains(word_handle), key: word.key.clone().into(), value: word.value.clone().into(), tags: word.tags.clone().into(), handle: *word_handle, selection, hover: hovered, }; let lazy_line = lazy(word_line_data, Self::word_line); col = col.push(lazy_line); } info!("Words rendering time: {:?}", time.elapsed()); scrollable(col).height(Fill).into() } fn word_line<'a>(data: &WordLineState) -> Row<'a, DictionaryMessage> { let mut line = Row::new().width(Fill).align_y(Center); let handle = data.handle; line = line.push( checkbox(data.is_included) .label("") .spacing(15) .on_toggle(move |b| Include(handle, b)), ); let field = |placeholder: &str, data: &str, selected: bool| { text_input(placeholder, data) .size(ACCENT_FONT_SIZE) .width(Fill) .style(move |x, _| { let mut default_style = default(x, Active); default_style.border.radius = 0.0.into(); if selected { default_style.border.color = x.extended_palette().secondary.base.color; default_style.border.width = 3.0_f32.into(); } default_style }) }; line = line.push( hard_mouse_area(field( "Слово", &data.key, data.selection.is_some() && data.selection.unwrap() == BasicWordFields::Key, )) .on_press(SelectField(data.handle, BasicWordFields::Key)) ); line = line.push( hard_mouse_area(field( "Перевод", &data.value, data.selection.is_some() && data.selection.unwrap() == BasicWordFields::Value, )) .on_press(SelectField(data.handle, BasicWordFields::Value)) ); line = line.push( hard_mouse_area(field( "Теги", &data.tags, data.selection.is_some() && data.selection.unwrap() == BasicWordFields::Tags, )) .on_press(SelectField(data.handle, BasicWordFields::Tags)) ); let line_button = || { if !data.handle.real() { return button("-") .on_press(WordAction(data.handle)) .style(|_x: &Theme, _status: button::Status| Style { background: None, text_color: Color::BLACK, border: Border::default(), shadow: Shadow::default(), snap: false, }) .width(15); } button("") .on_press(WordAction(data.handle)) .style(button::primary) .width(15) }; line.push(line_button()).push(space().width(10)) } fn filters(&self) -> iced::Element<'_, DictionaryMessage> { let dict = &self.state.dictionary.read(); iced::widget::column![ text_input("Поиск", &self.search) .on_input(Search) .width(Fill), text!("Всего слов: {}", dict.len()), text!("Выбрано слов: {}", self.included.iter().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(GotoTest) .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) { self.words_tags.clear(); let mut tags_list: HashSet = HashSet::new(); let dict = &self.state.dictionary.write(); let mut words_query = dict.full_query(); let handles = words_query.handles().collect::>(); words_query .iter(dict) .zip(handles) .for_each(|(element, handle)| { let tags = split_with_coma(element.tags.as_str()); tags.iter().for_each(|tag| { tags_list.insert(tag.clone()); }); self.words_tags.push((handle, tags)) }); 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: Id) { let include_tags = self .tag_map .iter() .filter(|(_, value)| **value) .map(|(t, _)| t.clone()) .collect::>(); if include_tags.is_empty() { self.included.clear(); return; } let dict = &self.state.dictionary.read(); let time = Instant::now(); self.included.clear(); for (handle, tags) in &mut self.words_tags { if let Some(word) = dict.get_ref(handle) { if word.group_id != group_id { continue; } if !tags.is_empty() && tags.iter().all(|t| include_tags.contains(t)) { self.included.insert(*handle); } } else { continue; } } info!("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 groups = &self.state.word_groups.read(); for mut group in self.groups_query.handles() { let group_name = groups.get(&mut group).unwrap().name.clone(); row = row.push( button(text!("{}", group_name)) .style(text) .on_press(SelectGroup(group)), ); } let groups = self.state.word_groups.read(); let group = groups.get_static(self.selected_group).unwrap(); iced::widget::column![ scrollable(row).width(Fill).horizontal(), row![ jl_button!("⇳").on_press(ChangeDirection), 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.real() && self.selected_group.id != 1.into() { button("Удалить").style(danger).on_press(DeleteGroup).into() } else { space().into() } } fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { let button = jl_button!("Добавить слово"); if self.selected_group.real() { return button.on_press(NewWord).into(); } button.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::>() } #[derive(Clone, Hash, PartialEq, Eq)] struct WordLineState { is_included: bool, key: Box, value: Box, tags: Box, handle: EntityHandle, selection: Option, hover: Option, }