use crate::app::AppState; use crate::data::entities::{EntityHandle, WordData, WordGroup}; use crate::data::repositories::{Query, Repository}; use crate::data::Id; 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; use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage}; use crate::pages::dictionary::BasicWordFields::*; use crate::pages::dictionary::DictionaryMessage::*; use crate::pages::dictionary::SelectionChangeDirection::*; use crate::pages::dictionary_quiz::DictionaryQuizState; use crate::pages::import::ImportState; use crate::pages::word::{WordEditor, WordMessage}; 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::default; use iced::widget::text_input::Status::Active; use iced::widget::*; use iced::{Border, Color, Shadow, Task}; use iced_core::keyboard; use iced_core::keyboard::key::Physical::Code; use iced_core::keyboard::{Event, Modifiers}; use iced_core::keyboard::Key::Named; use iced_core::Length::{Fill, FillPortion}; 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, usize)>, word_editor: Option>, } #[derive(Clone)] pub enum DictionaryMessage { Back, ExtendedEdit(EntityHandle), DeleteWord(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, usize), EditField(String), ChangeSelectedField(SelectionChangeDirection), EditorAction(WordMessage), ExtendedWordEdited } #[derive(Clone, PartialEq, Eq, Hash, Copy)] pub enum SelectionChangeDirection { Next, Previous, Forward, Backward, } #[derive(Clone, PartialEq, Eq, Hash, Copy)] pub enum BasicWordFields { Key, Value, Tags, } const FIELDS: &[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 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); } ExtendedEdit(i) => { self.word_editor = Some(self.editor_for_word(i)); if let Some((handle, cell, index)) = self.selected_cell.clone() { self.selected_cell = Some((i, cell, index)) } } 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 original = handle; let now = Utc::now(); if !self.auto_save_queue.contains_key(&handle) { return Task::none(); } if self.auto_save_queue[&handle] <= now { self.update_tags(); 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); if let Some(selected) = self.selected_cell && selected.0 == original { self.selected_cell = Some((handle, selected.1, selected.2)); if self.word_editor.is_some() { self.word_editor = Some(self.editor_for_word(handle)); } } } info!("Saved word"); return Task::none(); } } GotoImport => {} SelectField(e, f, index) => { info!("Editing word {}", e.id); if self.word_editor.is_some() && let Some(selected) = &self.selected_cell && selected.0 != e { self.word_editor = Some(self.editor_for_word(e)); } self.selected_cell = Some((e, f, index)); 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 { Key => { word.key = value; } Value => { word.value = value; } Tags => { word.tags = value; } } } return self.launch_auto_save_offset(selected.0); } } ChangeSelectedField(change) => { return if let Some(selected) = self.selected_cell { let update_vertical = move |mut dir: bool| { if self.reverse_list { dir = !dir; } let new_index = if dir { selected.2.saturating_add(1) } else { selected.2.saturating_sub(1) }; if new_index < self.displayed_words_query.len() { return SelectField( self.displayed_words_query.get(new_index).unwrap(), selected.1, new_index, ); } SelectField(selected.0, selected.1, selected.2) }; let update_horizontal = move |dir: bool| { let current = selected.1; let delta = if dir { 1 } else { -1 }; let mut index = FIELDS.iter().position(|&f| f == current).unwrap() as i8 + delta; if index < 0 { index = (FIELDS.len() - 1) as i8; } if index >= FIELDS.len() as i8 { index = 0; } SelectField(selected.0, FIELDS[index as usize], selected.2) }; Task::done(RootMessage::Dictionary(match change { Next => update_vertical(true), Previous => update_vertical(false), Forward => update_horizontal(true), Backward => update_horizontal(false), })) } else { Task::none() }; } DeleteWord(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); self.selected_cell = None; self.word_editor = None; } EditorAction(action) => { let result = self.word_editor.as_mut().unwrap().update(action); if let Some(result) = result { match result { DeleteWord(_) | ExtendedWordEdited => { return Task::done(RootMessage::Dictionary(result)); }, _ => {} } } } ExtendedWordEdited => { return self.launch_auto_save_offset(self.selected_cell.unwrap().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(),], self.selection_editor(), row![ self.add_word_button(), horizontal().width(Fill), button("Импорт").style(text).on_press(GotoImport), ] ] .spacing(5).width(FillPortion(4)), self.right_panel(), ] .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, word_editor: None, }; result.init(); result.update_tags(); result } fn editor_for_word(&self, handle: EntityHandle) -> WordEditor { WordEditor::new(handle, self.state.clone()).on_delete(DeleteWord(handle)).on_edit(ExtendedWordEdited) } 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; if let Some(cell) = self.selected_cell.as_ref() && cell.0 == *word_handle { selection = 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, number: i, }; 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; } default_style }) }; line = line.push( hard_mouse_area(field( "Слово", &data.key, data.selection.is_some() && data.selection.unwrap() == Key, )) .on_press(SelectField(data.handle, Key, data.number)), ); line = line.push( hard_mouse_area(field( "Перевод", &data.value, data.selection.is_some() && data.selection.unwrap() == Value, )) .on_press(SelectField(data.handle, Value, data.number)), ); line = line.push( hard_mouse_area(field( "Теги", &data.tags, data.selection.is_some() && data.selection.unwrap() == Tags, )) .on_press(SelectField(data.handle, Tags, data.number)), ); let line_button = || { if !data.handle.real() { return button("-") .on_press(DeleteWord(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(ExtendedEdit(data.handle)) .style(button::primary) .width(15) }; line.push(line_button()).push(space().width(10)) } fn right_panel(&self) -> iced::Element<'_, DictionaryMessage> { if let Some(editor) = &self.word_editor { container(editor.view().map(EditorAction)).width(FillPortion(3)).into() } else { self.filters() } } 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(FillPortion(1)) .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 selection_editor(&self) -> iced::Element<'_, DictionaryMessage> { 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 { Key => { display = word.key.clone(); } Value => { display = word.value.clone(); } Tags => { display = word.tags.clone(); } } } text_input("", display.as_str()) .width(Fill) .id(iced::widget::Id::new("editor")) .on_input(EditField) .into() } 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() } } impl KeyPressedPage for DictionaryState { fn press(&mut self, message: &Event) -> Task { if let Event::KeyPressed { key: _, modified_key: modifier, physical_key: pk, location: _, modifiers: mods, text: _, repeat: _, } = message && let Code(code) = pk { return match code { keyboard::key::Code::ArrowDown => { Task::done(RootMessage::Dictionary(ChangeSelectedField(Next))) } keyboard::key::Code::ArrowUp => { Task::done(RootMessage::Dictionary(ChangeSelectedField(Previous))) } keyboard::key::Code::Tab => { return if *mods == Modifiers::CTRL && let Some(selected) = self.selected_cell { Task::done(RootMessage::Dictionary(ExtendedEdit(selected.0))) } else if let Named(keyboard::key::Named::Alt) = modifier { Task::done(RootMessage::Dictionary(ChangeSelectedField(Backward))) } else { Task::done(RootMessage::Dictionary(ChangeSelectedField(Forward))) } } keyboard::key::Code::Escape => { if self.word_editor.is_some() { self.word_editor = None; }else if self.selected_cell.is_some() { self.selected_cell = None; } Task::none() } _ => Task::none(), }; } Task::none() } } 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: String, value: String, tags: String, handle: EntityHandle, selection: Option, number: usize, }