From 06bf9138c08fb9eacf224b5be3dd67708f820bbf Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 17:33:21 +0300 Subject: [PATCH 01/11] First database deunwrap --- src/app.rs | 2 +- src/data_provider/card_sets.rs | 33 ++++++++++++++++++-------------- src/navigation.rs | 3 ++- src/notification.rs | 8 ++++++++ src/pages/repetition_settings.rs | 17 ++++++++++------ src/pages/repetitions.rs | 5 ++++- 6 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/app.rs b/src/app.rs index f6f66a1..aec8db7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -95,7 +95,7 @@ pub fn fill_state(state: &mut AppState) { let setting = load_settings(&state.connection); state.dictionary = list; - state.decks = sets; + state.decks = sets.unwrap_or_default(); state.word_groups = groups; state.sync_data = setting } diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index de6e569..ef82bcb 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -2,11 +2,13 @@ use crate::core::srs::OrderMode; use crate::data::entities::DeckSettings; use rusqlite::Connection; +use crate::notification::NotificationData; +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; -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,17 @@ 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()); + for word in iter.filter_map(|result| result.ok()) { + buffer.push(word); } - buffer + Ok(buffer) } -pub fn add_set(set: &mut DeckSettings, connection: &Connection) { +pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> Result<(), NotificationData> { let index : u32 = connection .query_row( "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", @@ -41,14 +43,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) -> Result<(), NotificationData> { if !set.id.is_valid() { - add_set(set, connection); + add_set(set, connection)?; } else { connection .execute( @@ -61,15 +64,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) -> Result<(), NotificationData> { 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/navigation.rs b/src/navigation.rs index 66f3cfe..6c50c53 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -77,7 +77,8 @@ pub enum RootMessage { None, UpdateData, } - +#[allow(clippy::enum_variant_names)] +#[allow(clippy::large_enum_variant)] pub enum Page { Selector(SelectorState), Quiz(QuizState), diff --git a/src/notification.rs b/src/notification.rs index 5c65124..5e91855 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -157,6 +157,12 @@ impl NotificationManager { NotificationData::WebApiUnavailable => { text!("Не получается подключиться к Api") } + NotificationData::DbSaveError => { + text!("Не получается сохранить данные") + } + NotificationData::DbLoadError => { + text!("Не получается загрузить данные") + } } .width(Fill) .into() @@ -179,4 +185,6 @@ pub enum NotificationData { OpenFileError, VoiceVoxUnavailable, WebApiUnavailable, + DbSaveError, + DbLoadError, } diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index 62f5517..0fac784 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -1,8 +1,10 @@ - +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::NotificationRequest; use crate::pages::repetition_settings::RepetitionSettingsMessage::*; use crate::styling::*; use iced::widget::button::danger; @@ -10,12 +12,10 @@ use iced::widget::text_input::Catalog; use iced::widget::{button, column, row, scrollable, space, text, text_input}; use iced::{Element, Task}; use iced_core::Length::Fill; -use iced_core::{Padding, Theme, color}; +use iced_core::{color, Padding, Theme}; 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,7 +61,9 @@ impl NavigatedPage for RepetitionSettingsState { Back => {} Save => { let mut state = self.state.lock(); - update_deck(&mut self.set, &state.connection); + if let Err(e) = update_deck(&mut self.set, &state.connection) { + return Task::done(RootMessage::PushNotification(NotificationRequest::Error(e))); + } if self.filter_changed { update_deck_stats(&self.set, &state); } @@ -95,7 +97,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 Task::done(RootMessage::PushNotification(NotificationRequest::Error(e))); + } + state.decks.remove(self.index); return Task::done(RootMessage::RepetitionSettings(Back)); } diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 6fbcf31..4475acb 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -27,6 +27,7 @@ use iced_core::svg::Handle; use parking_lot::Mutex; use std::ops::Deref; use std::sync::Arc; +use crate::notification::NotificationRequest; #[derive(Clone)] pub struct RepetitionsState { @@ -146,7 +147,9 @@ impl NavigatedPage for RepetitionsState { 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 { From 8841a51897b9e624597ff9a6cee885d6e8419609 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 18:16:41 +0300 Subject: [PATCH 02/11] Rewrite fully second db table into result --- src/core/deck.rs | 19 ++++++----- src/data_provider/card_sets.rs | 2 +- src/data_provider/card_stats.rs | 46 +++++++++++++++----------- src/helpers.rs | 13 ++++++++ src/main.rs | 1 + src/notification.rs | 2 +- src/pages/import.rs | 7 ++-- src/pages/repetition.rs | 29 ++++++++++------ src/pages/repetition_settings.rs | 22 +++++++----- src/pages/repetitions.rs | 57 +++++++++++++++++++++----------- src/pages/sync.rs | 3 +- 11 files changed, 127 insertions(+), 74 deletions(-) create mode 100644 src/helpers.rs diff --git a/src/core/deck.rs b/src/core/deck.rs index 307d7fc..d2c6558 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -6,7 +6,7 @@ use crate::data_provider::history::push_note; use chrono::Utc; use parking_lot::Mutex; use std::sync::Arc; - +use crate::notification::NotificationData; #[derive(Clone)] pub struct DeckData { @@ -18,24 +18,24 @@ 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,7 +48,7 @@ impl DeckData { OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), }, settings: settings.clone(), - } + }) } pub fn next(&mut self) -> (WordData, CardStatistics) { @@ -82,9 +82,9 @@ impl DeckData { self.current_word_index = Some(index); (self.words[index].clone(), self.set[index].clone()) } - pub fn open(&mut self, status: WordOpenMode) { + pub fn open(&mut self, status: WordOpenMode) -> Result<(), NotificationData> { if self.current_word_index.is_none() { - return; + return Ok(()); } let index = self.current_word_index.unwrap(); @@ -104,7 +104,7 @@ impl DeckData { module.open(status, index, word); } } - update_stat_score(word, &self.state.lock().connection); + update_stat_score(word, &self.state.lock().connection)?; let _ = push_note( self.settings.id, HistoryItem { @@ -115,6 +115,7 @@ impl DeckData { after: new_score, }, ); + Ok(()) } pub fn len(&self) -> usize { self.set.len() diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index ef82bcb..d5c5230 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -23,7 +23,7 @@ pub fn load_sets(connection: &Connection) -> Result, Notificat }) .map_err(|_| DbLoadError)?; - let mut buffer = vec![]; + let mut buffer = Vec::with_capacity(16); for word in iter.filter_map(|result| result.ok()) { buffer.push(word); } diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index deb81fd..bac5ccb 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; +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +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 Result<(), NotificationData> { 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) -> Result<(), NotificationData> { 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) -> Result<(), NotificationData> { 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/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..b8536f5 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,13 @@ +#[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)) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 4c9bbf2..8980fbe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod navigation; mod fs; mod core; mod data; +pub mod helpers; use crate::app::start; use mimalloc::MiMalloc; diff --git a/src/notification.rs b/src/notification.rs index 5e91855..e54a97a 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -176,7 +176,7 @@ pub enum NotificationRequest { Error(NotificationData), } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum NotificationData { PermissionDenied, DataLoaded, diff --git a/src/pages/import.rs b/src/pages/import.rs index 71db597..85c2fd5 100644 --- a/src/pages/import.rs +++ b/src/pages/import.rs @@ -4,7 +4,7 @@ use crate::pages::import::ImportMessage::*; use crate::data::entities::{WordData, WordGroup}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::NotificationData::OpenFileError; -use crate::notification::{NotificationData, NotificationRequest}; +use crate::notification::NotificationData; use crate::styling::*; use iced::widget::button::danger; use iced::widget::container::success; @@ -28,6 +28,7 @@ use tokio::task::spawn_blocking; use zip::ZipArchive; use crate::app::AppState; use crate::fs::*; +use crate::push_error; const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; @@ -367,7 +368,7 @@ impl ImportState { }, |result: Result| match result { Ok(import) => RootMessage::Import(UpdateImport(import)), - Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err)), + Err(err) => push_error!(err), }, ) } @@ -499,7 +500,7 @@ 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/repetition.rs b/src/pages/repetition.rs index f40e33f..868d875 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::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(); diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index 0fac784..139f607 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -4,15 +4,16 @@ 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::NotificationRequest; +use crate::notification::NotificationData; 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}; use iced::{Element, Task}; use iced_core::Length::Fill; -use iced_core::{color, Padding, Theme}; +use iced_core::{Padding, Theme, color}; use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; @@ -62,10 +63,12 @@ impl NavigatedPage for RepetitionSettingsState { Save => { let mut state = self.state.lock(); if let Err(e) = update_deck(&mut self.set, &state.connection) { - return Task::done(RootMessage::PushNotification(NotificationRequest::Error(e))); + return push_error_task!(e) } - if self.filter_changed { - update_deck_stats(&self.set, &state); + 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(); } @@ -98,7 +101,7 @@ impl NavigatedPage for RepetitionSettingsState { } let mut state = self.state.lock(); if let Err(e) = delete_set(&self.set, &state.connection) { - return Task::done(RootMessage::PushNotification(NotificationRequest::Error(e))); + return push_error_task!(e); } state.decks.remove(self.index); @@ -176,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) -> Result<(), NotificationData> { + 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; @@ -186,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 4475acb..3d1be75 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::{NotificationData, 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}; @@ -27,7 +30,6 @@ use iced_core::svg::Handle; use parking_lot::Mutex; use std::ops::Deref; use std::sync::Arc; -use crate::notification::NotificationRequest; #[derive(Clone)] pub struct RepetitionsState { @@ -51,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() @@ -61,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(), @@ -83,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); @@ -139,16 +145,20 @@ 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(); { - if let Err(e) = update_deck(&mut deck.general_settings, &state.connection){ - return Task::done(RootMessage::PushNotification(NotificationRequest::Error(e))) + 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(); @@ -212,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() @@ -548,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) -> Result<(), NotificationData> { let existing = &deck.existing_words_indices; let mut created_set = HashSet::with_capacity(existing.len()); existing.iter().for_each(|c| { @@ -559,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, - ) { + ) -> Result<(), NotificationData> { let words = &state.dictionary; let stats = &mut indices .map(|i| { @@ -581,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) { @@ -602,10 +618,10 @@ impl RepetitionsState { self.selected_deck_index = None; } - fn fill_deck_word_indices(&mut self) { + fn fill_deck_word_indices(&mut self) -> Result<(), NotificationData> { 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(); @@ -613,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/sync.rs b/src/pages/sync.rs index a04cae5..c48931e 100644 --- a/src/pages/sync.rs +++ b/src/pages/sync.rs @@ -14,6 +14,7 @@ use std::time::Duration; use log::error; use crate::app::{fill_state, AppState}; use crate::notification::{NotificationRequest, NotificationData}; +use crate::push_error; #[derive(Clone)] pub enum SyncMessage { @@ -64,7 +65,7 @@ impl NavigatedPage for SyncState { return Task::perform(first_sync(), |id| { match id { Ok(id) => RootMessage::Sync(IdReceived(id)), - Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err)) + Err(err) => push_error!(err) } }); } From 6f8adde06bec6c2c2b08e278145a6a6fbd2ec688 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 18:23:29 +0300 Subject: [PATCH 03/11] Rewrite settings to result --- src/data_provider/settings.rs | 38 ++++++++++++++++++++++----------- src/pages/sync.rs | 40 +++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/data_provider/settings.rs b/src/data_provider/settings.rs index dff727f..ad12034 100644 --- a/src/data_provider/settings.rs +++ b/src/data_provider/settings.rs @@ -1,3 +1,5 @@ +use crate::notification::NotificationData; +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use rusqlite::Connection; pub fn get_setting(key: String, connection: &Connection) -> Option { @@ -15,13 +17,19 @@ 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, +) -> Result<(), NotificationData> { + 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) { @@ -30,16 +38,21 @@ pub fn delete_settings(key: String, connection: &Connection) { .unwrap(); } -fn create_settings(key: String, value: String, connection: &Connection) { +fn create_settings(key: String, value: String, connection: &Connection) -> Result<(), NotificationData>{ 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, +) -> Result<(), NotificationData> { connection .execute( "update settings @@ -47,11 +60,12 @@ 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.map(|row| row.unwrap()).collect()) } diff --git a/src/pages/sync.rs b/src/pages/sync.rs index c48931e..0ef132a 100644 --- a/src/pages/sync.rs +++ b/src/pages/sync.rs @@ -1,20 +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}; -use crate::push_error; #[derive(Clone)] pub enum SyncMessage { @@ -62,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) => push_error!(err) - } + return Task::perform(first_sync(), |id| match id { + Ok(id) => RootMessage::Sync(IdReceived(id)), + Err(err) => push_error!(err), }); } GetKey => { @@ -80,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(); @@ -131,7 +131,9 @@ 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 => { @@ -144,11 +146,13 @@ impl NavigatedPage for SyncState { 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() @@ -227,10 +231,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> { From c56d135ce86a82367a3505732994706790c5c9e1 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 18:27:32 +0300 Subject: [PATCH 04/11] More web-api notifications --- src/data_provider/web_api.rs | 26 ++++++++++++-------------- src/notification.rs | 4 ++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/data_provider/web_api.rs b/src/data_provider/web_api.rs index 09b36e2..01d08f4 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -5,7 +5,7 @@ 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}; +use crate::notification::NotificationData::{CompressError, PermissionDenied, WebApiUnavailable}; // const API_URL: &str = "http://127.0.0.1:8089/"; const API_URL: &str = "https://learning.micialware.ru/"; @@ -13,7 +13,7 @@ const API_URL: &str = "https://learning.micialware.ru/"; pub async fn send_data(id: String) -> Result<(), NotificationData> { let db_file = db_path(); let data = tokio::fs::read(db_file).await.map_err(|_| WebApiUnavailable)?; - let data = compress(data); + let data = compress(data)?; let api_url = API_URL.to_string(); let id_url = format!("{api_url}upload/stream/{id}"); @@ -30,10 +30,11 @@ pub async fn send_data(id: String) -> Result<(), NotificationData> { 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)?; + Ok(data) } pub async fn load_data(id: String, temp: bool) -> Result<(), NotificationData> { @@ -47,7 +48,7 @@ pub async fn load_data(id: String, temp: bool) -> Result<(), NotificationData> { .bytes() .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" }; @@ -66,14 +67,11 @@ pub async fn load_data(id: String, temp: bool) -> Result<(), NotificationData> { 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 { diff --git a/src/notification.rs b/src/notification.rs index e54a97a..35be9cb 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -163,6 +163,9 @@ impl NotificationManager { NotificationData::DbLoadError => { text!("Не получается загрузить данные") } + NotificationData::CompressError => { + text!("Ошибка сжатия или распаковки данных") + } } .width(Fill) .into() @@ -187,4 +190,5 @@ pub enum NotificationData { WebApiUnavailable, DbSaveError, DbLoadError, + CompressError, } From 5a6747030bffdc8125b820bc64725ec49b0ccbd8 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 18:46:54 +0300 Subject: [PATCH 05/11] Rewrite words and groups into result --- src/app.rs | 4 +- src/data_provider/card_sets.rs | 5 +- src/data_provider/card_stats.rs | 4 +- src/data_provider/words.rs | 94 +++++++++++++++++++-------------- src/pages/dictionary.rs | 41 +++++++++----- src/pages/import.rs | 4 +- src/pages/word.rs | 17 ++++-- 7 files changed, 99 insertions(+), 70 deletions(-) diff --git a/src/app.rs b/src/app.rs index aec8db7..3ff506d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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.dictionary = list.unwrap_or_default(); state.decks = sets.unwrap_or_default(); - state.word_groups = groups; + state.word_groups = groups.unwrap_or_default(); state.sync_data = setting } diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index d5c5230..b4bb18a 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -24,10 +24,7 @@ pub fn load_sets(connection: &Connection) -> Result, Notificat .map_err(|_| DbLoadError)?; let mut buffer = Vec::with_capacity(16); - for word in iter.filter_map(|result| result.ok()) { - buffer.push(word); - } - + iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i)); Ok(buffer) } diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index bac5ccb..95cf49d 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -25,9 +25,7 @@ pub fn load_stats_of_deck( .map_err(|_| DbLoadError)?; let mut buffer = Vec::with_capacity(512); - for word in iter.filter_map(|result| result.ok()) { - buffer.push(word); - } + iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i)); Ok(buffer) } diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 68aec73..178cfb6 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; +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; 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) -> Result<(), NotificationData> { 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,21 @@ 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, +) -> Result<(), NotificationData> { + let tx = connection.transaction().map_err(|_| DbLoadError)?; let count = words.len(); { @@ -28,33 +34,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) -> Result<(), NotificationData> { if !word.id.is_valid() { - add_word(word, connection); + Ok(add_word(word, connection)?) } else { connection .execute( @@ -67,23 +75,25 @@ pub fn update_word(word: &mut WordData, connection: &Connection) { &word.id, ), ) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } } -pub fn delete_word(word: &WordData, connection: &Connection) { +pub fn delete_word(word: &WordData, connection: &Connection) -> Result<(), NotificationData> { 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 +102,22 @@ 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,50 @@ 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) -> Result<(), NotificationData> { 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, +) -> Result<(), NotificationData> { 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) -> Result<(), NotificationData> { 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/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/import.rs b/src/pages/import.rs index 85c2fd5..91fbb93 100644 --- a/src/pages/import.rs +++ b/src/pages/import.rs @@ -444,7 +444,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; @@ -484,7 +484,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); 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, From b5b1eb768cc71a4684e241cd64f62794dfc5e706 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 23:10:23 +0300 Subject: [PATCH 06/11] remove more unwrap --- src/data.rs | 2 +- src/data_provider/history.rs | 22 ++++++++++++---------- src/data_provider/import.rs | 10 +++++----- src/data_provider/settings.rs | 26 ++++++++++++++++++-------- src/data_provider/sqlite.rs | 2 +- src/pages/repetitions.rs | 2 +- 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/data.rs b/src/data.rs index f8c8120..c16091e 100644 --- a/src/data.rs +++ b/src/data.rs @@ -6,7 +6,7 @@ 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 { diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index ead134b..aa92149 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -1,6 +1,6 @@ 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; @@ -20,13 +20,16 @@ pub fn get_history_of_set(id: Id) -> Result, NotificationData> 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) -> Result<(), NotificationData> { 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 +40,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, @@ -84,5 +88,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..b46d8a9 100644 --- a/src/data_provider/import.rs +++ b/src/data_provider/import.rs @@ -39,8 +39,8 @@ pub fn load_groups(connection: &Connection) -> Result>(); for (_, collection) in sets { @@ -54,12 +54,12 @@ pub fn load_groups(connection: &Connection) -> Result Result>()) } diff --git a/src/data_provider/settings.rs b/src/data_provider/settings.rs index ad12034..5d7a5ea 100644 --- a/src/data_provider/settings.rs +++ b/src/data_provider/settings.rs @@ -1,12 +1,13 @@ use crate::notification::NotificationData; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use iced::futures::StreamExt; 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 @@ -32,13 +33,18 @@ pub fn set_setting( Ok(()) } -pub fn delete_settings(key: String, connection: &Connection) { +pub fn delete_settings(key: String, connection: &Connection) -> Result<(), NotificationData> { connection .execute("DELETE FROM settings WHERE id = ?1", (&key,)) - .unwrap(); + .map_err(|_| DbSaveError)?; + Ok(()) } -fn create_settings(key: String, value: String, connection: &Connection) -> Result<(), NotificationData>{ +fn create_settings( + key: String, + value: String, + connection: &Connection, +) -> Result<(), NotificationData> { connection .execute( "INSERT into settings (id, value) VALUES (?1, ?2)", @@ -65,7 +71,11 @@ where id = ?1", } 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.map(|row| row.unwrap()).collect()) + 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..0abf6a2 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -4,7 +4,7 @@ use crate::fs::*; pub fn create_db() { let db_file = db_path(); if !db_file.exists() { - std::fs::File::create(&db_file).unwrap(); + std::fs::File::create(&db_file).expect("Database file creation failed"); let connection = Connection::open(&db_file).unwrap(); connection.execute("PRAGMA foreign_keys = ON;", []).unwrap(); diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 3d1be75..50b5018 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -450,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( From d836b57d2a835d0dcf32fd5ecbb110d636b62d5a Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 23:27:24 +0300 Subject: [PATCH 07/11] Remove more unwrap --- src/core/deck.rs | 69 +++++++++++++++++++++++--------------------- src/core/srs.rs | 2 +- src/data/entities.rs | 7 +++-- src/fs.rs | 4 +-- src/navigation.rs | 54 +++++++++++++++++++--------------- 5 files changed, 74 insertions(+), 62 deletions(-) diff --git a/src/core/deck.rs b/src/core/deck.rs index d2c6558..a0fbd05 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -1,12 +1,15 @@ 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::NotificationData; use chrono::Utc; use parking_lot::Mutex; use std::sync::Arc; -use crate::notification::NotificationData; #[derive(Clone)] pub struct DeckData { @@ -18,7 +21,10 @@ pub struct DeckData { settings: DeckSettings, } impl DeckData { - pub fn new(settings: &DeckSettings, state: Arc>) -> Result { + pub fn new( + settings: &DeckSettings, + state: Arc>, + ) -> Result { let state_for = state.clone(); let state_locked = state.lock(); @@ -83,42 +89,39 @@ impl DeckData { (self.words[index].clone(), self.set[index].clone()) } pub fn open(&mut self, status: WordOpenMode) -> Result<(), NotificationData> { - if self.current_word_index.is_none() { - return Ok(()); - } - let index = self.current_word_index.unwrap(); + 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/srs.rs b/src/core/srs.rs index 33cf983..0099890 100644 --- a/src/core/srs.rs +++ b/src/core/srs.rs @@ -111,7 +111,7 @@ impl SRSModule for SemiRandomSRSModule { .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 { diff --git a/src/data/entities.rs b/src/data/entities.rs index f5a2036..cafe09a 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/fs.rs b/src/fs.rs index c09e388..9895faf 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -4,10 +4,10 @@ use crate::notification::NotificationData; use crate::notification::NotificationData::PermissionDenied; 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 diff --git a/src/navigation.rs b/src/navigation.rs index 6c50c53..6dbd736 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,11 +1,12 @@ use crate::app::{fill_state, AppState}; +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::{message_navigation, push_error, push_error_task}; use crate::navigation::Page::*; use crate::navigation::RootMessage::{ DataLoaded, Keyboard, Notification, PushNotification, UpdateData, @@ -40,7 +41,7 @@ use parking_lot::Mutex; use rusqlite::Connection; use std::sync::Arc; use std::time::Instant; -use crate::data::Id; +use crate::notification::NotificationData::PermissionDenied; impl Default for ScreenState { fn default() -> Self { @@ -110,12 +111,13 @@ impl ScreenState { let state = ScreenState::default(); let reading_additional_task = Task::perform(Self::load_additional_data(), |data| data); - 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 { @@ -127,8 +129,6 @@ impl ScreenState { Err(err) => PushNotification(NotificationRequest::Warning(err)), }), ]); - } else { - final_task = reading_additional_task; } drop(state_guard); @@ -138,21 +138,23 @@ impl ScreenState { 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() { + for file in directory + .read_dir() + .expect("Cant read history directory") + .flatten() + { let mut vec = vec![]; let history_file_name = match file.file_name().into_string() { Err(_) => continue, Ok(v) => v, }; - let id = history_file_name[4..history_file_name.len() - 12] - .parse::() - .unwrap(); - - let history = get_history_of_set(id); - if let Ok(history) = history { + if let Ok(id) = history_file_name[4..history_file_name.len() - 12].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() }); @@ -161,7 +163,7 @@ impl ScreenState { } map.insert(id, vec); } else { - return PushNotification(NotificationRequest::Warning(history.err().unwrap())); + return PushNotification(NotificationRequest::Warning(PermissionDenied)); } } return DataLoaded(map); @@ -203,7 +205,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); @@ -290,11 +294,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() } } @@ -306,7 +312,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), )* @@ -320,7 +326,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 { @@ -355,7 +361,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(); From a60ed37cd4a9506d47eb1035f09d7f8baff1b806 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 23:42:58 +0300 Subject: [PATCH 08/11] Introduce MaybeError type --- src/core/deck.rs | 4 +- src/data_provider/card_sets.rs | 8 ++-- src/data_provider/card_stats.rs | 8 ++-- src/data_provider/history.rs | 6 +-- src/data_provider/import.rs | 25 ++++++----- src/data_provider/settings.rs | 10 ++--- src/data_provider/sqlite.rs | 18 ++++---- src/data_provider/web_api.rs | 75 ++++++++++++++++++++------------ src/data_provider/words.rs | 18 ++++---- src/helpers.rs | 10 +++++ src/navigation.rs | 2 +- src/notification.rs | 2 + src/pages/import.rs | 4 +- src/pages/repetition.rs | 6 +-- src/pages/repetition_settings.rs | 6 +-- src/pages/repetitions.rs | 18 ++++---- 16 files changed, 127 insertions(+), 93 deletions(-) diff --git a/src/core/deck.rs b/src/core/deck.rs index a0fbd05..03e0c76 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -6,7 +6,7 @@ use crate::core::srs::{ 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::NotificationData; +use crate::notification::{MaybeError, NotificationData}; use chrono::Utc; use parking_lot::Mutex; use std::sync::Arc; @@ -88,7 +88,7 @@ impl DeckData { self.current_word_index = Some(index); (self.words[index].clone(), self.set[index].clone()) } - pub fn open(&mut self, status: WordOpenMode) -> Result<(), NotificationData> { + 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; diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index b4bb18a..4e836b4 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -2,7 +2,7 @@ use crate::core::srs::OrderMode; use crate::data::entities::DeckSettings; use rusqlite::Connection; -use crate::notification::NotificationData; +use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; pub fn load_sets(connection: &Connection) -> Result, NotificationData> { @@ -28,7 +28,7 @@ pub fn load_sets(connection: &Connection) -> Result, Notificat Ok(buffer) } -pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> Result<(), NotificationData> { +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", @@ -46,7 +46,7 @@ pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> Result<(), No Ok(()) } -pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> Result<(), NotificationData> { +pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> MaybeError { if !set.id.is_valid() { add_set(set, connection)?; } else { @@ -66,7 +66,7 @@ pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> Result<() Ok(()) } -pub fn delete_set(set: &DeckSettings, connection: &Connection) -> Result<(), NotificationData> { +pub fn delete_set(set: &DeckSettings, connection: &Connection) -> MaybeError { if !set.id.is_valid() { return Err(DbSaveError); } diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 95cf49d..22dff12 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,5 +1,5 @@ use crate::data::entities::{CardStatistics, DeckSettings}; -use crate::notification::NotificationData; +use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use log::info; use rusqlite::{Connection, params}; @@ -30,7 +30,7 @@ pub fn load_stats_of_deck( Ok(buffer) } -pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) -> Result<(), NotificationData> { +pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) -> MaybeError { let time = Instant::now(); let tx = connection.transaction().expect("Failed to create transaction"); let count = stats.len(); @@ -66,7 +66,7 @@ pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) Ok(()) } -pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) -> Result<(), NotificationData> { +pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) -> MaybeError { let time = Instant::now(); connection @@ -79,7 +79,7 @@ pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) -> Resu Ok(()) } -pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) -> Result<(), NotificationData> { +pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) -> MaybeError { if stat.is_empty() { return Ok(()); } diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index aa92149..085c1fe 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -2,7 +2,7 @@ use crate::core::srs::WordOpenMode; use crate::data::Id; use crate::data::entities::HistoryItem; use crate::fs::*; -use crate::notification::NotificationData; +use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::PermissionDenied; use chrono::DateTime; use std::fs::{File, OpenOptions}; @@ -20,7 +20,7 @@ pub fn get_history_of_set(id: Id) -> Result, NotificationData> Ok(parse_history_items(lines)) } -fn append_lines_if_exists(path: PathBuf, lines: &mut Vec) -> Result<(), NotificationData> { +fn append_lines_if_exists(path: PathBuf, lines: &mut Vec) -> MaybeError { if path.exists() { let reader = BufReader::new(File::open(path).map_err(|_| PermissionDenied)?); reader @@ -63,7 +63,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)); diff --git a/src/data_provider/import.rs b/src/data_provider/import.rs index b46d8a9..da4eae5 100644 --- a/src/data_provider/import.rs +++ b/src/data_provider/import.rs @@ -3,6 +3,8 @@ use hashbrown::HashMap; use log::info; use rusqlite::Connection; use serde_json::Value; +use crate::helpers::into_result; +use crate::notification::NotificationData::IncorrectAnkiFile; #[derive(Clone)] pub struct ImportData(pub(crate) Vec); @@ -26,41 +28,40 @@ 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)?; + .map_err(|_| IncorrectAnkiFile)?; let raw: Value = serde_json::from_str(json_models.as_str()) - .map_err(|_| NotificationData::IncorrectAnkiFile)?; + .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)?))) - .map_err(|_| NotificationData::IncorrectAnkiFile)? + .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() || id.unwrap().as_u64().is_none(){ - return Err(NotificationData::IncorrectAnkiFile); + return Err(IncorrectAnkiFile); } let id = id.unwrap().as_u64().unwrap(); let name = collection.get("name"); if name.is_none() || name.unwrap().as_str().is_none() { - return Err(NotificationData::IncorrectAnkiFile); + return Err(IncorrectAnkiFile); } let name = name.unwrap().as_str().unwrap().to_string(); @@ -81,13 +82,13 @@ pub fn load_groups(connection: &Connection) -> Result 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 +101,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 5d7a5ea..dce970d 100644 --- a/src/data_provider/settings.rs +++ b/src/data_provider/settings.rs @@ -1,4 +1,4 @@ -use crate::notification::NotificationData; +use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use iced::futures::StreamExt; use rusqlite::Connection; @@ -22,7 +22,7 @@ pub fn set_setting( key: String, value: String, connection: &Connection, -) -> Result<(), NotificationData> { +) -> MaybeError { let current = get_settings_list(connection)?; if current.contains(&key) { update_settings(key, value, connection)?; @@ -33,7 +33,7 @@ pub fn set_setting( Ok(()) } -pub fn delete_settings(key: String, connection: &Connection) -> Result<(), NotificationData> { +pub fn delete_settings(key: String, connection: &Connection) -> MaybeError { connection .execute("DELETE FROM settings WHERE id = ?1", (&key,)) .map_err(|_| DbSaveError)?; @@ -44,7 +44,7 @@ fn create_settings( key: String, value: String, connection: &Connection, -) -> Result<(), NotificationData> { +) -> MaybeError { connection .execute( "INSERT into settings (id, value) VALUES (?1, ?2)", @@ -58,7 +58,7 @@ fn update_settings( key: String, value: String, connection: &Connection, -) -> Result<(), NotificationData> { +) -> MaybeError { connection .execute( "update settings diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 0abf6a2..0843d6f 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -5,19 +5,19 @@ pub fn create_db() { let db_file = db_path(); if !db_file.exists() { std::fs::File::create(&db_file).expect("Database file creation failed"); - let connection = Connection::open(&db_file).unwrap(); - connection.execute("PRAGMA foreign_keys = ON;", []).unwrap(); + 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 +51,7 @@ 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/web_api.rs b/src/data_provider/web_api.rs index 01d08f4..24f3dec 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -1,18 +1,20 @@ -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::{CompressError, PermissionDenied, WebApiUnavailable}; +use zstd::{Decoder, Encoder, DEFAULT_COMPRESSION_LEVEL}; // 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 = tokio::fs::read(db_file) + .await + .map_err(|_| WebApiUnavailable)?; let data = compress(data)?; let api_url = API_URL.to_string(); @@ -23,30 +25,35 @@ 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)?; + .await + .map_err(|_| WebApiUnavailable)?; set_local_version(new_version.parse::().unwrap_or_default()).await; Ok(()) } fn compress(data: Vec) -> Result, NotificationData> { - let mut encoder = Encoder::new(Vec::new(), DEFAULT_COMPRESSION_LEVEL).map_err(|_| CompressError)?; + 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)?; Ok(data) } -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)?; @@ -54,15 +61,19 @@ pub async fn load_data(id: String, temp: bool) -> Result<(), NotificationData> { 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)?; + .await + .map_err(|_| WebApiUnavailable)?; set_local_version(version.parse::().unwrap_or_default()).await; Ok(()) } @@ -93,22 +104,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() @@ -117,12 +135,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) @@ -130,8 +150,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 178cfb6..9c49b6c 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,10 +1,10 @@ use crate::data::entities::{WordData, WordGroup}; -use crate::notification::NotificationData; +use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use rusqlite::{Connection, params}; use std::collections::HashMap; -pub fn add_word(word: &mut WordData, connection: &Connection) -> Result<(), NotificationData> { +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", @@ -25,7 +25,7 @@ pub fn add_word(word: &mut WordData, connection: &Connection) -> Result<(), Noti pub fn add_words( words: &mut [WordData], connection: &mut Connection, -) -> Result<(), NotificationData> { +) -> MaybeError { let tx = connection.transaction().map_err(|_| DbLoadError)?; let count = words.len(); @@ -60,7 +60,7 @@ pub fn add_words( Ok(()) } -pub fn update_word(word: &mut WordData, connection: &Connection) -> Result<(), NotificationData> { +pub fn update_word(word: &mut WordData, connection: &Connection) -> MaybeError { if !word.id.is_valid() { Ok(add_word(word, connection)?) } else { @@ -71,7 +71,7 @@ pub fn update_word(word: &mut WordData, connection: &Connection) -> Result<(), N &word.key, &word.value, &word.tags, - serde_json::to_string(&word.additional).unwrap(), + serde_json::to_string(&word.additional).map_err(|_| DbSaveError)?, &word.id, ), ) @@ -80,7 +80,7 @@ pub fn update_word(word: &mut WordData, connection: &Connection) -> Result<(), N } } -pub fn delete_word(word: &WordData, connection: &Connection) -> Result<(), NotificationData> { +pub fn delete_word(word: &WordData, connection: &Connection) -> MaybeError { if !word.id.is_valid() { return Err(DbSaveError); } @@ -133,7 +133,7 @@ pub fn load_word_groups(connection: &Connection) -> Result, Notif Ok(buffer) } -pub fn add_group(group: &mut WordGroup, connection: &Connection) -> Result<(), NotificationData> { +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", @@ -149,7 +149,7 @@ pub fn add_group(group: &mut WordGroup, connection: &Connection) -> Result<(), N pub fn update_group( group: &mut WordGroup, connection: &Connection, -) -> Result<(), NotificationData> { +) -> MaybeError { if !group.id.is_valid() { Ok(add_group(group, connection)?) } else { @@ -163,7 +163,7 @@ pub fn update_group( } } -pub fn delete_group(group: &WordGroup, connection: &Connection) -> Result<(), NotificationData> { +pub fn delete_group(group: &WordGroup, connection: &Connection) -> MaybeError { if !group.id.is_valid() { return Err(DbSaveError); } diff --git a/src/helpers.rs b/src/helpers.rs index b8536f5..25a02c7 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -10,4 +10,14 @@ 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) + } } \ No newline at end of file diff --git a/src/navigation.rs b/src/navigation.rs index 6dbd736..35c4c1b 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -172,7 +172,7 @@ impl ScreenState { } 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 { diff --git a/src/notification.rs b/src/notification.rs index 35be9cb..2336fc0 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -192,3 +192,5 @@ pub enum NotificationData { DbLoadError, CompressError, } + +pub type MaybeError = Result<(), NotificationData>; \ No newline at end of file diff --git a/src/pages/import.rs b/src/pages/import.rs index 91fbb93..7f2302d 100644 --- a/src/pages/import.rs +++ b/src/pages/import.rs @@ -4,7 +4,7 @@ use crate::pages::import::ImportMessage::*; use crate::data::entities::{WordData, WordGroup}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::NotificationData::OpenFileError; -use crate::notification::NotificationData; +use crate::notification::{MaybeError, NotificationData}; use crate::styling::*; use iced::widget::button::danger; use iced::widget::container::success; @@ -373,7 +373,7 @@ impl ImportState { ) } - 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)?; diff --git a/src/pages/repetition.rs b/src/pages/repetition.rs index 868d875..b38e739 100644 --- a/src/pages/repetition.rs +++ b/src/pages/repetition.rs @@ -6,7 +6,7 @@ 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; +use crate::notification::{MaybeError, NotificationData}; use crate::styling::*; use crate::{push_error, push_error_task}; use chrono::Local; @@ -337,7 +337,7 @@ pub enum RepetitionMessage { async fn play_sound( sink: Arc>, text: String, -) -> Result<(), NotificationData> { +) -> MaybeError { let data = get_voice(text.as_str()).await?; spawn_blocking(move || { if let Some(sink) = sink.as_ref() @@ -347,6 +347,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 139f607..3e52d83 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -4,7 +4,7 @@ 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::NotificationData; +use crate::notification::MaybeError; use crate::pages::repetition_settings::RepetitionSettingsMessage::*; use crate::styling::*; use crate::{push_error, push_error_task}; @@ -13,7 +13,7 @@ use iced::widget::text_input::Catalog; use iced::widget::{button, column, row, scrollable, space, text, text_input}; use iced::{Element, Task}; use iced_core::Length::Fill; -use iced_core::{Padding, Theme, color}; +use iced_core::{color, Padding, Theme}; use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; @@ -179,7 +179,7 @@ impl NavigatedPage for RepetitionSettingsState { } } -fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> Result<(), NotificationData> { +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| { diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 50b5018..76e655f 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -1,12 +1,12 @@ use crate::app::AppState; use crate::core::srs::{AppendMode, OrderMode}; -use crate::data::Id; use crate::data::entities::{CardStatistics, DeckSettings}; +use crate::data::Id; 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::{NotificationData, NotificationRequest}; +use crate::notification::{MaybeError, NotificationRequest}; use crate::pages::history::HistoryState; use crate::pages::repetition::RepetitionState; use crate::pages::repetition_settings::RepetitionSettingsState; @@ -16,17 +16,17 @@ use crate::push_error_task; use crate::styling::*; use chrono::{Days, Local, Utc}; use hashbrown::{HashMap, HashSet}; +use iced::widget::button::{danger, Status}; pub use iced::widget::button::{Catalog, Style}; -use iced::widget::button::{Status, danger}; use iced::widget::tooltip::Position::Top; use iced::widget::{ - Column, Row, button, column, container, lazy, radio, row, scrollable, space, svg, text, - text_input, tooltip, + button, column, container, lazy, radio, row, scrollable, space, svg, text, text_input, tooltip, + Column, Row, }; use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme}; -use iced_core::Padding; use iced_core::border::Radius; use iced_core::svg::Handle; +use iced_core::Padding; use parking_lot::Mutex; use std::ops::Deref; use std::sync::Arc; @@ -562,7 +562,7 @@ impl RepetitionsState { } impl RepetitionsState { - fn append_all_words(state: &mut AppState, deck: &DeckViewData) -> Result<(), NotificationData> { + 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| { @@ -580,7 +580,7 @@ impl RepetitionsState { state: &mut AppState, deck: &DeckSettings, indices: impl Iterator, - ) -> Result<(), NotificationData> { + ) -> MaybeError { let words = &state.dictionary; let stats = &mut indices .map(|i| { @@ -618,7 +618,7 @@ impl RepetitionsState { self.selected_deck_index = None; } - fn fill_deck_word_indices(&mut self) -> Result<(), NotificationData> { + 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)? From 1d24fe2631c664e6b6b2b5e65cbb3c0304b03726 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 23:48:00 +0300 Subject: [PATCH 09/11] Finishing deunprap --- src/data_provider/history.rs | 2 +- src/data_provider/settings.rs | 3 +-- src/data_provider/web_api.rs | 4 ++-- src/navigation.rs | 4 ++-- src/pages/sync.rs | 11 +++++++++-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 085c1fe..1671f19 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -15,7 +15,7 @@ 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)) } diff --git a/src/data_provider/settings.rs b/src/data_provider/settings.rs index dce970d..7336105 100644 --- a/src/data_provider/settings.rs +++ b/src/data_provider/settings.rs @@ -1,6 +1,5 @@ -use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; -use iced::futures::StreamExt; +use crate::notification::{MaybeError, NotificationData}; use rusqlite::Connection; pub fn get_setting(key: String, connection: &Connection) -> Option { diff --git a/src/data_provider/web_api.rs b/src/data_provider/web_api.rs index 24f3dec..b166775 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -30,7 +30,7 @@ pub async fn send_data(id: String) -> MaybeError { .text() .await .map_err(|_| WebApiUnavailable)?; - set_local_version(new_version.parse::().unwrap_or_default()).await; + set_local_version(new_version.parse::().unwrap_or_default()).await?; Ok(()) } @@ -74,7 +74,7 @@ pub async fn load_data(id: String, temp: bool) -> MaybeError { .text() .await .map_err(|_| WebApiUnavailable)?; - set_local_version(version.parse::().unwrap_or_default()).await; + set_local_version(version.parse::().unwrap_or_default()).await?; Ok(()) } diff --git a/src/navigation.rs b/src/navigation.rs index 35c4c1b..1b3f223 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -178,7 +178,7 @@ impl ScreenState { 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); } @@ -298,7 +298,7 @@ impl ScreenState { return match page { Repetition(page) => page.press(&message), _ => Task::none(), - } + } } Task::none() } diff --git a/src/pages/sync.rs b/src/pages/sync.rs index 0ef132a..e9f7900 100644 --- a/src/pages/sync.rs +++ b/src/pages/sync.rs @@ -139,8 +139,15 @@ impl NavigatedPage for SyncState { 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; From dddac01bdfe50b6c40cac2c9d6ad3d681078ab84 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 25 Aug 2026 01:05:15 +0300 Subject: [PATCH 10/11] Fix compression bug --- src/core/deck.rs | 26 ++++++-------- src/core/srs.rs | 38 +++++++++----------- src/data_provider/web_api.rs | 3 +- src/navigation.rs | 70 ++++++++++++++++++------------------ 4 files changed, 61 insertions(+), 76 deletions(-) diff --git a/src/core/deck.rs b/src/core/deck.rs index 03e0c76..cffec4b 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -58,30 +58,24 @@ impl DeckData { } 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()) } }; diff --git a/src/core/srs.rs b/src/core/srs.rs index 0099890..b6c0d5a 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,10 +103,9 @@ 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::>(); @@ -115,7 +113,7 @@ impl SRSModule for SemiRandomSRSModule { } } 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,13 @@ impl WorstWordsSRSModule { queue: vec![], } } - fn fill_pool(&mut self, set: &DeckData) { - let mut sorted = set - .set - .clone() - .into_iter() - .zip(0..set.set.len()) + fn fill_pool(&mut self, set: &[CardStatistics]) { + let mut sorted = set.iter().enumerate() .collect::>(); - sorted.sort_by_key(|c| c.0.score); + 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_provider/web_api.rs b/src/data_provider/web_api.rs index b166775..d1d6133 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -38,8 +38,7 @@ 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)?; - Ok(data) + encoder.finish().map_err(|_| CompressError) } pub async fn load_data(id: String, temp: bool) -> MaybeError { diff --git a/src/navigation.rs b/src/navigation.rs index 1b3f223..fbe96f5 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -6,11 +6,11 @@ use crate::data_provider::web_api::{ get_local_version, get_web_version, load_data, set_local_version, }; use crate::fs::*; -use crate::{message_navigation, push_error, push_error_task}; 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, }; @@ -30,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; @@ -41,7 +42,6 @@ use parking_lot::Mutex; use rusqlite::Connection; use std::sync::Arc; use std::time::Instant; -use crate::notification::NotificationData::PermissionDenied; impl Default for ScreenState { fn default() -> Self { @@ -109,7 +109,8 @@ 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 mut final_task = reading_additional_task; let state_guard = state.app_state.lock(); @@ -126,7 +127,7 @@ impl ScreenState { RootMessage::None } } - Err(err) => PushNotification(NotificationRequest::Warning(err)), + Err(err) => push_error!(err), }), ]); } @@ -136,39 +137,36 @@ impl ScreenState { (state, final_task) } - async fn load_additional_data() -> RootMessage { - let directory = history_dir(); + async fn load_additional_data() -> Result { + let directory = history_dir()?; - if let Ok(directory) = directory { - let mut map = HashMap::new(); - for file in directory - .read_dir() - .expect("Cant read history directory") - .flatten() + 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, + }; + + if let Ok(id) = history_file_name.split('_').collect::>()[1].parse::() + && let Ok(history) = get_history_of_set(id) { - let mut vec = vec![]; - let history_file_name = match file.file_name().into_string() { - Err(_) => continue, - Ok(v) => v, - }; - - if let Ok(id) = history_file_name[4..history_file_name.len() - 12].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 { - return PushNotification(NotificationRequest::Warning(PermissionDenied)); + 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 { @@ -205,8 +203,8 @@ impl ScreenState { } let db_file = db_path(); let temp_db_file = temp_db_path(); - if std::fs::rename(temp_db_file, &db_file).is_err(){ - return push_error_task!(PermissionDenied) + if std::fs::rename(temp_db_file, &db_file).is_err() { + return push_error_task!(PermissionDenied); } state.connection = default_connection(); @@ -294,11 +292,11 @@ impl ScreenState { } fn handle_keyboard(&mut self, message: Event) -> Task { - if let Some(page) = self.stack.last_mut(){ + if let Some(page) = self.stack.last_mut() { return match page { Repetition(page) => page.press(&message), _ => Task::none(), - } + }; } Task::none() } From 10cf7f0d2ee6f2958017a514346dd14af6a413e0 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 25 Aug 2026 12:04:07 +0300 Subject: [PATCH 11/11] Formatting --- src/app.rs | 24 ++++++++++++------------ src/core.rs | 2 +- src/core/kana_set.rs | 2 +- src/core/srs.rs | 3 +-- src/data.rs | 6 +++--- src/data/entities.rs | 2 +- src/data_provider/card_sets.rs | 4 ++-- src/data_provider/card_stats.rs | 6 ++++-- src/data_provider/history.rs | 3 +-- src/data_provider/import.rs | 19 ++++++++++--------- src/data_provider/settings.rs | 18 +++--------------- src/data_provider/sqlite.rs | 10 +++++++--- src/data_provider/voice.rs | 20 ++++++++++++++------ src/data_provider/web_api.rs | 2 +- src/data_provider/words.rs | 25 ++++++++++++------------- src/fs.rs | 10 +++++----- src/helpers.rs | 15 ++++++++------- src/main.rs | 13 ++++++------- src/navigation.rs | 7 ++++--- src/notification.rs | 6 ++++-- src/pages/dictionary_quiz.rs | 9 +++++---- src/pages/history.rs | 10 +++++----- src/pages/import.rs | 19 ++++++++++--------- src/pages/quiz.rs | 5 ++--- src/pages/repetition.rs | 5 +---- src/pages/repetition_settings.rs | 6 +++--- src/pages/repetitions.rs | 12 ++++++------ src/pages/selector.rs | 10 +++++----- src/pages/writing.rs | 3 +-- 29 files changed, 138 insertions(+), 138 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3ff506d..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 { 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/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 b6c0d5a..fbb668f 100644 --- a/src/core/srs.rs +++ b/src/core/srs.rs @@ -161,8 +161,7 @@ impl WorstWordsSRSModule { } } fn fill_pool(&mut self, set: &[CardStatistics]) { - let mut sorted = set.iter().enumerate() - .collect::>(); + let mut sorted = set.iter().enumerate().collect::>(); sorted.sort_by_key(|c| c.1.score); let mut worst = sorted[0..self.pool_size] .iter() diff --git a/src/data.rs b/src/data.rs index c16091e..4589b9b 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,10 +1,10 @@ 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, Default)] pub struct Id(pub(crate) u32); @@ -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 cafe09a..7342947 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -111,7 +111,7 @@ impl DeckSettings { let result = engine.eval_ast_with_scope::(&mut scope, &ast); if let Ok(result) = result { - return result + return result; } false }) diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 4e836b4..d6f6885 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,9 +1,9 @@ use crate::core::srs::OrderMode; use crate::data::entities::DeckSettings; -use rusqlite::Connection; -use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use crate::notification::{MaybeError, NotificationData}; +use rusqlite::Connection; pub fn load_sets(connection: &Connection) -> Result, NotificationData> { let mut stmt = connection diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 22dff12..cf19019 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,6 +1,6 @@ use crate::data::entities::{CardStatistics, DeckSettings}; -use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use crate::notification::{MaybeError, NotificationData}; use log::info; use rusqlite::{Connection, params}; use std::time::Instant; @@ -32,7 +32,9 @@ pub fn load_stats_of_deck( pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) -> MaybeError { let time = Instant::now(); - let tx = connection.transaction().expect("Failed to create transaction"); + let tx = connection + .transaction() + .expect("Failed to create transaction"); let count = stats.len(); { diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 1671f19..023d6b7 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -2,8 +2,8 @@ use crate::core::srs::WordOpenMode; use crate::data::Id; use crate::data::entities::HistoryItem; use crate::fs::*; -use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::PermissionDenied; +use crate::notification::{MaybeError, NotificationData}; use chrono::DateTime; use std::fs::{File, OpenOptions}; use std::io::Write; @@ -29,7 +29,6 @@ fn append_lines_if_exists(path: PathBuf, lines: &mut Vec) -> MaybeError .for_each(|i| lines.push(i)); } Ok(()) - } fn parse_history_items(strings: Vec) -> Vec { diff --git a/src/data_provider/import.rs b/src/data_provider/import.rs index da4eae5..1640fbb 100644 --- a/src/data_provider/import.rs +++ b/src/data_provider/import.rs @@ -1,10 +1,10 @@ +use crate::helpers::into_result; use crate::notification::NotificationData; +use crate::notification::NotificationData::IncorrectAnkiFile; use hashbrown::HashMap; use log::info; use rusqlite::Connection; use serde_json::Value; -use crate::helpers::into_result; -use crate::notification::NotificationData::IncorrectAnkiFile; #[derive(Clone)] pub struct ImportData(pub(crate) Vec); @@ -29,8 +29,7 @@ pub fn load_groups(connection: &Connection) -> Result Result 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(|_| IncorrectAnkiFile)?; diff --git a/src/data_provider/settings.rs b/src/data_provider/settings.rs index 7336105..b972070 100644 --- a/src/data_provider/settings.rs +++ b/src/data_provider/settings.rs @@ -17,11 +17,7 @@ pub fn get_setting(key: String, connection: &Connection) -> Option { None } -pub fn set_setting( - key: String, - value: String, - connection: &Connection, -) -> MaybeError { +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)?; @@ -39,11 +35,7 @@ pub fn delete_settings(key: String, connection: &Connection) -> MaybeError { Ok(()) } -fn create_settings( - key: String, - value: String, - connection: &Connection, -) -> MaybeError { +fn create_settings(key: String, value: String, connection: &Connection) -> MaybeError { connection .execute( "INSERT into settings (id, value) VALUES (?1, ?2)", @@ -53,11 +45,7 @@ fn create_settings( Ok(()) } -fn update_settings( - key: String, - value: String, - connection: &Connection, -) -> MaybeError { +fn update_settings(key: String, value: String, connection: &Connection) -> MaybeError { connection .execute( "update settings diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 0843d6f..cbe539d 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -1,12 +1,14 @@ -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).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"); + connection + .execute("PRAGMA foreign_keys = ON;", []) + .expect("Failed to set PRAGMA"); create_tables(&connection); } @@ -52,6 +54,8 @@ 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).expect("Failed to open database"); - connection.execute("PRAGMA foreign_keys = ON;", []).expect("Failed to set PRAGMA"); + 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 d1d6133..2376f4e 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -5,7 +5,7 @@ use log::info; use std::io; use tokio::fs::{File, OpenOptions}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use zstd::{Decoder, Encoder, DEFAULT_COMPRESSION_LEVEL}; +use zstd::{DEFAULT_COMPRESSION_LEVEL, Decoder, Encoder}; // const API_URL: &str = "http://127.0.0.1:8089/"; const API_URL: &str = "https://learning.micialware.ru/"; diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 9c49b6c..7e20323 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,6 +1,6 @@ use crate::data::entities::{WordData, WordGroup}; -use crate::notification::{MaybeError, NotificationData}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use crate::notification::{MaybeError, NotificationData}; use rusqlite::{Connection, params}; use std::collections::HashMap; @@ -22,10 +22,7 @@ pub fn add_word(word: &mut WordData, connection: &Connection) -> MaybeError { Ok(()) } -pub fn add_words( - words: &mut [WordData], - connection: &mut Connection, -) -> MaybeError { +pub fn add_words(words: &mut [WordData], connection: &mut Connection) -> MaybeError { let tx = connection.transaction().map_err(|_| DbLoadError)?; let count = words.len(); @@ -44,7 +41,7 @@ pub fn add_words( serde_json::to_string(&word.additional).unwrap_or_default(), word.group_id ]) - .map_err(|_| DbSaveError)?; + .map_err(|_| DbSaveError)?; } } @@ -102,14 +99,17 @@ pub fn load_words(connection: &Connection) -> Result, Notification key: row.get(1)?, value: row.get(2)?, tags: row.get(3)?, - additional: serde_json::from_str::>(&additional).unwrap_or_default(), + additional: serde_json::from_str::>(&additional) + .unwrap_or_default(), group_id: row.get(5)?, }) }) .map_err(|_| DbLoadError)?; let mut buffer = Vec::with_capacity(1024); - word_iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i)); + word_iter + .filter_map(|res| res.ok()) + .for_each(|i| buffer.push(i)); Ok(buffer) } @@ -128,7 +128,9 @@ pub fn load_word_groups(connection: &Connection) -> Result, Notif .map_err(|_| DbLoadError)?; let mut buffer = Vec::with_capacity(8); - group_iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i)); + group_iter + .filter_map(|res| res.ok()) + .for_each(|i| buffer.push(i)); Ok(buffer) } @@ -146,10 +148,7 @@ pub fn add_group(group: &mut WordGroup, connection: &Connection) -> MaybeError { Ok(()) } -pub fn update_group( - group: &mut WordGroup, - connection: &Connection, -) -> MaybeError { +pub fn update_group(group: &mut WordGroup, connection: &Connection) -> MaybeError { if !group.id.is_valid() { Ok(add_group(group, connection)?) } else { diff --git a/src/fs.rs b/src/fs.rs index 9895faf..5ef1e37 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,7 +1,7 @@ -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().expect("Unsupported OS"); @@ -14,10 +14,10 @@ pub fn app_data_dir() -> PathBuf { } 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 index 25a02c7..004729c 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -2,22 +2,23 @@ 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)) - } + $crate::navigation::RootMessage::PushNotification( + $crate::notification::NotificationRequest::Error($e), + ) + }; } #[inline(always)] -pub fn into_result(option: Option, error: E) -> Result{ +pub fn into_result(option: Option, error: E) -> Result { if let Some(r) = option { Ok(r) - } - else { + } else { Err(error) } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 8980fbe..34b8108 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +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; @@ -16,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 fbe96f5..9506416 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,4 +1,4 @@ -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}; @@ -109,8 +109,9 @@ impl ScreenState { create_db(); info!("Db created"); let state = ScreenState::default(); - let reading_additional_task = - Task::perform(Self::load_additional_data(), |data| data.unwrap_or_else(|err| push_error!(err))); + let reading_additional_task = Task::perform(Self::load_additional_data(), |data| { + data.unwrap_or_else(|err| push_error!(err)) + }); let mut final_task = reading_additional_task; let state_guard = state.app_state.lock(); diff --git a/src/notification.rs b/src/notification.rs index 2336fc0..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 @@ -193,4 +195,4 @@ pub enum NotificationData { CompressError, } -pub type MaybeError = Result<(), NotificationData>; \ No newline at end of file +pub type MaybeError = Result<(), NotificationData>; 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 7f2302d..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::{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,12 +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::*; -use crate::push_error; const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; @@ -433,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()); @@ -500,8 +501,8 @@ impl ImportState { }, |r: Result<(), NotificationData>| match r { Ok(_) => RootMessage::Import(ImportFinished), - Err(err) => push_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 b38e739..15f0a70 100644 --- a/src/pages/repetition.rs +++ b/src/pages/repetition.rs @@ -334,10 +334,7 @@ pub enum RepetitionMessage { PlayFinished, } -async fn play_sound( - sink: Arc>, - text: String, -) -> MaybeError { +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() diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index 3e52d83..b648c6b 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -13,7 +13,7 @@ use iced::widget::text_input::Catalog; use iced::widget::{button, column, row, scrollable, space, text, text_input}; use iced::{Element, Task}; use iced_core::Length::Fill; -use iced_core::{color, Padding, Theme}; +use iced_core::{Padding, Theme, color}; use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; @@ -63,12 +63,12 @@ impl NavigatedPage for RepetitionSettingsState { Save => { let mut state = self.state.lock(); if let Err(e) = update_deck(&mut self.set, &state.connection) { - return push_error_task!(e) + return push_error_task!(e); } if self.filter_changed && let Err(e) = update_deck_stats(&self.set, &state) { - return push_error_task!(e) + return push_error_task!(e); } state.decks[self.index] = self.set.clone(); } diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 76e655f..075ad28 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -1,7 +1,7 @@ use crate::app::AppState; use crate::core::srs::{AppendMode, OrderMode}; -use crate::data::entities::{CardStatistics, DeckSettings}; use crate::data::Id; +use crate::data::entities::{CardStatistics, DeckSettings}; 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}; @@ -16,17 +16,17 @@ use crate::push_error_task; use crate::styling::*; use chrono::{Days, Local, Utc}; use hashbrown::{HashMap, HashSet}; -use iced::widget::button::{danger, Status}; pub use iced::widget::button::{Catalog, Style}; +use iced::widget::button::{Status, danger}; use iced::widget::tooltip::Position::Top; use iced::widget::{ - button, column, container, lazy, radio, row, scrollable, space, svg, text, text_input, tooltip, - Column, Row, + Column, Row, button, column, container, lazy, radio, row, scrollable, space, svg, text, + text_input, tooltip, }; use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme}; +use iced_core::Padding; use iced_core::border::Radius; use iced_core::svg::Handle; -use iced_core::Padding; use parking_lot::Mutex; use std::ops::Deref; use std::sync::Arc; @@ -53,7 +53,7 @@ impl NavigatedPage for RepetitionsState { 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 clone.lock(), selected_set) { return None; } selected_set.existing_words_indices = selected_set.available_words_indices.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/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 {