From f2035ca749c9a32c80d5df1123b6f286dc5396fb Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 2 Sep 2026 00:19:58 +0300 Subject: [PATCH] Partial dictionary rewrite --- Cargo.lock | 14 +- Cargo.toml | 4 +- src/data/entities.rs | 61 +----- src/data/repositories.rs | 97 ++++++++- src/data_provider/decks.rs | 86 +------- src/data_provider/word_groups.rs | 6 +- src/data_provider/words.rs | 5 +- src/pages/dictionary.rs | 363 ++++++++++++++----------------- src/pages/dictionary_quiz.rs | 17 +- src/pages/repetition_settings.rs | 1 - src/pages/repetitions.rs | 1 - src/pages/word.rs | 12 +- 12 files changed, 304 insertions(+), 363 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4eab091..cbed46f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1531,11 +1531,11 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsrs" -version = "6.6.1" +version = "6.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a99ea3dec9af37c9ed3835463ff6820a578a0026a8e370b4c1c0db48dfab3f" +checksum = "89836c0a26b347b585b23824034600b69bfc7f0b0112d0cd656791799eb3a717" dependencies = [ - "itertools 0.14.0", + "itertools 0.15.0", "log", "ndarray", "priority-queue", @@ -2430,9 +2430,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" dependencies = [ "either", ] @@ -4236,9 +4236,9 @@ dependencies = [ [[package]] name = "rhai" -version = "1.25.1" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4dd0f8c36625202a4ba553c416c19b719947cd2a31d1bda06126e4a5727daf" +checksum = "5e6e1be9d697d537ce450766df42ad6adf1a93f25f21d73217354ad7e3d3dae1" dependencies = [ "ahash", "bitflags 2.10.0", diff --git a/Cargo.toml b/Cargo.toml index 7f51155..d6deef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ iced_core = "0.14.0" rand = "0.10.2" dirs = "6.0.0" serde_json = "1.0.151" -rhai = { version = "1.25.1", features = ["sync"] } +rhai = { version = "1.26.0", features = ["sync"] } chrono = "0.4.45" rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] } reqwest = { version = "0.13.4", features = ["json", "stream", "multipart"] } @@ -26,7 +26,7 @@ rayon = "1.12.0" parking_lot = "0.12.5" env_logger = "0.11.11" log = "0.4.34" -fsrs = "6.6.1" +fsrs = "6.6.2" litemap = { version = "0.8.3", features = ["serde"] } [profile.super-release] diff --git a/src/data/entities.rs b/src/data/entities.rs index 3e90dc8..8aab411 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -6,9 +6,9 @@ use chrono::{DateTime, NaiveDate, Utc}; use fsrs::MemoryState; use litemap::LiteMap; use log::info; -use rayon::prelude::*; -use rhai::{Engine, Scope}; +use rhai::Engine; use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::time::Instant; @@ -77,8 +77,6 @@ pub struct DeckSettings { pub forward: String, pub backward: String, pub filter: String, - pub count: Option, - pub open_mode: OrderMode, } impl EntityData for DeckSettings { @@ -99,8 +97,6 @@ impl DeckSettings { forward: "".to_string(), backward: "".to_string(), filter: "true".to_string(), - count: None, - open_mode: OrderMode::Default, } } pub fn check_filter(&self) -> bool { @@ -110,50 +106,6 @@ impl DeckSettings { info!("Compile time is {:?}", time.elapsed()); ast.is_ok() } - pub fn get_word_list(&self, state: &AppState) -> Vec { - todo!() - // let time = Instant::now(); - // let mut list = vec![]; - // let engine = Engine::new(); - // - // let ast = engine.compile(&self.filter); - // if let Ok(ast) = ast { - // let groups = state.word_groups.read(); - // let query = state - // .dictionary.read().query_par(| word: &WordData|{ - // let mut more = rhai::Map::new(); - // for iced in &word.additional { - // more.insert(iced.0.clone().into(), iced.1.clone().into()); - // } - // let mut scope = Scope::new(); - // let group_name = groups.get_ref(&mut groups.handle_by_id(word.group_id)).unwrap().name; - // - // scope - // .push_constant("id", word.id.0 as i64) - // .push_constant("key", word.key.clone()) - // .push_constant("value", word.value.clone()) - // .push_constant("tags", word.tags.clone()) - // .push_constant("more", more) - // .push_constant( - // "group", - // group_name, - // ); - // - // let result = engine.eval_ast_with_scope::(&mut scope, &ast); - // if let Ok(result) = result { - // return result; - // } - // false - // }); - // - // - // info!("Collecting available words is {:?}", time.elapsed()); - // - // list - // } else { - // list - // } - } pub fn require_speech(&self) -> bool { self.forward == "speech" || self.backward == "speech" } @@ -213,7 +165,6 @@ pub struct CustomField { pub name: String, } -#[derive(Copy)] pub struct EntityHandle { pub(crate) id: Id, index: u32, @@ -229,6 +180,7 @@ impl Clone for EntityHandle { } } } +impl Copy for EntityHandle {} impl EntityHandle { pub fn new(id: Id, index: usize, real: bool) -> Self { Self { @@ -280,6 +232,13 @@ impl Ord for EntityHandle { } } +impl Hash for EntityHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.real().hash(state); + } +} + pub trait EntityData { fn id(&self) -> Id; fn set_id(&mut self, id: Id); diff --git a/src/data/repositories.rs b/src/data/repositories.rs index 56c9c1d..73b1fe5 100644 --- a/src/data/repositories.rs +++ b/src/data/repositories.rs @@ -8,8 +8,9 @@ use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rusqlite::Connection; use std::marker::PhantomData; -use std::ops::Deref; +use std::ops::{Deref, Index, IndexMut}; use std::rc::Rc; +use std::slice::Iter; use std::sync::Arc; use crate::data_provider::sqlite::SqliteConnection; @@ -20,6 +21,9 @@ where fn get(&self, handle: &mut EntityHandle) -> Option; fn get_ref(&self, handle: &mut EntityHandle) -> Option<&T>; fn get_mut(&mut self, handle: &mut EntityHandle) -> Option<&mut T>; + fn get_static(&self, handle: EntityHandle) -> Option; + fn get_ref_static(&self, handle: EntityHandle) -> Option<&T>; + fn get_mut_static(&mut self, handle: EntityHandle) -> Option<&mut T>; fn handle_by_id(&self, id: Id) -> EntityHandle; fn create(&mut self, new: T) -> EntityHandle; fn create_now(&mut self, new: T) -> Result, NotificationData>; @@ -30,6 +34,8 @@ where fn start(&mut self) -> MaybeError; fn reload(&mut self) -> MaybeError; fn shifts(&self) -> usize; + fn iter<'a>(&'a self) -> impl Iterator where T: 'a; + fn iter_mut<'a>(&'a mut self) -> impl Iterator where T: 'a; } struct QueryManger @@ -122,10 +128,25 @@ impl, D: DbLayer> Qu .filter_map(move |e| data_repository_inner.get_ref(e)) } - pub fn for_each_mut(&mut self, repo: &mut R, mut f: F) + pub fn iter_with_handles<'a>(&mut self, data_repository_inner: &'a DataRepositoryInner) -> impl Iterator)> + where + T: 'a, + { + if self.shift_stamp != data_repository_inner.shifts() { + self.load(data_repository_inner); + } + self.cache + .iter_mut() + .filter_map(move |e| Some((data_repository_inner.get_ref(e)?, e.clone()))) + } + + pub fn for_each_mut(&mut self, repo: &mut DataRepositoryInner, mut f: F) where F: FnMut(&mut T), { + if self.shift_stamp != repo.shifts() { + self.load(repo); + } for handle in &mut self.cache { if let Some(entity) = repo.get_mut(handle) { f(entity); @@ -166,6 +187,28 @@ impl, D: DbLayer> Qu .collect::>(), ); } + + pub fn handles(&self, data_repository: &DataRepositoryInner) -> impl Iterator> { + self.cache.iter().cloned() + } + + pub fn len(&self) -> usize { + self.cache.len() + } +} + +impl, D: DbLayer> Index for Query { + type Output = EntityHandle; + + fn index(&self, index: usize) -> &Self::Output { + &self.cache[index] + } +} + +impl, D: DbLayer> IndexMut for Query { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.cache[index] + } } pub trait BatchedRepository @@ -300,6 +343,30 @@ impl> DataRepositoryInner { shift_stamp: self.shifts, } } + + pub fn empty_query(&self) -> Query, D> { + Query{ + cache: vec![], + rule: Arc::new(|_| false), + parallel: false, + _marker: Default::default(), + shift_stamp: 0, + } + } + + pub fn empty_query_par(&self) -> Query, D> { + Query{ + cache: vec![], + rule: Arc::new(|_| false), + parallel: true, + _marker: Default::default(), + shift_stamp: 0, + } + } + + pub fn len(&self) -> usize { + self.cache.len() + self.temporal.len() + } } impl> Repository for DataRepositoryInner { @@ -333,6 +400,18 @@ impl> Repository for DataRepositoryIn Some(&mut slice[index]) } + fn get_static(&self, mut handle: EntityHandle) -> Option { + self.get(&mut handle) + } + + fn get_ref_static(&self, mut handle: EntityHandle) -> Option<&T> { + self.get_ref(&mut handle) + } + + fn get_mut_static(&mut self, mut handle: EntityHandle) -> Option<&mut T> { + self.get_mut(&mut handle) + } + fn handle_by_id(&self, id: Id) -> EntityHandle { EntityHandle::new(id, id.0 as usize, true) } @@ -439,6 +518,20 @@ impl> Repository for DataRepositoryIn fn shifts(&self) -> usize { self.shifts } + + fn iter<'a>(&'a self) -> impl Iterator + where + T: 'a + { + self.cache.iter().chain(self.temporal.iter()) + } + + fn iter_mut<'a>(&'a mut self) -> impl Iterator + where + T: 'a + { + self.cache.iter_mut().chain(self.temporal.iter_mut()) + } } impl + DbLayer> BatchedRepository for DataRepositoryInner diff --git a/src/data_provider/decks.rs b/src/data_provider/decks.rs index f498b20..f9bc21d 100644 --- a/src/data_provider/decks.rs +++ b/src/data_provider/decks.rs @@ -1,12 +1,7 @@ -use crate::core::srs::OrderMode; -use crate::data::entities::{CardState, DeckSettings}; -use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; -use crate::notification::NotificationData::{DbLoadError, DbSaveError}; -use crate::notification::{MaybeError, NotificationData}; -use chrono::NaiveDate; -use rusqlite::{Connection, Error, Params, Row}; +use crate::data::entities::DeckSettings; use crate::data::repositories::DataRepository; -use crate::data_provider::card_states::CardStatesQuerySet; +use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; +use rusqlite::{Error, Params, Row}; pub type DeckRepository = DataRepository>; @@ -36,8 +31,6 @@ impl TableAccessQuerySet for DeckQuerySet { forward: row.get(2)?, backward: row.get(3)?, filter: row.get(4)?, - count: None, - open_mode: OrderMode::Default, }) } @@ -59,75 +52,4 @@ impl TableAccessQuerySet for DeckQuerySet { entity.id, ) } -} - -pub fn load_sets(connection: &Connection) -> Result, NotificationData> { - let mut stmt = connection - .prepare("SELECT id, name, forward, backward, filter FROM decks") - .expect("Failed to prepare state query"); - let iter = stmt - .query_map([], |row| { - Ok(DeckSettings { - id: row.get(0)?, - name: row.get(1)?, - forward: row.get(2)?, - backward: row.get(3)?, - filter: row.get(4)?, - count: None, - open_mode: OrderMode::Default, - }) - }) - .map_err(|_| DbLoadError)?; - - let mut buffer = Vec::with_capacity(16); - iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i)); - Ok(buffer) -} - -pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> MaybeError { - let index : u32 = connection - .query_row( - "INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", - ( - &set.name, - &set.forward, - &set.backward, - &set.filter, - ), - |row| row.get(0) - ) - .map_err(|_| DbSaveError)?; - - set.id = index.into(); - Ok(()) -} - -pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> MaybeError { - if !set.id.is_valid() { - add_set(set, connection)?; - } else { - connection - .execute( - "UPDATE decks SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5", - ( - &set.name, - &set.forward, - &set.backward, - &set.filter, - &set.id - ), - ) - .map_err(|_| DbSaveError)?; - } - Ok(()) -} - -pub fn delete_set(set: &DeckSettings, connection: &Connection) -> MaybeError { - if !set.id.is_valid() { - return Err(DbSaveError); - } - connection - .execute("DELETE FROM decks WHERE id = ?1", (&set.id,)) - .map_err(|_| DbSaveError)?; - Ok(()) -} +} \ No newline at end of file diff --git a/src/data_provider/word_groups.rs b/src/data_provider/word_groups.rs index 93fe5f2..c6e67fc 100644 --- a/src/data_provider/word_groups.rs +++ b/src/data_provider/word_groups.rs @@ -1,10 +1,12 @@ use crate::data::entities::{DeckSettings, WordGroup}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use rusqlite::{Error, Params, Row}; -use crate::data::repositories::DataRepository; +use crate::data::repositories::{DataRepository, DataRepositoryInner}; use crate::data_provider::decks::DeckQuerySet; -pub type WordGroupRepository = DataRepository>; +pub type WordGroupRepository = DataRepository; +pub type WordGroupRepositoryInner = DataRepositoryInner; +pub type WordGroupDataLayer = SqliteDbLayer; #[derive(Clone)] pub struct WordGroupQuerySet; diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 45f780e..82253f8 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -5,10 +5,11 @@ use crate::notification::{MaybeError, NotificationData}; use litemap::LiteMap; use rusqlite::{Connection, Error, Params, Row, params}; use std::collections::HashMap; -use crate::data::repositories::DataRepository; +use crate::data::repositories::{BatchedDbLayer, DataRepository, DataRepositoryInner}; pub type WordRepository = DataRepository>; - +pub type WordRepositoryInner = DataRepositoryInner>; +pub type WordDataLayer = SqliteDbLayer; #[derive(Clone)] pub struct WordQuerySet; impl TableAccessQuerySet for WordQuerySet { diff --git a/src/pages/dictionary.rs b/src/pages/dictionary.rs index 16bd9f3..55f81bc 100644 --- a/src/pages/dictionary.rs +++ b/src/pages/dictionary.rs @@ -1,7 +1,8 @@ use crate::app::AppState; use crate::data::Id; -use crate::data::entities::{WordData, WordGroup}; -use crate::data_provider::words::{delete_group, delete_word, update_group, update_word}; +use crate::data::entities::{EntityHandle, WordData, WordGroup}; +use crate::data::repositories::{Query, Repository}; +use crate::data_provider::words::{WordRepository, WordDataLayer, WordQuerySet, WordRepositoryInner}; use crate::navigation::Page::{Import, Word}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::pages::dictionary::DictionaryMessage::*; @@ -22,37 +23,38 @@ use iced::widget::*; use iced::{Border, Color, Shadow, Task}; use iced_core::Length::Fill; use log::info; -use parking_lot::Mutex; use rand::random_range; use std::ops::Add; use std::sync::Arc; use std::time::{Duration, Instant}; +use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner}; -#[derive(Clone)] pub struct DictionaryState { state: AppState, - include_map: Vec, + included: HashSet>, tag_map: HashMap, + words_tags: Vec<(EntityHandle, Vec)>, reverse: bool, search: String, no_typing: bool, - selected_group_index: usize, + selected_group: EntityHandle, reverse_list: bool, - auto_save_queue: HashMap>, + auto_save_queue: HashMap, DateTime>, + displayed_words_query: Query, + groups_query: Query, } -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum DictionaryMessage { Back, - SetTags(usize, String), - SetKey(usize, String), - SetValue(usize, String), - SubmitWord(usize), - WordAction(usize), + SetTags(EntityHandle, String), + SetKey(EntityHandle, String), + SetValue(EntityHandle, String), + WordAction(EntityHandle), NewWord, - Include(usize, bool), + Include(EntityHandle, bool), IncludeTag(String, bool), - Test, + GotoTest, ResetTags, SetReverse(bool), Search(String), @@ -60,11 +62,11 @@ pub enum DictionaryMessage { CreateGroup, EditGroup(String), SaveGroup, - SelectGroup(usize), + SelectGroup(EntityHandle), DeleteGroup, ChangeDirection, - TrySave(usize), - ToImport, + TrySave(EntityHandle), + GotoImport, } impl NavigatedPage for DictionaryState { @@ -72,19 +74,15 @@ impl NavigatedPage for DictionaryState { if let Back = message { return Some(Page::PreviousPage); } - if let Test = message - && self.include_map.iter().any(|x| *x) + if let GotoTest = message + && !self.included.is_empty() { let mut words = vec![]; - let dict = &self.state.lock().dictionary; + let dict = &self.state.dictionary.read(); - words = self - .include_map - .iter() - .zip(0..self.include_map.len()) - .filter(|(flag, _)| **flag) - .map(|(_, index)| dict[index].clone()) - .collect(); + let mut query = dict.query(Arc::new(|_| true)); + let handles = query.handles(dict); + words = handles.filter(|h| self.included.contains(h)).collect(); return Some(Page::DictionaryQuiz(DictionaryQuizState::new( words, @@ -92,27 +90,19 @@ impl NavigatedPage for DictionaryState { self.no_typing, ))); } - if let WordAction(index) = message { - let word: WordData; - { - let state = self.state.lock(); - 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 WordAction(handle) = message + && handle.real() + { + return Some(Word(WordState::new(*handle, self.state.clone()))); } - if let ToImport = message { + if let GotoImport = message { return Some(Import(ImportState::new(self.state.clone()))); } None } fn navigated(&mut self) { - let len = self.state.lock().dictionary.len(); - - self.include_map = vec![false; len]; + self.included = Default::default(); self.tag_map = Default::default(); self.update_tags(); } @@ -120,34 +110,30 @@ impl NavigatedPage for DictionaryState { fn update(&mut self, message: DictionaryMessage) -> Task { match message { NewWord => { - let mut state = self.state.lock(); + let mut dictionary = self.state.dictionary.write(); 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); + word.group_id = self.selected_group.id; + dictionary.create(word); } - SetKey(i, v) => { - { - let dict = &mut self.state.lock().dictionary; - dict[i].key = v; - } - return self.launch_auto_save_offset(i); + 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(i, v) => { - { - let dict = &mut self.state.lock().dictionary; - dict[i].value = v - } - return self.launch_auto_save_offset(i); + 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 dict = &mut self.state.lock().dictionary; + let mut dictionary = self.state.dictionary.write(); - let current_tags_value = dict[i].tags.clone(); + 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() @@ -157,7 +143,7 @@ impl NavigatedPage for DictionaryState { v.remove(index); } - dict[i].tags = v; + word.tags = v; } self.update_tags(); @@ -165,93 +151,76 @@ impl NavigatedPage for DictionaryState { } WordAction(i) => { - let state = &mut self.state.lock(); - let dict = &mut state.dictionary; - let word = dict.remove(i); - self.include_map.remove(i); - self.auto_save_queue.remove(&i); - if let Err(err) = delete_word(&word, &state.connection) { + let mut dictionary = self.state.dictionary.write(); + + if let Err(err) = dictionary.delete(i) { return push_error_task!(err); } } - Include(i, b) => self.include_map[i] = b, - IncludeTag(t, v) => { - let index; - { - let state = self.state.lock(); - index = state.word_groups[self.selected_group_index].id; + 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(index) + self.update_words_include(self.selected_group.id) } ResetTags => { self.tag_map.iter_mut().for_each(|(_, v)| *v = false); - self.include_map.iter_mut().for_each(|x| *x = false) + self.included.clear(); } SetReverse(v) => self.reverse = v, Search(s) => { self.search = s; } SetTyping(b) => self.no_typing = b, - SubmitWord(i) => return self.save_word(i), Back => {} - Test => {} + GotoTest => {} CreateGroup => { - let state = &mut self.state.lock(); - - state.word_groups.push(WordGroup { + let mut groups = self.state.word_groups.write(); + groups.create(WordGroup { id: 0.into(), name: format!("Группа слов {}", random_range(100..1000)), }); } EditGroup(new) => { - let state = &mut self.state.lock(); - let group = state.word_groups.get_mut(self.selected_group_index); + 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.clone(); + group.name = new; } } SaveGroup => { - let state = &mut self.state.lock(); - let connection = &state.connection; - let mut group = state.word_groups[self.selected_group_index].clone(); + let mut groups = self.state.word_groups.write(); - if let Err(err) = update_group(&mut group, connection) { + if let Err(err) = groups.save(&mut self.selected_group) { return push_error_task!(err); } - - state.word_groups[self.selected_group_index] = group; } SelectGroup(i) => { - self.selected_group_index = i; - let index; - { - let state = self.state.lock(); - index = state.word_groups[i].id; - } - self.update_words_include(index) + self.selected_group = i; + self.update_words_include(i.id) } DeleteGroup => { - if self.selected_group_index == 0 { + if self.selected_group.id.0 == 1 { return Task::none(); } { - let state = &mut self.state.lock(); + let mut groups = self.state.word_groups.write(); + let delete_group = self.selected_group; + self.selected_group = groups.handle_by_id(Id(1)); - if let Some(group) = state.word_groups.get(self.selected_group_index) { - let remove_group_id = group.id; - let connection = &state.connection; + if let Err(err) = groups.delete(delete_group) { + return push_error_task!(err); + } - if let Err(err) = delete_group(group, connection) { - return push_error_task!(err); - } - - state.word_groups.remove(self.selected_group_index); - state - .dictionary - .retain(|word| word.group_id != remove_group_id); - self.selected_group_index = 0; + if let Err(err) = self.state.dictionary.write().reload() { + return push_error_task!(err); } } @@ -260,18 +229,21 @@ impl NavigatedPage for DictionaryState { ChangeDirection => { self.reverse_list = !self.reverse_list; } - TrySave(word_index) => { + TrySave(mut handle) => { let now = Utc::now(); - if !self.auto_save_queue.contains_key(&word_index) { + if !self.auto_save_queue.contains_key(&handle) { return Task::none(); } - if self.auto_save_queue[&word_index] <= now { - self.auto_save_queue.remove(&word_index); + if self.auto_save_queue[&handle] <= now { + self.auto_save_queue.remove(&handle); - return self.save_word(word_index); + let mut dictionary = self.state.dictionary.write(); + + let _ = dictionary.save(&mut handle); + return Task::none(); } } - ToImport => {} + GotoImport => {} } Task::none() @@ -286,7 +258,7 @@ impl NavigatedPage for DictionaryState { row![ self.add_word_button(), horizontal().width(Fill), - button("Импорт").style(text).on_press(ToImport), + button("Импорт").style(text).on_press(GotoImport), ] ] .spacing(5), @@ -301,42 +273,41 @@ impl NavigatedPage for DictionaryState { impl DictionaryState { pub fn new(state: AppState) -> Self { - let len = state.lock().dictionary.len(); - + let words_query = state.dictionary.read().empty_query_par(); + let groups_query = state.word_groups.read().query(Arc::new(|_| true)); let mut result = DictionaryState { - include_map: vec![false; len], - selected_group_index: 0, + 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, }; + result.init(); result.update_tags(); result } - fn save_word(&mut self, i: usize) -> Task { - let state = &mut self.state.lock(); - let connection = &state.connection; - let word = &mut state.dictionary[i].clone(); - - if let Err(err) = update_word(word, connection) { - return push_error_task!(err); - } - - state.dictionary[i] = word.clone(); - Task::none() + fn init(&mut self) { + self.displayed_words_query = self.state.dictionary.read().query_par(Arc::new(|word| word.group_id == Id(1))); } - fn launch_auto_save_offset(&mut self, index: usize) -> Task { + fn setup_words_query(&mut self, id: Id) { + // self.displayed_words_query = self.state.dictionary.read().query_par(Arc::new(|word| word.group_id == Self::get_id(self))); + } + + fn launch_auto_save_offset(&mut self, handle: EntityHandle) -> Task { let save_time = Utc::now().add(TimeDelta::milliseconds(900)); - self.auto_save_queue.insert(index, save_time); - let message = RootMessage::Dictionary(TrySave(index)); + self.auto_save_queue.insert(handle, save_time); + let message = RootMessage::Dictionary(TrySave(handle)); Task::perform( async { tokio::time::sleep(Duration::from_secs(1)).await }, |_| message, @@ -347,22 +318,17 @@ impl DictionaryState { let time = Instant::now(); let mut col = Column::new().width(Fill); - let state = self.state.lock(); - let group_id = state.word_groups[self.selected_group_index].id; + let query = &self.displayed_words_query; + let dictionary = self.state.dictionary.read(); - let dict = &state.dictionary; - let mut index = 0; - - for access_index in 0..dict.len() { + for access_index in 0..query.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; + 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) @@ -373,32 +339,28 @@ impl DictionaryState { } 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, + 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.clone(), }; - 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)), + .on_toggle(move |b| Include(data.handle, 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)) + .on_input(move |string| SetKey(data.handle, string)) .style(|x, status| { let mut default_style = default(x, status); default_style.border.radius = 0.0.into(); @@ -409,8 +371,7 @@ impl DictionaryState { text_input("Перевод", &data.value) .size(ACCENT_FONT_SIZE) .width(Fill) - .on_input(move |string| SetValue(index, string)) - .on_submit(SubmitWord(index)) + .on_input(move |string| SetValue(data.handle, string)) .style(|x, status| { let mut default_style = default(x, status); default_style.border.radius = 0.0.into(); @@ -419,11 +380,10 @@ impl DictionaryState { ); line = line.push( - text_input("Теги", &data.tags) + text_input("Теги", data.tags.as_ref()) .size(ACCENT_FONT_SIZE) .width(Fill) - .on_input(move |string| SetTags(index, string)) - .on_submit(SubmitWord(index)) + .on_input(move |string| SetTags(data.handle, string)) .style(|x, status| { let mut default_style = default(x, status); default_style.border.radius = 0.0.into(); @@ -432,9 +392,9 @@ impl DictionaryState { ); let line_button = || { - let action = WordAction(index); + let action = WordAction(data.handle); - if !data.id.is_valid() { + if !data.handle.real() { return button("-").on_press(action).style(|_x, _status| Style { background: None, text_color: Color::BLACK, @@ -444,22 +404,20 @@ impl DictionaryState { }); } - button("").on_press(WordAction(index)).width(15) + button("").on_press(WordAction(data.handle)).width(15) }; - line = line.push(line_button()).push(space().width(10)); - line + line.push(line_button()).push(space().width(10)) }); col = col.push(lazy_line); } - info!("Drawing {index} lines"); info!("Words rendering time: {:?}", time.elapsed()); scrollable(col).height(Fill).into() } fn filters(&self) -> iced::Element<'_, DictionaryMessage> { - let dict = &self.state.lock().dictionary; + let dict = &self.state.dictionary.read(); iced::widget::column![ text_input("Поиск", &self.search) @@ -468,7 +426,7 @@ impl DictionaryState { text!("Всего слов: {}", dict.len()), text!( "Выбрано слов: {}", - self.include_map.iter().filter(|i| **i).count() + self.included.iter().count() ), self.tags_selector(), toggler(self.no_typing) @@ -479,7 +437,7 @@ impl DictionaryState { .on_toggle(SetReverse), button(text!("Тест").center().width(Fill)) .style(cta_button) - .on_press(Test) + .on_press(GotoTest) .width(Fill), ] .width(250) @@ -515,7 +473,7 @@ impl DictionaryState { fn update_tags(&mut self) { let mut tags_list: HashSet = HashSet::new(); - let dict = &self.state.lock().dictionary; + let dict = &self.state.dictionary.write(); dict.iter().for_each(|element| { split_with_coma(element.tags.as_str()) @@ -552,22 +510,29 @@ impl DictionaryState { .collect::>(); if include_tags.is_empty() { - self.include_map.iter_mut().for_each(|x| *x = false); + self.included.clear(); return; } - let dict = &self.state.lock().dictionary; + let dict = &self.state.dictionary.read(); 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(); + 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()); } @@ -576,18 +541,19 @@ impl DictionaryState { let mut row = Row::new(); row = row.push(button("+").style(text).on_press(CreateGroup)); - let state = &self.state.lock(); - let groups = &state.word_groups; + let groups = &self.state.word_groups.read(); - for (index, group) in groups.iter().enumerate() { + for mut group in self.groups_query.handles(groups) { + let group_name = groups.get(&mut group).unwrap().name.clone(); row = row.push( - button(text!("{}", group.name.clone())) + button(text!("{}", group_name)) .style(text) - .on_press(SelectGroup(index)), + .on_press(SelectGroup(group)), ); } - let group = state.word_groups[self.selected_group_index].clone(); + let groups = self.state.word_groups.read(); + let group = groups.get_static(self.selected_group.clone()).unwrap(); iced::widget::column![ scrollable(row).width(Fill).horizontal(), @@ -606,7 +572,7 @@ impl DictionaryState { } fn group_delete_button(&self) -> iced::Element<'_, DictionaryMessage> { - if self.selected_group_index != 0 { + if self.selected_group.real() && self.selected_group.id != 1.into() { button("Удалить").style(danger).on_press(DeleteGroup).into() } else { space().into() @@ -614,11 +580,9 @@ impl DictionaryState { } fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { - let state = self.state.lock(); - let group_id = state.word_groups[self.selected_group_index].id; let button = button("Добавить слово").style(jl_button); - if group_id.is_valid() { + if self.selected_group.real() { return button.on_press(NewWord).into(); } @@ -636,9 +600,8 @@ pub fn split_with_coma(ts: &str) -> Vec { #[derive(Hash, PartialEq, Eq)] struct WordLineState { is_included: bool, - key: String, - value: String, - tags: String, - id: Id, - index: usize, + key: Box, + value: Box, + tags: Box, + handle: EntityHandle, } diff --git a/src/pages/dictionary_quiz.rs b/src/pages/dictionary_quiz.rs index a2a814c..9e27dfb 100644 --- a/src/pages/dictionary_quiz.rs +++ b/src/pages/dictionary_quiz.rs @@ -1,4 +1,4 @@ -use crate::data::entities::WordData; +use crate::data::entities::{EntityHandle, WordData}; use crate::navigation::Page::PreviousPage; use crate::navigation::RootMessage; use crate::navigation::*; @@ -13,10 +13,9 @@ use iced::widget::{Row, button, container, row, space, text, text_input}; use iced::{Border, Element, Fill, Task, Theme, alignment}; use rand::prelude::SliceRandom; -#[derive(Debug, Clone)] pub struct DictionaryQuizState { - words: Vec, - current_set: Vec, + words: Vec>, + current_set: Vec>, answer: String, view: String, correct: String, @@ -104,8 +103,8 @@ impl NavigatedPage for DictionaryQuizState { } impl DictionaryQuizState { - pub fn new(words: Vec, reverse: bool, no_typing: bool) -> DictionaryQuizState { - DictionaryQuizState { + pub fn new(words: Vec>, reverse: bool, no_typing: bool) -> DictionaryQuizState { + let state = DictionaryQuizState { words, current_set: Vec::new(), answer: "".to_string(), @@ -116,7 +115,9 @@ impl DictionaryQuizState { reverse, laps: 0, no_typing, - } + }; + + state } fn submit(&mut self) { @@ -172,7 +173,7 @@ impl DictionaryQuizState { self.is_help = false; self.answer = String::new(); - let next: WordData = match self.current_set.pop() { + let next: EntityHandle = match self.current_set.pop() { Some(word) => word, None => { self.update_set(); diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index a48cca0..5e02039 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -1,7 +1,6 @@ use crate::app::AppState; use crate::data::entities::DeckSettings; use crate::data_provider::card_states::{delete_stats, load_stats_of_deck}; -use crate::data_provider::decks::{delete_set, update_deck}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::MaybeError; diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 7179ca7..6f1df7c 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -3,7 +3,6 @@ use crate::core::srs::{AppendMode, OrderMode}; use crate::data::Id; use crate::data::entities::{CardState, DeckSettings}; use crate::data_provider::card_states::{add_stat_list, load_stats_of_deck}; -use crate::data_provider::decks::update_deck; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::{MaybeError, NotificationRequest}; diff --git a/src/pages/word.rs b/src/pages/word.rs index 3ff5300..5ae317b 100644 --- a/src/pages/word.rs +++ b/src/pages/word.rs @@ -1,5 +1,5 @@ use crate::app::AppState; -use crate::data::entities::WordData; +use crate::data::entities::{EntityHandle, WordData}; use crate::data_provider::words::{delete_word, update_word}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; @@ -16,8 +16,7 @@ use std::sync::Arc; #[derive(Clone)] pub struct WordState { state: AppState, - index: usize, - word: WordData, + handle: EntityHandle, } impl NavigatedPage for WordState { @@ -136,8 +135,11 @@ impl NavigatedPage for WordState { } impl WordState { - pub(crate) fn new(word: WordData, index: usize, state: AppState) -> WordState { - WordState { state, index, word } + pub fn new(handle: EntityHandle, state: AppState) -> Self { + Self{ + state, + handle, + } } }