diff --git a/src/app.rs b/src/app.rs index f6f66a1..7b273c5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,21 +1,21 @@ -use chrono::NaiveDate; -use hashbrown::HashMap; -use iced::{keyboard, window, Subscription}; -use iced_core::{Font, Size, Theme}; -use iced_core::window::Position; -#[cfg(target_os = "linux")] -use iced_core::window::settings::PlatformSpecific; -use log::info; -use rusqlite::Connection; +use crate::data::Id; use crate::data::entities::DeckSettings; +use crate::data::entities::{WordData, WordGroup}; use crate::data_provider::card_sets::load_sets; use crate::data_provider::settings::get_setting; use crate::data_provider::sqlite::default_connection; use crate::data_provider::words::{load_word_groups, load_words}; -use crate::data::entities::{ WordData, WordGroup}; -use crate::data::Id; -use crate::navigation::{AppSettings, RootMessage, ScreenState}; use crate::navigation::RootMessage::Keyboard; +use crate::navigation::{AppSettings, RootMessage, ScreenState}; +use chrono::NaiveDate; +use hashbrown::HashMap; +use iced::{Subscription, keyboard, window}; +use iced_core::window::Position; +#[cfg(target_os = "linux")] +use iced_core::window::settings::PlatformSpecific; +use iced_core::{Font, Size, Theme}; +use log::info; +use rusqlite::Connection; pub const USER_FONT: Font = Font::with_name("Noto Sans JP"); pub fn start() -> iced::Result { @@ -94,9 +94,9 @@ pub fn fill_state(state: &mut AppState) { let groups = load_word_groups(&state.connection); let setting = load_settings(&state.connection); - state.dictionary = list; - state.decks = sets; - state.word_groups = groups; + state.dictionary = list.unwrap_or_default(); + state.decks = sets.unwrap_or_default(); + state.word_groups = groups.unwrap_or_default(); state.sync_data = setting } diff --git a/src/core.rs b/src/core.rs index ed001e5..92871fd 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,3 +1,3 @@ +pub mod deck; pub mod kana_set; pub mod srs; -pub mod deck; \ No newline at end of file diff --git a/src/core/deck.rs b/src/core/deck.rs index 307d7fc..cffec4b 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -1,13 +1,16 @@ use crate::app::AppState; -use crate::core::srs::{OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, WordOpenMode, WorstWordsSRSModule}; +use crate::core::srs::{ + OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, WordOpenMode, + WorstWordsSRSModule, +}; use crate::data::entities::*; use crate::data_provider::card_stats::{load_stats_of_deck, update_stat_score}; use crate::data_provider::history::push_note; +use crate::notification::{MaybeError, NotificationData}; use chrono::Utc; use parking_lot::Mutex; use std::sync::Arc; - #[derive(Clone)] pub struct DeckData { pub(crate) words: Vec, @@ -18,24 +21,27 @@ pub struct DeckData { settings: DeckSettings, } impl DeckData { - pub fn new(settings: &DeckSettings, state: Arc>) -> Self { + pub fn new( + settings: &DeckSettings, + state: Arc>, + ) -> Result { let state_for = state.clone(); let state_locked = state.lock(); - let stats = load_stats_of_deck(settings, &state_locked.connection); + let stats = load_stats_of_deck(settings, &state_locked.connection)?; let words = stats .iter() .map(|stat| { state_locked .dictionary .binary_search_by_key(&stat.word_id, |w| w.id) - .unwrap() + .expect("No word found in dictionary") }) .map(|index| &state_locked.dictionary[index]) .cloned() .collect(); - Self { + Ok(Self { set: stats, words, current_word_index: None, @@ -48,76 +54,68 @@ impl DeckData { OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), }, settings: settings.clone(), - } + }) } pub fn next(&mut self) -> (WordData, CardStatistics) { - let index = match self.order_module.clone() { - OrderModule::SemiRandomSRS(mut module) => { + let index = match self.order_module { + OrderModule::SemiRandomSRS(ref mut module) => { if !module.initialized { - module.init(self) + module.init(self.set.as_slice()) } - let index = module.next(self); - self.order_module = OrderModule::SemiRandomSRS(module); - index + module.next(self.set.as_slice()) } - OrderModule::RandomSRS(mut module) => { + OrderModule::RandomSRS(ref mut module) => { if !module.initialized { - module.init(self) + module.init(self.set.as_slice()) } - let index = module.next(self); - self.order_module = OrderModule::RandomSRS(module); - index + module.next(self.set.as_slice()) } - OrderModule::WorstWordsSRS(mut module) => { + OrderModule::WorstWordsSRS(ref mut module) => { if !module.initialized { - module.init(self) + module.init(self.set.as_slice()) } - let index = module.next(self); - self.order_module = OrderModule::WorstWordsSRS(module); - index + module.next(self.set.as_slice()) } }; self.current_word_index = Some(index); (self.words[index].clone(), self.set[index].clone()) } - pub fn open(&mut self, status: WordOpenMode) { - if self.current_word_index.is_none() { - return; - } - let index = self.current_word_index.unwrap(); + pub fn open(&mut self, status: WordOpenMode) -> MaybeError { + if let Some(index) = self.current_word_index { + let word: &mut CardStatistics = &mut self.set[index]; + let old_score = word.score; + word.update(status); + let new_score = word.score; - let word : &mut CardStatistics = &mut self.set[index]; - let old_score = word.score; - word.update(status); - let new_score = word.score; - - match self.order_module { - OrderModule::SemiRandomSRS(ref mut module) => { - module.open(status, index, word); - } - OrderModule::RandomSRS(ref mut module) => { - module.open(status, index, word); - } - OrderModule::WorstWordsSRS(ref mut module) => { - module.open(status, index, word); + match self.order_module { + OrderModule::SemiRandomSRS(ref mut module) => { + module.open(status, index, word); + } + OrderModule::RandomSRS(ref mut module) => { + module.open(status, index, word); + } + OrderModule::WorstWordsSRS(ref mut module) => { + module.open(status, index, word); + } } + update_stat_score(word, &self.state.lock().connection)?; + let _ = push_note( + self.settings.id, + HistoryItem { + timestamp: Utc::now(), + word_id: word.word_id, + mode: status, + before: old_score, + after: new_score, + }, + ); } - update_stat_score(word, &self.state.lock().connection); - let _ = push_note( - self.settings.id, - HistoryItem { - timestamp: Utc::now(), - word_id: word.word_id, - mode: status, - before: old_score, - after: new_score, - }, - ); + + Ok(()) } pub fn len(&self) -> usize { self.set.len() } } - diff --git a/src/core/kana_set.rs b/src/core/kana_set.rs index ccd06ed..9f53ffe 100644 --- a/src/core/kana_set.rs +++ b/src/core/kana_set.rs @@ -198,4 +198,4 @@ impl PartialEq for KanaSet { fn eq(&self, other: &Self) -> bool { self.name == other.name } -} \ No newline at end of file +} diff --git a/src/core/srs.rs b/src/core/srs.rs index 33cf983..fbb668f 100644 --- a/src/core/srs.rs +++ b/src/core/srs.rs @@ -1,9 +1,8 @@ -use crate::core::deck::DeckData; +use crate::data::entities::CardStatistics; use rand::distr::weighted::WeightedIndex; use rand::prelude::ThreadRng; use rand::prelude::{Distribution, SliceRandom}; use rand::rng; -use crate::data::entities::CardStatistics; const MAX_HISTORY_LEN: usize = 20; const MAX_HISTORY_LEN_PART: f32 = 0.33; @@ -29,9 +28,9 @@ pub enum OrderModule { WorstWordsSRS(WorstWordsSRSModule), } pub trait SRSModule { - fn next(&mut self, set: &mut DeckData) -> usize; + fn next(&mut self, set: &[CardStatistics]) -> usize; fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &CardStatistics); - fn init(&mut self, set: &mut DeckData); + fn init(&mut self, set: &[CardStatistics]); } #[derive(Clone)] pub struct RandomSRSModule { @@ -47,11 +46,11 @@ impl RandomSRSModule { } } impl SRSModule for RandomSRSModule { - fn next(&mut self, set: &mut DeckData) -> usize { + fn next(&mut self, set: &[CardStatistics]) -> usize { match self.basket.pop() { Some(index) => index, None => { - self.basket = (0..set.words.len()).collect::>(); + self.basket = (0..set.len()).collect::>(); self.basket.shuffle(&mut rand::rng()); self.basket.pop().expect("empty basket") } @@ -60,9 +59,9 @@ impl SRSModule for RandomSRSModule { fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {} - fn init(&mut self, set: &mut DeckData) { + fn init(&mut self, set: &[CardStatistics]) { self.initialized = true; - self.basket = (0..set.set.len()).collect::>(); + self.basket = (0..set.len()).collect::>(); self.basket.shuffle(&mut rand::rng()) } } @@ -84,7 +83,7 @@ impl SemiRandomSRSModule { } } impl SRSModule for SemiRandomSRSModule { - fn next(&mut self, set: &mut DeckData) -> usize { + fn next(&mut self, set: &[CardStatistics]) -> usize { let index = self.last_weights.sample(&mut self.generator); if self.history.contains(&index) && self.history.len() != 1 { @@ -104,18 +103,17 @@ impl SRSModule for SemiRandomSRSModule { .update_weights(&[(index, &new_weight)]) .expect("update weights error"); } - fn init(&mut self, set: &mut DeckData) { + fn init(&mut self, set: &[CardStatistics]) { self.initialized = true; let weights = set - .set .iter() .map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0) .collect::>(); - self.last_weights = WeightedIndex::new(weights).unwrap(); + self.last_weights = WeightedIndex::new(weights).expect("Incorrect weights update"); } } impl SemiRandomSRSModule { - fn history_len(&self, set: &DeckData) -> usize { + fn history_len(&self, set: &[CardStatistics]) -> usize { ((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN) } } @@ -129,7 +127,7 @@ pub struct WorstWordsSRSModule { rounds_count: u8, } impl SRSModule for WorstWordsSRSModule { - fn next(&mut self, set: &mut DeckData) -> usize { + fn next(&mut self, set: &[CardStatistics]) -> usize { if self.rounds_remaining == 0 { self.fill_pool(set); self.rounds_remaining = self.rounds_count; @@ -147,7 +145,7 @@ impl SRSModule for WorstWordsSRSModule { fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {} - fn init(&mut self, _: &mut DeckData) { + fn init(&mut self, _: &[CardStatistics]) { self.initialized = true; } } @@ -162,17 +160,12 @@ impl WorstWordsSRSModule { queue: vec![], } } - fn fill_pool(&mut self, set: &DeckData) { - let mut sorted = set - .set - .clone() - .into_iter() - .zip(0..set.set.len()) - .collect::>(); - sorted.sort_by_key(|c| c.0.score); + fn fill_pool(&mut self, set: &[CardStatistics]) { + let mut sorted = set.iter().enumerate().collect::>(); + sorted.sort_by_key(|c| c.1.score); let mut worst = sorted[0..self.pool_size] .iter() - .map(|(_, index)| *index) + .map(|(index, _)| *index) .collect::>(); worst.shuffle(&mut rand::rng()); self.pool = worst; diff --git a/src/data.rs b/src/data.rs index f8c8120..4589b9b 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,12 +1,12 @@ pub mod entities; +use rusqlite::ToSql; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use std::fmt::{Display, Formatter}; use std::num::ParseIntError; use std::str::FromStr; -use rusqlite::ToSql; -use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; -#[derive(Clone, PartialEq, Eq, Debug, Hash, Copy, Ord, PartialOrd)] +#[derive(Clone, PartialEq, Eq, Debug, Hash, Copy, Ord, PartialOrd, Default)] pub struct Id(pub(crate) u32); pub const INVALID_ID: Id = Id(0); impl FromStr for Id { @@ -59,4 +59,4 @@ impl Id { pub(crate) fn is_valid(self) -> bool { self.0 != 0 } -} \ No newline at end of file +} diff --git a/src/data/entities.rs b/src/data/entities.rs index f5a2036..7342947 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -105,12 +105,15 @@ impl DeckSettings { .iter() .find(|g| g.id == word.group_id) .cloned() - .unwrap() + .expect("No group id") .name, ); let result = engine.eval_ast_with_scope::(&mut scope, &ast); - result.is_ok() && result.unwrap() + if let Ok(result) = result { + return result; + } + false }) .map(|(index, _)| index) .collect(); diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index de6e569..d6f6885 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,12 +1,14 @@ use crate::core::srs::OrderMode; use crate::data::entities::DeckSettings; +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use crate::notification::{MaybeError, NotificationData}; use rusqlite::Connection; -pub fn load_sets(connection: &Connection) -> Vec { +pub fn load_sets(connection: &Connection) -> Result, NotificationData> { let mut stmt = connection .prepare("SELECT id, name, forward, backward, filter FROM card_set") - .unwrap(); + .expect("Failed to prepare state query"); let iter = stmt .query_map([], |row| { Ok(DeckSettings { @@ -19,17 +21,14 @@ pub fn load_sets(connection: &Connection) -> Vec { open_mode: OrderMode::Default, }) }) - .unwrap(); + .map_err(|_| DbLoadError)?; - let mut buffer = vec![]; - for word in iter { - buffer.push(word.unwrap()); - } - - buffer + 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) { +pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> MaybeError { let index : u32 = connection .query_row( "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", @@ -41,14 +40,15 @@ pub fn add_set(set: &mut DeckSettings, connection: &Connection) { ), |row| row.get(0) ) - .unwrap(); + .map_err(|_| DbSaveError)?; set.id = index.into(); + Ok(()) } -pub fn update_deck(set: &mut DeckSettings, connection: &Connection) { +pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> MaybeError { if !set.id.is_valid() { - add_set(set, connection); + add_set(set, connection)?; } else { connection .execute( @@ -61,15 +61,17 @@ pub fn update_deck(set: &mut DeckSettings, connection: &Connection) { &set.id ), ) - .unwrap(); + .map_err(|_| DbSaveError)?; } + Ok(()) } -pub fn delete_set(set: &DeckSettings, connection: &Connection) { +pub fn delete_set(set: &DeckSettings, connection: &Connection) -> MaybeError { if !set.id.is_valid() { - return; + return Err(DbSaveError); } connection .execute("DELETE FROM card_set WHERE id = ?1", (&set.id,)) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index deb81fd..cf19019 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,12 +1,17 @@ +use crate::data::entities::{CardStatistics, DeckSettings}; +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use crate::notification::{MaybeError, NotificationData}; +use log::info; use rusqlite::{Connection, params}; use std::time::Instant; -use log::info; -use crate::data::entities::{CardStatistics, DeckSettings}; -pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec { +pub fn load_stats_of_deck( + set: &DeckSettings, + connection: &Connection, +) -> Result, NotificationData> { let mut stmt = connection .prepare("SELECT id, word_id, score, last_opened FROM card_stats WHERE set_id = ?1") - .unwrap(); + .expect("Failed to prepare state query"); let iter = stmt .query_map((set.id,), |row| { Ok(CardStatistics { @@ -17,19 +22,19 @@ pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec MaybeError { let time = Instant::now(); - let tx = connection.transaction().unwrap(); + let tx = connection + .transaction() + .expect("Failed to create transaction"); let count = stats.len(); { @@ -37,7 +42,7 @@ pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) .prepare( "INSERT INTO card_stats (word_id, set_id, score, last_opened) VALUES (?1, ?2, ?3, ?4)", ) - .unwrap(); + .expect("Failed to prepare state query"); for stat in stats.iter() { stmt.execute(params![ @@ -46,12 +51,12 @@ pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) stat.score, stat.last_open.timestamp() ]) - .unwrap(); + .map_err(|_| DbSaveError)?; } } let last_index: u32 = tx.last_insert_rowid() as u32; - tx.commit().unwrap(); + tx.commit().expect("Failed to commit"); let start_index = last_index - (count as u32) + 1; @@ -60,9 +65,10 @@ pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) } info!("Added {} cards for {:?}", stats.len(), time.elapsed()); + Ok(()) } -pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) { +pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) -> MaybeError { let time = Instant::now(); connection @@ -70,13 +76,14 @@ pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) { "UPDATE card_stats SET score = ?1, last_opened = ?2 WHERE id = ?3", (&stat.score, &stat.last_open.timestamp(), &stat.id), ) - .unwrap(); + .map_err(|_| DbSaveError)?; info!("Updated stat: {}", time.elapsed().as_millis()); + Ok(()) } -pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) { +pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) -> MaybeError { if stat.is_empty() { - return; + return Ok(()); } let ids = stat .iter() @@ -88,5 +95,6 @@ pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) { format!("DELETE FROM card_stats WHERE id in ({ids})").as_str(), (), ) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index ead134b..023d6b7 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -1,9 +1,9 @@ use crate::core::srs::WordOpenMode; -use crate::data::entities::HistoryItem; use crate::data::Id; +use crate::data::entities::HistoryItem; use crate::fs::*; -use crate::notification::NotificationData; use crate::notification::NotificationData::PermissionDenied; +use crate::notification::{MaybeError, NotificationData}; use chrono::DateTime; use std::fs::{File, OpenOptions}; use std::io::Write; @@ -15,18 +15,20 @@ pub fn get_history_of_set(id: Id) -> Result, NotificationData> let head = app_dir.clone().join(format!("set_{}_history.csv", id)); let mut lines = Vec::new(); - append_lines_if_exists(head, &mut lines); + append_lines_if_exists(head, &mut lines)?; lines.reverse(); Ok(parse_history_items(lines)) } -fn append_lines_if_exists(path: PathBuf, lines: &mut Vec) { +fn append_lines_if_exists(path: PathBuf, lines: &mut Vec) -> MaybeError { if path.exists() { - let reader = BufReader::new(File::open(path).unwrap()); - for line in reader.lines() { - lines.push(line.unwrap()); - } + let reader = BufReader::new(File::open(path).map_err(|_| PermissionDenied)?); + reader + .lines() + .map_while(Result::ok) + .for_each(|i| lines.push(i)); } + Ok(()) } fn parse_history_items(strings: Vec) -> Vec { @@ -37,12 +39,13 @@ fn parse_history_items(strings: Vec) -> Vec { if id.is_err() { continue; } - let id = id.unwrap(); + let id = id.unwrap_or_default(); if !id.is_valid() { continue; } items.push(HistoryItem { - timestamp: DateTime::from_timestamp(time.parse().unwrap_or_default(), 0).unwrap_or_default(), + timestamp: DateTime::from_timestamp(time.parse().unwrap_or_default(), 0) + .unwrap_or_default(), word_id: id, mode: match mode.parse::().unwrap_or_default() { 2 => WordOpenMode::Hard, @@ -59,7 +62,7 @@ fn parse_history_items(strings: Vec) -> Vec { items } -pub fn push_note(set_id: Id, item: HistoryItem) -> Result<(), NotificationData> { +pub fn push_note(set_id: Id, item: HistoryItem) -> MaybeError { let app_dir = history_dir()?; let path = app_dir.clone().join(format!("set_{}_history.csv", set_id)); @@ -84,5 +87,3 @@ pub fn push_note(set_id: Id, item: HistoryItem) -> Result<(), NotificationData> writeln!(&mut file, "{}", line_str).map_err(|_| PermissionDenied)?; Ok(()) } - - diff --git a/src/data_provider/import.rs b/src/data_provider/import.rs index a2116d5..1640fbb 100644 --- a/src/data_provider/import.rs +++ b/src/data_provider/import.rs @@ -1,4 +1,6 @@ +use crate::helpers::into_result; use crate::notification::NotificationData; +use crate::notification::NotificationData::IncorrectAnkiFile; use hashbrown::HashMap; use log::info; use rusqlite::Connection; @@ -26,41 +28,38 @@ pub struct ImportNote { pub fn load_groups(connection: &Connection) -> Result { let json_models: String = connection .query_one("select models from col", (), |row| row.get(0)) - .map_err(|_| NotificationData::IncorrectAnkiFile)?; - let raw: Value = serde_json::from_str(json_models.as_str()) - .map_err(|_| NotificationData::IncorrectAnkiFile)?; + .map_err(|_| IncorrectAnkiFile)?; + let raw: Value = serde_json::from_str(json_models.as_str()).map_err(|_| IncorrectAnkiFile)?; let sets = raw.as_object(); if let Some(sets) = sets { let mut total_data = ImportData(vec![]); let mut count_stmt = connection .prepare("select mid, count(id) from notes group by mid") - .map_err(|_| NotificationData::IncorrectAnkiFile)?; + .map_err(|_| IncorrectAnkiFile)?; let counts = count_stmt .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) - .unwrap() - .map(|x| x.unwrap()) + .map_err(|_| IncorrectAnkiFile)? + .filter_map(|x| x.ok()) .collect::>(); for (_, collection) in sets { let fields_property = collection.get("fields"); if let Some(fields_property) = fields_property { - let fields = fields_property - .as_array() - .unwrap() + let fields = into_result(fields_property.as_array(), IncorrectAnkiFile)? .iter() .filter_map(|x| x.get("name")) .map(|x| x.as_str().unwrap().to_string()) .collect(); let id = collection.get("id"); - if id.is_none() { - return Err(NotificationData::IncorrectAnkiFile); + if id.is_none() || id.unwrap().as_u64().is_none() { + return Err(IncorrectAnkiFile); } let id = id.unwrap().as_u64().unwrap(); let name = collection.get("name"); - if name.is_none() { - return Err(NotificationData::IncorrectAnkiFile); + if name.is_none() || name.unwrap().as_str().is_none() { + return Err(IncorrectAnkiFile); } let name = name.unwrap().as_str().unwrap().to_string(); @@ -79,15 +78,18 @@ pub fn load_groups(connection: &Connection) -> Result Result, NotificationData> { +pub fn get_words_of_group( + connection: &Connection, + group_id: u64, +) -> Result, NotificationData> { let mut count_stmt = connection .prepare("select tags, flds from notes where mid == ?1;") - .map_err(|_| NotificationData::IncorrectAnkiFile)?; + .map_err(|_| IncorrectAnkiFile)?; Ok(count_stmt .query_map((group_id as i64,), |row| { @@ -100,7 +102,7 @@ pub fn get_words_of_group(connection: &Connection, group_id: u64) -> Result>()) } diff --git a/src/data_provider/settings.rs b/src/data_provider/settings.rs index dff727f..b972070 100644 --- a/src/data_provider/settings.rs +++ b/src/data_provider/settings.rs @@ -1,10 +1,12 @@ +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use crate::notification::{MaybeError, NotificationData}; use rusqlite::Connection; pub fn get_setting(key: String, connection: &Connection) -> Option { let mut stmt = connection .prepare("SELECT value FROM settings WHERE id = ?1") - .unwrap(); - let mut iter = stmt.query_map((key,), |row| row.get(0)).unwrap(); + .expect("Failed to prepare statement."); + let mut iter = stmt.query_map((key,), |row| row.get(0)).ok()?; if let Some(row) = iter.next() && let Ok(value) = row @@ -15,31 +17,35 @@ pub fn get_setting(key: String, connection: &Connection) -> Option { None } -pub fn set_setting(key: String, value: String, connection: &Connection) { - let current = get_settings_list(connection); +pub fn set_setting(key: String, value: String, connection: &Connection) -> MaybeError { + let current = get_settings_list(connection)?; if current.contains(&key) { - update_settings(key, value, connection); + update_settings(key, value, connection)?; } else { - create_settings(key, value, connection); + create_settings(key, value, connection)?; } + + Ok(()) } -pub fn delete_settings(key: String, connection: &Connection) { +pub fn delete_settings(key: String, connection: &Connection) -> MaybeError { connection .execute("DELETE FROM settings WHERE id = ?1", (&key,)) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } -fn create_settings(key: String, value: String, connection: &Connection) { +fn create_settings(key: String, value: String, connection: &Connection) -> MaybeError { connection .execute( "INSERT into settings (id, value) VALUES (?1, ?2)", (key, value), ) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } -fn update_settings(key: String, value: String, connection: &Connection) { +fn update_settings(key: String, value: String, connection: &Connection) -> MaybeError { connection .execute( "update settings @@ -47,11 +53,16 @@ set value = ?2 where id = ?1", (key, value), ) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } -fn get_settings_list(connection: &Connection) -> Vec { - let mut stmt = connection.prepare("SELECT id FROM settings").unwrap(); - let iter = stmt.query_map((), |row| row.get(0)).unwrap(); - iter.map(|row| row.unwrap()).collect() +fn get_settings_list(connection: &Connection) -> Result, NotificationData> { + let mut stmt = connection + .prepare("SELECT id FROM settings") + .expect("prepare stmt failed"); + let iter = stmt + .query_map((), |row| row.get(0)) + .map_err(|_| DbLoadError)?; + Ok(iter.filter_map(|i| i.ok()).collect()) } diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 46b850c..cbe539d 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -1,23 +1,25 @@ -use rusqlite::Connection; use crate::fs::*; +use rusqlite::Connection; pub fn create_db() { let db_file = db_path(); if !db_file.exists() { - std::fs::File::create(&db_file).unwrap(); - let connection = Connection::open(&db_file).unwrap(); - connection.execute("PRAGMA foreign_keys = ON;", []).unwrap(); + std::fs::File::create(&db_file).expect("Database file creation failed"); + let connection = Connection::open(&db_file).expect("Connection open failed"); + connection + .execute("PRAGMA foreign_keys = ON;", []) + .expect("Failed to set PRAGMA"); create_tables(&connection); } } fn create_tables(conn: &Connection) { - make_card_set(conn).unwrap(); - make_settings(conn).unwrap(); - make_word_group(conn).unwrap(); - make_words(conn).unwrap(); - make_card_stats(conn).unwrap(); + make_card_set(conn).expect("Failed to init table"); + make_settings(conn).expect("Failed to init table"); + make_word_group(conn).expect("Failed to init table"); + make_words(conn).expect("Failed to init table"); + make_card_stats(conn).expect("Failed to init table"); } fn make_settings(conn: &Connection) -> Result<(), rusqlite::Error> { @@ -51,7 +53,9 @@ fn make_card_stats(conn: &Connection) -> Result<(), rusqlite::Error> { pub fn default_connection() -> Connection { let db_file = db_path(); - let connection = Connection::open(db_file).unwrap(); - connection.execute("PRAGMA foreign_keys = ON;", []).unwrap(); + let connection = Connection::open(db_file).expect("Failed to open database"); + connection + .execute("PRAGMA foreign_keys = ON;", []) + .expect("Failed to set PRAGMA"); connection } diff --git a/src/data_provider/voice.rs b/src/data_provider/voice.rs index ccfcce4..fac1152 100644 --- a/src/data_provider/voice.rs +++ b/src/data_provider/voice.rs @@ -1,10 +1,10 @@ +use crate::fs::*; +use crate::notification::NotificationData; +use crate::notification::NotificationData::*; use reqwest::Client; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::BufReader; -use crate::fs::*; -use crate::notification::NotificationData; -use crate::notification::NotificationData::*; pub async fn get_voice(text: &str) -> Result, NotificationData> { let mut path = voice_dir()?; @@ -19,7 +19,11 @@ pub async fn get_voice(text: &str) -> Result, NotificationData> .send() .await; - let query = query.map_err(|_| VoiceVoxUnavailable)?.text().await.map_err(|_| VoiceVoxUnavailable)?; + let query = query + .map_err(|_| VoiceVoxUnavailable)? + .text() + .await + .map_err(|_| VoiceVoxUnavailable)?; // Synthesis let audio = client .post(format!("{engine_url}/synthesis?speaker=11")) @@ -32,8 +36,12 @@ pub async fn get_voice(text: &str) -> Result, NotificationData> .await .map_err(|_| VoiceVoxUnavailable)?; - tokio::fs::write(&path, &audio).await.map_err(|_| PermissionDenied)?; + tokio::fs::write(&path, &audio) + .await + .map_err(|_| PermissionDenied)?; } - Ok(BufReader::new(File::open(path).map_err(|_| PermissionDenied)?)) + Ok(BufReader::new( + File::open(path).map_err(|_| PermissionDenied)?, + )) } diff --git a/src/data_provider/web_api.rs b/src/data_provider/web_api.rs index 09b36e2..2376f4e 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -1,19 +1,21 @@ -use std::io; +use crate::fs::*; +use crate::notification::NotificationData::{CompressError, PermissionDenied, WebApiUnavailable}; +use crate::notification::{MaybeError, NotificationData}; use log::info; +use std::io; use tokio::fs::{File, OpenOptions}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use zstd::{DEFAULT_COMPRESSION_LEVEL, Decoder, Encoder}; -use crate::fs::*; -use crate::notification::NotificationData; -use crate::notification::NotificationData::{PermissionDenied, WebApiUnavailable}; // const API_URL: &str = "http://127.0.0.1:8089/"; const API_URL: &str = "https://learning.micialware.ru/"; -pub async fn send_data(id: String) -> Result<(), NotificationData> { +pub async fn send_data(id: String) -> MaybeError { let db_file = db_path(); - let data = tokio::fs::read(db_file).await.map_err(|_| WebApiUnavailable)?; - let data = compress(data); + let data = tokio::fs::read(db_file) + .await + .map_err(|_| WebApiUnavailable)?; + let data = compress(data)?; let api_url = API_URL.to_string(); let id_url = format!("{api_url}upload/stream/{id}"); @@ -23,57 +25,63 @@ pub async fn send_data(id: String) -> Result<(), NotificationData> { .post(&id_url) .body(data) .send() - .await.map_err(|_| WebApiUnavailable)? + .await + .map_err(|_| WebApiUnavailable)? .text() - .await.map_err(|_| WebApiUnavailable)?; - set_local_version(new_version.parse::().unwrap_or_default()).await; + .await + .map_err(|_| WebApiUnavailable)?; + set_local_version(new_version.parse::().unwrap_or_default()).await?; Ok(()) } -fn compress(data: Vec) -> Vec { - let mut encoder = Encoder::new(Vec::new(), DEFAULT_COMPRESSION_LEVEL).unwrap(); - io::copy(&mut &data[..], &mut encoder).unwrap(); - encoder.finish().unwrap() +fn compress(data: Vec) -> Result, NotificationData> { + let mut encoder = + Encoder::new(Vec::new(), DEFAULT_COMPRESSION_LEVEL).map_err(|_| CompressError)?; + io::copy(&mut &data[..], &mut encoder).map_err(|_| PermissionDenied)?; + encoder.finish().map_err(|_| CompressError) } -pub async fn load_data(id: String, temp: bool) -> Result<(), NotificationData> { +pub async fn load_data(id: String, temp: bool) -> MaybeError { let api_url = API_URL.to_string(); let id_url = format!("{api_url}download/{id}"); let client = reqwest::Client::new(); let data = client .get(&id_url) .send() - .await.map_err(|_| WebApiUnavailable)? + .await + .map_err(|_| WebApiUnavailable)? .bytes() - .await.map_err(|_| WebApiUnavailable)? + .await + .map_err(|_| WebApiUnavailable)? .to_vec(); - let data = decompress(data); + let data = decompress(data)?; let path = app_data_dir(); let file_name = if temp { "data.db.tmp" } else { "data.db" }; let db_file = path.join(file_name); - tokio::fs::write(db_file, data).await.map_err(|_| PermissionDenied)?; + tokio::fs::write(db_file, data) + .await + .map_err(|_| PermissionDenied)?; let version_url = format!("{API_URL}{id}/version"); let version = client .get(&version_url) .send() - .await.map_err(|_| WebApiUnavailable)? + .await + .map_err(|_| WebApiUnavailable)? .text() - .await.map_err(|_| WebApiUnavailable)?; - set_local_version(version.parse::().unwrap_or_default()).await; + .await + .map_err(|_| WebApiUnavailable)?; + set_local_version(version.parse::().unwrap_or_default()).await?; Ok(()) } -pub fn decompress(data: Vec) -> Vec { - let mut decoder = Decoder::new(&data[..]).unwrap(); +pub fn decompress(data: Vec) -> Result, NotificationData> { + let mut decoder = Decoder::new(&data[..]).map_err(|_| CompressError)?; let mut decompressed = Vec::new(); - let res = io::copy(&mut decoder, &mut decompressed); - if let Err(e) = res { - info!("{}", e); - } - decompressed + io::copy(&mut decoder, &mut decompressed).map_err(|_| PermissionDenied)?; + Ok(decompressed) } pub async fn first_sync() -> Result { @@ -95,22 +103,29 @@ pub async fn first_sync() -> Result { pub async fn get_web_version(key: &str) -> Result { let id_url = format!("{API_URL}{key}/version"); let client = reqwest::Client::new(); - let version = client.get(&id_url).send().await.map_err(|_| WebApiUnavailable)?.text().await.map_err(|_| WebApiUnavailable)?; + let version = client + .get(&id_url) + .send() + .await + .map_err(|_| WebApiUnavailable)? + .text() + .await + .map_err(|_| WebApiUnavailable)?; info!("version: {}", version); Ok(version.parse::().unwrap_or_default()) } -pub async fn get_local_version() -> u32 { +pub async fn get_local_version() -> Result { let file = db_meta_path(); if file.exists() { let mut data = String::new(); File::open(file.clone()) .await - .unwrap() + .map_err(|_| PermissionDenied)? .read_to_string(&mut data) .await - .unwrap(); - return data.parse::().unwrap_or_default(); + .map_err(|_| PermissionDenied)?; + return Ok(data.parse::().unwrap_or_default()); } let mut file = OpenOptions::new() @@ -119,12 +134,14 @@ pub async fn get_local_version() -> u32 { .truncate(true) .open(file) .await - .unwrap(); - file.write_all("0".as_bytes()).await.unwrap(); - 0 + .map_err(|_| PermissionDenied)?; + file.write_all("0".as_bytes()) + .await + .map_err(|_| PermissionDenied)?; + Ok(0) } -pub async fn set_local_version(version: u32) { +pub async fn set_local_version(version: u32) -> Result<(), NotificationData> { let file_path = db_meta_path(); let mut file = OpenOptions::new() .write(true) @@ -132,8 +149,9 @@ pub async fn set_local_version(version: u32) { .truncate(true) .open(file_path) .await - .unwrap(); + .map_err(|_| PermissionDenied)?; file.write_all(version.to_string().as_bytes()) .await - .unwrap(); + .map_err(|_| PermissionDenied)?; + Ok(()) } diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 68aec73..7e20323 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,8 +1,10 @@ +use crate::data::entities::{WordData, WordGroup}; +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use crate::notification::{MaybeError, NotificationData}; use rusqlite::{Connection, params}; use std::collections::HashMap; -use crate::data::entities::{WordData, WordGroup}; -pub fn add_word(word: &mut WordData, connection: &Connection) { +pub fn add_word(word: &mut WordData, connection: &Connection) -> MaybeError { let index : u32 = connection .query_row( "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id", @@ -10,17 +12,18 @@ pub fn add_word(word: &mut WordData, connection: &Connection) { &word.key, &word.value, &word.tags, - serde_json::to_string(&word.additional).unwrap(), + serde_json::to_string(&word.additional).unwrap_or_default(), &word.group_id, ), |row| row.get(0), - ).unwrap(); + ).map_err(|_| DbSaveError)?; word.id = index.into(); + Ok(()) } -pub fn add_words(words: &mut [WordData], connection: &mut Connection) { - let tx = connection.transaction().unwrap(); +pub fn add_words(words: &mut [WordData], connection: &mut Connection) -> MaybeError { + let tx = connection.transaction().map_err(|_| DbLoadError)?; let count = words.len(); { @@ -28,33 +31,35 @@ pub fn add_words(words: &mut [WordData], connection: &mut Connection) { .prepare( "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5)", ) - .unwrap(); + .expect("Failed to prepare statement"); for word in words.iter() { stmt.execute(params![ word.key, word.value, word.tags, - serde_json::to_string(&word.additional).unwrap(), + serde_json::to_string(&word.additional).unwrap_or_default(), word.group_id ]) - .unwrap(); + .map_err(|_| DbSaveError)?; } } let last_index: u32 = tx.last_insert_rowid() as u32; - tx.commit().unwrap(); + tx.commit().map_err(|_| DbSaveError)?; let start_index = last_index - (count as u32) + 1; for (index, id) in (start_index..=last_index).enumerate() { words[index].id = id.into(); } + + Ok(()) } -pub fn update_word(word: &mut WordData, connection: &Connection) { +pub fn update_word(word: &mut WordData, connection: &Connection) -> MaybeError { if !word.id.is_valid() { - add_word(word, connection); + Ok(add_word(word, connection)?) } else { connection .execute( @@ -63,27 +68,29 @@ pub fn update_word(word: &mut WordData, connection: &Connection) { &word.key, &word.value, &word.tags, - serde_json::to_string(&word.additional).unwrap(), + serde_json::to_string(&word.additional).map_err(|_| DbSaveError)?, &word.id, ), ) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } } -pub fn delete_word(word: &WordData, connection: &Connection) { +pub fn delete_word(word: &WordData, connection: &Connection) -> MaybeError { if !word.id.is_valid() { - return; + return Err(DbSaveError); } connection .execute("DELETE FROM words WHERE id = ?1", (&word.id,)) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } -pub fn load_words(connection: &Connection) -> Vec { +pub fn load_words(connection: &Connection) -> Result, NotificationData> { let mut stmt = connection .prepare("SELECT id, key, value, tags, more, group_id FROM words") - .unwrap(); + .expect("Failed to prepare statement"); let word_iter = stmt .query_map([], |row| { let additional: String = row.get(4)?; @@ -92,24 +99,25 @@ pub fn load_words(connection: &Connection) -> Vec { key: row.get(1)?, value: row.get(2)?, tags: row.get(3)?, - additional: serde_json::from_str::>(&additional).unwrap(), + additional: serde_json::from_str::>(&additional) + .unwrap_or_default(), group_id: row.get(5)?, }) }) - .unwrap(); + .map_err(|_| DbLoadError)?; - let mut buffer = vec![]; - for word in word_iter { - buffer.push(word.unwrap()); - } + let mut buffer = Vec::with_capacity(1024); + word_iter + .filter_map(|res| res.ok()) + .for_each(|i| buffer.push(i)); - buffer + Ok(buffer) } -pub fn load_word_groups(connection: &Connection) -> Vec { +pub fn load_word_groups(connection: &Connection) -> Result, NotificationData> { let mut stmt = connection .prepare("SELECT id, name FROM word_group") - .unwrap(); + .expect("Failed to prepare statement"); let group_iter = stmt .query_map([], |row| { Ok(WordGroup { @@ -117,46 +125,49 @@ pub fn load_word_groups(connection: &Connection) -> Vec { name: row.get(1)?, }) }) - .unwrap(); + .map_err(|_| DbLoadError)?; - let mut buffer = vec![]; - for group in group_iter { - buffer.push(group.unwrap()); - } + let mut buffer = Vec::with_capacity(8); + group_iter + .filter_map(|res| res.ok()) + .for_each(|i| buffer.push(i)); - buffer + Ok(buffer) } -pub fn add_group(group: &mut WordGroup, connection: &Connection) { +pub fn add_group(group: &mut WordGroup, connection: &Connection) -> MaybeError { let index: u32 = connection .query_row( "INSERT INTO word_group (name) VALUES (?1) RETURNING id", (&group.name,), |row| row.get(0), ) - .unwrap(); + .map_err(|_| DbSaveError)?; group.id = index.into(); + Ok(()) } -pub fn update_group(group: &mut WordGroup, connection: &Connection) { +pub fn update_group(group: &mut WordGroup, connection: &Connection) -> MaybeError { if !group.id.is_valid() { - add_group(group, connection); + Ok(add_group(group, connection)?) } else { connection .execute( "UPDATE word_group SET name = ?1 WHERE id = ?2", (&group.name, &group.id), ) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } } -pub fn delete_group(group: &WordGroup, connection: &Connection) { +pub fn delete_group(group: &WordGroup, connection: &Connection) -> MaybeError { if !group.id.is_valid() { - return; + return Err(DbSaveError); } connection .execute("DELETE FROM word_group WHERE id = ?1", (&group.id,)) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } diff --git a/src/fs.rs b/src/fs.rs index c09e388..5ef1e37 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,23 +1,23 @@ -use std::fs; -use std::path::PathBuf; use crate::notification::NotificationData; use crate::notification::NotificationData::PermissionDenied; +use std::fs; +use std::path::PathBuf; pub fn app_data_dir() -> PathBuf { - let mut dir = dirs::data_dir().unwrap(); + let mut dir = dirs::data_dir().expect("Unsupported OS"); dir.push("jap_learn"); if !dir.exists() { - fs::create_dir_all(dir.clone()).unwrap(); + fs::create_dir_all(dir.clone()).expect("Unable to create directory"); } dir } pub fn db_path() -> PathBuf { - app_data_dir().join("data.db") + app_data_dir().join("data.db") } pub fn temp_db_path() -> PathBuf { - app_data_dir().join("data.db.tmp") + app_data_dir().join("data.db.tmp") } pub fn temp_import_path() -> PathBuf { @@ -42,4 +42,4 @@ pub fn voice_dir() -> Result { fs::create_dir_all(directory.clone()).map_err(|_| PermissionDenied)?; } Ok(directory) -} \ No newline at end of file +} diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..004729c --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,24 @@ +#[macro_export] +macro_rules! push_error_task { + ($e:expr) => { + iced::Task::done(push_error!($e)) + }; +} + +#[macro_export] +macro_rules! push_error { + ($e:expr) => { + $crate::navigation::RootMessage::PushNotification( + $crate::notification::NotificationRequest::Error($e), + ) + }; +} + +#[inline(always)] +pub fn into_result(option: Option, error: E) -> Result { + if let Some(r) = option { + Ok(r) + } else { + Err(error) + } +} diff --git a/src/main.rs b/src/main.rs index 4c9bbf2..34b8108 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ #![windows_subsystem = "windows"] -mod data_provider; -pub mod notification; pub mod app; -mod styling; -mod pages; -mod navigation; -mod fs; mod core; mod data; +mod data_provider; +mod fs; +pub mod helpers; +mod navigation; +pub mod notification; +mod pages; +mod styling; use crate::app::start; use mimalloc::MiMalloc; @@ -15,7 +16,6 @@ use mimalloc::MiMalloc; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; - fn main() -> iced::Result { env_logger::init(); start() diff --git a/src/navigation.rs b/src/navigation.rs index 66f3cfe..9506416 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,15 +1,16 @@ -use crate::app::{fill_state, AppState}; +use crate::app::{AppState, fill_state}; +use crate::data::Id; use crate::data_provider::history::get_history_of_set; use crate::data_provider::sqlite::{create_db, default_connection}; use crate::data_provider::web_api::{ get_local_version, get_web_version, load_data, set_local_version, }; use crate::fs::*; -use crate::message_navigation; use crate::navigation::Page::*; use crate::navigation::RootMessage::{ DataLoaded, Keyboard, Notification, PushNotification, UpdateData, }; +use crate::notification::NotificationData::PermissionDenied; use crate::notification::{ NotificationData, NotificationManager, NotificationMessage, NotificationRequest, }; @@ -29,6 +30,7 @@ use crate::pages::writing::{WritingMessage, WritingState}; use crate::state_navigate_handle; use crate::state_update; use crate::view_navigation; +use crate::{message_navigation, push_error, push_error_task}; use chrono::NaiveDate; use hashbrown::HashMap; use iced::keyboard::Event; @@ -40,7 +42,6 @@ use parking_lot::Mutex; use rusqlite::Connection; use std::sync::Arc; use std::time::Instant; -use crate::data::Id; impl Default for ScreenState { fn default() -> Self { @@ -77,7 +78,8 @@ pub enum RootMessage { None, UpdateData, } - +#[allow(clippy::enum_variant_names)] +#[allow(clippy::large_enum_variant)] pub enum Page { Selector(SelectorState), Quiz(QuizState), @@ -107,14 +109,17 @@ impl ScreenState { create_db(); info!("Db created"); let state = ScreenState::default(); - let reading_additional_task = Task::perform(Self::load_additional_data(), |data| data); + let reading_additional_task = Task::perform(Self::load_additional_data(), |data| { + data.unwrap_or_else(|err| push_error!(err)) + }); - let final_task; + let mut final_task = reading_additional_task; let state_guard = state.app_state.lock(); - if state_guard.sync_data.auto_web_fetch { - let key = state_guard.sync_data.key.clone().unwrap(); + if state_guard.sync_data.auto_web_fetch + && let Some(key) = state_guard.sync_data.key.clone() + { final_task = Task::batch([ - reading_additional_task, + final_task, Task::perform(Self::load_web_backup(key), |result| match result { Ok(update) => { if update { @@ -123,11 +128,9 @@ impl ScreenState { RootMessage::None } } - Err(err) => PushNotification(NotificationRequest::Warning(err)), + Err(err) => push_error!(err), }), ]); - } else { - final_task = reading_additional_task; } drop(state_guard); @@ -135,47 +138,46 @@ impl ScreenState { (state, final_task) } - async fn load_additional_data() -> RootMessage { - let directory = history_dir(); - if let Ok(directory) = directory { - let mut map = HashMap::new(); - for file in directory.read_dir().unwrap().flatten() { - let mut vec = vec![]; - let history_file_name = match file.file_name().into_string() { - Err(_) => continue, - Ok(v) => v, - }; + async fn load_additional_data() -> Result { + let directory = history_dir()?; - let id = history_file_name[4..history_file_name.len() - 12] - .parse::() - .unwrap(); + let mut map = HashMap::new(); + for file in directory + .read_dir() + .map_err(|_| PermissionDenied)? + .filter_map(Result::ok) + { + let mut vec = vec![]; + let history_file_name = match file.file_name().into_string() { + Err(_) => continue, + Ok(v) => v, + }; - let history = get_history_of_set(id); - if let Ok(history) = history { - let by_date = history.chunk_by(|x, x1| { - x.timestamp.naive_local().date() == x1.timestamp.naive_local().date() - }); - for group in by_date { - vec.push((group[0].timestamp.naive_local().date(), group.len() as u32)); - } - map.insert(id, vec); - } else { - return PushNotification(NotificationRequest::Warning(history.err().unwrap())); + if let Ok(id) = history_file_name.split('_').collect::>()[1].parse::() + && let Ok(history) = get_history_of_set(id) + { + let by_date = history.chunk_by(|x, x1| { + x.timestamp.naive_local().date() == x1.timestamp.naive_local().date() + }); + for group in by_date { + vec.push((group[0].timestamp.naive_local().date(), group.len() as u32)); } + map.insert(id, vec); + } else { + continue; } - return DataLoaded(map); } - PushNotification(NotificationRequest::Warning(directory.err().unwrap())) + Ok(DataLoaded(map)) } async fn load_web_backup(string: String) -> Result { - let local = get_local_version().await; + let local = get_local_version().await?; let web = get_web_version(string.as_str()).await?; info!("{} {}", local, web); if web > local { info!("Web version is newer than local version"); load_data(string, true).await?; - set_local_version(web).await; + set_local_version(web).await?; } else { return Ok(false); } @@ -202,7 +204,9 @@ impl ScreenState { } let db_file = db_path(); let temp_db_file = temp_db_path(); - std::fs::rename(temp_db_file, &db_file).unwrap(); + if std::fs::rename(temp_db_file, &db_file).is_err() { + return push_error_task!(PermissionDenied); + } state.connection = default_connection(); fill_state(&mut state); @@ -289,11 +293,13 @@ impl ScreenState { } fn handle_keyboard(&mut self, message: Event) -> Task { - let page = self.stack.last_mut().unwrap(); - match page { - Repetition(page) => page.press(&message), - _ => Task::none(), + if let Some(page) = self.stack.last_mut() { + return match page { + Repetition(page) => page.press(&message), + _ => Task::none(), + }; } + Task::none() } } @@ -305,7 +311,7 @@ pub struct AppSettings { #[macro_export] macro_rules! view_navigation { ($stack:expr, $($e:ident), *) => { - match &$stack.last().unwrap() { + match &$stack.last().expect("Unexpected stack state") { $( $e(s) => s.view().map(RootMessage::$e), )* @@ -319,7 +325,7 @@ macro_rules! state_update { match $message { $( RootMessage::$e(msg) => { - if let $e(s) = $stack.last_mut().unwrap() { + if let $e(s) = $stack.last_mut().expect("Unexpected stack state") { message_navigation!(msg, $stack, s) } else { @@ -354,7 +360,7 @@ macro_rules! message_navigation { #[macro_export] macro_rules! state_navigate_handle { ($stack:expr, $($e:ident), *) => { - match $stack.last_mut().unwrap() { + match $stack.last_mut().expect("Unexpected stack state") { $( Page::$e(page) => { page.navigated(); diff --git a/src/notification.rs b/src/notification.rs index 5c65124..d519a88 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -109,7 +109,9 @@ impl NotificationManager { iced::widget::button("✕") .on_press(NotificationMessage::Remove(*id)) .style(iced::widget::button::text) - ].align_y(Center).width(300) + ] + .align_y(Center) + .width(300) .padding(HALF_SPACING), ); container @@ -157,6 +159,15 @@ impl NotificationManager { NotificationData::WebApiUnavailable => { text!("Не получается подключиться к Api") } + NotificationData::DbSaveError => { + text!("Не получается сохранить данные") + } + NotificationData::DbLoadError => { + text!("Не получается загрузить данные") + } + NotificationData::CompressError => { + text!("Ошибка сжатия или распаковки данных") + } } .width(Fill) .into() @@ -170,7 +181,7 @@ pub enum NotificationRequest { Error(NotificationData), } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum NotificationData { PermissionDenied, DataLoaded, @@ -179,4 +190,9 @@ pub enum NotificationData { OpenFileError, VoiceVoxUnavailable, WebApiUnavailable, + DbSaveError, + DbLoadError, + CompressError, } + +pub type MaybeError = Result<(), NotificationData>; diff --git a/src/pages/dictionary.rs b/src/pages/dictionary.rs index cc808dc..b800564 100644 --- a/src/pages/dictionary.rs +++ b/src/pages/dictionary.rs @@ -1,12 +1,16 @@ +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::navigation::Page::{Import, Word}; +use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::pages::dictionary::DictionaryMessage::*; use crate::pages::dictionary_quiz::DictionaryQuizState; use crate::pages::import::ImportState; -use crate::navigation::Page::{Import, Word}; -use crate::navigation::{NavigatedPage, Page, RootMessage}; -use crate::styling::*; use crate::pages::word::WordState; +use crate::push_error; +use crate::push_error_task; +use crate::styling::*; use chrono::{DateTime, TimeDelta, Utc}; use hashbrown::{HashMap, HashSet}; use iced::alignment::Vertical::Center; @@ -17,14 +21,12 @@ use iced::widget::text_input::default; 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 log::info; -use crate::app::AppState; -use crate::data::entities::{WordData, WordGroup}; #[derive(Clone)] pub struct DictionaryState { @@ -168,7 +170,9 @@ impl NavigatedPage for DictionaryState { let word = dict.remove(i); self.include_map.remove(i); self.auto_save_queue.remove(&i); - delete_word(&word, &state.connection) + if let Err(err) = delete_word(&word, &state.connection) { + return push_error_task!(err); + } } Include(i, b) => self.include_map[i] = b, IncludeTag(t, v) => { @@ -190,7 +194,7 @@ impl NavigatedPage for DictionaryState { self.search = s; } SetTyping(b) => self.no_typing = b, - SubmitWord(i) => self.save_word(i), + SubmitWord(i) => return self.save_word(i), Back => {} Test => {} CreateGroup => { @@ -213,7 +217,10 @@ impl NavigatedPage for DictionaryState { let connection = &state.connection; let mut group = state.word_groups[self.selected_group_index].clone(); - update_group(&mut group, connection); + if let Err(err) = update_group(&mut group, connection) { + return push_error_task!(err); + } + state.word_groups[self.selected_group_index] = group; } SelectGroup(i) => { @@ -236,7 +243,10 @@ impl NavigatedPage for DictionaryState { let remove_group_id = group.id; let connection = &state.connection; - delete_group(group, connection); + if let Err(err) = delete_group(group, connection) { + return push_error_task!(err); + } + state.word_groups.remove(self.selected_group_index); state .dictionary @@ -257,7 +267,8 @@ impl NavigatedPage for DictionaryState { } if self.auto_save_queue[&word_index] <= now { self.auto_save_queue.remove(&word_index); - self.save_word(word_index); + + return self.save_word(word_index); } } ToImport => {} @@ -309,13 +320,17 @@ impl DictionaryState { result } - fn save_word(&mut self, i: usize) { + 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(); - update_word(word, connection); + if let Err(err) = update_word(word, connection) { + return push_error_task!(err); + } + state.dictionary[i] = word.clone(); + Task::none() } fn launch_auto_save_offset(&mut self, index: usize) -> Task { diff --git a/src/pages/dictionary_quiz.rs b/src/pages/dictionary_quiz.rs index 0f48535..a2a814c 100644 --- a/src/pages/dictionary_quiz.rs +++ b/src/pages/dictionary_quiz.rs @@ -1,8 +1,9 @@ +use crate::data::entities::WordData; +use crate::navigation::Page::PreviousPage; use crate::navigation::RootMessage; +use crate::navigation::*; use crate::pages::dictionary::split_with_coma; use crate::pages::dictionary_quiz::DictionaryQuizMessage::*; -use crate::navigation::Page::PreviousPage; -use crate::navigation::*; use crate::pages::quiz::Score; use crate::styling::*; use iced::Background::Color; @@ -11,7 +12,6 @@ use iced::widget::container::Style; use iced::widget::{Row, button, container, row, space, text, text_input}; use iced::{Border, Element, Fill, Task, Theme, alignment}; use rand::prelude::SliceRandom; -use crate::data::entities::WordData; #[derive(Debug, Clone)] pub struct DictionaryQuizState { @@ -147,7 +147,8 @@ impl DictionaryQuizState { self.score.total += 1; } if self.answer.to_lowercase() == self.correct.to_lowercase() - || split_with_coma(self.correct.to_lowercase().as_str()).contains(&self.answer.to_lowercase()) + || split_with_coma(self.correct.to_lowercase().as_str()) + .contains(&self.answer.to_lowercase()) { if !self.is_help { self.score.correct += 1; diff --git a/src/pages/history.rs b/src/pages/history.rs index f17bb53..4cc0558 100644 --- a/src/pages/history.rs +++ b/src/pages/history.rs @@ -1,6 +1,10 @@ -use crate::data_provider::history::{get_history_of_set}; +use crate::app::AppState; +use crate::data::INVALID_ID; +use crate::data::entities::{HistoryItem, WordData}; +use crate::data_provider::history::get_history_of_set; use crate::navigation::*; use crate::styling::*; +use HistoryMessage::Back; use chrono::Local; use iced::alignment::Horizontal::Center; use iced::widget::space::horizontal; @@ -8,10 +12,6 @@ use iced::widget::*; use iced::{Element, Fill, FillPortion, Task}; use parking_lot::Mutex; use std::sync::Arc; -use HistoryMessage::Back; -use crate::app::AppState; -use crate::data::entities::{HistoryItem, WordData}; -use crate::data::INVALID_ID; #[derive(Clone)] pub struct HistoryState { diff --git a/src/pages/import.rs b/src/pages/import.rs index 71db597..5a4de5d 100644 --- a/src/pages/import.rs +++ b/src/pages/import.rs @@ -1,10 +1,13 @@ +use crate::app::AppState; +use crate::data::entities::{WordData, WordGroup}; use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups}; use crate::data_provider::words::{add_group, add_words}; -use crate::pages::import::ImportMessage::*; -use crate::data::entities::{WordData, WordGroup}; +use crate::fs::*; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::NotificationData::OpenFileError; -use crate::notification::{NotificationData, NotificationRequest}; +use crate::notification::{MaybeError, NotificationData}; +use crate::pages::import::ImportMessage::*; +use crate::push_error; use crate::styling::*; use iced::widget::button::danger; use iced::widget::container::success; @@ -16,6 +19,7 @@ use iced::{Element, Task}; use iced_core::Alignment::Center; use iced_core::Length::Fill; use iced_core::Padding; +use log::info; use parking_lot::Mutex; use rfd::AsyncFileDialog; use rusqlite::Connection; @@ -23,11 +27,8 @@ use std::fs::{File, OpenOptions}; use std::io::BufReader; use std::path::PathBuf; use std::sync::Arc; -use log::info; use tokio::task::spawn_blocking; use zip::ZipArchive; -use crate::app::AppState; -use crate::fs::*; const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; @@ -367,12 +368,12 @@ impl ImportState { }, |result: Result| match result { Ok(import) => RootMessage::Import(UpdateImport(import)), - Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err)), + Err(err) => push_error!(err), }, ) } - fn extract_import_file(path: PathBuf) -> Result<(), NotificationData> { + fn extract_import_file(path: PathBuf) -> MaybeError { let file = File::open(path).map_err(|_| OpenFileError)?; let reader = BufReader::new(file); let mut archive = ZipArchive::new(reader).map_err(|_| OpenFileError)?; @@ -432,7 +433,8 @@ impl ImportState { async move { spawn_blocking(move || { let temp_file_path = temp_import_path(); - let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?; + let connection = + Connection::open(&temp_file_path).map_err(|_| OpenFileError)?; let import_list = get_words_of_group(&connection, group.id)?; let mut words_list = Vec::with_capacity(import_list.len()); @@ -443,7 +445,7 @@ impl ImportState { }; { let state = state.lock(); - add_group(&mut group_entity, &state.connection); + add_group(&mut group_entity, &state.connection)?; } let group_id = group_entity.id; @@ -483,7 +485,7 @@ impl ImportState { let mut index = 0; let total_len = words_list.len() as f32 / 1024.0; for word in &mut words_list.chunks_mut(1024) { - add_words(word, connection); + add_words(word, connection)?; index += 1; let mut progress = progress.lock(); *progress = (index as f32 / total_len).min(1.0); @@ -499,8 +501,8 @@ impl ImportState { }, |r: Result<(), NotificationData>| match r { Ok(_) => RootMessage::Import(ImportFinished), - Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err)) - } + Err(err) => push_error!(err), + }, ) } diff --git a/src/pages/quiz.rs b/src/pages/quiz.rs index b94d71f..3e2fa5f 100644 --- a/src/pages/quiz.rs +++ b/src/pages/quiz.rs @@ -1,4 +1,5 @@ - +use crate::app::USER_FONT; +use crate::core::kana_set::KanaSet; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::pages::quiz::QuizMessage::*; @@ -6,8 +7,6 @@ use crate::styling::*; use iced::widget::*; use iced::{Element, Fill, Task, alignment}; use rand::seq::SliceRandom; -use crate::app::USER_FONT; -use crate::core::kana_set::KanaSet; #[derive(Clone, Debug)] pub struct QuizState { diff --git a/src/pages/repetition.rs b/src/pages/repetition.rs index f40e33f..15f0a70 100644 --- a/src/pages/repetition.rs +++ b/src/pages/repetition.rs @@ -1,19 +1,20 @@ use crate::app::AppState; use crate::core::deck::DeckData; use crate::core::srs::WordOpenMode; -use crate::data::entities::{CardStatistics, DeckSettings, WordData}; use crate::data::Id; +use crate::data::entities::{CardStatistics, DeckSettings, WordData}; use crate::data_provider::voice::get_voice; use crate::navigation::Page::PreviousPage; use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage}; -use crate::notification::{NotificationData, NotificationRequest}; +use crate::notification::{MaybeError, NotificationData}; use crate::styling::*; +use crate::{push_error, push_error_task}; use chrono::Local; use iced::alignment::Horizontal::Center; use iced::keyboard::key::Physical::Code; use iced::widget::text::Style; -use iced::widget::{button, column, container, row, rule, space, text, tooltip, Column}; -use iced::{alignment, keyboard, Element, Fill, Task, Theme}; +use iced::widget::{Column, button, column, container, row, rule, space, text, tooltip}; +use iced::{Element, Fill, Task, Theme, alignment, keyboard}; use parking_lot::Mutex; use rodio::MixerDeviceSink; use std::collections::HashSet; @@ -57,7 +58,7 @@ impl NavigatedPage for RepetitionState { if self.settings.require_speech() { return Task::perform(play_sound(self.sink.clone(), value), |r| match r { Ok(_) => RootMessage::Repetition(RepetitionMessage::PlayFinished), - Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err)), + Err(err) => push_error!(err), }); } } @@ -101,8 +102,11 @@ impl NavigatedPage for RepetitionState { } impl RepetitionState { - pub(crate) fn new(set: DeckSettings, state: Arc>) -> RepetitionState { - let mut card_set = DeckData::new(&set, state.clone()); + pub(crate) fn new( + set: DeckSettings, + state: Arc>, + ) -> Result { + let mut card_set = DeckData::new(&set, state.clone())?; let (word, stat) = card_set.next(); let sink_handle = if set.forward.contains("speech") || set.backward.contains("speech") { @@ -111,7 +115,7 @@ impl RepetitionState { None }; - RepetitionState { + Ok(RepetitionState { state, settings: set, set: card_set, @@ -121,7 +125,7 @@ impl RepetitionState { can_play: true, sink: Arc::new(sink_handle), opened: HashSet::new(), - } + }) } } @@ -140,7 +144,10 @@ impl RepetitionState { return Task::none(); } - self.set.open(mode); + if let Err(err) = self.set.open(mode) { + return push_error_task!(err); + } + self.open = false; self.opened.insert(self.current_word.id); let next = self.set.next(); @@ -152,7 +159,7 @@ impl RepetitionState { let task = play_sound(sink, self.current_word.key.clone()); return Task::perform(task, |r| match r { Ok(_) => RootMessage::Repetition(RepetitionMessage::PlayFinished), - Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err)), + Err(err) => push_error!(err), }); } self.increment_counter(); @@ -327,10 +334,7 @@ pub enum RepetitionMessage { PlayFinished, } -async fn play_sound( - sink: Arc>, - text: String, -) -> Result<(), NotificationData> { +async fn play_sound(sink: Arc>, text: String) -> MaybeError { let data = get_voice(text.as_str()).await?; spawn_blocking(move || { if let Some(sink) = sink.as_ref() @@ -340,6 +344,6 @@ async fn play_sound( } }) .await - .unwrap(); + .expect("Playing sound failed"); Ok(()) } diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index 62f5517..b648c6b 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -1,10 +1,13 @@ - +use crate::app::AppState; +use crate::data::entities::DeckSettings; use crate::data_provider::card_sets::{delete_set, update_deck}; use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; +use crate::notification::MaybeError; use crate::pages::repetition_settings::RepetitionSettingsMessage::*; use crate::styling::*; +use crate::{push_error, push_error_task}; use iced::widget::button::danger; use iced::widget::text_input::Catalog; use iced::widget::{button, column, row, scrollable, space, text, text_input}; @@ -14,8 +17,6 @@ use iced_core::{Padding, Theme, color}; use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; -use crate::app::AppState; -use crate::data::entities::DeckSettings; pub struct RepetitionSettingsState { set: DeckSettings, @@ -61,9 +62,13 @@ impl NavigatedPage for RepetitionSettingsState { Back => {} Save => { let mut state = self.state.lock(); - update_deck(&mut self.set, &state.connection); - if self.filter_changed { - update_deck_stats(&self.set, &state); + if let Err(e) = update_deck(&mut self.set, &state.connection) { + return push_error_task!(e); + } + if self.filter_changed + && let Err(e) = update_deck_stats(&self.set, &state) + { + return push_error_task!(e); } state.decks[self.index] = self.set.clone(); } @@ -95,7 +100,10 @@ impl NavigatedPage for RepetitionSettingsState { }); } let mut state = self.state.lock(); - delete_set(&self.set, &state.connection); + if let Err(e) = delete_set(&self.set, &state.connection) { + return push_error_task!(e); + } + state.decks.remove(self.index); return Task::done(RootMessage::RepetitionSettings(Back)); } @@ -171,8 +179,8 @@ impl NavigatedPage for RepetitionSettingsState { } } -fn update_deck_stats(deck: &DeckSettings, state: &AppState) { - let mut db_stats = load_stats_of_deck(deck, &state.connection); +fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError { + let mut db_stats = load_stats_of_deck(deck, &state.connection).expect("Wrong db operation"); let filtered_words = deck.get_word_list(state); filtered_words.iter().for_each(|word| { let word_id = state.dictionary[*word].id; @@ -181,7 +189,8 @@ fn update_deck_stats(deck: &DeckSettings, state: &AppState) { } }); - delete_stats(db_stats.as_slice(), &state.connection); + delete_stats(db_stats.as_slice(), &state.connection)?; + Ok(()) } impl RepetitionSettingsState { diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 6fbcf31..075ad28 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -6,10 +6,13 @@ use crate::data_provider::card_sets::update_deck; use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; +use crate::notification::{MaybeError, NotificationRequest}; use crate::pages::history::HistoryState; use crate::pages::repetition::RepetitionState; use crate::pages::repetition_settings::RepetitionSettingsState; use crate::pages::repetitions::RepetitionsMessage::*; +use crate::push_error; +use crate::push_error_task; use crate::styling::*; use chrono::{Days, Local, Utc}; use hashbrown::{HashMap, HashSet}; @@ -50,7 +53,9 @@ impl NavigatedPage for RepetitionsState { let selected_set = self.selected_deck_mut().unwrap(); if selected_set.append_mode == AppendMode::Full { - Self::append_all_words(&mut clone.lock(), selected_set); + if let Err(_err) = Self::append_all_words(&mut clone.lock(), selected_set) { + return None; + } selected_set.existing_words_indices = selected_set.available_words_indices.clone(); } else { if selected_set.available_words_indices.is_empty() @@ -60,10 +65,13 @@ impl NavigatedPage for RepetitionsState { } } - Some(Repetition(RepetitionState::new( - selected_set.general_settings.clone(), - clone, - ))) + if let Ok(repetition_state) = + RepetitionState::new(selected_set.general_settings.clone(), clone) + { + return Some(Repetition(repetition_state)); + } + + None } else if let GoToSettings = message { Some(RepetitionSettings(RepetitionSettingsState::new( self.selected_deck().unwrap().index.unwrap(), @@ -82,10 +90,9 @@ impl NavigatedPage for RepetitionsState { self.decks.remove(index); self.clear_selection(); } else { - self.decks[index].general_settings = - state.decks[index].clone(); + self.decks[index].general_settings = state.decks[index].clone(); drop(state); - self.fill_deck_word_indices(); + let _ = self.fill_deck_word_indices(); } } else { drop(state); @@ -138,15 +145,21 @@ impl NavigatedPage for RepetitionsState { SelectSet(index) => { self.select_deck(index); let deck = self.selected_deck().unwrap(); - if !deck.indices_ready { - self.fill_deck_word_indices(); + if !deck.indices_ready + && let Err(err) = self.fill_deck_word_indices() + { + return push_error_task!(err); } } Save => { let mut deck = self.selected_deck().unwrap().clone(); let mut state = self.state.lock(); { - update_deck(&mut deck.general_settings, &state.connection); + 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 { @@ -209,7 +222,11 @@ impl NavigatedPage for RepetitionsState { let deck = self.selected_deck().unwrap(); let mut state = self.state.lock(); - Self::append_words(&mut state, &deck.general_settings, adding.into_iter()); + if let Err(err) = + Self::append_words(&mut state, &deck.general_settings, adding.into_iter()) + { + return push_error_task!(err); + } } } Task::none() @@ -433,7 +450,7 @@ impl RepetitionsState { let date = (*now) .checked_sub_days(Days::new(30 * 7 - i * 7 - j - 1)) - .unwrap(); + .unwrap_or_default(); column = column.push(tooltip( iced::widget::container(space().height(15).width(15)).style( @@ -545,7 +562,7 @@ impl RepetitionsState { } impl RepetitionsState { - fn append_all_words(state: &mut AppState, deck: &DeckViewData) { + fn append_all_words(state: &mut AppState, deck: &DeckViewData) -> MaybeError { let existing = &deck.existing_words_indices; let mut created_set = HashSet::with_capacity(existing.len()); existing.iter().for_each(|c| { @@ -556,13 +573,14 @@ impl RepetitionsState { .iter() .filter(|i| !created_set.contains(i)) .cloned(); - Self::append_words(state, deck, required); + Self::append_words(state, deck, required)?; + Ok(()) } fn append_words( state: &mut AppState, deck: &DeckSettings, indices: impl Iterator, - ) { + ) -> MaybeError { let words = &state.dictionary; let stats = &mut indices .map(|i| { @@ -578,8 +596,9 @@ impl RepetitionsState { .collect::>(); if !stats.is_empty() { - add_stat_list(stats, &mut state.connection); + add_stat_list(stats, &mut state.connection)?; } + Ok(()) } fn select_deck(&mut self, index: usize) { @@ -599,10 +618,10 @@ impl RepetitionsState { self.selected_deck_index = None; } - fn fill_deck_word_indices(&mut self) { + fn fill_deck_word_indices(&mut self) -> MaybeError { let state = &self.state.lock(); let mut deck = self.selected_deck().unwrap().clone(); - let added: Vec<_> = load_stats_of_deck(&deck, &state.connection) + let added: Vec<_> = load_stats_of_deck(&deck, &state.connection)? .iter() .map(|c| self.word_id_index_map[&c.word_id]) .collect(); @@ -610,6 +629,7 @@ impl RepetitionsState { deck.existing_words_indices = added; deck.available_words_indices = total; self.decks[self.selected_deck_index.unwrap()] = deck; + Ok(()) } } #[derive(Clone)] diff --git a/src/pages/selector.rs b/src/pages/selector.rs index 5d70f1e..f641efa 100644 --- a/src/pages/selector.rs +++ b/src/pages/selector.rs @@ -1,12 +1,15 @@ -use crate::pages::dictionary::DictionaryState; +use crate::app::AppState; +use crate::core::kana_set::{KanaSet, KanaType}; use crate::navigation::Page::*; use crate::navigation::{NavigatedPage, Page, RootMessage}; +use crate::pages::dictionary::DictionaryState; +use crate::pages::quiz::QuizState; use crate::pages::randomizer::RandomizerState; use crate::pages::repetitions::RepetitionsState; use crate::pages::selector::SelectorMessage::ChangeMode; -use crate::styling::*; use crate::pages::sync::SyncState; use crate::pages::writing::WritingState; +use crate::styling::*; use iced::widget::button::{Status, Style}; use iced::widget::*; use iced::{Element, Task, alignment}; @@ -15,9 +18,6 @@ use iced_core::Background; use iced_core::Color; use parking_lot::Mutex; use std::sync::Arc; -use crate::app::AppState; -use crate::core::kana_set::{KanaSet, KanaType}; -use crate::pages::quiz::QuizState; pub struct SelectorState { pub set: KanaSet, diff --git a/src/pages/sync.rs b/src/pages/sync.rs index a04cae5..e9f7900 100644 --- a/src/pages/sync.rs +++ b/src/pages/sync.rs @@ -1,19 +1,20 @@ +use crate::app::{AppState, fill_state}; use crate::data_provider::settings::{delete_settings, set_setting}; use crate::data_provider::web_api::*; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; -use crate::styling::*; +use crate::notification::{NotificationData, NotificationRequest}; use crate::pages::sync::SyncMessage::*; +use crate::styling::*; +use crate::{push_error, push_error_task}; use iced::widget::button::danger; 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; -use log::error; -use crate::app::{fill_state, AppState}; -use crate::notification::{NotificationRequest, NotificationData}; #[derive(Clone)] pub enum SyncMessage { @@ -61,11 +62,9 @@ impl NavigatedPage for SyncState { .map(|_val: String| RootMessage::Sync(KeyCopied)); } InitSync => { - return Task::perform(first_sync(), |id| { - match id { - Ok(id) => RootMessage::Sync(IdReceived(id)), - Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err)) - } + return Task::perform(first_sync(), |id| match id { + Ok(id) => RootMessage::Sync(IdReceived(id)), + Err(err) => push_error!(err), }); } GetKey => { @@ -79,7 +78,9 @@ impl NavigatedPage for SyncState { } let mut state = self.state.lock(); state.sync_data.key = Some(new_id.clone()); - set_setting("SYNC_KEY".to_string(), new_id, &state.connection); + if let Err(err) = set_setting("SYNC_KEY".to_string(), new_id, &state.connection) { + return push_error_task!(err); + } } Send => { self.prepare_to_db_interaction(); @@ -130,24 +131,35 @@ impl NavigatedPage for SyncState { 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))) + return Task::done(RootMessage::PushNotification(NotificationRequest::Info( + NotificationData::DataLoaded, + ))); } Disable => {} DisableSync => { let mut state = self.state.lock(); state.sync_data.key = None; - delete_settings("SYNC_KEY".to_string(), &state.connection); - delete_settings("AUTO_WEB_FETCH".to_string(), &state.connection); + let save_result = || { + delete_settings("SYNC_KEY".to_string(), &state.connection)?; + delete_settings("AUTO_WEB_FETCH".to_string(), &state.connection)?; + Ok::<(), NotificationData>(()) + }; + + if let Err(error) = save_result() { + return push_error_task!(error); + } } SwitchAutoSync => { self.auto_fetch = !self.auto_fetch; let mut state = self.state.lock(); state.sync_data.auto_web_fetch = self.auto_fetch; - set_setting( + if let Err(err) = set_setting( "AUTO_WEB_FETCH".to_string(), self.auto_fetch.to_string(), &state.connection, - ); + ) { + return push_error_task!(err); + } } } Task::none() @@ -226,10 +238,10 @@ impl SyncState { ] } } - .spacing(DEFAULT_SPACING) - .align_x(Center) - .width(Length::Shrink) - .into() + .spacing(DEFAULT_SPACING) + .align_x(Center) + .width(Length::Shrink) + .into() } /* fn app_updater(&self) -> Element<'_, SyncMessage> { diff --git a/src/pages/word.rs b/src/pages/word.rs index d7e6a13..fa95d58 100644 --- a/src/pages/word.rs +++ b/src/pages/word.rs @@ -1,15 +1,17 @@ -use crate::data_provider::words::{delete_word, update_word}; +use crate::app::AppState; use crate::data::entities::WordData; +use crate::data_provider::words::{delete_word, update_word}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; -use crate::styling::*; use crate::pages::word::WordMessage::*; +use crate::push_error; +use crate::push_error_task; +use crate::styling::*; use iced::widget::button::danger; use iced::widget::{button, column, row, rule, scrollable, text, text_input}; use iced::{Element, Fill, Task}; use parking_lot::Mutex; use std::sync::Arc; -use crate::app::AppState; #[derive(Clone)] pub struct WordState { @@ -34,13 +36,18 @@ impl NavigatedPage for WordState { Save => { let mut state = self.state.lock(); state.dictionary[self.index] = self.word.clone(); - update_word(&mut self.word, &state.connection); + + if let Err(err) = update_word(&mut self.word, &state.connection) { + return push_error_task!(err); + } return Task::done(RootMessage::Word(Back)); } Delete => { let mut state = self.state.lock(); state.dictionary.remove(self.index); - delete_word(&self.word, &state.connection); + if let Err(err) = delete_word(&self.word, &state.connection) { + return push_error_task!(err); + } return Task::done(RootMessage::Word(Back)); } SetTags(n) => self.word.tags = n, diff --git a/src/pages/writing.rs b/src/pages/writing.rs index 2231197..8fa24ad 100644 --- a/src/pages/writing.rs +++ b/src/pages/writing.rs @@ -1,11 +1,10 @@ - +use crate::core::kana_set::KanaSet; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; use iced::widget::*; use iced::{Element, Fill, Task, alignment}; use rand::seq::SliceRandom; -use crate::core::kana_set::KanaSet; #[derive(Clone, Debug)] pub struct WritingState {