diff --git a/sql/states.sql b/sql/states.sql index 32c0b1b..da3d6f0 100644 --- a/sql/states.sql +++ b/sql/states.sql @@ -5,7 +5,7 @@ create table states word_id INTEGER not null references words on delete cascade, - set_id INTEGER not null + deck_id INTEGER not null references decks on delete cascade, score INTEGER default 1 not null, diff --git a/src/app.rs b/src/app.rs index 0ebd160..7926ff8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,11 @@ -use std::sync::Arc; -use crate::data_provider::decks::{DeckRepository}; +use crate::data::repositories::Repository; +use crate::data_provider::card_states::CardStateRepository; +use crate::data_provider::decks::DeckRepository; +use crate::data_provider::history::HistoryRepository; use crate::data_provider::settings::get_setting; -use crate::data_provider::sqlite::{SqliteConnection}; -use crate::data_provider::words::{WordRepository}; +use crate::data_provider::sqlite::SqliteConnection; +use crate::data_provider::word_groups::WordGroupRepository; +use crate::data_provider::words::WordRepository; use crate::navigation::RootMessage::Keyboard; use crate::navigation::{AppSettings, RootMessage, ScreenState}; use iced::{Subscription, keyboard, window}; @@ -13,10 +16,7 @@ use iced_core::{Font, Size, Theme}; use log::info; use parking_lot::{Mutex, RwLock}; use rusqlite::Connection; -use crate::data::repositories::Repository; -use crate::data_provider::card_states::CardStateRepository; -use crate::data_provider::history::HistoryRepository; -use crate::data_provider::word_groups::WordGroupRepository; +use std::sync::Arc; pub const USER_FONT: Font = Font::with_name("Noto Sans JP"); @@ -100,10 +100,9 @@ pub fn fill_state(state: &mut AppState) { let setting = load_settings(&connection); state.word_groups.write().start().unwrap(); - state.dictionary.write().start().unwrap(); + state.dictionary.write().start().unwrap(); state.decks.write().start().unwrap(); - state.sync_data = Arc::new(RwLock::new(setting)) } diff --git a/src/core/deck.rs b/src/core/deck.rs index 0cee651..fc7976b 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -164,9 +164,9 @@ impl DeckData { self.set.len() } - pub fn list(deck: &DeckSettings, state: &AppState) -> Result>, NotificationData> { + pub fn list(deck: &DeckSettings, state: &AppState) -> Vec> { let time = Instant::now(); - let mut list = vec![]; + let list = vec![]; let engine = Engine::new(); let ast = engine.compile(&deck.filter); @@ -201,9 +201,9 @@ impl DeckData { info!("Collecting available words is {:?}", time.elapsed()); let handles =query.handles().collect(); drop(query); - Ok(handles) + handles } else { - Ok(list) + list } } } diff --git a/src/data/entities.rs b/src/data/entities.rs index 3986462..7731d08 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -126,7 +126,7 @@ impl DeckSettings { pub struct CardState { pub id: Id, pub word_id: Id, - pub set_id: Id, + pub deck_id: Id, pub last_open: DateTime, pub score: u8, pub fsrs_opened: NaiveDate, @@ -200,6 +200,11 @@ impl EntityHandle { _phantom: Default::default(), } } + + pub fn from_id(id: Id) -> Self { + Self::new(id, id.0 as usize, false) + } + pub fn index(&self) -> usize { (self.index << 1 >> 1) as usize } diff --git a/src/data_provider/card_states.rs b/src/data_provider/card_states.rs index e8f98a5..c708f93 100644 --- a/src/data_provider/card_states.rs +++ b/src/data_provider/card_states.rs @@ -15,7 +15,7 @@ pub type CardStateRepositoryInner = DataRepositoryInner for CardStatesQuerySet { fn load() -> &'static str { - "SELECT id, word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states" + "SELECT id, word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states" } fn save() -> &'static str { @@ -23,7 +23,7 @@ impl TableAccessQuerySet for CardStatesQuerySet { } fn add() -> &'static str { - "INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)" + "INSERT INTO states (word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)" } fn name() -> &'static str { @@ -34,7 +34,7 @@ impl TableAccessQuerySet for CardStatesQuerySet { Ok(CardState { id: row.get(0)?, word_id: row.get(1)?, - set_id: row.get(2)?, + deck_id: row.get(2)?, score: row.get(3)?, fsrs_opened: NaiveDate::from_epoch_days(row.get::(5)?) .expect("Incorrect date encoding"), @@ -48,7 +48,7 @@ impl TableAccessQuerySet for CardStatesQuerySet { fn insert_params(entity: &CardState) -> impl Params { ( entity.word_id, - entity.set_id, + entity.deck_id, entity.score, entity.last_open.timestamp(), entity.fsrs_opened.to_epoch_days(), @@ -73,7 +73,7 @@ pub fn load_stats_of_deck( Ok(CardState { id: row.get(0)?, word_id: row.get(1)?, - set_id: set.id, + deck_id: set.id, score: row.get(2)?, fsrs_opened: NaiveDate::from_epoch_days(row.get::(4)?) .expect("Incorrect date encoding"), @@ -126,7 +126,7 @@ pub fn add_stat_list(stats: &mut [CardState], connection: &mut Connection) -> Ma for stat in stats.iter() { stmt.execute(params![ stat.word_id, - stat.set_id, + stat.deck_id, stat.score, stat.last_open.timestamp(), stat.fsrs_opened.to_epoch_days(), diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 2021209..5e98b0d 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -110,4 +110,7 @@ impl SqliteConnection { pub fn finish(&mut self) { self.0 = None; } + pub fn restart(&mut self) { + self.0 = Some(default_connection()); + } } diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index 697a10a..3c0f6da 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -86,12 +86,10 @@ impl NavigatedPage for RepetitionSettingsState { self.correct_filter = self.settings_data.check_filter(); self.filter_changed = true; } - TryFilter => match DeckData::list(&self.settings_data, &self.state) { - Ok(list) => { - self.filtered_words = Some(list.len() as u32); - } - Err(err) => return push_error_task!(err), - }, + TryFilter => { + let list = DeckData::list(&self.settings_data, &self.state); + self.filtered_words = Some(list.len() as u32); + } DeleteSet => { if !self.real_delete { self.real_delete = true; @@ -183,10 +181,10 @@ fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError { let states_repo = &mut state.card_states.write(); let deck_id = deck.id; let mut db_stats = states_repo - .query_par(Arc::new(move |s| s.set_id == deck_id)) + .query_par(Arc::new(move |s| s.deck_id == deck_id)) .iter(&states_repo) .collect::>(); - let filtered_words = DeckData::list(deck, state)? + let filtered_words = DeckData::list(deck, state) .iter() .map(|w| w.id) .collect::>(); diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 6f1df7c..37a3c20 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -1,8 +1,9 @@ use crate::app::AppState; +use crate::core::deck::DeckData; 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::entities::{CardState, DeckSettings, EntityHandle, WordData}; +use crate::data::repositories::Repository; +use crate::data_provider::card_states::add_stat_list; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::{MaybeError, NotificationRequest}; @@ -14,7 +15,7 @@ use crate::push_error; use crate::push_error_task; use crate::styling::*; use chrono::{Days, Local, Utc}; -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashSet; pub use iced::widget::button::{Catalog, Style}; use iced::widget::button::{Status, danger}; use iced::widget::tooltip::Position::Top; @@ -26,14 +27,12 @@ use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Tas use iced_core::Padding; use iced_core::border::Radius; use iced_core::svg::Handle; -use parking_lot::Mutex; use std::ops::Deref; use std::sync::Arc; #[derive(Clone)] pub struct RepetitionsState { - word_id_index_map: HashMap, - selected_deck_index: Option, + selected_deck_handle: Option>, decks: Vec, state: AppState, } @@ -48,11 +47,10 @@ impl NavigatedPage for RepetitionsState { self.state.clone(), ))) } else if let GoToRepetition = message { - let clone = self.state.clone(); - + let mut state_clone = self.state.clone(); let selected_set = self.selected_deck_mut().unwrap(); if selected_set.append_mode == AppendMode::Full { - if let Err(_err) = Self::append_all_words(&mut clone.lock(), selected_set) { + if let Err(_err) = Self::append_all_words(&mut state_clone, selected_set) { return None; } selected_set.existing_words_indices = selected_set.available_words_indices.clone(); @@ -65,7 +63,7 @@ impl NavigatedPage for RepetitionsState { } if let Ok(repetition_state) = - RepetitionState::new(selected_set.general_settings.clone(), clone) + RepetitionState::new(selected_set.general_settings.clone(), self.state.clone()) { return Some(Repetition(repetition_state)); } @@ -73,7 +71,7 @@ impl NavigatedPage for RepetitionsState { None } else if let GoToSettings = message { Some(RepetitionSettings(RepetitionSettingsState::new( - self.selected_deck().unwrap().index.unwrap(), + EntityHandle::from_id(self.selected_deck().unwrap().id), self.state.clone(), ))) } else { @@ -81,23 +79,20 @@ impl NavigatedPage for RepetitionsState { } } fn navigated(&mut self) { - if let Some(index) = self.selected_deck_index { - let state = self.state.lock(); - if let Some(global_deck) = state.decks.get(index) { - if global_deck.id != self.selected_deck().unwrap().id { - drop(state); - self.decks.remove(index); - self.clear_selection(); - } else { - self.decks[index].general_settings = state.decks[index].clone(); - drop(state); - let _ = self.fill_deck_word_indices(); - } - } else { - drop(state); - self.decks.remove(index); - self.clear_selection() + if let Some(index) = self.selected_deck_handle { + let decks = self.state.decks.read(); + if let None = decks.get_static(index) { + let position = self + .decks + .iter() + .position(|deck_view_data: &DeckViewData| deck_view_data.id == index.id) + .unwrap(); + self.decks.remove(position); + drop(decks); + self.clear_selection(); } + } else { + let _ = self.fill_deck_word_indices(); } } fn update(&mut self, message: RepetitionsMessage) -> Task { @@ -116,25 +111,27 @@ impl NavigatedPage for RepetitionsState { available_words_indices: Default::default(), indices_ready: false, append_mode: Default::default(), - index: None, + count: None, + open_mode: OrderMode::Default, }); } DeleteSet => { - let index = self.selected_deck_index.unwrap(); + let index = self.selected_deck_handle.unwrap(); self.clear_selection(); - let deck = self.decks.remove(index); - if deck.id.is_valid() { - let mut state = self.state.lock(); - state.decks.remove(index); - self.decks = self - .decks - .clone() - .into_iter() - .map(|mut deck| { - deck.index = state.decks.iter().position(|d| d.id == deck.id); - deck - }) - .collect(); + let deck = self.decks.remove( + self.decks + .iter() + .position(|dvd| dvd.id == index.id) + .unwrap(), + ); + if index.real() { + let mut repo = self.state.decks.write(); + if let Err(err) = repo.delete(index) { + return push_error_task!(err); + } + + let index = self.decks.iter().position(|dvd| dvd.id == index.id); + self.decks.remove(index.unwrap()); } } SetName(value) => { @@ -152,20 +149,19 @@ impl NavigatedPage for RepetitionsState { } Save => { let mut deck = self.selected_deck().unwrap().clone(); - let mut state = self.state.lock(); - { - if let Err(e) = update_deck(&mut deck.general_settings, &state.connection) { - return Task::done(RootMessage::PushNotification( - NotificationRequest::Error(e), - )); - } - if let Some(index) = deck.index { - state.decks[index] = deck.general_settings.clone(); - } else { - state.decks.push(deck.general_settings.clone()); - deck.index = Some(state.decks.len() - 1); - self.decks[self.selected_deck_index.unwrap()] = deck.clone(); - } + let handle = self.selected_deck_handle.unwrap(); + let mut repo = self.state.decks.write(); + + repo.set_static(handle, *deck); + if let Err(err) = repo.save(&mut handle.clone()) { + return push_error_task!(err); + } + if let Some(index) = deck.index { + state.decks[index] = deck.general_settings.clone(); + } else { + state.decks.push(deck.general_settings.clone()); + deck.index = Some(state.decks.len() - 1); + self.decks[self.selected_deck_handle.unwrap()] = deck.clone(); } } SetForward(value) => { @@ -181,23 +177,21 @@ impl NavigatedPage for RepetitionsState { deck.general_settings.filter = value; } TryFilter => { - let state = self.state.lock(); let deck = self.selected_deck().unwrap(); - let count = deck.get_word_list(&state).len(); - drop(state); + let count = DeckData::list(&deck, &self.state).len(); let deck = self.selected_deck_mut().unwrap(); - deck.general_settings.count = Some(count); + deck.count = Some(count); } SetOpenMode(mode) => { let deck = self.selected_deck_mut().unwrap(); - deck.general_settings.open_mode = mode; + deck.open_mode = mode; } SetAppendMode(mode) => { let deck = self.selected_deck_mut().unwrap(); deck.append_mode = mode; } AppendWords(count) => { - let adding: Vec; + let adding: Vec>; { { let deck = self.selected_deck().unwrap(); @@ -257,22 +251,9 @@ impl NavigatedPage for RepetitionsState { impl RepetitionsState { pub(crate) fn new(state: AppState) -> RepetitionsState { - let state_ = state.lock(); - let mut map = HashMap::with_capacity(state_.dictionary.len()); - - state_ - .dictionary - .iter() - .enumerate() - .for_each(|(index, word)| { - map.insert(word.id, index); - }); - - let decks = Self::get_decks_from_state(&state_); - drop(state_); + let decks = Self::get_decks_from_state(&state); RepetitionsState { - word_id_index_map: map, - selected_deck_index: None, + selected_deck_handle: None, decks, state, } @@ -281,6 +262,7 @@ impl RepetitionsState { fn get_decks_from_state(state: &AppState) -> Vec { state .decks + .read() .iter() .enumerate() .map(|(index, deck)| DeckViewData { @@ -289,7 +271,9 @@ impl RepetitionsState { available_words_indices: Default::default(), indices_ready: false, append_mode: Default::default(), + count: None, index: Some(index), + open_mode: OrderMode::Default, }) .collect() } @@ -526,7 +510,7 @@ impl RepetitionsState { text!("{} слова добавлено из {}", now, available).into() } fn count_view(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { - if let Some(count) = deck.count { + if let Some(count) = self.decks.iter().find() { return text!("Количество слов: {}", count).into(); } space().into() @@ -578,7 +562,7 @@ impl RepetitionsState { fn append_words( state: &mut AppState, deck: &DeckSettings, - indices: impl Iterator, + indices: impl Iterator>, ) -> MaybeError { let words = &state.dictionary; let stats = &mut indices @@ -591,7 +575,7 @@ impl RepetitionsState { score: 1, fsrs_opened: Utc::now().date_naive(), fsrs_memory: None, - set_id: deck.id, + deck_id: deck.id, fsrs_open_day: Utc::now().date_naive(), } }) @@ -605,32 +589,34 @@ impl RepetitionsState { fn select_deck(&mut self, index: usize) { if self.decks.get(index).is_some() { - self.selected_deck_index = Some(index); + self.selected_deck_handle = Some(index); } } fn selected_deck(&self) -> Option<&DeckViewData> { - self.decks.get(self.selected_deck_index?) + self.decks.get(self.selected_deck_handle?) } fn selected_deck_mut(&mut self) -> Option<&mut DeckViewData> { - self.decks.get_mut(self.selected_deck_index?) + self.decks.get_mut(self.selected_deck_handle?) } fn clear_selection(&mut self) { - self.selected_deck_index = None; + self.selected_deck_handle = None; } fn fill_deck_word_indices(&mut self) -> MaybeError { - let state = &self.state.lock(); + let card_states = self.state.card_states.read(); + let deck_id = self.selected_deck().unwrap().id; + let added = card_states + .query_par(Arc::new(move |state| state.deck_id == deck_id)) + .iter(&card_states) + .map(|state| EntityHandle::from_id(state.word_id)) + .collect::>(); let mut deck = self.selected_deck().unwrap().clone(); - let added: Vec<_> = load_stats_of_deck(&deck, &state.connection)? - .iter() - .map(|c| self.word_id_index_map[&c.word_id]) - .collect(); - let total = deck.get_word_list(state); + let total = DeckData::list(&deck, &self.state)?; deck.existing_words_indices = added; deck.available_words_indices = total; - self.decks[self.selected_deck_index.unwrap()] = deck; + self.decks[self.selected_deck_handle.unwrap()] = deck; Ok(()) } } @@ -658,11 +644,12 @@ pub enum RepetitionsMessage { #[derive(Clone)] struct DeckViewData { general_settings: DeckSettings, - existing_words_indices: Vec, - available_words_indices: Vec, + existing_words_indices: Vec>, + available_words_indices: Vec>, indices_ready: bool, append_mode: AppendMode, - index: Option, + count: Option, + open_mode: OrderMode, } impl Deref for DeckViewData { diff --git a/src/pages/sync.rs b/src/pages/sync.rs index d5cf083..554ace7 100644 --- a/src/pages/sync.rs +++ b/src/pages/sync.rs @@ -1,4 +1,5 @@ -use crate::app::{AppState, fill_state}; +use crate::app::AppState; +use crate::data::repositories::Repository; use crate::data_provider::settings::{delete_settings, set_setting}; use crate::data_provider::web_api::*; use crate::navigation::Page::PreviousPage; @@ -12,8 +13,6 @@ use iced::widget::container::rounded_box; use iced::widget::{button, column, container, progress_bar, row, space, text, toggler}; use iced::{Center, Element, Fill, Font, Length, Task}; use log::error; -use parking_lot::Mutex; -use std::sync::Arc; use std::time::Duration; #[derive(Clone)] @@ -56,8 +55,7 @@ impl NavigatedPage for SyncState { match message { Back => {} CopyKey => { - let state = self.state.lock(); - let key = state.sync_data.key.clone().unwrap(); + let key = self.state.sync_data.read().key.clone().unwrap(); return iced::clipboard::write(key) .map(|_val: String| RootMessage::Sync(KeyCopied)); } @@ -76,16 +74,20 @@ impl NavigatedPage for SyncState { if !validate_id(&new_id) { return Task::none(); } - let mut state = self.state.lock(); - state.sync_data.key = Some(new_id.clone()); - if let Err(err) = set_setting("SYNC_KEY".to_string(), new_id, &state.connection) { + let mut settings = self.state.sync_data.write(); + settings.key = Some(new_id.clone()); + if let Err(err) = set_setting( + "SYNC_KEY".to_string(), + new_id, + &self.state.connection.lock(), + ) { return push_error_task!(err); } } Send => { self.prepare_to_db_interaction(); - let id = self.state.lock().sync_data.key.clone().unwrap(); + let id = self.state.sync_data.read().key.clone().unwrap(); let tasks = Task::batch([ Task::perform( async { tokio::time::sleep(Duration::from_millis(200)).await }, @@ -97,7 +99,7 @@ impl NavigatedPage for SyncState { return tasks; } GetLast => { - let id = self.state.lock().sync_data.key.clone().unwrap(); + let id = self.state.sync_data.read().key.clone().unwrap(); let tasks = Task::batch([ Task::perform( async { tokio::time::sleep(Duration::from_millis(200)).await }, @@ -122,26 +124,27 @@ impl NavigatedPage for SyncState { } NetworkFinished => { self.frozen = false; - let mut updated_state = AppState::new(); - fill_state(&mut updated_state); + { + let mut connection = self.state.connection.lock(); + connection.restart(); + } + + if let Err(err) = self.reload_data() { + return push_error_task!(err); + } - let mut state = self.state.lock(); - state.sync_data = updated_state.sync_data; - state.connection = updated_state.connection; - state.decks = updated_state.decks; - state.dictionary = updated_state.dictionary; - state.word_groups = updated_state.word_groups; return Task::done(RootMessage::PushNotification(NotificationRequest::Info( NotificationData::DataLoaded, ))); } Disable => {} DisableSync => { - let mut state = self.state.lock(); - state.sync_data.key = None; + let mut sync_data = self.state.sync_data.write(); + let connection = self.state.connection.lock(); + sync_data.key = None; let save_result = || { - delete_settings("SYNC_KEY".to_string(), &state.connection)?; - delete_settings("AUTO_WEB_FETCH".to_string(), &state.connection)?; + delete_settings("SYNC_KEY".to_string(), &connection)?; + delete_settings("AUTO_WEB_FETCH".to_string(), &connection)?; Ok::<(), NotificationData>(()) }; @@ -151,12 +154,14 @@ impl NavigatedPage for SyncState { } SwitchAutoSync => { self.auto_fetch = !self.auto_fetch; - let mut state = self.state.lock(); - state.sync_data.auto_web_fetch = self.auto_fetch; + let mut sync_data = self.state.sync_data.write(); + let connection = self.state.connection.lock(); + + sync_data.auto_web_fetch = self.auto_fetch; if let Err(err) = set_setting( "AUTO_WEB_FETCH".to_string(), self.auto_fetch.to_string(), - &state.connection, + &connection, ) { return push_error_task!(err); } @@ -178,10 +183,8 @@ impl NavigatedPage for SyncState { impl SyncState { pub fn new(state: AppState) -> SyncState { - let auto_fetch; - { - auto_fetch = state.lock().sync_data.auto_web_fetch; - } + let auto_fetch = state.sync_data.read().auto_web_fetch; + Self { auto_fetch, state, @@ -190,6 +193,15 @@ impl SyncState { } } + fn reload_data(&mut self) -> Result<(), NotificationData> { + self.state.card_states.write().reload()?; + self.state.word_groups.write().reload()?; + self.state.decks.write().reload()?; + self.state.dictionary.write().reload()?; + self.state.history.write().reload()?; + Ok(()) + } + fn sync_column(&self) -> Element<'_, SyncMessage> { let network_view: Element<'_, SyncMessage> = if self.frozen { progress_bar(0.0..=5.0, self.progress).into() @@ -197,7 +209,7 @@ impl SyncState { space().into() }; { - if let Some(key) = self.state.lock().sync_data.key.clone() { + if let Some(key) = self.state.sync_data.read().key.as_ref() { column![ text!("Ваш ключ синхронизации"), container( @@ -254,13 +266,15 @@ impl SyncState { } self.frozen = true; self.progress = 0.0; - let state = self.state.lock(); - state - .connection + let mut connection = self.state.connection.lock(); + + connection .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(())) .unwrap_or_else(|e| { error!("Ошибка при checkpoint WAL: {e}"); }); + + connection.finish(); } }