diff --git a/Cargo.lock b/Cargo.lock index 6bb6f03..e2441ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2309,6 +2309,7 @@ dependencies = [ "hex", "iced", "iced_core", + "mimalloc", "rand", "reqwest", "rfd", @@ -2492,6 +2493,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + [[package]] name = "libredox" version = "0.1.12" @@ -2646,6 +2656,15 @@ dependencies = [ "paste", ] +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" diff --git a/Cargo.toml b/Cargo.toml index 5eb0a70..e42f034 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ hex = "0.4.3" zstd = "0.13.3" zip = "8.6.0" rfd = "0.17.2" - +mimalloc = "0.1.52" [profile.super-release] inherits = "release" codegen-units = 1 diff --git a/src/data_provider/import.rs b/src/data_provider/import.rs index 609aa24..b2c08b6 100644 --- a/src/data_provider/import.rs +++ b/src/data_provider/import.rs @@ -1,23 +1,26 @@ -use rusqlite::Connection; +use crate::lang::WordData; +use rusqlite::fallible_iterator::FallibleIterator; +use rusqlite::{Connection, MappedRows}; use serde_json::Value; use std::collections::HashMap; #[derive(Clone)] -pub struct ImportData(Vec); +pub struct ImportData(pub(crate) Vec); #[derive(Clone, Debug)] pub struct ImportGroup { - id: u64, - name: String, - fields: Vec, - mapping: HashMap, + pub id: u64, + pub name: String, + pub fields: Vec, + pub length: u64, + pub mapping: HashMap, + pub imported: bool } #[derive(Clone)] pub struct ImportNote { - name: String, - tags: String, - fields: Vec, + pub tags: String, + pub fields: Vec, } pub fn load_groups(connection: &Connection) -> ImportData { @@ -28,6 +31,16 @@ pub fn load_groups(connection: &Connection) -> ImportData { let sets = raw.as_object().unwrap(); let mut total_data = ImportData(vec![]); + let mut count_stmt = connection + .prepare("select mid, count(id) from notes group by mid") + .unwrap(); + + let counts = count_stmt + .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap() + .map(|x| x.unwrap()) + .collect::>(); + for (_, collection) in sets { let fields = collection["flds"] .as_array() @@ -35,14 +48,35 @@ pub fn load_groups(connection: &Connection) -> ImportData { .iter() .map(|x| x["name"].as_str().unwrap().to_string()) .collect(); - let group = ImportGroup { + let mut group = ImportGroup { id: collection["id"].as_u64().unwrap(), name: collection["name"].as_str().unwrap().to_string(), fields, mapping: Default::default(), + length: 0, + imported: false, }; + if let Some((_, count)) = counts.iter().find(|(id, _)| *id == group.id as i64) { + group.length = *count as u64 + } println!("{:?}", group); total_data.0.push(group); } total_data } + +pub fn get_words_of_group(connection: &Connection, group_id: u64) -> Vec { + let mut count_stmt = connection + .prepare("select tags, flds from notes where mid == ?1;") + .unwrap(); + + count_stmt.query_map((group_id as i64, ), |row| { + Ok(ImportNote { + tags: row.get(0)?, + fields: row.get::(1)? + .split('') + .map(|x| x.to_string()) + .collect() + }) + }).unwrap().map(|x| x.unwrap()).collect::>() +} diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 24a5e36..46ff5a5 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,12 +1,11 @@ use crate::lang::{WordData, WordGroup}; -use rusqlite::Connection; +use rusqlite::{params, Connection}; use std::collections::HashMap; pub fn add_word(word: &mut WordData, connection: &Connection) { let index = connection .query_row( - "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5\ - ) RETURNING id", + "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id", ( &word.key, &word.value, @@ -24,6 +23,40 @@ pub fn add_word(word: &mut WordData, connection: &Connection) { word.id = index; } +pub fn add_words(words: &mut[WordData], connection: &mut Connection) { + let tx = connection.transaction().unwrap(); + let count = words.len(); + + { + let mut stmt = tx.prepare( + "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5)", + ).unwrap(); + + + for word in words.iter() { + stmt.execute(params![word.key, word.value, word.tags, serde_json::to_string(&word.additional).unwrap(), word.group_id]).unwrap(); + } + } + + tx.commit().unwrap(); + + let last_index: u32 = connection + .query_one( + "SELECT seq from sqlite_sequence WHERE name == ?1", + ("words".to_string(),), + |row| row.get(0), + ) + .unwrap(); + + let start_index = last_index - (count as u32) + 1; + + let mut index = 0; + for id in start_index..=last_index { + words[index].id = id; + index += 1; + } +} + pub fn update_word(word: &mut WordData, connection: &Connection) { if word.id == 0 { add_word(word, &connection); @@ -126,7 +159,7 @@ pub fn update_group(group: &mut WordGroup, connection: &Connection) { } else { connection .execute( - "UPDATE word_group SET name = ?1 WHERE id = ?5", + "UPDATE word_group SET name = ?1 WHERE id = ?2", (&group.name, &group.id), ) .unwrap_or_else(|e| { diff --git a/src/dictionary.rs b/src/dictionary.rs index 6095013..474258c 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -1,6 +1,7 @@ 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}; @@ -14,7 +15,8 @@ use iced::widget::button::{danger, text}; use iced::widget::space::horizontal; use iced::widget::text_input::default; use iced::widget::*; -use iced::{Border, Color, Length, Shadow, Task}; +use iced::{Border, Color, Shadow, Task}; +use iced_core::Length::Fill; use rand::random_range; use std::collections::{HashMap, HashSet}; use std::fs; @@ -22,8 +24,6 @@ use std::ops::Add; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use iced_core::Length::Fill; -use crate::import::ImportState; #[derive(Clone)] pub struct DictionaryState { @@ -61,7 +61,7 @@ pub enum DictionaryMessage { DeleteGroup, ChangeDirection, TrySave(usize), - ToImport + ToImport, } impl NavigatedPage for DictionaryState { @@ -101,12 +101,18 @@ impl NavigatedPage for DictionaryState { } } if let ToImport = message { - return Some(Import(ImportState::new(self.state.clone()))) + return Some(Import(ImportState::new(self.state.clone()))); } None } - fn navigated(&mut self) {} + 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 { @@ -227,15 +233,23 @@ impl NavigatedPage for DictionaryState { if self.selected_group_index == 0 { return Task::none(); } - let state = &mut self.state.lock().unwrap(); + { + let state = &mut self.state.lock().unwrap(); - if let Some(group) = state.word_groups.get(self.selected_group_index) { - let connection = &state.connection; + 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); - self.selected_group_index = 0; + 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; @@ -262,8 +276,9 @@ impl NavigatedPage for DictionaryState { iced::widget::column![ self.groups_panel(), row![horizontal().width(8), self.words_list(),], - row![button("Добавить слово").style(jl_button).on_press(NewWord), - horizontal().width(Fill), + row![ + button("Добавить слово").style(jl_button).on_press(NewWord), + horizontal().width(Fill), button("Импорт").style(text).on_press(ToImport), ] ] @@ -319,21 +334,25 @@ impl DictionaryState { fn words_list(&self) -> iced::Element<'_, DictionaryMessage> { let time = Instant::now(); - let mut col = Column::new().width(Length::Fill); + let mut col = Column::new().width(Fill); - let mut range = (0..self.include_map.len()).collect::>(); let state = self.state.lock().unwrap(); let group_id = state.word_groups[self.selected_group_index].id; - let dict = &mut state.dictionary.clone(); - if self.reverse_list { - dict.reverse() - } else { - range = range.iter().rev().map(|x| *x).collect::>(); - } + 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; + } - for word in dict { - let i = range.pop().unwrap(); if !self.search.is_empty() { if word.key.contains(&self.search) == false && word.value.contains(&self.search) == false @@ -343,10 +362,6 @@ impl DictionaryState { } } - if word.group_id != group_id { - continue; - } - let word_line_data = WordLineState { is_included: self.include_map[i], key: word.key.clone(), @@ -356,9 +371,11 @@ impl DictionaryState { index: i, }; + index += 1; + let lazy_line = lazy(word_line_data, move |data| { let index = data.index; - let mut line = Row::new().width(Length::Fill).align_y(Center); + let mut line = Row::new().width(Fill).align_y(Center); line = line.push( checkbox(data.is_included) .label("") @@ -369,7 +386,7 @@ impl DictionaryState { line = line.push( text_input("Слово", &data.key) .size(ACCENT_FONT_SIZE) - .width(Length::Fill) + .width(Fill) .on_input(move |string| SetKey(index, string)) .on_submit(SubmitWord(index)) .style(|x, status| { @@ -381,7 +398,7 @@ impl DictionaryState { line = line.push( text_input("Перевод", &data.value) .size(ACCENT_FONT_SIZE) - .width(Length::Fill) + .width(Fill) .on_input(move |string| SetValue(index, string)) .on_submit(SubmitWord(index)) .style(|x, status| { @@ -394,7 +411,7 @@ impl DictionaryState { line = line.push( text_input("Теги", &data.tags) .size(ACCENT_FONT_SIZE) - .width(Length::Fill) + .width(Fill) .on_input(move |string| SetTags(index, string)) .on_submit(SubmitWord(index)) .style(|x, status| { @@ -420,14 +437,16 @@ impl DictionaryState { button("").on_press(WordAction(index)).width(15) }; line = line.push(line_button()).push(space().width(10)); + println!("Updating word {}", &data.key); line }); col = col.push(lazy_line); } + println!("Drawing {index} lines"); println!("Words rendering time: {:?}", time.elapsed()); - scrollable(col).height(Length::Fill).into() + scrollable(col).height(Fill).into() } fn filters(&self) -> iced::Element<'_, DictionaryMessage> { @@ -436,7 +455,7 @@ impl DictionaryState { iced::widget::column![ text_input("Поиск", &self.search) .on_input(Search) - .width(Length::Fill), + .width(Fill), text!("Всего слов: {}", dict.len()), text!( "Выбрано слов: {}", @@ -449,10 +468,10 @@ impl DictionaryState { toggler(self.reverse) .label("Обратный тест") .on_toggle(SetReverse), - button(text!("Тест").center().width(Length::Fill)) + button(text!("Тест").center().width(Fill)) .style(cta_button) .on_press(Test) - .width(Length::Fill), + .width(Fill), ] .width(250) .spacing(DEFAULT_SPACING) @@ -460,7 +479,7 @@ impl DictionaryState { } fn tags_selector(&self) -> iced::Element<'_, DictionaryMessage> { - let mut col = Column::new().width(Length::Fill); + let mut col = Column::new().width(Fill); col = col.push( button("Сбросить") .on_press(ResetTags) @@ -482,7 +501,7 @@ impl DictionaryState { ) } - container(scrollable(col)).height(Length::Fill).into() + container(scrollable(col)).height(Fill).into() } fn update_tags(&mut self) { @@ -535,7 +554,7 @@ impl DictionaryState { .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 + tags.iter().all(|t| include_tags.contains(t)) && tags.len() != 0 && word_group_id == group_id }) .collect(); @@ -562,7 +581,7 @@ impl DictionaryState { let group = state.word_groups[self.selected_group_index].clone(); iced::widget::column![ - scrollable(row).width(Length::Fill).horizontal(), + scrollable(row).width(Fill).horizontal(), row![ button("⇳").on_press(ChangeDirection).style(jl_button), text_input("Название группы слов", &group.name) @@ -570,12 +589,20 @@ impl DictionaryState { .width(250) .on_submit(SaveGroup), horizontal(), - button("Удалить").style(danger).on_press(DeleteGroup), + 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 { diff --git a/src/import.rs b/src/import.rs index 7ee258a..b195c80 100644 --- a/src/import.rs +++ b/src/import.rs @@ -1,13 +1,21 @@ -use crate::data_provider::import::{load_groups, ImportData}; +use crate::AppState; +use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups}; +use crate::data_provider::words::{add_group, add_words}; use crate::dictionary::app_data_dir; use crate::import::ImportMessage::*; +use crate::lang::{WordData, WordGroup}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; -use crate::AppState; -use iced::widget::scrollable; -use iced::widget::{button, column, row, text}; +use iced::widget::button::danger; +use iced::widget::container::success; +use iced::widget::{ + Row, button, checkbox, column, container, progress_bar, row, rule, text, text_input, +}; +use iced::widget::{scrollable, space}; use iced::{Element, Task}; use iced_core::Alignment::Center; +use iced_core::Length::Fill; +use iced_core::Padding; use rfd::AsyncFileDialog; use rusqlite::Connection; use std::fs::{File, OpenOptions}; @@ -17,12 +25,19 @@ use std::sync::{Arc, Mutex}; use tokio::task::spawn_blocking; use zip::ZipArchive; +const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; + #[derive(Clone)] pub struct ImportState { state: Arc>, path: Option, import_data: Option, selected_index: usize, + selected_property: Option, + custom_property_name: String, + skip_empty: bool, + separator: String, + progress: Option>>, } #[derive(Clone)] @@ -31,10 +46,27 @@ pub enum ImportMessage { SelectFile, UpdateFile(PathBuf), UpdateImport(ImportData), + NextGroup, + PreviousGroup, + OpenPropertyMapper(String), + EditCustomProperty(String), + SetupCustomProperty, + SetupDefaultProperty(String), + SetupDirect, + RemoveMapping(String), + SwitchSkipEmpty(bool), + EditSeparator(String), + StartImport, + NextProgress, + ImportFinished, } impl NavigatedPage for ImportState { fn navigate(&self, message: &ImportMessage) -> Option { + if let Some(_) = self.progress { + return None; + } + if let Back = message { return Some(Page::PreviousPage); } @@ -54,41 +86,248 @@ impl NavigatedPage for ImportState { UpdateImport(import) => { self.import_data = Some(import); } + NextGroup => { + self.selected_index += 1; + self.selected_property = None; + self.custom_property_name.clear(); + } + PreviousGroup => { + self.selected_index -= 1; + self.selected_property = None; + self.custom_property_name.clear(); + } + OpenPropertyMapper(prop) => self.selected_property = Some(prop), + EditCustomProperty(prop_name) => self.custom_property_name = prop_name, + SetupCustomProperty => { + self.setup_mapping(self.custom_property_name.clone()); + } + SetupDefaultProperty(prop_name) => { + self.setup_mapping(prop_name); + } + SetupDirect => { + self.setup_mapping(self.selected_property.clone().unwrap()); + } + RemoveMapping(pro_name) => { + self.remove_mapping(pro_name); + } + SwitchSkipEmpty(skip) => { + self.skip_empty = skip; + } + EditSeparator(separator) => { + self.separator = separator.clone(); + } + StartImport => { + self.progress = Some(Arc::new(Mutex::new(0.0))); + return Task::batch([self.start_import(), self.next_progress()]); + } + NextProgress => { + if self.progress.is_none() { + return Task::none(); + } + return self.next_progress(); + } + ImportFinished => { + self.progress = None; + let group = self + .import_data.as_mut() + .unwrap() + .0 + .get_mut(self.selected_index) + .unwrap(); + group.imported = true; + } } Task::none() } fn view(&self) -> Element<'_, ImportMessage> { back_overlay( - scrollable( - column![ - row![ - button("Выбрать файл").style(jl_button).on_press(SelectFile), - text!("{}", { - if let Some(path) = &self.path { - path.to_string_lossy().to_string() - } else { - "Файл не выбран".to_string() - } - }) - ] - .align_y(Center) - .spacing(DEFAULT_SPACING) - ] - .spacing(DEFAULT_SPACING), - ) - .into(), + { + if let Some(value) = &self.progress { + column![progress_bar(0f32..=1f32, *value.lock().unwrap()),] + .spacing(QUARTER_SPACING) + .into() + } else { + scrollable( + column![ + row![ + button("Выбрать файл").style(jl_button).on_press(SelectFile), + text!("{}", { + if let Some(path) = &self.path { + path.to_string_lossy().to_string() + } else { + "Файл не выбран".to_string() + } + }) + ] + .align_y(Center) + .spacing(DEFAULT_SPACING), + self.selected_group() + ] + .spacing(DEFAULT_SPACING), + ) + .into() + } + }, Back, ) } } + +impl ImportState { + fn selected_group(&self) -> Element<'_, ImportMessage> { + if let Some(data) = &self.import_data { + let group = &data.0[self.selected_index]; + return column![ + row![ + self.back_button(), + space().width(Fill), + text!("{}", group.name.clone()), + space().width(Fill), + self.next_button() + ] + .width(Fill), + text!("Количество карточек: {}", group.length), + self.property_mapper(group), + self.import_settings(), + self.import_button(group), + ] + .spacing(DEFAULT_SPACING) + .into(); + } + space().into() + } + + fn import_button(&self, group: &ImportGroup) -> Element<'_, ImportMessage> { + if group.imported { + return container(text!("Группа слов успешно импортирована")).padding(HALF_SPACING).align_x(Center) + .style(success) + .width(Fill) + .into(); + } + button("Начать импорт") + .style(cta_button) + .on_press(StartImport) + .into() + } + + fn property_mapper(&self, group: &ImportGroup) -> Element<'_, ImportMessage> { + column![ + self.property_list(group), + rule::horizontal(2), + self.property_selector() + ] + .spacing(HALF_SPACING) + .into() + } + + fn property_list(&self, group: &ImportGroup) -> Element<'_, ImportMessage> { + let mut row = Row::new(); + for prop in &group.fields { + let primary_button_name = (*prop).clone(); + if let Some(mapped) = group.mapping.get(prop.clone().as_str()) { + row = row.push( + column![ + button(text!("{}", primary_button_name)), + "↕", + button(text!("{}", mapped)) + .style(danger) + .on_press(RemoveMapping(mapped.as_str().to_string())), + ] + .align_x(Center), + ); + } else { + row = row.push( + button(text!("{}", primary_button_name)) + .on_press(OpenPropertyMapper(prop.clone())), + ); + } + } + scrollable(row.spacing(HALF_SPACING).padding(Padding { + top: 0.0, + right: 0.0, + bottom: DEFAULT_SPACING, + left: 0.0, + })) + .horizontal() + .into() + } + + fn property_selector(&self) -> Element<'_, ImportMessage> { + if self.selected_property == None { + return space().into(); + } + column![ + self.available_properties(), + button("Добавить напрямую").on_press(SetupDirect), + row![ + text_input("Пользовательское поле", &self.custom_property_name) + .on_input(EditCustomProperty), + button("Установить привязку").on_press(SetupCustomProperty) + ] + .spacing(QUARTER_SPACING) + ] + .spacing(HALF_SPACING) + .into() + } + + fn available_properties(&self) -> Element<'_, ImportMessage> { + let mut row = Row::new(); + + for default in DEFAULT_FIELDS { + // if group.mapping.values().any(|name| name == default) { + // continue; + // } + row = row.push( + button(text!("{default}")).on_press(SetupDefaultProperty(default.to_string())), + ); + } + row.spacing(HALF_SPACING).into() + } + + fn import_settings(&self) -> Element<'_, ImportMessage> { + column![ + checkbox(self.skip_empty) + .label("Пропускать пустые строки") + .on_toggle(SwitchSkipEmpty), + column![ + text!("Разделитель при множественном объединении"), + text_input("", &self.separator).on_input(EditSeparator) + ] + ] + .spacing(DEFAULT_SPACING) + .into() + } + + fn back_button(&self) -> Element<'_, ImportMessage> { + let mut button = button("←").style(button::text); + if self.selected_index > 0 { + button = button.on_press(PreviousGroup); + } + button.into() + } + + fn next_button(&self) -> Element<'_, ImportMessage> { + let mut button = button("→").style(button::text); + if self.selected_index < self.import_data.as_ref().unwrap().0.len() - 1 { + button = button.on_press(NextGroup); + } + button.into() + } +} + impl ImportState { pub fn new(state: Arc>) -> ImportState { ImportState { state, path: None, import_data: None, - selected_index: 0 + selected_index: 0, + selected_property: None, + custom_property_name: "".to_string(), + skip_empty: true, + separator: ", ".to_string(), + progress: None, } } @@ -161,4 +400,155 @@ impl ImportState { let data = load_groups(&connection); Ok(data) } + fn setup_mapping(&mut self, property_name: String) { + let group = self.import_data.as_mut().unwrap(); + let group = group.0.get_mut(self.selected_index).unwrap(); + group + .mapping + .insert(self.selected_property.clone().unwrap(), property_name); + self.selected_property = None; + self.custom_property_name.clear(); + } + fn remove_mapping(&mut self, property_name: String) { + let group = self.import_data.as_mut().unwrap(); + let group = group.0.get_mut(self.selected_index).unwrap(); + let remove_key = group + .mapping + .keys() + .find(|key| group.mapping[*key] == property_name) + .unwrap(); + group.mapping.remove(remove_key.clone().as_str()); + self.selected_property = None; + self.custom_property_name.clear(); + } + + fn start_import(&self) -> Task { + let state = self.state.clone(); + let separator = self.separator.clone(); + let skip_empty = self.skip_empty; + let group = self.import_data.as_ref().unwrap().0[self.selected_index].clone(); + let progress = self.progress.clone().unwrap(); + println!("{}", group.name); + Task::perform( + async move { + spawn_blocking(move || { + let temp_file_path = app_data_dir().join("import"); + let connection = Connection::open(&temp_file_path).map_err(|_| ())?; + let import_list = get_words_of_group(&connection, group.id); + let mut words_list = Vec::with_capacity(import_list.len()); + + let map_indices = Self::get_mapping_indices(&group); + let mut group_entity = WordGroup { + id: 0, + name: group.name.clone(), + }; + { + let state = state.lock().unwrap(); + add_group(&mut group_entity, &state.connection); + } + + let group_id = group_entity.id; + for import in import_list { + let mut word = WordData::new(); + + word.tags = import.tags.trim().replace(" ", ", "); + word.group_id = group_id.clone(); + for (dest, indices) in &map_indices { + let collected_string = Self::collect_strings( + &import.fields, + &indices, + &separator, + skip_empty, + ); + match dest.as_str() { + "key" => { + word.key = collected_string; + } + "value" => { + word.value = collected_string; + } + "tags" => { + word.tags = collected_string; + } + &_ => { + word.additional.insert((*dest).clone(), collected_string); + } + } + } + + println!("{word:?}"); + words_list.push(word); + } + + let mut state = state.lock().unwrap(); + let connection = &mut state.connection; + let mut index = 0; + let total_len = words_list.len() as f32 / 256.0; + for word in &mut words_list.chunks_mut(256) { + add_words(word, connection); + index += 1; + let mut progress = progress.lock().unwrap(); + *progress = index as f32 / total_len; + } + + state.dictionary.append(&mut words_list); + state.word_groups.push(group_entity); + + Ok(()) + }) + .await + .unwrap() + }, + |_: Result<(), ()>| RootMessage::Import(ImportFinished), + ) + } + + fn get_mapping_indices(group: &ImportGroup) -> Vec<(String, Vec)> { + let mut final_props = group.mapping.values().cloned().collect::>(); + final_props.sort(); + final_props.dedup(); + let mut result = final_props + .iter() + .map(|name| (name.clone(), Vec::::with_capacity(1))) + .collect::>(); + for key in group.mapping.keys() { + let endpoint = group.mapping.get(key).unwrap(); + let property_index = group.fields.iter().position(|f| f == key).unwrap(); + let group_index = result + .iter() + .position(|(name, _)| name == endpoint) + .unwrap(); + result.get_mut(group_index).unwrap().1.push(property_index); + } + + result + } + + fn collect_strings( + properties: &Vec, + indices: &Vec, + separator: &str, + skip_empty: bool, + ) -> String { + let mut working_words = Vec::with_capacity(indices.len()); + for i in 0..indices.len() { + let index = indices[i]; + let str = properties.get(index).unwrap(); + if skip_empty && str.is_empty() { + continue; + } + working_words.push(str.as_str()); + } + + working_words.join(separator) + } + + fn next_progress(&self) -> Task { + Task::perform( + async { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + }, + |_| RootMessage::Import(NextProgress), + ) + } } diff --git a/src/main.rs b/src/main.rs index 8f5bbab..887859c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,10 @@ use iced_core::Size; use rusqlite::Connection; use std::collections::HashMap; use iced_core::window::settings::PlatformSpecific; +use mimalloc::MiMalloc; + +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; const USER_FONT: Font = Font::with_name("Noto Sans JP"); diff --git a/src/repetition.rs b/src/repetition.rs index 022db20..9219910 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -199,6 +199,7 @@ impl RepetitionState { "speech" => self.draw_voice(), "reading" => self.draw_reading(word), "context" => self.draw_context(word), + "description" => self.draw_description(word), _ => space().into(), }) } @@ -274,6 +275,12 @@ impl RepetitionState { Some(context) => text!("{}", context).size(24).into(), } } + fn draw_description(&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/word.rs b/src/word.rs index 1dd9c9e..e5046ef 100644 --- a/src/word.rs +++ b/src/word.rs @@ -26,8 +26,7 @@ impl NavigatedPage for WordState { } } - fn navigated(&mut self) { - } + fn navigated(&mut self) {} fn update(&mut self, message: WordMessage) -> Task { match message { Back => {} @@ -123,12 +122,11 @@ impl NavigatedPage for WordState { ] .spacing(DEFAULT_SPACING) ] - .spacing(DEFAULT_SPACING) - .into(), + .spacing(DEFAULT_SPACING) + .into(), Back, ) } - } impl WordState { @@ -138,38 +136,36 @@ impl WordState { } impl WordState { - fn get_view_for_more(&self, value: (&String, &String)) -> Element<'_, WordMessage> { match value.0.as_str() { - "reading" => self.reading_field(value), - "description" => self.description_field(value), - "context" => self.context_field(value), - _ => space().into(), + "reading" => self.reading_field(value.1), + "description" => self.description_field(value.1), + "context" => self.context_field(value.1), + _ => self.additional_field(value.1, value.0.clone(), value.0.clone()), } } - fn reading_field(&self, value: (&String, &String)) -> Element<'_, WordMessage> { + fn reading_field(&self, value: &String) -> Element<'_, WordMessage> { self.additional_field(value, "Чтение слова".to_string(), "reading".to_string()) } - fn description_field(&self, value: (&String, &String)) -> Element<'_, WordMessage> { + fn description_field(&self, value: &String) -> Element<'_, WordMessage> { self.additional_field(value, "Описание".to_string(), "description".to_string()) } - fn context_field(&self, value: (&String, &String)) -> Element<'_, WordMessage> { + fn context_field(&self, value: &String) -> Element<'_, WordMessage> { self.additional_field(value, "В контексте".to_string(), "context".to_string()) } - fn additional_field( &self, - value: (&String, &String), + value: &String, name: String, id: String, ) -> Element<'_, WordMessage> { column![ text!("{}", name), row![ - text_input(id.clone().as_str(), &value.1) + text_input(id.clone().as_str(), value) .on_input({ let value = id.clone(); move |string| SetAdditional(value.clone(), string)