From 99b381818109725bb825ff03a5d28c33ad7c0137 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 12:14:35 +0300 Subject: [PATCH 01/30] Rename and clear bad code --- src/data_provider/card_sets.rs | 13 +- src/data_provider/card_stats.rs | 4 +- src/data_provider/web_api.rs | 3 +- src/dictionary.rs | 2 +- src/dictionary_test.rs | 2 +- src/history.rs | 2 +- src/import.rs | 2 +- src/lang.rs | 79 +++------ src/main.rs | 4 +- src/navigation.rs | 2 +- src/quiz.rs | 2 +- src/randomizer.rs | 2 +- src/repetition.rs | 12 +- src/repetition_settings.rs | 8 +- src/repetitions.rs | 297 ++++++++++++++------------------ src/selector.rs | 2 +- src/sync.rs | 2 +- src/word.rs | 2 +- src/writing.rs | 2 +- 19 files changed, 186 insertions(+), 256 deletions(-) diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 46c771f..78b9764 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,14 +1,14 @@ -use crate::lang::{AppendMode, CardSetSettings, OrderMode}; +use crate::lang::{DeckSettings, OrderMode}; use rusqlite::Connection; -pub fn load_sets(connection: &Connection) -> Vec { +pub fn load_sets(connection: &Connection) -> Vec { let mut stmt = connection .prepare("SELECT id, name, forward, backward, filter FROM card_set") .unwrap(); let iter = stmt .query_map([], |row| { - Ok(CardSetSettings { + Ok(DeckSettings { id: row.get(0)?, name: row.get(1)?, forward: row.get(2)?, @@ -17,7 +17,6 @@ pub fn load_sets(connection: &Connection) -> Vec { count: None, worst_words_list: None, open_mode: OrderMode::Default, - append_mode: AppendMode::Manual, }) }) .unwrap(); @@ -30,7 +29,7 @@ pub fn load_sets(connection: &Connection) -> Vec { buffer } -pub fn add_set(set: &mut CardSetSettings, connection: &Connection) { +pub fn add_set(set: &mut DeckSettings, connection: &Connection) { let index = connection .query_row( "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", @@ -47,7 +46,7 @@ pub fn add_set(set: &mut CardSetSettings, connection: &Connection) { set.id = index; } -pub fn update_card_set(set: &mut CardSetSettings, connection: &Connection) { +pub fn update_card_set(set: &mut DeckSettings, connection: &Connection) { if set.id == 0 { add_set(set, connection); } else { @@ -66,7 +65,7 @@ pub fn update_card_set(set: &mut CardSetSettings, connection: &Connection) { } } -pub fn delete_set(set: &CardSetSettings, connection: &Connection) { +pub fn delete_set(set: &DeckSettings, connection: &Connection) { if set.id == 0 { return; } diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 048c7b0..3327cb6 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,8 +1,8 @@ -use crate::lang::{CardSetSettings, CardStatistics}; +use crate::lang::{DeckSettings, CardStatistics}; use rusqlite::Connection; use std::time::Instant; -pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec { +pub fn load_stats_of_set(set: &DeckSettings, connection: &Connection) -> Vec { let mut stmt = connection .prepare("SELECT id, word_id, score, last_opened FROM card_stats WHERE set_id = ?1") .unwrap(); diff --git a/src/data_provider/web_api.rs b/src/data_provider/web_api.rs index f73519a..2c07ad1 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -98,7 +98,8 @@ 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?.text().await?; - Ok(version.parse::().unwrap()) + println!("version: {}", version); + Ok(version.parse::().unwrap_or_default()) } pub async fn get_local_version() -> u32 { diff --git a/src/dictionary.rs b/src/dictionary.rs index 06f12e7..3e00195 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -65,7 +65,7 @@ pub enum DictionaryMessage { } impl NavigatedPage for DictionaryState { - fn navigate(&self, message: &DictionaryMessage) -> Option { + fn navigate(&mut self, message: &DictionaryMessage) -> Option { if let Back = message { return Some(Page::PreviousPage); } diff --git a/src/dictionary_test.rs b/src/dictionary_test.rs index 49bafa8..a56e4b4 100644 --- a/src/dictionary_test.rs +++ b/src/dictionary_test.rs @@ -35,7 +35,7 @@ pub enum DictionaryQuizMessage { } impl NavigatedPage for DictionaryQuizState { - fn navigate(&self, message: &DictionaryQuizMessage) -> Option { + fn navigate(&mut self, message: &DictionaryQuizMessage) -> Option { match message { Back => Some(PreviousPage), _ => None, diff --git a/src/history.rs b/src/history.rs index 28bfb6f..a45d062 100644 --- a/src/history.rs +++ b/src/history.rs @@ -18,7 +18,7 @@ pub struct HistoryState { } impl NavigatedPage for HistoryState { - fn navigate(&self, message: &HistoryMessage) -> Option { + fn navigate(&mut self, message: &HistoryMessage) -> Option { match message { Back => Some(Page::PreviousPage), } diff --git a/src/import.rs b/src/import.rs index 18f7e57..c2c694b 100644 --- a/src/import.rs +++ b/src/import.rs @@ -62,7 +62,7 @@ pub enum ImportMessage { } impl NavigatedPage for ImportState { - fn navigate(&self, message: &ImportMessage) -> Option { + fn navigate(&mut self, message: &ImportMessage) -> Option { if self.progress.is_some() { return None; } diff --git a/src/lang.rs b/src/lang.rs index 1bdc880..689dc6d 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,9 +1,9 @@ -use crate::AppState; use crate::data_provider::card_stats::{delete_stat, load_stats_of_set, update_stat_score}; -use crate::data_provider::history::{HistoryItem, push_note}; +use crate::data_provider::history::{push_note, HistoryItem}; +use crate::AppState; use chrono::{DateTime, Utc}; -use rand::distr::Distribution; use rand::distr::weighted::WeightedIndex; +use rand::distr::Distribution; use rand::prelude::SliceRandom; use rand::rng; use rand::rngs::ThreadRng; @@ -12,7 +12,6 @@ use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rhai::{Engine, Scope}; use serde::{Deserialize, Serialize}; -use std::cmp::min; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -300,17 +299,17 @@ pub enum WordOpenMode { } #[derive(Clone)] -pub struct CardSet { +pub struct DeckData { words: Vec, set: Vec, current_word_index: Option, state: Arc>, order_module: OrderModule, - settings: CardSetSettings, + settings: DeckSettings, } -impl CardSet { - pub fn new(settings: &CardSetSettings, state: Arc>) -> Self { +impl DeckData { + pub fn new(settings: &DeckSettings, state: Arc>) -> Self { let state_for = state.clone(); let state_locked = state.lock().unwrap(); @@ -322,31 +321,7 @@ impl CardSet { .cloned() .collect(); let word_ids = last_list.iter().map(|l| l.id).collect::>(); - - // let new_stats: &mut Vec = &mut last_list - // .iter() - // .filter(|word| !saved_ids.contains(&word.id)) - // .map(|word| CardStatistics { - // id: 0, - // word_id: word.id, - // last_open: Utc::now(), - // score: 1, - // set_id: settings.id, - // }) - // .collect(); - // - // let time = Instant::now(); - // - // if !new_stats.is_empty() { - // add_stat_list(new_stats, &state_locked.connection); - // current_set.append(new_stats); - // } - // - // println!( - // "Added {} stats: {}", - // new_stats.len(), - // time.elapsed().as_millis() - // ); + let mut index = 0; for stat in current_set.clone() { if !word_ids.contains(&stat.word_id) { @@ -455,9 +430,10 @@ pub enum OrderMode { FullRandom, } -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Clone, Copy, Eq, PartialEq, Default)] pub enum AppendMode { Full, + #[default] Manual, } @@ -469,9 +445,9 @@ enum OrderModule { } trait SRSModule { - fn next(&mut self, set: &mut CardSet) -> usize; + fn next(&mut self, set: &mut DeckData) -> usize; fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics); - fn init(&mut self, set: &mut CardSet); + fn init(&mut self, set: &mut DeckData); } #[derive(Clone)] @@ -490,7 +466,7 @@ impl RandomSRSModule { } impl SRSModule for RandomSRSModule { - fn next(&mut self, set: &mut CardSet) -> usize { + fn next(&mut self, set: &mut DeckData) -> usize { if self.basket.is_empty() { self.basket = (0..set.words.len()).collect::>(); self.basket.shuffle(&mut rand::rng()) @@ -501,7 +477,7 @@ impl SRSModule for RandomSRSModule { fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {} - fn init(&mut self, set: &mut CardSet) { + fn init(&mut self, set: &mut DeckData) { self.initialized = true; self.basket = (0..set.words.len()).collect::>(); self.basket.shuffle(&mut rand::rng()) @@ -528,7 +504,7 @@ impl SemiRandomSRSModule { } impl SRSModule for SemiRandomSRSModule { - fn next(&mut self, set: &mut CardSet) -> usize { + fn next(&mut self, set: &mut DeckData) -> usize { let index = self.last_weights.sample(&mut self.generator); if self.history.contains(&index) { @@ -550,7 +526,7 @@ impl SRSModule for SemiRandomSRSModule { .unwrap(); } - fn init(&mut self, set: &mut CardSet) { + fn init(&mut self, set: &mut DeckData) { self.initialized = true; let weights = set .set @@ -562,11 +538,8 @@ impl SRSModule for SemiRandomSRSModule { } impl SemiRandomSRSModule { - fn history_len(&self, set: &CardSet) -> usize { - min( - MAX_HISTORY_LEN, - (set.len() as f32 * MAX_HISTORY_LEN_PART) as usize, - ) + fn history_len(&self, set: &DeckData) -> usize { + ((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN) } } @@ -581,7 +554,7 @@ struct WorstWordsSRSModule { } impl SRSModule for WorstWordsSRSModule { - fn next(&mut self, set: &mut CardSet) -> usize { + fn next(&mut self, set: &mut DeckData) -> usize { if self.rounds_remaining == 0 { self.fill_pool(set); self.rounds_remaining = self.rounds_count; @@ -597,7 +570,7 @@ impl SRSModule for WorstWordsSRSModule { fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {} - fn init(&mut self, _: &mut CardSet) { + fn init(&mut self, _: &mut DeckData) { self.initialized = true; } } @@ -614,7 +587,7 @@ impl WorstWordsSRSModule { } } - fn fill_pool(&mut self, set: &CardSet) { + fn fill_pool(&mut self, set: &DeckData) { let mut sorted = set .set .clone() @@ -632,7 +605,7 @@ impl WorstWordsSRSModule { } #[derive(Clone)] -pub struct CardSetSettings { +pub struct DeckSettings { pub id: u32, pub name: String, pub forward: String, @@ -641,12 +614,11 @@ pub struct CardSetSettings { pub count: Option, pub worst_words_list: Option>, pub open_mode: OrderMode, - pub append_mode: AppendMode, } -impl CardSetSettings { - pub(crate) fn with_name(name: String) -> CardSetSettings { - CardSetSettings { +impl DeckSettings { + pub(crate) fn with_name(name: String) -> DeckSettings { + DeckSettings { id: 0, name, forward: "".to_string(), @@ -655,7 +627,6 @@ impl CardSetSettings { count: None, worst_words_list: None, open_mode: OrderMode::Default, - append_mode: AppendMode::Manual, } } diff --git a/src/main.rs b/src/main.rs index 8cf0b6e..12281c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ 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::lang::{CardSetSettings, WordData, WordGroup}; +use crate::lang::{DeckSettings, WordData, WordGroup}; use crate::navigation::{AppSettings, RootMessage, ScreenState}; use crate::quiz::*; use chrono::NaiveDate; @@ -81,7 +81,7 @@ fn subscription(_state: &ScreenState) -> Subscription { pub struct AppState { pub dictionary: Vec, - pub card_sets: Vec, + pub card_sets: Vec, pub word_groups: Vec, pub connection: Connection, pub sync_data: AppSettings, diff --git a/src/navigation.rs b/src/navigation.rs index 5cb84fd..f8bbbed 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -330,7 +330,7 @@ macro_rules! state_navigate_handle { } pub trait NavigatedPage { - fn navigate(&self, message: &T) -> Option; + fn navigate(&mut self, message: &T) -> Option; fn navigated(&mut self); fn update(&mut self, message: T) -> Task; fn view(&self) -> Element<'_, T>; diff --git a/src/quiz.rs b/src/quiz.rs index d8c62d0..423a629 100644 --- a/src/quiz.rs +++ b/src/quiz.rs @@ -20,7 +20,7 @@ pub struct QuizState { } impl NavigatedPage for QuizState { - fn navigate(&self, message: &QuizMessage) -> Option { + fn navigate(&mut self, message: &QuizMessage) -> Option { if let Back = message { Some(PreviousPage) } else { diff --git a/src/randomizer.rs b/src/randomizer.rs index bce52a9..7c3f684 100644 --- a/src/randomizer.rs +++ b/src/randomizer.rs @@ -20,7 +20,7 @@ pub enum RandomizerMessage { } impl NavigatedPage for RandomizerState { - fn navigate(&self, message: &RandomizerMessage) -> Option { + fn navigate(&mut self, message: &RandomizerMessage) -> Option { if let Back = message { return Some(Page::PreviousPage); } diff --git a/src/repetition.rs b/src/repetition.rs index 1b6e283..7985083 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -1,5 +1,5 @@ use crate::data_provider::voice::get_voice; -use crate::lang::{CardSet, CardSetSettings, CardStatistics, WordData, WordOpenMode}; +use crate::lang::{DeckData, DeckSettings, CardStatistics, WordData, WordOpenMode}; use crate::navigation::Page::PreviousPage; use crate::navigation::{KeyPressedPage, NavigatedPage, Page}; use crate::styling::*; @@ -17,8 +17,8 @@ use tokio::task::spawn_blocking; pub struct RepetitionState { state: Arc>, - pub settings: CardSetSettings, - pub set: CardSet, + pub settings: DeckSettings, + pub set: DeckData, current_word: WordData, current_statistic: CardStatistics, open: bool, @@ -28,7 +28,7 @@ pub struct RepetitionState { } impl NavigatedPage for RepetitionState { - fn navigate(&self, message: &RepetitionMessage) -> Option { + fn navigate(&mut self, message: &RepetitionMessage) -> Option { if let RepetitionMessage::Back = message { Some(PreviousPage) } else { @@ -95,8 +95,8 @@ impl NavigatedPage for RepetitionState { } impl RepetitionState { - pub(crate) fn new(set: CardSetSettings, state: Arc>) -> RepetitionState { - let mut card_set = CardSet::new(&set, state.clone()); + pub(crate) fn new(set: DeckSettings, state: Arc>) -> RepetitionState { + let mut card_set = DeckData::new(&set, state.clone()); let (word, stat) = card_set.next(); let sink_handle = rodio::DeviceSinkBuilder::open_default_sink().unwrap(); diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index 02169ec..de80607 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -1,6 +1,6 @@ use crate::AppState; use crate::data_provider::card_sets::{delete_set, update_card_set}; -use crate::lang::CardSetSettings; +use crate::lang::DeckSettings; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::repetition_settings::RepetitionSettingsMessage::*; @@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; pub struct RepetitionSettingsState { - set: CardSetSettings, + set: DeckSettings, index: usize, state: Arc>, real_delete: bool, @@ -37,7 +37,7 @@ impl RepetitionSettingsState { } impl NavigatedPage for RepetitionSettingsState { - fn navigate(&self, message: &RepetitionSettingsMessage) -> Option { + fn navigate(&mut self, message: &RepetitionSettingsMessage) -> Option { if let Back = message { Some(PreviousPage) } else { @@ -149,7 +149,7 @@ impl NavigatedPage for RepetitionSettingsState { } impl RepetitionSettingsState { - fn count_view(&self, set: &CardSetSettings) -> Element<'_, RepetitionSettingsMessage> { + fn count_view(&self, set: &DeckSettings) -> Element<'_, RepetitionSettingsMessage> { if let Some(count) = set.count { return text!("Количество слов: {}", count).into(); } diff --git a/src/repetitions.rs b/src/repetitions.rs index dc1d496..e2095fb 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,7 +1,8 @@ +use std::ops::Deref; use crate::data_provider::card_sets::{delete_set, update_card_set}; use crate::data_provider::card_stats::{add_stat_list, load_stats_of_set}; use crate::history::HistoryState; -use crate::lang::{AppendMode, CardSetSettings, CardStatistics, OrderMode}; +use crate::lang::{AppendMode, DeckSettings, CardStatistics, OrderMode}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page}; use crate::repetition::RepetitionState; @@ -11,53 +12,65 @@ use crate::styling::*; use crate::{AppState, RootMessage}; 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 std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct RepetitionsState { - selected_set: Option, + selected_set_index: Option, correct_filters: Vec, current_sets_cards_cache: HashMap, Vec)>, word_id_index_map: HashMap, + local_settings: HashMap, + view_data: Option, pub state: Arc>, + set_names: Vec } impl NavigatedPage for RepetitionsState { - fn navigate(&self, message: &RepetitionsMessage) -> Option { + fn navigate(&mut self, message: &RepetitionsMessage) -> Option { if let Back = message { Some(PreviousPage) } else if let GoToHistory = message { let card_set; { - card_set = self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].id; + card_set = self.state.lock().unwrap().card_sets[self.selected_set_index.unwrap()].id; } Some(History(HistoryState::new(card_set, self.state.clone()))) } else if let GoToRepetition = message { let clone = self.state.clone(); let card_set; { - card_set = self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].clone(); + card_set = self.state.lock().unwrap().card_sets[self.selected_set_index.unwrap()].clone(); } - if card_set.append_mode == AppendMode::Full { + if self.local_settings[&card_set.id].append_mode == AppendMode::Full { self.append_all_words(&card_set); + } else { + let cache = self + .current_sets_cards_cache + .get(&self.selected_set_index.unwrap()) + .unwrap(); + if cache.1.is_empty() || cache.0.is_empty() { + let local_settings = self.local_settings.get_mut(&card_set.id).unwrap(); + local_settings.append_warning = true; + } } Some(Repetition(RepetitionState::new(card_set, clone))) } else if let GoToSettings = message { Some(RepetitionSettings(RepetitionSettingsState::new( - self.selected_set.unwrap(), + self.selected_set_index.unwrap(), self.state.clone(), ))) } else { @@ -65,110 +78,29 @@ impl NavigatedPage for RepetitionsState { } } fn navigated(&mut self) { - self.selected_set = None; + self.clear_selection(); self.current_sets_cards_cache.clear(); } fn update(&mut self, message: RepetitionsMessage) -> Task { - let mut state = self.state.lock().unwrap(); match message { Next => {} Back => {} GoToRepetition => {} GoToHistory => {} GoToSettings => {} - CreateSet => { - let index = state.card_sets.len() + 1; - state - .card_sets - .push(CardSetSettings::with_name(format!("Card set #{}", index))); - self.correct_filters.push(true); - } - DeleteSet => { - let set = state.card_sets.remove(self.selected_set.unwrap()); - self.correct_filters.remove(self.selected_set.unwrap()); - self.selected_set = None; - delete_set(&set, &state.connection); - } - SelectSet(index) => { - self.selected_set = Some(index); - let set = state.card_sets.get(index).unwrap().clone(); - if !self.current_sets_cards_cache.contains_key(&index) { - let added: Vec<_> = load_stats_of_set(&set, &state.connection) - .iter() - .map(|c| self.word_id_index_map[&c.word_id]) - .collect(); - let total = set.get_word_list(&state); - self.current_sets_cards_cache.insert(index, (added, total)); - - } - state.card_sets[index] = set; - } - SetName(new) => { - state.card_sets[self.selected_set.unwrap()].name = new; - } - Save => { - for i in 0..state.card_sets.len() { - self.correct_filters[i] = state.card_sets[i].check_filter(); - } - - let set = &mut state.card_sets[self.selected_set.unwrap()].clone(); - - update_card_set(set, &state.connection); - - state.card_sets[self.selected_set.unwrap()] = set.clone(); - } - SetForward(new) => { - state.card_sets[self.selected_set.unwrap()].forward = new; - } - SetBackward(new) => { - state.card_sets[self.selected_set.unwrap()].backward = new; - } - SetFilter(new) => { - state.card_sets[self.selected_set.unwrap()].filter = new; - } - TryFilter => { - let set = state.card_sets.get(self.selected_set.unwrap()).unwrap(); - let count = set.get_word_list(&state).len(); - state.card_sets[self.selected_set.unwrap()].count = Some(count); - } - SetOpenMode(mode) => { - state - .card_sets - .get_mut(self.selected_set.unwrap()) - .unwrap() - .open_mode = mode; - } - SetAppendMode(mode) => { - let set = state.card_sets.get_mut(self.selected_set.unwrap()).unwrap(); - set.append_mode = mode; - } - AppendWords(count) => { - let adding: Vec; - let set = state.card_sets.get(self.selected_set.unwrap()).unwrap(); - let index = self.selected_set.unwrap(); - { - let total_cache = &mut self.current_sets_cards_cache; - let cache = total_cache.get_mut(&index).unwrap().clone(); - let mut created_set = HashSet::with_capacity(cache.0.len()); - cache.0.iter().for_each(|c| { - created_set.insert(c); - }); - adding = cache - .1 - .iter() - .filter(|c| !created_set.contains(c)) - .take(count) - .cloned() - .collect(); - - let mut new_current = cache.0.clone(); - new_current.append(&mut adding.clone()); - total_cache.insert(index, (new_current, cache.1.clone())); - } - - Self::append_words(&state, set, adding.into_iter()); - } + CreateSet => {} + DeleteSet => {} + SetName(_) => {} + SelectSet(_) => {} + Save => {} + SetForward(_) => {} + SetBackward(_) => {} + SetFilter(_) => {} + TryFilter => {} + SetOpenMode(_) => {} + SetAppendMode(_) => {} + AppendWords(_) => {} } Task::none() } @@ -203,55 +135,34 @@ impl RepetitionsState { let count = state_.card_sets.len(); let mut map = HashMap::with_capacity(state_.dictionary.len()); - state_.dictionary.iter().enumerate().for_each(|(index, word)| {map.insert(word.id, index);}); + state_ + .dictionary + .iter() + .enumerate() + .for_each(|(index, word)| { + map.insert(word.id, index); + }); + let mut local_settings = HashMap::with_capacity(state_.card_sets.len()); + state_.card_sets.iter().for_each(|card_set| { + local_settings.insert(card_set.id, Default::default()); + }); drop(state_); RepetitionsState { - selected_set: None, + selected_set_index: None, correct_filters: vec![true; count], current_sets_cards_cache: Default::default(), word_id_index_map: map, + local_settings, + view_data: None, state, + set_names: vec![], } } } impl RepetitionsState { - fn append_all_words(&self, set: &CardSetSettings) { - let index = self.selected_set.unwrap(); - let cache = &self.current_sets_cards_cache[&index]; - let mut created_set = HashSet::with_capacity(cache.0.len()); - cache.0.iter().for_each(|c| { - created_set.insert(c); - }); - let required = cache - .1 - .iter() - .filter(|i| !created_set.contains(i)).cloned(); - Self::append_words(&self.state.lock().unwrap(), set, required); - } - - fn append_words(state: &AppState, set: &CardSetSettings, indices: impl Iterator) { - let words = &state.dictionary; - let stats = &mut indices - .map(|i| { - let word = words.get(i).unwrap(); - CardStatistics { - id: 0, - word_id: word.id, - last_open: Utc::now(), - score: 1, - set_id: set.id, - } - }) - .collect::>(); - - if !stats.is_empty() { - add_stat_list(stats, &state.connection); - } - } - - fn launch_delete_button(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { + fn launch_delete_button(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { if set.id != 0 { return row![ space().width(Fill), @@ -263,17 +174,8 @@ impl RepetitionsState { } button("Удалить").style(danger).on_press(DeleteSet).into() } - fn selected_set_view(&self) -> Element<'_, RepetitionsMessage> { - if let Some(index) = self.selected_set { - let set; - { - let state = self.state.lock().unwrap(); - if state.card_sets.len() <= index { - return space().width(Length::FillPortion(3)).into(); - } - set = state.card_sets[index].clone(); - } + if let Some(set) = &self.view_data { return column![ scrollable( column![ @@ -362,14 +264,13 @@ impl RepetitionsState { } space().width(Length::FillPortion(2)).into() } - - fn filled_set_data_view(&self, set: &CardSetSettings) -> Column<'_, RepetitionsMessage> { + fn filled_set_data_view(&self, set: &DeckSettings) -> Column<'_, RepetitionsMessage> { column![self.activity_bar(set), self.count_comparator_view()] .align_x(Center) .width(Fill) .spacing(DEFAULT_SPACING) } - fn activity_bar(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { + fn activity_bar(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { const MAX_DAY_COUNT: f32 = 128.0; let state = self.state.lock().unwrap(); @@ -446,21 +347,20 @@ impl RepetitionsState { }) .into() } - - fn word_append_panel(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { + fn word_append_panel(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { column![ text!("Режим добавления карточек"), row![ radio( "Добавлять все доступные", AppendMode::Full, - Some(set.append_mode), + Some(self.local_settings[&set.id].append_mode), SetAppendMode ), radio( "Добавлять вручную", AppendMode::Manual, - Some(set.append_mode), + Some(self.local_settings[&set.id].append_mode), SetAppendMode ) ] @@ -470,9 +370,8 @@ impl RepetitionsState { .spacing(HALF_SPACING) .into() } - - fn adder_panel(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { - match set.append_mode { + fn adder_panel(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { + match self.local_settings[&set.id].append_mode { AppendMode::Full => space().into(), AppendMode::Manual => row![ button("+5").on_press(AppendWords(5)), @@ -484,27 +383,24 @@ impl RepetitionsState { .into(), } } - fn count_comparator_view(&self) -> Element<'_, RepetitionsMessage> { - let cache = &self.current_sets_cards_cache[&self.selected_set.unwrap()]; + let cache = &self.current_sets_cards_cache[&self.selected_set_index.unwrap()]; let now = cache.0.len(); let available = cache.1.len(); text!("{} слова добавлено из {}", now, available).into() } - fn count_view(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { + fn count_view(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { if let Some(count) = set.count { return text!("Количество слов: {}", count).into(); } space().into() } - fn sets_list(&self) -> Column<'_, RepetitionsMessage> { let mut column = Column::new(); - let sets = &self.state.lock().unwrap().card_sets; - for (i, set) in sets.iter().enumerate() { + for (i, set) in self.set_names.iter().enumerate() { column = column.push( - button(text!("{}", set.name.clone())) + button(text!("{}", set.clone())) .on_press_with(move || SelectSet(i)) .style(move |_x: &Theme, status: Status| Style { background: if status == Status::Hovered { @@ -532,6 +428,48 @@ impl RepetitionsState { } } +impl RepetitionsState { + fn append_all_words(&self, set: &DeckSettings) { + let index = self.selected_set_index.unwrap(); + let cache = &self.current_sets_cards_cache[&index]; + let mut created_set = HashSet::with_capacity(cache.0.len()); + cache.0.iter().for_each(|c| { + created_set.insert(c); + }); + let required = cache.1.iter().filter(|i| !created_set.contains(i)).cloned(); + Self::append_words(&self.state.lock().unwrap(), set, required); + } + fn append_words(state: &AppState, set: &DeckSettings, indices: impl Iterator) { + let words = &state.dictionary; + let stats = &mut indices + .map(|i| { + let word = words.get(i).unwrap(); + CardStatistics { + id: 0, + word_id: word.id, + last_open: Utc::now(), + score: 1, + set_id: set.id, + } + }) + .collect::>(); + + if !stats.is_empty() { + add_stat_list(stats, &state.connection); + } + } + fn select_set(&mut self, index: usize) { + let state = self.state.lock().unwrap(); + if let Some(set) = state.card_sets.get(index) { + self.selected_set_index = Some(index); + + } + } + + fn clear_selection(&mut self) { + self.selected_set_index = None; + } +} #[derive(Clone)] pub enum RepetitionsMessage { Next, @@ -552,3 +490,24 @@ pub enum RepetitionsMessage { GoToSettings, AppendWords(usize), } + +#[derive(Clone, Default)] +struct DeckLocalSetting { + append_mode: AppendMode, + append_warning: bool, +} + +#[derive(Clone)] +struct SetViewData{ + set_local_settings: DeckLocalSetting, + set_settings: DeckSettings, + existing_words_indices: Vec, + available_words_indices: Vec, +} +impl Deref for SetViewData { + type Target = DeckSettings; + + fn deref(&self) -> &Self::Target { + &self.set_settings + } +} \ No newline at end of file diff --git a/src/selector.rs b/src/selector.rs index 7faced7..8eae3db 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -36,7 +36,7 @@ pub enum SelectorMessage { } impl NavigatedPage for SelectorState { - fn navigate(&self, message: &SelectorMessage) -> Option { + fn navigate(&mut self, message: &SelectorMessage) -> Option { if let SelectorMessage::Goto = message { return if self.is_writing { let writing = WritingState::new(&self.set); diff --git a/src/sync.rs b/src/sync.rs index c45c6b9..a273fce 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -37,7 +37,7 @@ pub struct SyncState { } impl NavigatedPage for SyncState { - fn navigate(&self, message: &SyncMessage) -> Option { + fn navigate(&mut self, message: &SyncMessage) -> Option { if let Back = message && !self.frozen { diff --git a/src/word.rs b/src/word.rs index c8f8301..84cc90e 100644 --- a/src/word.rs +++ b/src/word.rs @@ -18,7 +18,7 @@ pub struct WordState { } impl NavigatedPage for WordState { - fn navigate(&self, message: &WordMessage) -> Option { + fn navigate(&mut self, message: &WordMessage) -> Option { if let Back = message { Some(PreviousPage) } else { diff --git a/src/writing.rs b/src/writing.rs index 409415d..d5a4228 100644 --- a/src/writing.rs +++ b/src/writing.rs @@ -19,7 +19,7 @@ pub struct WritingState { } impl NavigatedPage for WritingState { - fn navigate(&self, message: &WritingMessage) -> Option { + fn navigate(&mut self, message: &WritingMessage) -> Option { if let WritingMessage::Back = message { Some(PreviousPage) } else { From 97c76350e1916bbd1f434670bf6485c46194b29b Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 12:47:06 +0300 Subject: [PATCH 02/30] Migrate to Id type --- src/data_provider/card_sets.rs | 8 ++-- src/data_provider/card_stats.rs | 6 +-- src/data_provider/history.rs | 10 ++--- src/data_provider/words.rs | 16 +++---- src/dictionary.rs | 16 +++---- src/history.rs | 2 +- src/import.rs | 2 +- src/lang.rs | 76 ++++++++++++++++++++++++++------- src/main.rs | 4 +- src/navigation.rs | 7 +-- src/repetition.rs | 3 +- src/repetitions.rs | 18 ++++---- 12 files changed, 106 insertions(+), 62 deletions(-) diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 78b9764..6d3027a 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,4 +1,4 @@ -use crate::lang::{DeckSettings, OrderMode}; +use crate::lang::{DeckSettings, OrderMode, INVALID_ID}; use rusqlite::Connection; @@ -43,11 +43,11 @@ pub fn add_set(set: &mut DeckSettings, connection: &Connection) { ) .unwrap_or_else(|e| {println!("{}", e); 0}); - set.id = index; + set.id = index.into(); } pub fn update_card_set(set: &mut DeckSettings, connection: &Connection) { - if set.id == 0 { + if set.id == INVALID_ID { add_set(set, connection); } else { connection @@ -66,7 +66,7 @@ pub fn update_card_set(set: &mut DeckSettings, connection: &Connection) { } pub fn delete_set(set: &DeckSettings, connection: &Connection) { - if set.id == 0 { + if set.id == INVALID_ID { return; } connection diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 3327cb6..d5204e6 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,4 +1,4 @@ -use crate::lang::{DeckSettings, CardStatistics}; +use crate::lang::{DeckSettings, CardStatistics, INVALID_ID}; use rusqlite::Connection; use std::time::Instant; @@ -62,7 +62,7 @@ pub fn add_stat_list(stat: &mut [CardStatistics], connection: &Connection) { let start_index = last_index - (stat.len() as u32) + 1; for (index, id) in (start_index..=last_index).enumerate() { - stat[index].id = id; + stat[index].id = id.into(); } println!("Added {} cards for {:?}", stat.len(), time.elapsed()); @@ -103,7 +103,7 @@ pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) { } pub fn delete_stat(stat: &CardStatistics, connection: &Connection) { - if stat.id == 0 { + if stat.id == INVALID_ID { return; } connection diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 7b0a6ec..ff98019 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -1,5 +1,5 @@ use crate::dictionary::app_data_dir; -use crate::lang::WordOpenMode; +use crate::lang::{Id, WordOpenMode}; use chrono::{DateTime, Utc}; use std::fs; use std::fs::{File, OpenOptions}; @@ -7,7 +7,7 @@ use std::io::Write; use std::io::{BufRead, BufReader}; use std::path::PathBuf; -pub fn get_history_of_set(id: u32) -> Vec { +pub fn get_history_of_set(id: Id) -> Vec { let app_dir = history_dir(); let head = app_dir.clone().join(format!("set_{}_history.csv", id)); let mut lines = Vec::new(); @@ -32,7 +32,7 @@ fn parse_history_items(strings: Vec) -> Vec { if let [time, word, mode, before, after] = string.split(';').collect::>()[..] { items.push(HistoryItem { timestamp: DateTime::from_timestamp(time.parse().unwrap(), 0).unwrap(), - word_id: word.parse::().unwrap(), + word_id: word.parse::().unwrap().into(), mode: match mode.parse::().unwrap() { 2 => WordOpenMode::Hard, 3 => WordOpenMode::Ok, @@ -48,7 +48,7 @@ fn parse_history_items(strings: Vec) -> Vec { items } -pub fn push_note(set_id: u32, item: HistoryItem) { +pub fn push_note(set_id: Id, item: HistoryItem) { let app_dir = history_dir(); let path = app_dir.clone().join(format!("set_{}_history.csv", set_id)); @@ -84,7 +84,7 @@ pub fn history_dir() -> PathBuf { #[derive(Clone)] pub struct HistoryItem { pub timestamp: DateTime, - pub word_id: u32, + pub word_id: Id, pub mode: WordOpenMode, pub before: u8, pub after: u8, diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index a90fe4a..5947a96 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,4 +1,4 @@ -use crate::lang::{WordData, WordGroup}; +use crate::lang::{WordData, WordGroup, INVALID_ID}; use rusqlite::{Connection, params}; use std::collections::HashMap; @@ -20,7 +20,7 @@ pub fn add_word(word: &mut WordData, connection: &Connection) { 0 }); - word.id = index; + word.id = index.into(); } pub fn add_words(words: &mut [WordData], connection: &mut Connection) { @@ -59,12 +59,12 @@ pub fn add_words(words: &mut [WordData], connection: &mut Connection) { let start_index = last_index - (count as u32) + 1; for (index, id) in (start_index..=last_index).enumerate() { - words[index].id = id; + words[index].id = id.into(); } } pub fn update_word(word: &mut WordData, connection: &Connection) { - if word.id == 0 { + if word.id == INVALID_ID { add_word(word, connection); } else { connection @@ -86,7 +86,7 @@ pub fn update_word(word: &mut WordData, connection: &Connection) { } pub fn delete_word(word: &WordData, connection: &Connection) { - if word.id == 0 { + if word.id == INVALID_ID { return; } connection @@ -156,11 +156,11 @@ pub fn add_group(group: &mut WordGroup, connection: &Connection) { 0 }); - group.id = index; + group.id = index.into(); } pub fn update_group(group: &mut WordGroup, connection: &Connection) { - if group.id == 0 { + if group.id == INVALID_ID { add_group(group, connection); } else { connection @@ -176,7 +176,7 @@ pub fn update_group(group: &mut WordGroup, connection: &Connection) { } pub fn delete_group(group: &WordGroup, connection: &Connection) { - if group.id == 0 { + if group.id == INVALID_ID { return; } connection diff --git a/src/dictionary.rs b/src/dictionary.rs index 3e00195..30f2c3b 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -2,7 +2,7 @@ use crate::data_provider::words::{delete_group, delete_word, update_group, updat use crate::dictionary::DictionaryMessage::*; use crate::dictionary_test::DictionaryQuizState; use crate::import::ImportState; -use crate::lang::{WordData, WordGroup}; +use crate::lang::{WordData, WordGroup, INVALID_ID}; use crate::navigation::Page::{Import, Word}; use crate::navigation::{NavigatedPage, Page}; use crate::styling::*; @@ -96,7 +96,7 @@ impl NavigatedPage for DictionaryState { let dict = &state.dictionary; word = dict[*index].clone(); } - if word.id != 0 { + if word.id != INVALID_ID { return Some(Word(WordState::new(word, *index, self.state.clone()))); } } @@ -172,7 +172,7 @@ impl NavigatedPage for DictionaryState { } Include(i, b) => self.include_map[i] = b, IncludeTag(t, v) => { - let index: u32; + let index; { let state = self.state.lock().unwrap(); index = state.word_groups[self.selected_group_index].id; @@ -197,7 +197,7 @@ impl NavigatedPage for DictionaryState { let state = &mut self.state.lock().unwrap(); state.word_groups.push(WordGroup { - id: 0, + id: 0.into(), name: format!("Группа слов {}", random_range(100..1000)), }); } @@ -222,7 +222,7 @@ impl NavigatedPage for DictionaryState { } SelectGroup(i) => { self.selected_group_index = i; - let index: u32; + let index; { let state = self.state.lock().unwrap(); index = state.word_groups[i].id; @@ -423,7 +423,7 @@ impl DictionaryState { let line_button = || { let action = WordAction(index); - if data.id == 0 { + if data.id == INVALID_ID { return button("-").on_press(action).style(|_x, _status| Style { background: None, text_color: Color::BLACK, @@ -532,7 +532,7 @@ impl DictionaryState { }); } - fn update_words_include(&mut self, group_id: u32) { + fn update_words_include(&mut self, group_id: crate::lang::Id) { let include_tags = self .tag_map .iter() @@ -626,6 +626,6 @@ struct WordLineState { key: String, value: String, tags: String, - id: u32, + id: crate::lang::Id, index: usize, } diff --git a/src/history.rs b/src/history.rs index a45d062..d055d2f 100644 --- a/src/history.rs +++ b/src/history.rs @@ -46,7 +46,7 @@ impl NavigatedPage for HistoryState { } impl HistoryState { - pub fn new(id: u32, state: Arc>) -> Self { + pub fn new(id: crate::lang::Id, state: Arc>) -> Self { let state = state.lock().unwrap(); let history = get_history_of_set(id); let words = history diff --git a/src/import.rs b/src/import.rs index c2c694b..e3cff93 100644 --- a/src/import.rs +++ b/src/import.rs @@ -446,7 +446,7 @@ impl ImportState { let map_indices = Self::get_mapping_indices(&group); let mut group_entity = WordGroup { - id: 0, + id: 0.into(), name: group.name.clone(), }; { diff --git a/src/lang.rs b/src/lang.rs index 689dc6d..78d3da7 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -11,8 +11,11 @@ use rayon::iter::IndexedParallelIterator; use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rhai::{Engine, Scope}; -use serde::{Deserialize, Serialize}; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; +use rusqlite::ToSql; +use std::cmp::PartialEq; use std::collections::HashMap; +use std::fmt::{Display, Formatter}; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -20,6 +23,9 @@ const MAX_HISTORY_LEN: usize = 20; const MAX_HISTORY_LEN_PART: f32 = 0.33; const MAX_SCORE: u8 = 25; const FADE_PER_DAY: f32 = 0.95; +#[derive(Clone, PartialEq, Eq, Debug, Hash, Copy)] +pub(crate) struct Id(u32); +pub const INVALID_ID: Id = Id(0); #[derive(Clone, Debug)] pub struct KanaSet { name: String, @@ -225,40 +231,40 @@ impl PartialEq for KanaSet { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug)] pub struct WordData { - pub id: u32, + pub id: Id, pub key: String, pub value: String, pub tags: String, pub additional: HashMap, - pub group_id: u32, + pub group_id: Id, } impl WordData { pub fn new() -> Self { Self { - id: 0, + id: 0.into(), key: String::new(), value: String::new(), tags: String::new(), additional: Default::default(), - group_id: 1, + group_id: 1.into(), } } } #[derive(Clone)] pub struct WordGroup { - pub id: u32, + pub id: Id, pub name: String, } #[derive(Clone, PartialEq)] pub struct CardStatistics { - pub id: u32, - pub word_id: u32, - pub set_id: u32, + pub id: Id, + pub word_id: Id, + pub set_id: Id, pub last_open: DateTime, pub score: u8, } @@ -320,8 +326,8 @@ impl DeckData { .map(|w| state_locked.dictionary.get(*w).unwrap()) .cloned() .collect(); - let word_ids = last_list.iter().map(|l| l.id).collect::>(); - + let word_ids = last_list.iter().map(|l| l.id).collect::>(); + let mut index = 0; for stat in current_set.clone() { if !word_ids.contains(&stat.word_id) { @@ -410,7 +416,7 @@ impl DeckData { self.settings.id, HistoryItem { timestamp: Utc::now(), - word_id: word.word_id, + word_id: word.word_id.into(), mode: WordOpenMode::Easy, before: old_score, after: new_score, @@ -606,7 +612,7 @@ impl WorstWordsSRSModule { #[derive(Clone)] pub struct DeckSettings { - pub id: u32, + pub id: Id, pub name: String, pub forward: String, pub backward: String, @@ -619,7 +625,7 @@ pub struct DeckSettings { impl DeckSettings { pub(crate) fn with_name(name: String) -> DeckSettings { DeckSettings { - id: 0, + id: 0.into(), name, forward: "".to_string(), backward: "".to_string(), @@ -661,7 +667,7 @@ impl DeckSettings { } let mut scope = Scope::new(); scope - .push_constant("id", word.id) + .push_constant("id", word.id.0) .push_constant("key", word.key.clone()) .push_constant("value", word.value.clone()) .push_constant("tags", word.tags.clone()) @@ -691,3 +697,41 @@ impl DeckSettings { self.forward == "speech" || self.backward == "speech" } } + +impl Into for u32{ + fn into(self) -> Id { + Id(self) + } +} + +// impl From for Id{ +// fn from(id: u32) -> Id{ +// Id(id) +// } +// } + +impl Into for Id{ + fn into(self) -> u32 { + self.0 + } +} +impl Display for Id { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromSql for Id{ + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Integer(i) => Ok(Id(i as u32)), + _ => Err(FromSqlError::InvalidType), + } + } +} + +impl ToSql for Id{ + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::from(self.0 as i64)) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 12281c5..e2e95fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ 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::lang::{DeckSettings, WordData, WordGroup}; +use crate::lang::{DeckSettings, Id, WordData, WordGroup}; use crate::navigation::{AppSettings, RootMessage, ScreenState}; use crate::quiz::*; use chrono::NaiveDate; @@ -85,7 +85,7 @@ pub struct AppState { pub word_groups: Vec, pub connection: Connection, pub sync_data: AppSettings, - pub activity: HashMap>, + pub activity: HashMap>, } impl Default for AppState { diff --git a/src/navigation.rs b/src/navigation.rs index f8bbbed..1e3e7b1 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -31,6 +31,7 @@ use hashbrown::HashMap; use std::sync::{Arc, Mutex}; use std::time::Instant; use rusqlite::Connection; +use crate::lang::Id; impl Default for ScreenState { fn default() -> Self { @@ -60,7 +61,7 @@ pub enum RootMessage { RepetitionSettings(RepetitionSettingsMessage), Import(ImportMessage), Keyboard(Event), - DataLoaded(HashMap>), + DataLoaded(HashMap>), None, UpdateData, } @@ -126,9 +127,9 @@ impl ScreenState { for file in directory.read_dir().unwrap().flatten() { let mut vec = vec![]; let history_file_name = file.file_name().into_string().unwrap(); - let id = history_file_name[4..history_file_name.len() - 12] + let id : Id = history_file_name[4..history_file_name.len() - 12] .parse::() - .unwrap(); + .unwrap().into(); let history = get_history_of_set(id); let by_date = history.chunk_by(|x, x1| { diff --git a/src/repetition.rs b/src/repetition.rs index 7985083..efb38b4 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -13,6 +13,7 @@ use iced::{Element, Fill, Task, Theme, alignment, keyboard}; use rodio::MixerDeviceSink; use std::collections::HashSet; use std::sync::{Arc, Mutex}; +use crate::lang::Id; use tokio::task::spawn_blocking; pub struct RepetitionState { @@ -24,7 +25,7 @@ pub struct RepetitionState { open: bool, can_play: bool, sink: Arc, - opened: HashSet, + opened: HashSet, } impl NavigatedPage for RepetitionState { diff --git a/src/repetitions.rs b/src/repetitions.rs index e2095fb..d57f249 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -27,14 +27,12 @@ use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct RepetitionsState { - selected_set_index: Option, correct_filters: Vec, current_sets_cards_cache: HashMap, Vec)>, word_id_index_map: HashMap, local_settings: HashMap, view_data: Option, - pub state: Arc>, - set_names: Vec + state: Arc>, } impl NavigatedPage for RepetitionsState { @@ -89,7 +87,9 @@ impl NavigatedPage for RepetitionsState { GoToHistory => {} GoToSettings => {} - CreateSet => {} + CreateSet => { + + } DeleteSet => {} SetName(_) => {} SelectSet(_) => {} @@ -209,15 +209,15 @@ impl RepetitionsState { button("Проверить фильтр") .style(jl_button) .on_press(TryFilter), - self.count_view(&set), + self.count_view(set), ] .spacing(QUARTER_SPACING), ] .spacing(DEFAULT_SPACING) } else { column![ - self.filled_set_data_view(&set), - self.word_append_panel(&set), + self.filled_set_data_view(set), + self.word_append_panel(set), radio( "Обычный режим", OrderMode::Default, @@ -430,7 +430,6 @@ impl RepetitionsState { impl RepetitionsState { fn append_all_words(&self, set: &DeckSettings) { - let index = self.selected_set_index.unwrap(); let cache = &self.current_sets_cards_cache[&index]; let mut created_set = HashSet::with_capacity(cache.0.len()); cache.0.iter().for_each(|c| { @@ -461,13 +460,12 @@ impl RepetitionsState { fn select_set(&mut self, index: usize) { let state = self.state.lock().unwrap(); if let Some(set) = state.card_sets.get(index) { - self.selected_set_index = Some(index); } } fn clear_selection(&mut self) { - self.selected_set_index = None; + self.view_data = None; } } #[derive(Clone)] From 9e28b05167c829b00a0fe34c7e200dd195e29aa3 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 12:58:02 +0300 Subject: [PATCH 03/30] More migration code --- src/data_provider/history.rs | 2 +- src/lang.rs | 127 +++++++++++++---------------------- src/navigation.rs | 4 +- src/repetitions.rs | 25 +++---- 4 files changed, 61 insertions(+), 97 deletions(-) diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index ff98019..c85e04e 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -32,7 +32,7 @@ fn parse_history_items(strings: Vec) -> Vec { if let [time, word, mode, before, after] = string.split(';').collect::>()[..] { items.push(HistoryItem { timestamp: DateTime::from_timestamp(time.parse().unwrap(), 0).unwrap(), - word_id: word.parse::().unwrap().into(), + word_id: word.parse::().unwrap().into(), mode: match mode.parse::().unwrap() { 2 => WordOpenMode::Hard, 3 => WordOpenMode::Ok, diff --git a/src/lang.rs b/src/lang.rs index 78d3da7..5fe2b2e 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,9 +1,9 @@ -use crate::data_provider::card_stats::{delete_stat, load_stats_of_set, update_stat_score}; -use crate::data_provider::history::{push_note, HistoryItem}; use crate::AppState; +use crate::data_provider::card_stats::{delete_stat, load_stats_of_set, update_stat_score}; +use crate::data_provider::history::{HistoryItem, push_note}; use chrono::{DateTime, Utc}; -use rand::distr::weighted::WeightedIndex; use rand::distr::Distribution; +use rand::distr::weighted::WeightedIndex; use rand::prelude::SliceRandom; use rand::rng; use rand::rngs::ThreadRng; @@ -11,11 +11,13 @@ use rayon::iter::IndexedParallelIterator; use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rhai::{Engine, Scope}; -use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::ToSql; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use std::cmp::PartialEq; use std::collections::HashMap; use std::fmt::{Display, Formatter}; +use std::num::ParseIntError; +use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -26,6 +28,47 @@ const FADE_PER_DAY: f32 = 0.95; #[derive(Clone, PartialEq, Eq, Debug, Hash, Copy)] pub(crate) struct Id(u32); pub const INVALID_ID: Id = Id(0); + +impl FromStr for Id { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(Id(s.parse::()?)) + } +} + +impl Into for u32 { + fn into(self) -> Id { + Id(self) + } +} + +impl Into for Id { + fn into(self) -> u32 { + self.0 + } +} +impl Display for Id { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromSql for Id { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Integer(i) => Ok(Id(i as u32)), + _ => Err(FromSqlError::InvalidType), + } + } +} + +impl ToSql for Id { + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::from(self.0 as i64)) + } +} + #[derive(Clone, Debug)] pub struct KanaSet { name: String, @@ -33,13 +76,11 @@ pub struct KanaSet { pub(crate) dictionary: Vec>, pub(crate) include_map: [bool; 10], } - #[derive(Clone, Debug)] pub enum KanaType { Hiragana, Katakana, } - impl KanaSet { pub fn hiragana() -> Self { Self { @@ -219,7 +260,6 @@ impl KanaSet { current_set } } - impl Default for KanaSet { fn default() -> Self { KanaSet::hiragana() @@ -230,7 +270,6 @@ impl PartialEq for KanaSet { self.name == other.name } } - #[derive(Clone, Debug)] pub struct WordData { pub id: Id, @@ -240,11 +279,10 @@ pub struct WordData { pub additional: HashMap, pub group_id: Id, } - impl WordData { pub fn new() -> Self { Self { - id: 0.into(), + id: INVALID_ID, key: String::new(), value: String::new(), tags: String::new(), @@ -253,13 +291,11 @@ impl WordData { } } } - #[derive(Clone)] pub struct WordGroup { pub id: Id, pub name: String, } - #[derive(Clone, PartialEq)] pub struct CardStatistics { pub id: Id, @@ -268,7 +304,6 @@ pub struct CardStatistics { pub last_open: DateTime, pub score: u8, } - impl CardStatistics { pub fn update(&mut self, status: WordOpenMode) { match status { @@ -295,7 +330,6 @@ impl CardStatistics { (self.score as f32 * multiplier).max(1.0) } } - #[derive(Clone, Copy)] pub enum WordOpenMode { Easy, @@ -303,7 +337,6 @@ pub enum WordOpenMode { Hard, None, } - #[derive(Clone)] pub struct DeckData { words: Vec, @@ -313,7 +346,6 @@ pub struct DeckData { order_module: OrderModule, settings: DeckSettings, } - impl DeckData { pub fn new(settings: &DeckSettings, state: Arc>) -> Self { let state_for = state.clone(); @@ -353,7 +385,6 @@ impl DeckData { settings: settings.clone(), } } - pub fn next(&mut self) -> (WordData, CardStatistics) { let index = match self.order_module.clone() { OrderModule::SemiRandomSRS(mut module) => { @@ -385,7 +416,6 @@ impl DeckData { self.current_word_index = Some(index); (self.words[index].clone(), self.set[index].clone()) } - pub fn open(&mut self, status: WordOpenMode) { if self.current_word_index.is_none() { return; @@ -423,45 +453,38 @@ impl DeckData { }, ) } - pub fn len(&self) -> usize { self.set.len() } } - #[derive(Clone, PartialEq, Copy, Eq)] pub enum OrderMode { Default, TrainWorstFirst, FullRandom, } - #[derive(Clone, Copy, Eq, PartialEq, Default)] pub enum AppendMode { Full, #[default] Manual, } - #[derive(Clone)] enum OrderModule { SemiRandomSRS(SemiRandomSRSModule), RandomSRS(RandomSRSModule), WorstWordsSRS(WorstWordsSRSModule), } - trait SRSModule { fn next(&mut self, set: &mut DeckData) -> usize; fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics); fn init(&mut self, set: &mut DeckData); } - #[derive(Clone)] struct RandomSRSModule { basket: Vec, initialized: bool, } - impl RandomSRSModule { fn new() -> RandomSRSModule { Self { @@ -470,7 +493,6 @@ impl RandomSRSModule { } } } - impl SRSModule for RandomSRSModule { fn next(&mut self, set: &mut DeckData) -> usize { if self.basket.is_empty() { @@ -489,7 +511,6 @@ impl SRSModule for RandomSRSModule { self.basket.shuffle(&mut rand::rng()) } } - #[derive(Clone)] struct SemiRandomSRSModule { history: Vec, @@ -497,7 +518,6 @@ struct SemiRandomSRSModule { generator: ThreadRng, initialized: bool, } - impl SemiRandomSRSModule { fn new() -> SemiRandomSRSModule { SemiRandomSRSModule { @@ -508,7 +528,6 @@ impl SemiRandomSRSModule { } } } - impl SRSModule for SemiRandomSRSModule { fn next(&mut self, set: &mut DeckData) -> usize { let index = self.last_weights.sample(&mut self.generator); @@ -524,14 +543,12 @@ impl SRSModule for SemiRandomSRSModule { index } - fn open(&mut self, _: WordOpenMode, index: usize, word: CardStatistics) { let new_weight = (100.0 / word.calculated_score()).powf(2.0); self.last_weights .update_weights(&[(index, &new_weight)]) .unwrap(); } - fn init(&mut self, set: &mut DeckData) { self.initialized = true; let weights = set @@ -542,13 +559,11 @@ impl SRSModule for SemiRandomSRSModule { self.last_weights = WeightedIndex::new(weights).unwrap(); } } - impl SemiRandomSRSModule { fn history_len(&self, set: &DeckData) -> usize { ((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN) } } - #[derive(Clone)] struct WorstWordsSRSModule { initialized: bool, @@ -558,7 +573,6 @@ struct WorstWordsSRSModule { queue: Vec, rounds_count: u8, } - impl SRSModule for WorstWordsSRSModule { fn next(&mut self, set: &mut DeckData) -> usize { if self.rounds_remaining == 0 { @@ -580,7 +594,6 @@ impl SRSModule for WorstWordsSRSModule { self.initialized = true; } } - impl WorstWordsSRSModule { fn new() -> WorstWordsSRSModule { WorstWordsSRSModule { @@ -592,7 +605,6 @@ impl WorstWordsSRSModule { queue: vec![], } } - fn fill_pool(&mut self, set: &DeckData) { let mut sorted = set .set @@ -609,7 +621,6 @@ impl WorstWordsSRSModule { self.pool = worst; } } - #[derive(Clone)] pub struct DeckSettings { pub id: Id, @@ -621,7 +632,6 @@ pub struct DeckSettings { pub worst_words_list: Option>, pub open_mode: OrderMode, } - impl DeckSettings { pub(crate) fn with_name(name: String) -> DeckSettings { DeckSettings { @@ -635,13 +645,11 @@ impl DeckSettings { open_mode: OrderMode::Default, } } - pub(crate) fn check_filter(&self) -> bool { let engine = Engine::new(); let ast = engine.compile(&self.filter); ast.is_ok() } - pub fn get_word_list(&self, state: &AppState) -> Vec { let time = Instant::now(); let mut list = vec![]; @@ -692,46 +700,7 @@ impl DeckSettings { list } - pub fn require_speech(&self) -> bool { self.forward == "speech" || self.backward == "speech" } -} - -impl Into for u32{ - fn into(self) -> Id { - Id(self) - } -} - -// impl From for Id{ -// fn from(id: u32) -> Id{ -// Id(id) -// } -// } - -impl Into for Id{ - fn into(self) -> u32 { - self.0 - } -} -impl Display for Id { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromSql for Id{ - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - match value { - ValueRef::Integer(i) => Ok(Id(i as u32)), - _ => Err(FromSqlError::InvalidType), - } - } -} - -impl ToSql for Id{ - fn to_sql(&self) -> rusqlite::Result> { - Ok(ToSqlOutput::from(self.0 as i64)) - } } \ No newline at end of file diff --git a/src/navigation.rs b/src/navigation.rs index 1e3e7b1..bf2c459 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -128,8 +128,8 @@ impl ScreenState { let mut vec = vec![]; let history_file_name = file.file_name().into_string().unwrap(); let id : Id = history_file_name[4..history_file_name.len() - 12] - .parse::() - .unwrap().into(); + .parse::() + .unwrap(); let history = get_history_of_set(id); let by_date = history.chunk_by(|x, x1| { diff --git a/src/repetitions.rs b/src/repetitions.rs index d57f249..a5e1fa0 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -27,11 +27,9 @@ use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct RepetitionsState { - correct_filters: Vec, - current_sets_cards_cache: HashMap, Vec)>, word_id_index_map: HashMap, - local_settings: HashMap, - view_data: Option, + view_data: Option, + decks: Vec, state: Arc>, } @@ -489,20 +487,17 @@ pub enum RepetitionsMessage { AppendWords(usize), } -#[derive(Clone, Default)] -struct DeckLocalSetting { +#[derive(Clone)] +struct DeckViewData { + set_settings: DeckSettings, + existing_words_indices: Option>, + available_words_indices: Option>, append_mode: AppendMode, append_warning: bool, + valid_filter: bool, + index: usize, } - -#[derive(Clone)] -struct SetViewData{ - set_local_settings: DeckLocalSetting, - set_settings: DeckSettings, - existing_words_indices: Vec, - available_words_indices: Vec, -} -impl Deref for SetViewData { +impl Deref for DeckViewData { type Target = DeckSettings; fn deref(&self) -> &Self::Target { From 80785ff9e61bd9073aa6d1438337a7cfcc5f30da Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 15:13:10 +0300 Subject: [PATCH 04/30] Rewrite repetitions page --- src/repetitions.rs | 314 ++++++++++++++++++++++++++++++++------------- 1 file changed, 227 insertions(+), 87 deletions(-) diff --git a/src/repetitions.rs b/src/repetitions.rs index a5e1fa0..2738866 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,8 +1,7 @@ -use std::ops::Deref; -use crate::data_provider::card_sets::{delete_set, update_card_set}; +use crate::data_provider::card_sets::update_card_set; use crate::data_provider::card_stats::{add_stat_list, load_stats_of_set}; use crate::history::HistoryState; -use crate::lang::{AppendMode, DeckSettings, CardStatistics, OrderMode}; +use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode, INVALID_ID}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page}; use crate::repetition::RepetitionState; @@ -12,24 +11,25 @@ use crate::styling::*; use crate::{AppState, RootMessage}; 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 std::ops::Deref; use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct RepetitionsState { - word_id_index_map: HashMap, - view_data: Option, - decks: Vec, + word_id_index_map: HashMap, + selected_deck_index: Option, + decks: Vec, state: Arc>, } @@ -38,35 +38,42 @@ impl NavigatedPage for RepetitionsState { if let Back = message { Some(PreviousPage) } else if let GoToHistory = message { - let card_set; - { - card_set = self.state.lock().unwrap().card_sets[self.selected_set_index.unwrap()].id; - } - Some(History(HistoryState::new(card_set, self.state.clone()))) + Some(History(HistoryState::new( + self.selected_deck().unwrap().id, + self.state.clone(), + ))) } else if let GoToRepetition = message { let clone = self.state.clone(); - let card_set; - { - card_set = self.state.lock().unwrap().card_sets[self.selected_set_index.unwrap()].clone(); - } - if self.local_settings[&card_set.id].append_mode == AppendMode::Full { - self.append_all_words(&card_set); + let selected_set = self.selected_deck_mut().unwrap(); + if selected_set.append_mode == AppendMode::Full { + + Self::append_all_words(&clone.lock().unwrap(), selected_set); + selected_set.existing_words_indices = selected_set.available_words_indices.clone(); } else { - let cache = self - .current_sets_cards_cache - .get(&self.selected_set_index.unwrap()) - .unwrap(); - if cache.1.is_empty() || cache.0.is_empty() { - let local_settings = self.local_settings.get_mut(&card_set.id).unwrap(); - local_settings.append_warning = true; + if selected_set + .available_words_indices + .as_ref() + .unwrap() + .is_empty() + || selected_set + .existing_words_indices + .as_ref() + .unwrap() + .is_empty() + { + selected_set.append_warning = true; + return None; } } - Some(Repetition(RepetitionState::new(card_set, clone))) + Some(Repetition(RepetitionState::new( + selected_set.general_settings.clone(), + clone, + ))) } else if let GoToSettings = message { Some(RepetitionSettings(RepetitionSettingsState::new( - self.selected_set_index.unwrap(), + self.selected_deck().unwrap().index.unwrap(), self.state.clone(), ))) } else { @@ -74,8 +81,19 @@ impl NavigatedPage for RepetitionsState { } } fn navigated(&mut self) { - self.clear_selection(); - self.current_sets_cards_cache.clear(); + let index = self.selected_deck_index.unwrap(); + let state = self.state.lock().unwrap(); + if let Some(global_deck) = state.card_sets.get(index) { + if global_deck.id != self.selected_deck().unwrap().id { + drop(state); + self.decks.remove(index); + self.clear_selection(); + } + } else { + drop(state); + self.decks.remove(index); + self.clear_selection() + } } fn update(&mut self, message: RepetitionsMessage) -> Task { match message { @@ -86,19 +104,120 @@ impl NavigatedPage for RepetitionsState { GoToSettings => {} CreateSet => { - + let index = self.decks.len() + 1; + self.decks.push(DeckViewData { + general_settings: DeckSettings::with_name(format!("Колода #{}", index)), + existing_words_indices: None, + available_words_indices: None, + append_mode: Default::default(), + append_warning: false, + valid_filter: true, + index: None, + }); + } + DeleteSet => { + let index = self.selected_deck_index.unwrap(); + self.clear_selection(); + let deck = self.decks.remove(index); + if deck.id != INVALID_ID { + let mut state = self.state.lock().unwrap(); + state.card_sets.remove(index); + } + } + SetName(value) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.name = value; + } + SelectSet(index) => { + self.select_set(index); + let deck = self.selected_deck().unwrap(); + if deck.existing_words_indices.is_none() { + let state = self.state.lock().unwrap(); + let added: Vec<_> = load_stats_of_set(deck, &state.connection) + .iter() + .map(|c| self.word_id_index_map[&c.word_id]) + .collect(); + let total = deck.get_word_list(&state); + drop(state); + let deck = self.selected_deck_mut().unwrap(); + deck.existing_words_indices = Some(added); + deck.available_words_indices = Some(total); + } + } + Save => { + let mut deck = self.selected_deck().unwrap().clone(); + let mut state = self.state.lock().unwrap(); + { + update_card_set(&mut deck.general_settings, &state.connection); + if let Some(index) = deck.index { + state.card_sets[index] = deck.general_settings.clone(); + } else { + state.card_sets.push(deck.general_settings.clone()); + deck.index = Some(state.card_sets.len() - 1); + self.decks[deck.index.unwrap()] = deck.clone(); + } + } + } + SetForward(value) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.forward = value; + } + SetBackward(value) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.backward = value; + } + SetFilter(value) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.filter = value; + } + TryFilter => { + let state = self.state.lock().unwrap(); + let deck = self.selected_deck().unwrap(); + let count = deck.get_word_list(&state).len(); + drop(state); + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.count = Some(count); + } + SetOpenMode(mode) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.open_mode = mode; + } + SetAppendMode(mode) => { + let deck = self.selected_deck_mut().unwrap(); + deck.append_mode = mode; + } + AppendWords(count) => { + let adding: Vec; + { + { + let deck = self.selected_deck().unwrap(); + let existing_words_indices = deck.existing_words_indices.as_ref().unwrap(); + let mut created_set = HashSet::with_capacity(existing_words_indices.len()); + existing_words_indices.iter().for_each(|c| { + created_set.insert(c); + }); + adding = deck + .available_words_indices + .as_ref() + .unwrap() + .iter() + .filter(|c| !created_set.contains(c)) + .take(count) + .cloned() + .collect(); + } + + let deck = self.selected_deck_mut().unwrap(); + deck.existing_words_indices + .as_mut() + .unwrap() + .append(&mut adding.clone()); + } + + let deck = self.selected_deck().unwrap(); + let state = self.state.lock().unwrap(); + Self::append_words(&state, &deck.general_settings, adding.into_iter()); } - DeleteSet => {} - SetName(_) => {} - SelectSet(_) => {} - Save => {} - SetForward(_) => {} - SetBackward(_) => {} - SetFilter(_) => {} - TryFilter => {} - SetOpenMode(_) => {} - SetAppendMode(_) => {} - AppendWords(_) => {} } Task::none() } @@ -130,7 +249,6 @@ impl NavigatedPage for RepetitionsState { impl RepetitionsState { pub(crate) fn new(state: Arc>) -> RepetitionsState { let state_ = state.lock().unwrap(); - let count = state_.card_sets.len(); let mut map = HashMap::with_capacity(state_.dictionary.len()); state_ @@ -141,27 +259,31 @@ impl RepetitionsState { map.insert(word.id, index); }); - let mut local_settings = HashMap::with_capacity(state_.card_sets.len()); - state_.card_sets.iter().for_each(|card_set| { - local_settings.insert(card_set.id, Default::default()); - }); + let decks = state_.card_sets.iter().enumerate().map(|(index, set)| {DeckViewData{ + general_settings: set.clone(), + existing_words_indices: None, + available_words_indices: None, + append_mode: Default::default(), + append_warning: false, + valid_filter: true, + index: Some(index), + }}).collect(); drop(state_); RepetitionsState { - selected_set_index: None, - correct_filters: vec![true; count], - current_sets_cards_cache: Default::default(), word_id_index_map: map, - local_settings, - view_data: None, + selected_deck_index: None, + decks, state, - set_names: vec![], } } } impl RepetitionsState { - fn launch_delete_button(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { - if set.id != 0 { + fn launch_delete_button(&self, set: &DeckViewData) -> Element<'_, RepetitionsMessage> { + if set.id != INVALID_ID { + if set.append_mode == AppendMode::Manual && set.existing_words_indices.as_ref().unwrap().is_empty() { + return text!("Добавьте карточки или измените фильтр").into(); + } return row![ space().width(Fill), button(text!("Начать повторение").width(200).center()) @@ -173,7 +295,7 @@ impl RepetitionsState { button("Удалить").style(danger).on_press(DeleteSet).into() } fn selected_set_view(&self) -> Element<'_, RepetitionsMessage> { - if let Some(set) = &self.view_data { + if let Some(set) = &self.selected_deck() { return column![ scrollable( column![ @@ -189,7 +311,7 @@ impl RepetitionsState { ] .spacing(QUARTER_SPACING), { - if set.id == 0 { + if set.id == INVALID_ID { column![ column![ text!("Передняя сторона"), @@ -252,9 +374,9 @@ impl RepetitionsState { .height(Fill), row![ button("Сохранить").style(jl_button).on_press(Save), - self.launch_delete_button(&set) + self.launch_delete_button(set) ] - .spacing(DEFAULT_SPACING), + .spacing(DEFAULT_SPACING).align_y(Center), ] .spacing(DEFAULT_SPACING) .width(Length::FillPortion(2)) @@ -345,20 +467,20 @@ impl RepetitionsState { }) .into() } - fn word_append_panel(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { + fn word_append_panel(&self, set: &DeckViewData) -> Element<'_, RepetitionsMessage> { column![ text!("Режим добавления карточек"), row![ radio( "Добавлять все доступные", AppendMode::Full, - Some(self.local_settings[&set.id].append_mode), + Some(set.append_mode), SetAppendMode ), radio( "Добавлять вручную", AppendMode::Manual, - Some(self.local_settings[&set.id].append_mode), + Some(set.append_mode), SetAppendMode ) ] @@ -368,8 +490,8 @@ impl RepetitionsState { .spacing(HALF_SPACING) .into() } - fn adder_panel(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { - match self.local_settings[&set.id].append_mode { + fn adder_panel(&self, set: &DeckViewData) -> Element<'_, RepetitionsMessage> { + match set.append_mode { AppendMode::Full => space().into(), AppendMode::Manual => row![ button("+5").on_press(AppendWords(5)), @@ -381,10 +503,15 @@ impl RepetitionsState { .into(), } } + fn count_comparator_view(&self) -> Element<'_, RepetitionsMessage> { - let cache = &self.current_sets_cards_cache[&self.selected_set_index.unwrap()]; - let now = cache.0.len(); - let available = cache.1.len(); + let selected_deck = self.selected_deck().unwrap(); + let now = selected_deck.existing_words_indices.as_ref().unwrap().len(); + let available = selected_deck + .available_words_indices + .as_ref() + .unwrap() + .len(); text!("{} слова добавлено из {}", now, available).into() } fn count_view(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { @@ -396,9 +523,9 @@ impl RepetitionsState { fn sets_list(&self) -> Column<'_, RepetitionsMessage> { let mut column = Column::new(); - for (i, set) in self.set_names.iter().enumerate() { + for (i, set) in self.decks.iter().enumerate() { column = column.push( - button(text!("{}", set.clone())) + button(text!("{}", &set.name)) .on_press_with(move || SelectSet(i)) .style(move |_x: &Theme, status: Status| Style { background: if status == Status::Hovered { @@ -406,7 +533,7 @@ impl RepetitionsState { } else { None }, - text_color: if self.correct_filters[i] { + text_color: if set.valid_filter { _x.palette().primary } else { _x.palette().warning @@ -427,14 +554,20 @@ impl RepetitionsState { } impl RepetitionsState { - fn append_all_words(&self, set: &DeckSettings) { - let cache = &self.current_sets_cards_cache[&index]; - let mut created_set = HashSet::with_capacity(cache.0.len()); - cache.0.iter().for_each(|c| { + fn append_all_words(state: &AppState, deck: &DeckViewData) { + let existing = deck.existing_words_indices.as_ref().unwrap(); + let mut created_set = HashSet::with_capacity(existing.len()); + existing.iter().for_each(|c| { created_set.insert(c); }); - let required = cache.1.iter().filter(|i| !created_set.contains(i)).cloned(); - Self::append_words(&self.state.lock().unwrap(), set, required); + let required = deck + .available_words_indices + .as_ref() + .unwrap() + .iter() + .filter(|i| !created_set.contains(i)) + .cloned(); + Self::append_words(state, deck, required); } fn append_words(state: &AppState, set: &DeckSettings, indices: impl Iterator) { let words = &state.dictionary; @@ -442,7 +575,7 @@ impl RepetitionsState { .map(|i| { let word = words.get(i).unwrap(); CardStatistics { - id: 0, + id: 0.into(), word_id: word.id, last_open: Utc::now(), score: 1, @@ -455,15 +588,22 @@ impl RepetitionsState { add_stat_list(stats, &state.connection); } } - fn select_set(&mut self, index: usize) { - let state = self.state.lock().unwrap(); - if let Some(set) = state.card_sets.get(index) { + fn select_set(&mut self, index: usize) { + if self.decks.get(index).is_some() { + self.selected_deck_index = Some(index); } } + fn selected_deck(&self) -> Option<&DeckViewData> { + self.decks.get(self.selected_deck_index?) + } + fn selected_deck_mut(&mut self) -> Option<&mut DeckViewData> { + self.decks.get_mut(self.selected_deck_index?) + } + fn clear_selection(&mut self) { - self.view_data = None; + self.selected_deck_index = None; } } #[derive(Clone)] @@ -489,18 +629,18 @@ pub enum RepetitionsMessage { #[derive(Clone)] struct DeckViewData { - set_settings: DeckSettings, + general_settings: DeckSettings, existing_words_indices: Option>, available_words_indices: Option>, append_mode: AppendMode, append_warning: bool, valid_filter: bool, - index: usize, + index: Option, } impl Deref for DeckViewData { type Target = DeckSettings; fn deref(&self) -> &Self::Target { - &self.set_settings + &self.general_settings } -} \ No newline at end of file +} From 306febc51480ea9210eedcae56c9e558fb364f76 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 15:16:51 +0300 Subject: [PATCH 05/30] Rename set to deck in repetitions.rs --- src/data_provider/card_sets.rs | 2 +- src/data_provider/card_stats.rs | 2 +- src/lang.rs | 4 +- src/main.rs | 6 +- src/repetition_settings.rs | 10 ++-- src/repetitions.rs | 100 ++++++++++++++++---------------- src/sync.rs | 2 +- 7 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 6d3027a..86f4c22 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -46,7 +46,7 @@ pub fn add_set(set: &mut DeckSettings, connection: &Connection) { set.id = index.into(); } -pub fn update_card_set(set: &mut DeckSettings, connection: &Connection) { +pub fn update_deck(set: &mut DeckSettings, connection: &Connection) { if set.id == INVALID_ID { add_set(set, connection); } else { diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index d5204e6..e913bca 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -2,7 +2,7 @@ use crate::lang::{DeckSettings, CardStatistics, INVALID_ID}; use rusqlite::Connection; use std::time::Instant; -pub fn load_stats_of_set(set: &DeckSettings, connection: &Connection) -> Vec { +pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec { let mut stmt = connection .prepare("SELECT id, word_id, score, last_opened FROM card_stats WHERE set_id = ?1") .unwrap(); diff --git a/src/lang.rs b/src/lang.rs index 5fe2b2e..e0c5efa 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,5 +1,5 @@ use crate::AppState; -use crate::data_provider::card_stats::{delete_stat, load_stats_of_set, update_stat_score}; +use crate::data_provider::card_stats::{delete_stat, load_stats_of_deck, update_stat_score}; use crate::data_provider::history::{HistoryItem, push_note}; use chrono::{DateTime, Utc}; use rand::distr::Distribution; @@ -351,7 +351,7 @@ impl DeckData { let state_for = state.clone(); let state_locked = state.lock().unwrap(); - let mut current_set = load_stats_of_set(settings, &state_locked.connection); + let mut current_set = load_stats_of_deck(settings, &state_locked.connection); let last_list: Vec<_> = settings .get_word_list(&state_locked) .iter() diff --git a/src/main.rs b/src/main.rs index e2e95fd..866ecfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,7 +81,7 @@ fn subscription(_state: &ScreenState) -> Subscription { pub struct AppState { pub dictionary: Vec, - pub card_sets: Vec, + pub decks: Vec, pub word_groups: Vec, pub connection: Connection, pub sync_data: AppSettings, @@ -98,7 +98,7 @@ impl AppState { pub fn new() -> Self { Self { dictionary: vec![], - card_sets: vec![], + decks: vec![], word_groups: vec![], connection: default_connection(), sync_data: AppSettings { @@ -117,7 +117,7 @@ fn fill_state(state: &mut AppState) { let setting = load_settings(&state.connection); state.dictionary = list; - state.card_sets = sets; + state.decks = sets; state.word_groups = groups; state.sync_data = setting } diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index de80607..65f6c6b 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -1,5 +1,5 @@ use crate::AppState; -use crate::data_provider::card_sets::{delete_set, update_card_set}; +use crate::data_provider::card_sets::{delete_set, update_deck}; use crate::lang::DeckSettings; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; @@ -25,7 +25,7 @@ impl RepetitionSettingsState { let set; { let local_state = p1.lock().unwrap(); - set = local_state.card_sets[index].clone(); + set = local_state.decks[index].clone(); } RepetitionSettingsState { index, @@ -51,8 +51,8 @@ impl NavigatedPage for RepetitionSettingsState { Back => {} Save => { let mut state = self.state.lock().unwrap(); - update_card_set(&mut self.set, &state.connection); - state.card_sets[self.index] = self.set.clone(); + update_deck(&mut self.set, &state.connection); + state.decks[self.index] = self.set.clone(); } SetName(new) => { self.set.name = new; @@ -81,7 +81,7 @@ impl NavigatedPage for RepetitionSettingsState { } let mut state = self.state.lock().unwrap(); delete_set(&self.set, &state.connection); - state.card_sets.remove(self.index); + state.decks.remove(self.index); return Task::done(RootMessage::RepetitionSettings(Back)); } RevertDeleteSet => self.real_delete = false, diff --git a/src/repetitions.rs b/src/repetitions.rs index 2738866..7aa5a75 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,5 +1,5 @@ -use crate::data_provider::card_sets::update_card_set; -use crate::data_provider::card_stats::{add_stat_list, load_stats_of_set}; +use crate::data_provider::card_sets::update_deck; +use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck}; use crate::history::HistoryState; use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode, INVALID_ID}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; @@ -83,7 +83,7 @@ impl NavigatedPage for RepetitionsState { fn navigated(&mut self) { let index = self.selected_deck_index.unwrap(); let state = self.state.lock().unwrap(); - if let Some(global_deck) = state.card_sets.get(index) { + if let Some(global_deck) = state.decks.get(index) { if global_deck.id != self.selected_deck().unwrap().id { drop(state); self.decks.remove(index); @@ -121,7 +121,7 @@ impl NavigatedPage for RepetitionsState { let deck = self.decks.remove(index); if deck.id != INVALID_ID { let mut state = self.state.lock().unwrap(); - state.card_sets.remove(index); + state.decks.remove(index); } } SetName(value) => { @@ -129,11 +129,11 @@ impl NavigatedPage for RepetitionsState { deck.general_settings.name = value; } SelectSet(index) => { - self.select_set(index); + self.select_deck(index); let deck = self.selected_deck().unwrap(); if deck.existing_words_indices.is_none() { let state = self.state.lock().unwrap(); - let added: Vec<_> = load_stats_of_set(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(); @@ -148,12 +148,12 @@ impl NavigatedPage for RepetitionsState { let mut deck = self.selected_deck().unwrap().clone(); let mut state = self.state.lock().unwrap(); { - update_card_set(&mut deck.general_settings, &state.connection); + update_deck(&mut deck.general_settings, &state.connection); if let Some(index) = deck.index { - state.card_sets[index] = deck.general_settings.clone(); + state.decks[index] = deck.general_settings.clone(); } else { - state.card_sets.push(deck.general_settings.clone()); - deck.index = Some(state.card_sets.len() - 1); + state.decks.push(deck.general_settings.clone()); + deck.index = Some(state.decks.len() - 1); self.decks[deck.index.unwrap()] = deck.clone(); } } @@ -226,7 +226,7 @@ impl NavigatedPage for RepetitionsState { back_overlay( row![ column![ - scrollable(self.sets_list()).height(Fill), + scrollable(self.decks_list()).height(Fill), button("Добавить") .style(jl_button) .width(Fill) @@ -234,7 +234,7 @@ impl NavigatedPage for RepetitionsState { ] .spacing(DEFAULT_SPACING) .width(Length::FillPortion(1)), - self.selected_set_view(), + self.selected_deck_view(), ] .align_y(Center) .spacing(DEFAULT_SPACING) @@ -259,8 +259,8 @@ impl RepetitionsState { map.insert(word.id, index); }); - let decks = state_.card_sets.iter().enumerate().map(|(index, set)| {DeckViewData{ - general_settings: set.clone(), + let decks = state_.decks.iter().enumerate().map(|(index, deck)| {DeckViewData{ + general_settings: deck.clone(), existing_words_indices: None, available_words_indices: None, append_mode: Default::default(), @@ -279,9 +279,9 @@ impl RepetitionsState { } impl RepetitionsState { - fn launch_delete_button(&self, set: &DeckViewData) -> Element<'_, RepetitionsMessage> { - if set.id != INVALID_ID { - if set.append_mode == AppendMode::Manual && set.existing_words_indices.as_ref().unwrap().is_empty() { + fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { + if deck.id != INVALID_ID { + if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.as_ref().unwrap().is_empty() { return text!("Добавьте карточки или измените фильтр").into(); } return row![ @@ -294,15 +294,15 @@ impl RepetitionsState { } button("Удалить").style(danger).on_press(DeleteSet).into() } - fn selected_set_view(&self) -> Element<'_, RepetitionsMessage> { - if let Some(set) = &self.selected_deck() { + fn selected_deck_view(&self) -> Element<'_, RepetitionsMessage> { + if let Some(deck) = &self.selected_deck() { return column![ scrollable( column![ column![ text!("Название набора"), row![ - text_input("", &set.name).on_input(SetName), + text_input("", &deck.name).on_input(SetName), button(svg(Handle::from_memory(SETTINGS_ICON))) .width(43) .on_press(GoToSettings), @@ -311,49 +311,49 @@ impl RepetitionsState { ] .spacing(QUARTER_SPACING), { - if set.id == INVALID_ID { + if deck.id == INVALID_ID { column![ column![ text!("Передняя сторона"), - text_input("", &set.forward).on_input(SetForward), + text_input("", &deck.forward).on_input(SetForward), ] .spacing(QUARTER_SPACING), column![ text!("Задняя сторона"), - text_input("", &set.backward).on_input(SetBackward), + text_input("", &deck.backward).on_input(SetBackward), ] .spacing(QUARTER_SPACING), column![ text!("Фильтр"), - text_input("", &set.filter).on_input(SetFilter), + text_input("", &deck.filter).on_input(SetFilter), button("Проверить фильтр") .style(jl_button) .on_press(TryFilter), - self.count_view(set), + self.count_view(deck), ] .spacing(QUARTER_SPACING), ] .spacing(DEFAULT_SPACING) } else { column![ - self.filled_set_data_view(set), - self.word_append_panel(set), + self.filled_deck_data_view(deck), + self.word_append_panel(deck), radio( "Обычный режим", OrderMode::Default, - Some(set.open_mode), + Some(deck.open_mode), SetOpenMode ), radio( "Начать с плохих слов", OrderMode::TrainWorstFirst, - Some(set.open_mode), + Some(deck.open_mode), SetOpenMode ), radio( "Полностью случайно", OrderMode::FullRandom, - Some(set.open_mode), + Some(deck.open_mode), SetOpenMode ), button("История").style(jl_button).on_press(GoToHistory), @@ -374,7 +374,7 @@ impl RepetitionsState { .height(Fill), row![ button("Сохранить").style(jl_button).on_press(Save), - self.launch_delete_button(set) + self.launch_delete_button(deck) ] .spacing(DEFAULT_SPACING).align_y(Center), ] @@ -384,17 +384,17 @@ impl RepetitionsState { } space().width(Length::FillPortion(2)).into() } - fn filled_set_data_view(&self, set: &DeckSettings) -> Column<'_, RepetitionsMessage> { - column![self.activity_bar(set), self.count_comparator_view()] + fn filled_deck_data_view(&self, deck: &DeckSettings) -> Column<'_, RepetitionsMessage> { + column![self.activity_bar(deck), self.count_comparator_view()] .align_x(Center) .width(Fill) .spacing(DEFAULT_SPACING) } - fn activity_bar(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { + fn activity_bar(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { const MAX_DAY_COUNT: f32 = 128.0; let state = self.state.lock().unwrap(); - let history = state.activity.get(&set.id); + let history = state.activity.get(&deck.id); let mut counts: Vec = vec![0; 30 * 7]; let now = Local::now().date_naive(); @@ -467,31 +467,31 @@ impl RepetitionsState { }) .into() } - fn word_append_panel(&self, set: &DeckViewData) -> Element<'_, RepetitionsMessage> { + fn word_append_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { column![ text!("Режим добавления карточек"), row![ radio( "Добавлять все доступные", AppendMode::Full, - Some(set.append_mode), + Some(deck.append_mode), SetAppendMode ), radio( "Добавлять вручную", AppendMode::Manual, - Some(set.append_mode), + Some(deck.append_mode), SetAppendMode ) ] .spacing(HALF_SPACING), - self.adder_panel(set), + self.adder_panel(deck), ] .spacing(HALF_SPACING) .into() } - fn adder_panel(&self, set: &DeckViewData) -> Element<'_, RepetitionsMessage> { - match set.append_mode { + fn adder_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { + match deck.append_mode { AppendMode::Full => space().into(), AppendMode::Manual => row![ button("+5").on_press(AppendWords(5)), @@ -514,18 +514,18 @@ impl RepetitionsState { .len(); text!("{} слова добавлено из {}", now, available).into() } - fn count_view(&self, set: &DeckSettings) -> Element<'_, RepetitionsMessage> { - if let Some(count) = set.count { + fn count_view(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { + if let Some(count) = deck.count { return text!("Количество слов: {}", count).into(); } space().into() } - fn sets_list(&self) -> Column<'_, RepetitionsMessage> { + fn decks_list(&self) -> Column<'_, RepetitionsMessage> { let mut column = Column::new(); - for (i, set) in self.decks.iter().enumerate() { + for (i, deck) in self.decks.iter().enumerate() { column = column.push( - button(text!("{}", &set.name)) + button(text!("{}", &deck.name)) .on_press_with(move || SelectSet(i)) .style(move |_x: &Theme, status: Status| Style { background: if status == Status::Hovered { @@ -533,7 +533,7 @@ impl RepetitionsState { } else { None }, - text_color: if set.valid_filter { + text_color: if deck.valid_filter { _x.palette().primary } else { _x.palette().warning @@ -569,7 +569,7 @@ impl RepetitionsState { .cloned(); Self::append_words(state, deck, required); } - fn append_words(state: &AppState, set: &DeckSettings, indices: impl Iterator) { + fn append_words(state: &AppState, deck: &DeckSettings, indices: impl Iterator) { let words = &state.dictionary; let stats = &mut indices .map(|i| { @@ -579,7 +579,7 @@ impl RepetitionsState { word_id: word.id, last_open: Utc::now(), score: 1, - set_id: set.id, + set_id: deck.id, } }) .collect::>(); @@ -589,7 +589,7 @@ impl RepetitionsState { } } - fn select_set(&mut self, index: usize) { + fn select_deck(&mut self, index: usize) { if self.decks.get(index).is_some() { self.selected_deck_index = Some(index); } diff --git a/src/sync.rs b/src/sync.rs index a273fce..a3aa52b 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -119,7 +119,7 @@ impl NavigatedPage for SyncState { let mut state = self.state.lock().unwrap(); state.sync_data = updated_state.sync_data; state.connection = updated_state.connection; - state.card_sets = updated_state.card_sets; + state.decks = updated_state.decks; state.dictionary = updated_state.dictionary; state.word_groups = updated_state.word_groups; } From 3cd7c12baa67a7e542837737c78eb4cbd5433b9a Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 17:15:03 +0300 Subject: [PATCH 06/30] Fixing bugs --- src/repetitions.rs | 68 ++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/repetitions.rs b/src/repetitions.rs index 7aa5a75..2d5e052 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,7 +1,7 @@ use crate::data_provider::card_sets::update_deck; use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck}; use crate::history::HistoryState; -use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode, INVALID_ID}; +use crate::lang::{AppendMode, CardStatistics, DeckSettings, INVALID_ID, Id, OrderMode}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page}; use crate::repetition::RepetitionState; @@ -11,17 +11,17 @@ use crate::styling::*; use crate::{AppState, RootMessage}; 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 std::ops::Deref; use std::sync::{Arc, Mutex}; @@ -47,7 +47,6 @@ impl NavigatedPage for RepetitionsState { let selected_set = self.selected_deck_mut().unwrap(); if selected_set.append_mode == AppendMode::Full { - Self::append_all_words(&clone.lock().unwrap(), selected_set); selected_set.existing_words_indices = selected_set.available_words_indices.clone(); } else { @@ -62,7 +61,6 @@ impl NavigatedPage for RepetitionsState { .unwrap() .is_empty() { - selected_set.append_warning = true; return None; } } @@ -110,8 +108,6 @@ impl NavigatedPage for RepetitionsState { existing_words_indices: None, available_words_indices: None, append_mode: Default::default(), - append_warning: false, - valid_filter: true, index: None, }); } @@ -122,6 +118,15 @@ impl NavigatedPage for RepetitionsState { if deck.id != INVALID_ID { let mut state = self.state.lock().unwrap(); state.decks.remove(index); + self.decks = self + .decks + .clone() + .into_iter() + .map(|mut deck| { + deck.index = state.decks.iter().position(|d| d.id == deck.id); + deck + }) + .collect(); } } SetName(value) => { @@ -154,7 +159,7 @@ impl NavigatedPage for RepetitionsState { } else { state.decks.push(deck.general_settings.clone()); deck.index = Some(state.decks.len() - 1); - self.decks[deck.index.unwrap()] = deck.clone(); + self.decks[self.selected_deck_index.unwrap()] = deck.clone(); } } } @@ -259,15 +264,7 @@ impl RepetitionsState { map.insert(word.id, index); }); - let decks = state_.decks.iter().enumerate().map(|(index, deck)| {DeckViewData{ - general_settings: deck.clone(), - existing_words_indices: None, - available_words_indices: None, - append_mode: Default::default(), - append_warning: false, - valid_filter: true, - index: Some(index), - }}).collect(); + let decks = Self::get_decks_from_state(&state_); drop(state_); RepetitionsState { word_id_index_map: map, @@ -276,12 +273,29 @@ impl RepetitionsState { state, } } + + fn get_decks_from_state(state: &AppState) -> Vec { + state + .decks + .iter() + .enumerate() + .map(|(index, deck)| DeckViewData { + general_settings: deck.clone(), + existing_words_indices: None, + available_words_indices: None, + append_mode: Default::default(), + index: Some(index), + }) + .collect() + } } impl RepetitionsState { fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { if deck.id != INVALID_ID { - if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.as_ref().unwrap().is_empty() { + if deck.append_mode == AppendMode::Manual + && deck.existing_words_indices.as_ref().unwrap().is_empty() + { return text!("Добавьте карточки или измените фильтр").into(); } return row![ @@ -376,7 +390,8 @@ impl RepetitionsState { button("Сохранить").style(jl_button).on_press(Save), self.launch_delete_button(deck) ] - .spacing(DEFAULT_SPACING).align_y(Center), + .spacing(DEFAULT_SPACING) + .align_y(Center), ] .spacing(DEFAULT_SPACING) .width(Length::FillPortion(2)) @@ -533,11 +548,8 @@ impl RepetitionsState { } else { None }, - text_color: if deck.valid_filter { - _x.palette().primary - } else { - _x.palette().warning - }, + text_color: + _x.palette().primary, border: Border { color: Default::default(), width: 0.0, @@ -590,7 +602,7 @@ impl RepetitionsState { } fn select_deck(&mut self, index: usize) { - if self.decks.get(index).is_some() { + if self.decks.get(index).is_some() { self.selected_deck_index = Some(index); } } @@ -633,8 +645,6 @@ struct DeckViewData { existing_words_indices: Option>, available_words_indices: Option>, append_mode: AppendMode, - append_warning: bool, - valid_filter: bool, index: Option, } impl Deref for DeckViewData { From d3c28179b6bc6b5f747b86b5808dcdb578dda356 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 17:31:19 +0300 Subject: [PATCH 07/30] Use is_valid for Id --- src/data_provider/card_sets.rs | 6 +++--- src/data_provider/card_stats.rs | 6 +++--- src/data_provider/import.rs | 2 +- src/data_provider/words.rs | 10 +++++----- src/dictionary.rs | 8 ++++---- src/lang.rs | 9 +++++++-- src/main.rs | 2 +- src/navigation.rs | 10 +++++----- src/repetition.rs | 4 ++-- src/repetitions.rs | 11 +++++------ 10 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 86f4c22..00f75fe 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,4 +1,4 @@ -use crate::lang::{DeckSettings, OrderMode, INVALID_ID}; +use crate::lang::{DeckSettings, INVALID_ID, OrderMode}; use rusqlite::Connection; @@ -47,7 +47,7 @@ pub fn add_set(set: &mut DeckSettings, connection: &Connection) { } pub fn update_deck(set: &mut DeckSettings, connection: &Connection) { - if set.id == INVALID_ID { + if !set.id.is_valid() { add_set(set, connection); } else { connection @@ -66,7 +66,7 @@ pub fn update_deck(set: &mut DeckSettings, connection: &Connection) { } pub fn delete_set(set: &DeckSettings, connection: &Connection) { - if set.id == INVALID_ID { + if !set.id.is_valid() { return; } connection diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index e913bca..888f030 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,4 +1,4 @@ -use crate::lang::{DeckSettings, CardStatistics, INVALID_ID}; +use crate::lang::{CardStatistics, DeckSettings, INVALID_ID}; use rusqlite::Connection; use std::time::Instant; @@ -64,7 +64,7 @@ pub fn add_stat_list(stat: &mut [CardStatistics], connection: &Connection) { for (index, id) in (start_index..=last_index).enumerate() { stat[index].id = id.into(); } - + println!("Added {} cards for {:?}", stat.len(), time.elapsed()); } @@ -103,7 +103,7 @@ pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) { } pub fn delete_stat(stat: &CardStatistics, connection: &Connection) { - if stat.id == INVALID_ID { + if !stat.id.is_valid() { return; } connection diff --git a/src/data_provider/import.rs b/src/data_provider/import.rs index 54e360f..0e22cb0 100644 --- a/src/data_provider/import.rs +++ b/src/data_provider/import.rs @@ -1,6 +1,6 @@ +use hashbrown::HashMap; use rusqlite::Connection; use serde_json::Value; -use hashbrown::HashMap; #[derive(Clone)] pub struct ImportData(pub(crate) Vec); diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 5947a96..0d3e649 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,4 +1,4 @@ -use crate::lang::{WordData, WordGroup, INVALID_ID}; +use crate::lang::{INVALID_ID, WordData, WordGroup}; use rusqlite::{Connection, params}; use std::collections::HashMap; @@ -64,7 +64,7 @@ pub fn add_words(words: &mut [WordData], connection: &mut Connection) { } pub fn update_word(word: &mut WordData, connection: &Connection) { - if word.id == INVALID_ID { + if !word.id.is_valid() { add_word(word, connection); } else { connection @@ -86,7 +86,7 @@ pub fn update_word(word: &mut WordData, connection: &Connection) { } pub fn delete_word(word: &WordData, connection: &Connection) { - if word.id == INVALID_ID { + if !word.id.is_valid() { return; } connection @@ -160,7 +160,7 @@ pub fn add_group(group: &mut WordGroup, connection: &Connection) { } pub fn update_group(group: &mut WordGroup, connection: &Connection) { - if group.id == INVALID_ID { + if !group.id.is_valid() { add_group(group, connection); } else { connection @@ -176,7 +176,7 @@ pub fn update_group(group: &mut WordGroup, connection: &Connection) { } pub fn delete_group(group: &WordGroup, connection: &Connection) { - if group.id == INVALID_ID { + if !group.id.is_valid() { return; } connection diff --git a/src/dictionary.rs b/src/dictionary.rs index 30f2c3b..73182dc 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -2,13 +2,14 @@ use crate::data_provider::words::{delete_group, delete_word, update_group, updat use crate::dictionary::DictionaryMessage::*; use crate::dictionary_test::DictionaryQuizState; use crate::import::ImportState; -use crate::lang::{WordData, WordGroup, INVALID_ID}; +use crate::lang::{INVALID_ID, WordData, WordGroup}; use crate::navigation::Page::{Import, Word}; use crate::navigation::{NavigatedPage, Page}; use crate::styling::*; use crate::word::WordState; use crate::{AppState, RootMessage}; use chrono::{DateTime, TimeDelta, Utc}; +use hashbrown::{HashMap, HashSet}; use iced::alignment::Vertical::Center; use iced::widget::button::Style; use iced::widget::button::{danger, text}; @@ -18,7 +19,6 @@ use iced::widget::*; use iced::{Border, Color, Shadow, Task}; use iced_core::Length::Fill; use rand::random_range; -use hashbrown::{HashMap, HashSet}; use std::fs; use std::ops::Add; use std::path::PathBuf; @@ -96,7 +96,7 @@ impl NavigatedPage for DictionaryState { let dict = &state.dictionary; word = dict[*index].clone(); } - if word.id != INVALID_ID { + if word.id.is_valid() { return Some(Word(WordState::new(word, *index, self.state.clone()))); } } @@ -423,7 +423,7 @@ impl DictionaryState { let line_button = || { let action = WordAction(index); - if data.id == INVALID_ID { + if !data.id.is_valid() { return button("-").on_press(action).style(|_x, _status| Style { background: None, text_color: Color::BLACK, diff --git a/src/lang.rs b/src/lang.rs index e0c5efa..182650b 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -28,7 +28,6 @@ const FADE_PER_DAY: f32 = 0.95; #[derive(Clone, PartialEq, Eq, Debug, Hash, Copy)] pub(crate) struct Id(u32); pub const INVALID_ID: Id = Id(0); - impl FromStr for Id { type Err = ParseIntError; @@ -69,6 +68,12 @@ impl ToSql for Id { } } +impl Id { + pub(crate) fn is_valid(self) -> bool { + self.0 != 0 + } +} + #[derive(Clone, Debug)] pub struct KanaSet { name: String, @@ -703,4 +708,4 @@ impl DeckSettings { pub fn require_speech(&self) -> bool { self.forward == "speech" || self.backward == "speech" } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 866ecfa..479f092 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ use crate::lang::{DeckSettings, Id, WordData, WordGroup}; use crate::navigation::{AppSettings, RootMessage, ScreenState}; use crate::quiz::*; use chrono::NaiveDate; +use hashbrown::HashMap; use iced::{Font, window}; use iced::{Subscription, Theme, keyboard}; use iced_core::Size; @@ -34,7 +35,6 @@ use iced_core::window::Position; use iced_core::window::settings::PlatformSpecific; use mimalloc::MiMalloc; use rusqlite::Connection; -use hashbrown::HashMap; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; diff --git a/src/navigation.rs b/src/navigation.rs index bf2c459..1a6c4d1 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -7,6 +7,7 @@ use crate::dictionary::{DictionaryMessage, DictionaryState, app_data_dir}; use crate::dictionary_test::{DictionaryQuizMessage, DictionaryQuizState}; use crate::history::{HistoryMessage, HistoryState}; use crate::import::{ImportMessage, ImportState}; +use crate::lang::Id; use crate::message_navigation; use crate::navigation::Page::*; use crate::navigation::RootMessage::{DataLoaded, Keyboard, UpdateData}; @@ -24,14 +25,13 @@ use crate::word::{WordMessage, WordState}; use crate::writing::{WritingMessage, WritingState}; use crate::{AppState, fill_state}; use chrono::NaiveDate; +use hashbrown::HashMap; use iced::keyboard::Event; use iced::{Element, Task}; use reqwest::Error; -use hashbrown::HashMap; +use rusqlite::Connection; use std::sync::{Arc, Mutex}; use std::time::Instant; -use rusqlite::Connection; -use crate::lang::Id; impl Default for ScreenState { fn default() -> Self { @@ -127,7 +127,7 @@ impl ScreenState { for file in directory.read_dir().unwrap().flatten() { let mut vec = vec![]; let history_file_name = file.file_name().into_string().unwrap(); - let id : Id = history_file_name[4..history_file_name.len() - 12] + let id: Id = history_file_name[4..history_file_name.len() - 12] .parse::() .unwrap(); @@ -172,7 +172,7 @@ impl ScreenState { if let UpdateData = message { println!("Loading data"); let mut state = self.app_state.lock().unwrap(); - if cfg!(windows){ + if cfg!(windows) { state.connection = Connection::open_in_memory().unwrap(); } let path = app_data_dir(); diff --git a/src/repetition.rs b/src/repetition.rs index efb38b4..7d24a08 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -1,5 +1,6 @@ use crate::data_provider::voice::get_voice; -use crate::lang::{DeckData, DeckSettings, CardStatistics, WordData, WordOpenMode}; +use crate::lang::Id; +use crate::lang::{CardStatistics, DeckData, DeckSettings, WordData, WordOpenMode}; use crate::navigation::Page::PreviousPage; use crate::navigation::{KeyPressedPage, NavigatedPage, Page}; use crate::styling::*; @@ -13,7 +14,6 @@ use iced::{Element, Fill, Task, Theme, alignment, keyboard}; use rodio::MixerDeviceSink; use std::collections::HashSet; use std::sync::{Arc, Mutex}; -use crate::lang::Id; use tokio::task::spawn_blocking; pub struct RepetitionState { diff --git a/src/repetitions.rs b/src/repetitions.rs index 2d5e052..d86126d 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,7 +1,7 @@ use crate::data_provider::card_sets::update_deck; use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck}; use crate::history::HistoryState; -use crate::lang::{AppendMode, CardStatistics, DeckSettings, INVALID_ID, Id, OrderMode}; +use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page}; use crate::repetition::RepetitionState; @@ -115,7 +115,7 @@ impl NavigatedPage for RepetitionsState { let index = self.selected_deck_index.unwrap(); self.clear_selection(); let deck = self.decks.remove(index); - if deck.id != INVALID_ID { + if deck.id.is_valid() { let mut state = self.state.lock().unwrap(); state.decks.remove(index); self.decks = self @@ -292,7 +292,7 @@ impl RepetitionsState { impl RepetitionsState { fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { - if deck.id != INVALID_ID { + if deck.id.is_valid() { if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.as_ref().unwrap().is_empty() { @@ -325,7 +325,7 @@ impl RepetitionsState { ] .spacing(QUARTER_SPACING), { - if deck.id == INVALID_ID { + if !deck.id.is_valid() { column![ column![ text!("Передняя сторона"), @@ -548,8 +548,7 @@ impl RepetitionsState { } else { None }, - text_color: - _x.palette().primary, + text_color: _x.palette().primary, border: Border { color: Default::default(), width: 0.0, From 4187344a2948f359106cb5753bfb5bba85aa47a3 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 17:45:50 +0300 Subject: [PATCH 08/30] Filter check on edit page --- benches/split_bench.rs | 3 ++- src/data_provider/card_sets.rs | 2 +- src/data_provider/card_stats.rs | 2 +- src/data_provider/words.rs | 4 ++-- src/dictionary.rs | 2 +- src/lang.rs | 6 ++++-- src/main.rs | 4 ++-- src/repetition_settings.rs | 16 ++++++++++++++-- 8 files changed, 27 insertions(+), 12 deletions(-) diff --git a/benches/split_bench.rs b/benches/split_bench.rs index e15b4e7..3ecfd90 100644 --- a/benches/split_bench.rs +++ b/benches/split_bench.rs @@ -1,4 +1,5 @@ -use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use std::hint::black_box; +use criterion::{Criterion, criterion_group, criterion_main}; fn bench_split(c: &mut Criterion) { let input = "data_1"; diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 00f75fe..1e1e883 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,4 +1,4 @@ -use crate::lang::{DeckSettings, INVALID_ID, OrderMode}; +use crate::lang::{DeckSettings, OrderMode}; use rusqlite::Connection; diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 888f030..068612e 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,4 +1,4 @@ -use crate::lang::{CardStatistics, DeckSettings, INVALID_ID}; +use crate::lang::{CardStatistics, DeckSettings}; use rusqlite::Connection; use std::time::Instant; diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 0d3e649..e20eff5 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,5 +1,5 @@ -use crate::lang::{INVALID_ID, WordData, WordGroup}; -use rusqlite::{Connection, params}; +use crate::lang::{WordData, WordGroup}; +use rusqlite::{params, Connection}; use std::collections::HashMap; pub fn add_word(word: &mut WordData, connection: &Connection) { diff --git a/src/dictionary.rs b/src/dictionary.rs index 73182dc..e60318c 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -2,7 +2,7 @@ use crate::data_provider::words::{delete_group, delete_word, update_group, updat use crate::dictionary::DictionaryMessage::*; use crate::dictionary_test::DictionaryQuizState; use crate::import::ImportState; -use crate::lang::{INVALID_ID, WordData, WordGroup}; +use crate::lang::{WordData, WordGroup}; use crate::navigation::Page::{Import, Word}; use crate::navigation::{NavigatedPage, Page}; use crate::styling::*; diff --git a/src/lang.rs b/src/lang.rs index 182650b..ee2f99d 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -26,7 +26,7 @@ const MAX_HISTORY_LEN_PART: f32 = 0.33; const MAX_SCORE: u8 = 25; const FADE_PER_DAY: f32 = 0.95; #[derive(Clone, PartialEq, Eq, Debug, Hash, Copy)] -pub(crate) struct Id(u32); +pub struct Id(u32); pub const INVALID_ID: Id = Id(0); impl FromStr for Id { type Err = ParseIntError; @@ -650,9 +650,11 @@ impl DeckSettings { open_mode: OrderMode::Default, } } - pub(crate) fn check_filter(&self) -> bool { + pub fn check_filter(&self) -> bool { + let time = Instant::now(); let engine = Engine::new(); let ast = engine.compile(&self.filter); + println!("Compile time is {:?}", time.elapsed()); ast.is_ok() } pub fn get_word_list(&self, state: &AppState) -> Vec { diff --git a/src/main.rs b/src/main.rs index 479f092..297eafd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,10 +32,10 @@ use iced::{Font, window}; use iced::{Subscription, Theme, keyboard}; use iced_core::Size; use iced_core::window::Position; -use iced_core::window::settings::PlatformSpecific; use mimalloc::MiMalloc; use rusqlite::Connection; - +#[cfg(target_os = "linux")] +use iced_core::window::settings::PlatformSpecific; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index 65f6c6b..366a04f 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -9,15 +9,17 @@ use iced::widget::button::danger; use iced::widget::{button, column, row, scrollable, space, text, text_input}; use iced::{Element, Task}; use iced_core::Length::Fill; -use iced_core::Padding; +use iced_core::{color, Padding, Theme}; use std::sync::{Arc, Mutex}; use std::time::Duration; +use iced::widget::text_input::Catalog; pub struct RepetitionSettingsState { set: DeckSettings, index: usize, state: Arc>, real_delete: bool, + correct_filter: bool, } impl RepetitionSettingsState { @@ -27,11 +29,14 @@ impl RepetitionSettingsState { let local_state = p1.lock().unwrap(); set = local_state.decks[index].clone(); } + let filter_state = set.check_filter(); + RepetitionSettingsState { index, set, state: p1, real_delete: false, + correct_filter: filter_state, } } } @@ -65,6 +70,7 @@ impl NavigatedPage for RepetitionSettingsState { } SetFilter(new) => { self.set.filter = new; + self.correct_filter = self.set.check_filter(); } TryFilter => { let state = self.state.lock().unwrap(); @@ -111,7 +117,13 @@ impl NavigatedPage for RepetitionSettingsState { .spacing(QUARTER_SPACING), column![ text!("Фильтр"), - text_input("", &self.set.filter).on_input(SetFilter), + text_input("", &self.set.filter).style(|x, status| { + let mut primary = Theme::default()(x, status); + if !self.correct_filter { + primary.border.color = color!(255, 255, 0); + } + primary + }).on_input(SetFilter), button("Проверить фильтр") .style(jl_button) .on_press(TryFilter), From a4d77196d0c0ecc52d0434bf0fe0d955628f57e2 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 18 Aug 2026 17:47:37 +0300 Subject: [PATCH 09/30] Small code edit --- src/data_provider/history.rs | 2 +- src/lang.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index c85e04e..401bf03 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -32,7 +32,7 @@ fn parse_history_items(strings: Vec) -> Vec { if let [time, word, mode, before, after] = string.split(';').collect::>()[..] { items.push(HistoryItem { timestamp: DateTime::from_timestamp(time.parse().unwrap(), 0).unwrap(), - word_id: word.parse::().unwrap().into(), + word_id: word.parse::().unwrap(), mode: match mode.parse::().unwrap() { 2 => WordOpenMode::Hard, 3 => WordOpenMode::Ok, diff --git a/src/lang.rs b/src/lang.rs index ee2f99d..e0de7c1 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -36,9 +36,9 @@ impl FromStr for Id { } } -impl Into for u32 { - fn into(self) -> Id { - Id(self) +impl From for Id { + fn from(value: u32) -> Self { + Id(value) } } From 98c80939611a638741859605616302b05ea23b4f Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 19 Aug 2026 17:33:22 +0300 Subject: [PATCH 10/30] Fixing some code problems --- benches/split_bench.rs | 43 -------------------- src/data_provider/card_stats.rs | 70 +++++++++++---------------------- src/data_provider/words.rs | 8 +--- src/helpers.rs | 1 - src/lang.rs | 2 +- src/main.rs | 1 - src/quiz.rs | 2 +- src/repetitions.rs | 12 +++--- src/writing.rs | 2 +- 9 files changed, 32 insertions(+), 109 deletions(-) delete mode 100644 benches/split_bench.rs delete mode 100644 src/helpers.rs diff --git a/benches/split_bench.rs b/benches/split_bench.rs deleted file mode 100644 index 3ecfd90..0000000 --- a/benches/split_bench.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::hint::black_box; -use criterion::{Criterion, criterion_group, criterion_main}; - -fn bench_split(c: &mut Criterion) { - let input = "data_1"; - - c.bench_function("split_with_comma", |b| { - b.iter(|| { - // black_box запрещает компилятору оптимизировать результат - black_box(split_with_coma(black_box(input))) - }) - }); -} - -fn bench_split_new(c: &mut Criterion) { - let input = "data_1"; - - c.bench_function("split_with_comma gpt", |b| { - b.iter(|| { - // black_box запрещает компилятору оптимизировать результат - black_box(split_with_coma(black_box(input))) - }) - }); -} - -pub fn split_with_coma(ts: &str) -> Vec { - ts.split(',') - .map(|ts| ts.to_lowercase().trim().to_string()) - .filter(|t| !t.is_empty()) - .collect::>() -} - -pub fn new_split_with_coma(ts: &str) -> Vec { - ts.split(',') - .map(|s| s.trim()) // 0 аллокаций, просто срез - .filter(|s| !s.is_empty()) // отбрасываем пустые до аллокации - .map(|s| s.to_lowercase()) // 1 аллокация на валидный токен - .collect() -} - -// Можно добавить несколько бенчмарков в группу -criterion_group!(benches, bench_split, bench_split_new); -criterion_main!(benches); diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 068612e..abe4965 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,5 +1,5 @@ use crate::lang::{CardStatistics, DeckSettings}; -use rusqlite::Connection; +use rusqlite::{params, Connection}; use std::time::Instant; pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec { @@ -26,67 +26,41 @@ pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec>() - .join(", "); - let query = format!( - "INSERT INTO card_stats (word_id, set_id, score, last_opened) VALUES {}", - inserting - ); - let count = connection.execute(query.as_str(), ()); - - if count.is_err() { - return; + ]) + .unwrap(); + } } - let last_index: u32 = connection - .query_one( - "SELECT seq from sqlite_sequence WHERE name == ?1", - ("card_stats".to_string(),), - |row| row.get(0), - ) - .unwrap(); + let last_index: u32 = tx.last_insert_rowid() as u32; + tx.commit().unwrap(); - let start_index = last_index - (stat.len() as u32) + 1; + let start_index = last_index - (count as u32) + 1; for (index, id) in (start_index..=last_index).enumerate() { - stat[index].id = id.into(); + stats[index].id = id.into(); } - println!("Added {} cards for {:?}", stat.len(), time.elapsed()); + println!("Added {} cards for {:?}", stats.len(), time.elapsed()); } -// pub fn add_stat(stat: &mut CardStatistics, connection: &Connection) { -// let time = Instant::now(); -// let index = connection -// .query_row( -// "INSERT INTO card_stats (word_id, set_id, score, last_opened) VALUES (?1, ?2, ?3, ?4) RETURNING id", -// ( -// &stat.word_id, -// &stat.set_id, -// &stat.score, -// &stat.last_open.timestamp(), -// ), -// |row| row.get(0) -// ) -// .unwrap_or_else(|e| {println!("{}", e); 0}); -// -// stat.id = index; -// println!("Added stat: {}", time.elapsed().as_millis()); -// } - pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) { let time = Instant::now(); diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index e20eff5..bb52107 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -46,15 +46,9 @@ pub fn add_words(words: &mut [WordData], connection: &mut Connection) { } } + let last_index: u32 = tx.last_insert_rowid() as u32; tx.commit().unwrap(); - let last_index: u32 = connection - .query_one( - "SELECT seq from sqlite_sequence WHERE name == ?1", - ("words".to_string(),), - |row| row.get(0), - ) - .unwrap(); let start_index = last_index - (count as u32) + 1; diff --git a/src/helpers.rs b/src/helpers.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/helpers.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/lang.rs b/src/lang.rs index e0de7c1..9d33c7f 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -452,7 +452,7 @@ impl DeckData { HistoryItem { timestamp: Utc::now(), word_id: word.word_id.into(), - mode: WordOpenMode::Easy, + mode: status, before: old_score, after: new_score, }, diff --git a/src/main.rs b/src/main.rs index 297eafd..67bc533 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ mod data_provider; mod dictionary; mod dictionary_test; -pub mod helpers; mod history; pub mod import; mod lang; diff --git a/src/quiz.rs b/src/quiz.rs index 423a629..70af76c 100644 --- a/src/quiz.rs +++ b/src/quiz.rs @@ -54,7 +54,7 @@ impl NavigatedPage for QuizState { self.update_showed() } } - Back => todo!(), + Back => {}, } Task::none() } diff --git a/src/repetitions.rs b/src/repetitions.rs index d86126d..0e0c529 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -47,7 +47,7 @@ impl NavigatedPage for RepetitionsState { let selected_set = self.selected_deck_mut().unwrap(); if selected_set.append_mode == AppendMode::Full { - Self::append_all_words(&clone.lock().unwrap(), selected_set); + Self::append_all_words(&mut clone.lock().unwrap(), selected_set); selected_set.existing_words_indices = selected_set.available_words_indices.clone(); } else { if selected_set @@ -220,8 +220,8 @@ impl NavigatedPage for RepetitionsState { } let deck = self.selected_deck().unwrap(); - let state = self.state.lock().unwrap(); - Self::append_words(&state, &deck.general_settings, adding.into_iter()); + let mut state = self.state.lock().unwrap(); + Self::append_words(&mut state, &deck.general_settings, adding.into_iter()); } } Task::none() @@ -565,7 +565,7 @@ impl RepetitionsState { } impl RepetitionsState { - fn append_all_words(state: &AppState, deck: &DeckViewData) { + fn append_all_words(state: &mut AppState, deck: &DeckViewData) { let existing = deck.existing_words_indices.as_ref().unwrap(); let mut created_set = HashSet::with_capacity(existing.len()); existing.iter().for_each(|c| { @@ -580,7 +580,7 @@ impl RepetitionsState { .cloned(); Self::append_words(state, deck, required); } - fn append_words(state: &AppState, deck: &DeckSettings, indices: impl Iterator) { + fn append_words(state: &mut AppState, deck: &DeckSettings, indices: impl Iterator) { let words = &state.dictionary; let stats = &mut indices .map(|i| { @@ -596,7 +596,7 @@ impl RepetitionsState { .collect::>(); if !stats.is_empty() { - add_stat_list(stats, &state.connection); + add_stat_list(stats, &mut state.connection); } } diff --git a/src/writing.rs b/src/writing.rs index d5a4228..4f3a418 100644 --- a/src/writing.rs +++ b/src/writing.rs @@ -30,7 +30,7 @@ impl NavigatedPage for WritingState { fn navigated(&mut self) {} fn update(&mut self, message: WritingMessage) -> Task { match message { - WritingMessage::Back => todo!(), + WritingMessage::Back => {}, WritingMessage::Next => self.next(), WritingMessage::SwitchShowMode(b) => self.show_all = b, } From bb673ed7c7c427a8f105f13d2ce0e2168d4f24aa Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 19 Aug 2026 17:34:48 +0300 Subject: [PATCH 11/30] Remove debug from repetitions.rs --- src/repetitions.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/repetitions.rs b/src/repetitions.rs index 0e0c529..755acf4 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -417,10 +417,7 @@ impl RepetitionsState { for (date, activity) in history { let distance = now - *date; let index = counts.len() as i64 - distance.num_days() - 1; - - if index == 210 { - println!("invalid index") - } + if index < 0 { continue; } From 746071125a5889ea5b27b09c506803d9fe579d48 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 19 Aug 2026 17:39:52 +0300 Subject: [PATCH 12/30] Remove unnecessary into() --- src/lang.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index 9d33c7f..836ac16 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -451,7 +451,7 @@ impl DeckData { self.settings.id, HistoryItem { timestamp: Utc::now(), - word_id: word.word_id.into(), + word_id: word.word_id, mode: status, before: old_score, after: new_score, From e7023752f17289f71f38f27e3ef1655e27ce14f4 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 19 Aug 2026 18:00:03 +0300 Subject: [PATCH 13/30] Rewrite for dictionary indexer --- src/dictionary.rs | 15 ++++++--------- src/import.rs | 5 ++--- src/lang.rs | 2 +- src/repetitions.rs | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/dictionary.rs b/src/dictionary.rs index e60318c..00febfd 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -150,8 +150,7 @@ impl NavigatedPage for DictionaryState { v = v[..v.len() - 2].to_string() } - while v.contains(",,") { - let index = v.find(",,").unwrap(); + while let Some(index) = v.find(",,") { v.remove(index); } @@ -211,14 +210,12 @@ impl NavigatedPage for DictionaryState { SaveGroup => { let state = &mut self.state.lock().unwrap(); let connection = &state.connection; - let group = &mut state - .word_groups - .get(self.selected_group_index) - .unwrap() + let mut group = state + .word_groups[self.selected_group_index] .clone(); - update_group(group, connection); - state.word_groups[self.selected_group_index] = group.clone(); + update_group(&mut group, connection); + state.word_groups[self.selected_group_index] = group; } SelectGroup(i) => { self.selected_group_index = i; @@ -316,7 +313,7 @@ impl DictionaryState { fn save_word(&mut self, i: usize) { let state = &mut self.state.lock().unwrap(); let connection = &state.connection; - let word = &mut state.dictionary.get(i).unwrap().clone(); + let word = &mut state.dictionary[i].clone(); update_word(word, connection); state.dictionary[i] = word.clone(); diff --git a/src/import.rs b/src/import.rs index e3cff93..78d051f 100644 --- a/src/import.rs +++ b/src/import.rs @@ -517,8 +517,7 @@ impl ImportState { .iter() .map(|name| (name.clone(), Vec::::with_capacity(1))) .collect::>(); - for key in group.mapping.keys() { - let endpoint = group.mapping.get(key).unwrap(); + for (key, endpoint) in &group.mapping { let property_index = group.fields.iter().position(|f| f == key).unwrap(); let group_index = result .iter() @@ -538,7 +537,7 @@ impl ImportState { ) -> String { let mut working_words = Vec::with_capacity(indices.len()); for index in indices { - let str = properties.get(*index).unwrap(); + let str = &properties[*index]; if skip_empty && str.is_empty() { continue; } diff --git a/src/lang.rs b/src/lang.rs index 836ac16..473b3cd 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -360,7 +360,7 @@ impl DeckData { let last_list: Vec<_> = settings .get_word_list(&state_locked) .iter() - .map(|w| state_locked.dictionary.get(*w).unwrap()) + .map(|w| &state_locked.dictionary[*w]) .cloned() .collect(); let word_ids = last_list.iter().map(|l| l.id).collect::>(); diff --git a/src/repetitions.rs b/src/repetitions.rs index 755acf4..6c0d639 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -581,7 +581,7 @@ impl RepetitionsState { let words = &state.dictionary; let stats = &mut indices .map(|i| { - let word = words.get(i).unwrap(); + let word = &words[i]; CardStatistics { id: 0.into(), word_id: word.id, From 39f88451191d7bb4ebec49464eeee8759fb6fb52 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 19 Aug 2026 18:49:32 +0300 Subject: [PATCH 14/30] Rewrite for dictionary rw indexer --- src/dictionary.rs | 2 +- src/import.rs | 20 +++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/dictionary.rs b/src/dictionary.rs index 00febfd..fdfddae 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -154,7 +154,7 @@ impl NavigatedPage for DictionaryState { v.remove(index); } - dict.get_mut(i).unwrap().tags = v; + dict[i].tags = v; } self.update_tags(); diff --git a/src/import.rs b/src/import.rs index 78d051f..958801b 100644 --- a/src/import.rs +++ b/src/import.rs @@ -1,15 +1,15 @@ -use crate::AppState; -use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups}; +use crate::data_provider::import::{get_words_of_group, load_groups, ImportData, ImportGroup}; use crate::data_provider::words::{add_group, add_words}; use crate::dictionary::app_data_dir; use crate::import::ImportMessage::*; use crate::lang::{WordData, WordGroup}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; +use crate::AppState; use iced::widget::button::danger; use iced::widget::container::success; use iced::widget::{ - Row, button, checkbox, column, container, progress_bar, row, rule, text, text_input, + button, checkbox, column, container, progress_bar, row, rule, text, text_input, Row, }; use iced::widget::{scrollable, space}; use iced::{Element, Task}; @@ -130,13 +130,7 @@ impl NavigatedPage for ImportState { } ImportFinished => { self.progress = None; - let group = self - .import_data - .as_mut() - .unwrap() - .0 - .get_mut(self.selected_index) - .unwrap(); + let group = &mut self.import_data.as_mut().unwrap().0[self.selected_index]; group.imported = true; } } @@ -409,7 +403,7 @@ impl ImportState { } fn setup_mapping(&mut self, property_name: String) { let group = self.import_data.as_mut().unwrap(); - let group = group.0.get_mut(self.selected_index).unwrap(); + let group = &mut group.0[self.selected_index]; group .mapping .insert(self.selected_property.clone().unwrap(), property_name); @@ -418,7 +412,7 @@ impl ImportState { } fn remove_mapping(&mut self, property_name: String) { let group = self.import_data.as_mut().unwrap(); - let group = group.0.get_mut(self.selected_index).unwrap(); + let group = &mut group.0[self.selected_index]; let remove_key = group .mapping .keys() @@ -523,7 +517,7 @@ impl ImportState { .iter() .position(|(name, _)| name == endpoint) .unwrap(); - result.get_mut(group_index).unwrap().1.push(property_index); + result[group_index].1.push(property_index); } result From 01f0999772f403514e2657998c2922a025f09282 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 19 Aug 2026 19:21:21 +0300 Subject: [PATCH 15/30] Remove many unwrap --- src/dictionary.rs | 2 +- src/dictionary_test.rs | 14 ++++++++++---- src/import.rs | 2 +- src/lang.rs | 28 +++++++++++++++++----------- src/navigation.rs | 15 +++++++++++---- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/dictionary.rs b/src/dictionary.rs index fdfddae..52760dd 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -611,7 +611,7 @@ pub fn app_data_dir() -> PathBuf { let mut dir = dirs::data_dir().unwrap(); dir.push("jap_learn"); if !dir.exists() { - fs::create_dir(dir.clone()).unwrap(); + fs::create_dir_all(dir.clone()).unwrap(); } dir diff --git a/src/dictionary_test.rs b/src/dictionary_test.rs index a56e4b4..812924d 100644 --- a/src/dictionary_test.rs +++ b/src/dictionary_test.rs @@ -171,11 +171,17 @@ impl DictionaryQuizState { self.is_help = false; self.answer = String::new(); - if self.current_set.is_empty() { - self.update_set(); - } + let next : WordData = + match self.current_set.pop() { + Some(word) => { + word + } + None => { + self.update_set(); + self.current_set.pop().expect("Empty set") + } + }; - let next = self.current_set.pop().unwrap(); if self.reverse { self.view = next.value.clone(); self.correct = next.key.clone(); diff --git a/src/import.rs b/src/import.rs index 958801b..71b8494 100644 --- a/src/import.rs +++ b/src/import.rs @@ -512,7 +512,7 @@ impl ImportState { .map(|name| (name.clone(), Vec::::with_capacity(1))) .collect::>(); for (key, endpoint) in &group.mapping { - let property_index = group.fields.iter().position(|f| f == key).unwrap(); + let property_index = group.fields.iter().position(|f| f == key).expect("Unknown property"); let group_index = result .iter() .position(|(name, _)| name == endpoint) diff --git a/src/lang.rs b/src/lang.rs index 473b3cd..822c89d 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -500,12 +500,14 @@ impl RandomSRSModule { } impl SRSModule for RandomSRSModule { fn next(&mut self, set: &mut DeckData) -> usize { - if self.basket.is_empty() { - self.basket = (0..set.words.len()).collect::>(); - self.basket.shuffle(&mut rand::rng()) + match self.basket.pop() { + Some(index) => index, + None => { + self.basket = (0..set.words.len()).collect::>(); + self.basket.shuffle(&mut rand::rng()); + self.basket.pop().expect("empty basket") + } } - - self.basket.pop().unwrap() } fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {} @@ -552,7 +554,7 @@ impl SRSModule for SemiRandomSRSModule { let new_weight = (100.0 / word.calculated_score()).powf(2.0); self.last_weights .update_weights(&[(index, &new_weight)]) - .unwrap(); + .expect("update weights error"); } fn init(&mut self, set: &mut DeckData) { self.initialized = true; @@ -585,12 +587,16 @@ impl SRSModule for WorstWordsSRSModule { self.rounds_remaining = self.rounds_count; } - if self.queue.is_empty() { - self.queue.append(&mut self.pool.clone()); - self.rounds_remaining -= 1; + match self.queue.pop() { + Some(index) => { + index + } + None => { + self.queue.append(&mut self.pool.clone()); + self.rounds_remaining -= 1; + self.queue.pop().expect("empty queue") + } } - - self.queue.pop().unwrap() } fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {} diff --git a/src/navigation.rs b/src/navigation.rs index 1a6c4d1..3cb483f 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -3,7 +3,7 @@ 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::dictionary::{DictionaryMessage, DictionaryState, app_data_dir}; +use crate::dictionary::{app_data_dir, DictionaryMessage, DictionaryState}; use crate::dictionary_test::{DictionaryQuizMessage, DictionaryQuizState}; use crate::history::{HistoryMessage, HistoryState}; use crate::import::{ImportMessage, ImportState}; @@ -23,7 +23,7 @@ use crate::sync::{SyncMessage, SyncState}; use crate::view_navigation; use crate::word::{WordMessage, WordState}; use crate::writing::{WritingMessage, WritingState}; -use crate::{AppState, fill_state}; +use crate::{fill_state, AppState}; use chrono::NaiveDate; use hashbrown::HashMap; use iced::keyboard::Event; @@ -126,7 +126,11 @@ impl ScreenState { let mut map = HashMap::new(); for file in directory.read_dir().unwrap().flatten() { let mut vec = vec![]; - let history_file_name = file.file_name().into_string().unwrap(); + let history_file_name = match file.file_name().into_string() { + Err(_) => continue, + Ok(v) => v, + }; + let id: Id = history_file_name[4..history_file_name.len() - 12] .parse::() .unwrap(); @@ -303,7 +307,10 @@ macro_rules! message_navigation { ($msg:expr, $stack:expr, $state:expr) => { if let Some(new_page) = $state.navigate(&$msg) { if let Page::PreviousPage = new_page { - $stack.pop(); + if $stack.len() > 1 { + $stack.pop(); + } + return (Task::::none(), true); } else { $stack.push(new_page); From de2dbaf6c130d81a3f02b7b64ccfb9b03dc59574 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 19 Aug 2026 23:37:59 +0300 Subject: [PATCH 16/30] Remove silent unwrap in data providers --- src/data_provider/card_sets.rs | 11 ++++------- src/data_provider/card_stats.rs | 11 ++--------- src/data_provider/history.rs | 18 ++++++++++++----- src/data_provider/settings.rs | 15 +++----------- src/data_provider/words.rs | 35 ++++++++------------------------- src/dictionary.rs | 21 ++++++++++++++++---- 6 files changed, 47 insertions(+), 64 deletions(-) diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 1e1e883..7e223be 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -30,7 +30,7 @@ pub fn load_sets(connection: &Connection) -> Vec { } pub fn add_set(set: &mut DeckSettings, connection: &Connection) { - let index = connection + let index : u32 = connection .query_row( "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", ( @@ -41,7 +41,7 @@ pub fn add_set(set: &mut DeckSettings, connection: &Connection) { ), |row| row.get(0) ) - .unwrap_or_else(|e| {println!("{}", e); 0}); + .unwrap(); set.id = index.into(); } @@ -61,7 +61,7 @@ pub fn update_deck(set: &mut DeckSettings, connection: &Connection) { &set.id ), ) - .unwrap_or_else(|e| {println!("{}", e); 0}); + .unwrap(); } } @@ -71,8 +71,5 @@ pub fn delete_set(set: &DeckSettings, connection: &Connection) { } connection .execute("DELETE FROM card_set WHERE id = ?1", (&set.id,)) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index abe4965..2c22c46 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -69,10 +69,7 @@ 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_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); println!("Updated stat: {}", time.elapsed().as_millis()); } @@ -81,9 +78,5 @@ pub fn delete_stat(stat: &CardStatistics, connection: &Connection) { return; } connection - .execute("DELETE FROM card_stats WHERE id = ?1", (&stat.id,)) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .execute("DELETE FROM card_stats WHERE id = ?1", (&stat.id,)).unwrap(); } diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 401bf03..693cebd 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -30,17 +30,25 @@ fn parse_history_items(strings: Vec) -> Vec { let mut items = Vec::with_capacity(strings.len()); for string in strings { if let [time, word, mode, before, after] = string.split(';').collect::>()[..] { + let id = word.parse::(); + if id.is_err(){ + continue; + } + let id = id.unwrap(); + if !id.is_valid() { + continue; + } items.push(HistoryItem { - timestamp: DateTime::from_timestamp(time.parse().unwrap(), 0).unwrap(), - word_id: word.parse::().unwrap(), - mode: match mode.parse::().unwrap() { + timestamp: DateTime::from_timestamp(time.parse().unwrap_or_default(), 0).unwrap(), + word_id: id, + mode: match mode.parse::().unwrap_or_default() { 2 => WordOpenMode::Hard, 3 => WordOpenMode::Ok, 4 => WordOpenMode::Easy, _ => WordOpenMode::None, }, - before: before.parse().unwrap(), - after: after.parse().unwrap(), + before: before.parse().unwrap_or_default(), + after: after.parse().unwrap_or_default(), }); } } diff --git a/src/data_provider/settings.rs b/src/data_provider/settings.rs index 619ce80..dff727f 100644 --- a/src/data_provider/settings.rs +++ b/src/data_provider/settings.rs @@ -27,10 +27,7 @@ pub fn set_setting(key: String, value: String, connection: &Connection) { pub fn delete_settings(key: String, connection: &Connection) { connection .execute("DELETE FROM settings WHERE id = ?1", (&key,)) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } fn create_settings(key: String, value: String, connection: &Connection) { @@ -39,10 +36,7 @@ fn create_settings(key: String, value: String, connection: &Connection) { "INSERT into settings (id, value) VALUES (?1, ?2)", (key, value), ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } fn update_settings(key: String, value: String, connection: &Connection) { @@ -53,10 +47,7 @@ set value = ?2 where id = ?1", (key, value), ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } fn get_settings_list(connection: &Connection) -> Vec { diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index bb52107..c713c13 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -3,7 +3,7 @@ use rusqlite::{params, Connection}; use std::collections::HashMap; pub fn add_word(word: &mut WordData, connection: &Connection) { - let index = connection + let index : u32 = connection .query_row( "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id", ( @@ -14,11 +14,7 @@ pub fn add_word(word: &mut WordData, connection: &Connection) { &word.group_id, ), |row| row.get(0), - ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + ).unwrap(); word.id = index.into(); } @@ -72,10 +68,7 @@ pub fn update_word(word: &mut WordData, connection: &Connection) { &word.id, ), ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } } @@ -85,10 +78,7 @@ pub fn delete_word(word: &WordData, connection: &Connection) { } connection .execute("DELETE FROM words WHERE id = ?1", (&word.id,)) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } pub fn load_words(connection: &Connection) -> Vec { @@ -139,16 +129,13 @@ pub fn load_word_groups(connection: &Connection) -> Vec { } pub fn add_group(group: &mut WordGroup, connection: &Connection) { - let index = connection + let index : u32 = connection .query_row( "INSERT INTO word_group (name) VALUES (?1) RETURNING id", (&group.name,), |row| row.get(0), ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); group.id = index.into(); } @@ -162,10 +149,7 @@ pub fn update_group(group: &mut WordGroup, connection: &Connection) { "UPDATE word_group SET name = ?1 WHERE id = ?2", (&group.name, &group.id), ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } } @@ -175,8 +159,5 @@ pub fn delete_group(group: &WordGroup, connection: &Connection) { } connection .execute("DELETE FROM word_group WHERE id = ?1", (&group.id,)) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } diff --git a/src/dictionary.rs b/src/dictionary.rs index 52760dd..856a59a 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -210,9 +210,7 @@ impl NavigatedPage for DictionaryState { SaveGroup => { let state = &mut self.state.lock().unwrap(); let connection = &state.connection; - let mut group = state - .word_groups[self.selected_group_index] - .clone(); + let mut group = state.word_groups[self.selected_group_index].clone(); update_group(&mut group, connection); state.word_groups[self.selected_group_index] = group; @@ -274,7 +272,7 @@ impl NavigatedPage for DictionaryState { self.groups_panel(), row![horizontal().width(8), self.words_list(),], row![ - button("Добавить слово").style(jl_button).on_press(NewWord), + self.add_word_button(), horizontal().width(Fill), button("Импорт").style(text).on_press(ToImport), ] @@ -598,6 +596,21 @@ impl DictionaryState { space().into() } } + + fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { + let state = self.state.lock().unwrap(); + let group_id = state.word_groups[self.selected_group_index].id; + let button = button("Добавить слово") + .style(jl_button); + + if group_id.is_valid() { + return + button.on_press(NewWord) + .into(); + } + + button.into() + } } pub fn split_with_coma(ts: &str) -> Vec { From bf805b79e5662e8e5ab00f5b85d76c4f7986141e Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Wed, 19 Aug 2026 23:51:41 +0300 Subject: [PATCH 17/30] Formating with fmt --- src/data_provider/card_stats.rs | 7 ++++--- src/data_provider/history.rs | 2 +- src/data_provider/words.rs | 5 ++--- src/dictionary.rs | 7 ++----- src/dictionary_test.rs | 9 +++------ src/import.rs | 12 ++++++++---- src/lang.rs | 4 +--- src/main.rs | 4 ++-- src/navigation.rs | 4 ++-- src/quiz.rs | 2 +- src/repetition_settings.rs | 20 +++++++++++--------- src/repetitions.rs | 8 ++++++-- src/writing.rs | 2 +- 13 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 2c22c46..144b9b4 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,5 +1,5 @@ use crate::lang::{CardStatistics, DeckSettings}; -use rusqlite::{params, Connection}; +use rusqlite::{Connection, params}; use std::time::Instant; pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec { @@ -45,7 +45,7 @@ pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) stat.score, stat.last_open.timestamp() ]) - .unwrap(); + .unwrap(); } } @@ -78,5 +78,6 @@ pub fn delete_stat(stat: &CardStatistics, connection: &Connection) { return; } connection - .execute("DELETE FROM card_stats WHERE id = ?1", (&stat.id,)).unwrap(); + .execute("DELETE FROM card_stats WHERE id = ?1", (&stat.id,)) + .unwrap(); } diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 693cebd..d05f485 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -31,7 +31,7 @@ fn parse_history_items(strings: Vec) -> Vec { for string in strings { if let [time, word, mode, before, after] = string.split(';').collect::>()[..] { let id = word.parse::(); - if id.is_err(){ + if id.is_err() { continue; } let id = id.unwrap(); diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index c713c13..a02ea89 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,5 +1,5 @@ use crate::lang::{WordData, WordGroup}; -use rusqlite::{params, Connection}; +use rusqlite::{Connection, params}; use std::collections::HashMap; pub fn add_word(word: &mut WordData, connection: &Connection) { @@ -45,7 +45,6 @@ pub fn add_words(words: &mut [WordData], connection: &mut Connection) { let last_index: u32 = tx.last_insert_rowid() as u32; tx.commit().unwrap(); - let start_index = last_index - (count as u32) + 1; for (index, id) in (start_index..=last_index).enumerate() { @@ -129,7 +128,7 @@ pub fn load_word_groups(connection: &Connection) -> Vec { } pub fn add_group(group: &mut WordGroup, connection: &Connection) { - let index : u32 = connection + let index: u32 = connection .query_row( "INSERT INTO word_group (name) VALUES (?1) RETURNING id", (&group.name,), diff --git a/src/dictionary.rs b/src/dictionary.rs index 856a59a..3c3c5a3 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -600,13 +600,10 @@ impl DictionaryState { fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { let state = self.state.lock().unwrap(); let group_id = state.word_groups[self.selected_group_index].id; - let button = button("Добавить слово") - .style(jl_button); + let button = button("Добавить слово").style(jl_button); if group_id.is_valid() { - return - button.on_press(NewWord) - .into(); + return button.on_press(NewWord).into(); } button.into() diff --git a/src/dictionary_test.rs b/src/dictionary_test.rs index 812924d..cf508fc 100644 --- a/src/dictionary_test.rs +++ b/src/dictionary_test.rs @@ -171,14 +171,11 @@ impl DictionaryQuizState { self.is_help = false; self.answer = String::new(); - let next : WordData = - match self.current_set.pop() { - Some(word) => { - word - } + let next: WordData = match self.current_set.pop() { + Some(word) => word, None => { self.update_set(); - self.current_set.pop().expect("Empty set") + self.current_set.pop().expect("Empty set") } }; diff --git a/src/import.rs b/src/import.rs index 71b8494..3ead526 100644 --- a/src/import.rs +++ b/src/import.rs @@ -1,15 +1,15 @@ -use crate::data_provider::import::{get_words_of_group, load_groups, ImportData, ImportGroup}; +use crate::AppState; +use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups}; use crate::data_provider::words::{add_group, add_words}; use crate::dictionary::app_data_dir; use crate::import::ImportMessage::*; use crate::lang::{WordData, WordGroup}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; -use crate::AppState; use iced::widget::button::danger; use iced::widget::container::success; use iced::widget::{ - button, checkbox, column, container, progress_bar, row, rule, text, text_input, Row, + Row, button, checkbox, column, container, progress_bar, row, rule, text, text_input, }; use iced::widget::{scrollable, space}; use iced::{Element, Task}; @@ -512,7 +512,11 @@ impl ImportState { .map(|name| (name.clone(), Vec::::with_capacity(1))) .collect::>(); for (key, endpoint) in &group.mapping { - let property_index = group.fields.iter().position(|f| f == key).expect("Unknown property"); + let property_index = group + .fields + .iter() + .position(|f| f == key) + .expect("Unknown property"); let group_index = result .iter() .position(|(name, _)| name == endpoint) diff --git a/src/lang.rs b/src/lang.rs index 822c89d..14cb7b6 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -588,9 +588,7 @@ impl SRSModule for WorstWordsSRSModule { } match self.queue.pop() { - Some(index) => { - index - } + Some(index) => index, None => { self.queue.append(&mut self.pool.clone()); self.rounds_remaining -= 1; diff --git a/src/main.rs b/src/main.rs index 67bc533..e9706c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,10 +31,10 @@ use iced::{Font, window}; use iced::{Subscription, Theme, keyboard}; use iced_core::Size; use iced_core::window::Position; -use mimalloc::MiMalloc; -use rusqlite::Connection; #[cfg(target_os = "linux")] use iced_core::window::settings::PlatformSpecific; +use mimalloc::MiMalloc; +use rusqlite::Connection; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; diff --git a/src/navigation.rs b/src/navigation.rs index 3cb483f..92d1f56 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -3,7 +3,7 @@ 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::dictionary::{app_data_dir, DictionaryMessage, DictionaryState}; +use crate::dictionary::{DictionaryMessage, DictionaryState, app_data_dir}; use crate::dictionary_test::{DictionaryQuizMessage, DictionaryQuizState}; use crate::history::{HistoryMessage, HistoryState}; use crate::import::{ImportMessage, ImportState}; @@ -23,7 +23,7 @@ use crate::sync::{SyncMessage, SyncState}; use crate::view_navigation; use crate::word::{WordMessage, WordState}; use crate::writing::{WritingMessage, WritingState}; -use crate::{fill_state, AppState}; +use crate::{AppState, fill_state}; use chrono::NaiveDate; use hashbrown::HashMap; use iced::keyboard::Event; diff --git a/src/quiz.rs b/src/quiz.rs index 70af76c..7e8be3e 100644 --- a/src/quiz.rs +++ b/src/quiz.rs @@ -54,7 +54,7 @@ impl NavigatedPage for QuizState { self.update_showed() } } - Back => {}, + Back => {} } Task::none() } diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index 366a04f..9ccad7d 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -6,13 +6,13 @@ use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::repetition_settings::RepetitionSettingsMessage::*; use crate::styling::*; 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 std::sync::{Arc, Mutex}; use std::time::Duration; -use iced::widget::text_input::Catalog; pub struct RepetitionSettingsState { set: DeckSettings, @@ -117,13 +117,15 @@ impl NavigatedPage for RepetitionSettingsState { .spacing(QUARTER_SPACING), column![ text!("Фильтр"), - text_input("", &self.set.filter).style(|x, status| { - let mut primary = Theme::default()(x, status); - if !self.correct_filter { - primary.border.color = color!(255, 255, 0); - } - primary - }).on_input(SetFilter), + text_input("", &self.set.filter) + .style(|x, status| { + let mut primary = Theme::default()(x, status); + if !self.correct_filter { + primary.border.color = color!(255, 255, 0); + } + primary + }) + .on_input(SetFilter), button("Проверить фильтр") .style(jl_button) .on_press(TryFilter), diff --git a/src/repetitions.rs b/src/repetitions.rs index 6c0d639..67f009f 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -417,7 +417,7 @@ impl RepetitionsState { for (date, activity) in history { let distance = now - *date; let index = counts.len() as i64 - distance.num_days() - 1; - + if index < 0 { continue; } @@ -577,7 +577,11 @@ impl RepetitionsState { .cloned(); Self::append_words(state, deck, required); } - fn append_words(state: &mut AppState, deck: &DeckSettings, indices: impl Iterator) { + fn append_words( + state: &mut AppState, + deck: &DeckSettings, + indices: impl Iterator, + ) { let words = &state.dictionary; let stats = &mut indices .map(|i| { diff --git a/src/writing.rs b/src/writing.rs index 4f3a418..4f14199 100644 --- a/src/writing.rs +++ b/src/writing.rs @@ -30,7 +30,7 @@ impl NavigatedPage for WritingState { fn navigated(&mut self) {} fn update(&mut self, message: WritingMessage) -> Task { match message { - WritingMessage::Back => {}, + WritingMessage::Back => {} WritingMessage::Next => self.next(), WritingMessage::SwitchShowMode(b) => self.show_all = b, } From 66f8a83dec7c5ad4d1c490928be535bfd0854e1b Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 00:07:09 +0300 Subject: [PATCH 18/30] Migrate to parking_lot --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/dictionary.rs | 47 +++++++++++++++++++------------------- src/history.rs | 5 ++-- src/import.rs | 11 +++++---- src/lang.rs | 7 +++--- src/navigation.rs | 9 ++++---- src/repetition.rs | 5 ++-- src/repetition_settings.rs | 16 +++++++------ src/repetitions.rs | 21 +++++++++-------- src/selector.rs | 3 ++- src/sync.rs | 23 ++++++++++--------- src/word.rs | 7 +++--- 13 files changed, 85 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 379a3a1..edff5b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2319,6 +2319,7 @@ dependencies = [ "iced", "iced_core", "mimalloc", + "parking_lot", "rand", "rayon", "reqwest", @@ -2326,7 +2327,6 @@ dependencies = [ "rhai", "rodio", "rusqlite", - "serde", "serde_json", "sha2", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 2f57537..b0d03b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ iced = { version = "0.14.0", features = ["tokio", "svg", "lazy"]} iced_core = "0.14.0" rand = "0.10.2" dirs = "6.0.0" -serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" rhai = { version = "1.25.1", features = ["sync"] } chrono = "0.4.45" @@ -24,6 +23,7 @@ rfd = "0.17.2" mimalloc = "0.1.52" hashbrown = "0.17.1" rayon = "1.12.0" +parking_lot = "0.12.5" [profile.super-release] inherits = "release" diff --git a/src/dictionary.rs b/src/dictionary.rs index 3c3c5a3..4b065b5 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -22,7 +22,8 @@ use rand::random_range; use std::fs; use std::ops::Add; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; use std::time::{Duration, Instant}; #[derive(Clone)] @@ -73,7 +74,7 @@ impl NavigatedPage for DictionaryState { && self.include_map.iter().any(|x| *x) { let mut words = vec![]; - let dict = &self.state.lock().unwrap().dictionary; + let dict = &self.state.lock().dictionary; words = self .include_map @@ -92,7 +93,7 @@ impl NavigatedPage for DictionaryState { if let WordAction(index) = message { let word: WordData; { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); let dict = &state.dictionary; word = dict[*index].clone(); } @@ -107,7 +108,7 @@ impl NavigatedPage for DictionaryState { } fn navigated(&mut self) { - let len = self.state.lock().unwrap().dictionary.len(); + let len = self.state.lock().dictionary.len(); self.include_map = vec![false; len]; self.tag_map = Default::default(); @@ -117,7 +118,7 @@ impl NavigatedPage for DictionaryState { fn update(&mut self, message: DictionaryMessage) -> Task { match message { NewWord => { - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); let mut word = WordData::new(); word.group_id = state.word_groups[self.selected_group_index].id; @@ -128,21 +129,21 @@ impl NavigatedPage for DictionaryState { SetKey(i, v) => { { - let dict = &mut self.state.lock().unwrap().dictionary; + let dict = &mut self.state.lock().dictionary; dict[i].key = v; } return self.launch_auto_save_offset(i); } SetValue(i, v) => { { - let dict = &mut self.state.lock().unwrap().dictionary; + let dict = &mut self.state.lock().dictionary; dict[i].value = v } return self.launch_auto_save_offset(i); } SetTags(i, mut v) => { { - let dict = &mut self.state.lock().unwrap().dictionary; + let dict = &mut self.state.lock().dictionary; let current_tags_value = dict[i].tags.clone(); @@ -162,7 +163,7 @@ impl NavigatedPage for DictionaryState { } WordAction(i) => { - let state = &mut self.state.lock().unwrap(); + let state = &mut self.state.lock(); let dict = &mut state.dictionary; let word = dict.remove(i); self.include_map.remove(i); @@ -173,7 +174,7 @@ impl NavigatedPage for DictionaryState { IncludeTag(t, v) => { let index; { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); index = state.word_groups[self.selected_group_index].id; } self.tag_map.insert(t, v); @@ -193,7 +194,7 @@ impl NavigatedPage for DictionaryState { Back => {} Test => {} CreateGroup => { - let state = &mut self.state.lock().unwrap(); + let state = &mut self.state.lock(); state.word_groups.push(WordGroup { id: 0.into(), @@ -201,14 +202,14 @@ impl NavigatedPage for DictionaryState { }); } EditGroup(new) => { - let state = &mut self.state.lock().unwrap(); + let state = &mut self.state.lock(); let group = state.word_groups.get_mut(self.selected_group_index); if let Some(group) = group { group.name = new.clone(); } } SaveGroup => { - let state = &mut self.state.lock().unwrap(); + let state = &mut self.state.lock(); let connection = &state.connection; let mut group = state.word_groups[self.selected_group_index].clone(); @@ -219,7 +220,7 @@ impl NavigatedPage for DictionaryState { self.selected_group_index = i; let index; { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); index = state.word_groups[i].id; } self.update_words_include(index) @@ -229,7 +230,7 @@ impl NavigatedPage for DictionaryState { return Task::none(); } { - let state = &mut self.state.lock().unwrap(); + let state = &mut self.state.lock(); if let Some(group) = state.word_groups.get(self.selected_group_index) { let remove_group_id = group.id; @@ -289,7 +290,7 @@ impl NavigatedPage for DictionaryState { impl DictionaryState { pub fn new(state: Arc>) -> Self { - let len = state.lock().unwrap().dictionary.len(); + let len = state.lock().dictionary.len(); let mut result = DictionaryState { include_map: vec![false; len], @@ -309,7 +310,7 @@ impl DictionaryState { } fn save_word(&mut self, i: usize) { - let state = &mut self.state.lock().unwrap(); + let state = &mut self.state.lock(); let connection = &state.connection; let word = &mut state.dictionary[i].clone(); @@ -331,7 +332,7 @@ impl DictionaryState { let time = Instant::now(); let mut col = Column::new().width(Fill); - let state = self.state.lock().unwrap(); + let state = self.state.lock(); let group_id = state.word_groups[self.selected_group_index].id; let dict = &state.dictionary; @@ -443,7 +444,7 @@ impl DictionaryState { } fn filters(&self) -> iced::Element<'_, DictionaryMessage> { - let dict = &self.state.lock().unwrap().dictionary; + let dict = &self.state.lock().dictionary; iced::widget::column![ text_input("Поиск", &self.search) @@ -499,7 +500,7 @@ impl DictionaryState { fn update_tags(&mut self) { let mut tags_list: HashSet = HashSet::new(); - let dict = &self.state.lock().unwrap().dictionary; + let dict = &self.state.lock().dictionary; dict.iter().for_each(|element| { split_with_coma(element.tags.as_str()) @@ -540,7 +541,7 @@ impl DictionaryState { return; } - let dict = &self.state.lock().unwrap().dictionary; + let dict = &self.state.lock().dictionary; let time = Instant::now(); self.include_map = dict @@ -560,7 +561,7 @@ impl DictionaryState { let mut row = Row::new(); row = row.push(button("+").style(text).on_press(CreateGroup)); - let state = &self.state.lock().unwrap(); + let state = &self.state.lock(); let groups = &state.word_groups; for (index, group) in groups.iter().enumerate() { @@ -598,7 +599,7 @@ impl DictionaryState { } fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); let group_id = state.word_groups[self.selected_group_index].id; let button = button("Добавить слово").style(jl_button); diff --git a/src/history.rs b/src/history.rs index d055d2f..7e17885 100644 --- a/src/history.rs +++ b/src/history.rs @@ -9,7 +9,8 @@ use iced::alignment::Horizontal::Center; use iced::widget::space::horizontal; use iced::widget::*; use iced::{Element, Fill, FillPortion, Task}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; #[derive(Clone)] pub struct HistoryState { @@ -47,7 +48,7 @@ impl NavigatedPage for HistoryState { impl HistoryState { pub fn new(id: crate::lang::Id, state: Arc>) -> Self { - let state = state.lock().unwrap(); + let state = state.lock(); let history = get_history_of_set(id); let words = history .iter() diff --git a/src/import.rs b/src/import.rs index 3ead526..c2aeb04 100644 --- a/src/import.rs +++ b/src/import.rs @@ -21,7 +21,8 @@ use rusqlite::Connection; use std::fs::{File, OpenOptions}; use std::io::BufReader; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; use tokio::task::spawn_blocking; use zip::ZipArchive; @@ -141,7 +142,7 @@ impl NavigatedPage for ImportState { back_overlay( { if let Some(value) = &self.progress { - column![progress_bar(0f32..=1f32, *value.lock().unwrap()),] + column![progress_bar(0f32..=1f32, *value.lock()),] .spacing(QUARTER_SPACING) .into() } else { @@ -444,7 +445,7 @@ impl ImportState { name: group.name.clone(), }; { - let state = state.lock().unwrap(); + let state = state.lock(); add_group(&mut group_entity, &state.connection); } @@ -480,14 +481,14 @@ impl ImportState { words_list.push(word); } - let mut state = state.lock().unwrap(); + let mut state = state.lock(); let connection = &mut state.connection; 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); index += 1; - let mut progress = progress.lock().unwrap(); + let mut progress = progress.lock(); *progress = index as f32 / total_len; } diff --git a/src/lang.rs b/src/lang.rs index 14cb7b6..f842e8e 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -18,7 +18,8 @@ use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::num::ParseIntError; use std::str::FromStr; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; use std::time::Instant; const MAX_HISTORY_LEN: usize = 20; @@ -354,7 +355,7 @@ pub struct DeckData { impl DeckData { pub fn new(settings: &DeckSettings, state: Arc>) -> Self { let state_for = state.clone(); - let state_locked = state.lock().unwrap(); + let state_locked = state.lock(); let mut current_set = load_stats_of_deck(settings, &state_locked.connection); let last_list: Vec<_> = settings @@ -446,7 +447,7 @@ impl DeckData { self.order_module = OrderModule::WorstWordsSRS(module); } } - update_stat_score(word, &self.state.lock().unwrap().connection); + update_stat_score(word, &self.state.lock().connection); push_note( self.settings.id, HistoryItem { diff --git a/src/navigation.rs b/src/navigation.rs index 92d1f56..4709400 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -30,7 +30,8 @@ use iced::keyboard::Event; use iced::{Element, Task}; use reqwest::Error; use rusqlite::Connection; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; use std::time::Instant; impl Default for ScreenState { @@ -97,7 +98,7 @@ impl ScreenState { let reading_additional_task = Task::perform(Self::load_additional_data(), |data| data); let final_task; - let state_guard = state.app_state.lock().unwrap(); + let state_guard = state.app_state.lock(); if state_guard.sync_data.auto_web_fetch { let key = state_guard.sync_data.key.clone().unwrap(); final_task = Task::batch([ @@ -169,13 +170,13 @@ impl ScreenState { } if let DataLoaded(data) = message { - self.app_state.lock().unwrap().activity = data; + self.app_state.lock().activity = data; return Task::none(); } if let UpdateData = message { println!("Loading data"); - let mut state = self.app_state.lock().unwrap(); + let mut state = self.app_state.lock(); if cfg!(windows) { state.connection = Connection::open_in_memory().unwrap(); } diff --git a/src/repetition.rs b/src/repetition.rs index 7d24a08..20ce8b0 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -13,7 +13,8 @@ use iced::widget::{Column, button, column, container, row, rule, space, text, to use iced::{Element, Fill, Task, Theme, alignment, keyboard}; use rodio::MixerDeviceSink; use std::collections::HashSet; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; use tokio::task::spawn_blocking; pub struct RepetitionState { @@ -148,7 +149,7 @@ impl RepetitionState { } fn increment_counter(&mut self) { - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); let id = self.settings.id; let activity = state.activity.get_mut(&id); diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index 9ccad7d..f7a3a96 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -11,9 +11,11 @@ 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 std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; use std::time::Duration; + pub struct RepetitionSettingsState { set: DeckSettings, index: usize, @@ -23,10 +25,10 @@ pub struct RepetitionSettingsState { } impl RepetitionSettingsState { - pub(crate) fn new(index: usize, p1: Arc>) -> RepetitionSettingsState { + pub(crate) fn new(index: usize, state: Arc>) -> RepetitionSettingsState { let set; { - let local_state = p1.lock().unwrap(); + let local_state = state.lock(); set = local_state.decks[index].clone(); } let filter_state = set.check_filter(); @@ -34,7 +36,7 @@ impl RepetitionSettingsState { RepetitionSettingsState { index, set, - state: p1, + state, real_delete: false, correct_filter: filter_state, } @@ -55,7 +57,7 @@ impl NavigatedPage for RepetitionSettingsState { match message { Back => {} Save => { - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); update_deck(&mut self.set, &state.connection); state.decks[self.index] = self.set.clone(); } @@ -73,7 +75,7 @@ impl NavigatedPage for RepetitionSettingsState { self.correct_filter = self.set.check_filter(); } TryFilter => { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); let count = self.set.get_word_list(&state).len(); self.set.count = Some(count); } @@ -85,7 +87,7 @@ impl NavigatedPage for RepetitionSettingsState { RootMessage::RepetitionSettings(RevertDeleteSet) }); } - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); delete_set(&self.set, &state.connection); state.decks.remove(self.index); return Task::done(RootMessage::RepetitionSettings(Back)); diff --git a/src/repetitions.rs b/src/repetitions.rs index 67f009f..3d3c106 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -23,7 +23,8 @@ use iced_core::Padding; use iced_core::border::Radius; use iced_core::svg::Handle; use std::ops::Deref; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; #[derive(Clone)] pub struct RepetitionsState { @@ -47,7 +48,7 @@ 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().unwrap(), selected_set); + Self::append_all_words(&mut clone.lock(), selected_set); selected_set.existing_words_indices = selected_set.available_words_indices.clone(); } else { if selected_set @@ -80,7 +81,7 @@ impl NavigatedPage for RepetitionsState { } fn navigated(&mut self) { let index = self.selected_deck_index.unwrap(); - let state = self.state.lock().unwrap(); + let state = self.state.lock(); if let Some(global_deck) = state.decks.get(index) { if global_deck.id != self.selected_deck().unwrap().id { drop(state); @@ -116,7 +117,7 @@ impl NavigatedPage for RepetitionsState { self.clear_selection(); let deck = self.decks.remove(index); if deck.id.is_valid() { - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); state.decks.remove(index); self.decks = self .decks @@ -137,7 +138,7 @@ impl NavigatedPage for RepetitionsState { self.select_deck(index); let deck = self.selected_deck().unwrap(); if deck.existing_words_indices.is_none() { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); let added: Vec<_> = load_stats_of_deck(deck, &state.connection) .iter() .map(|c| self.word_id_index_map[&c.word_id]) @@ -151,7 +152,7 @@ impl NavigatedPage for RepetitionsState { } Save => { let mut deck = self.selected_deck().unwrap().clone(); - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); { update_deck(&mut deck.general_settings, &state.connection); if let Some(index) = deck.index { @@ -176,7 +177,7 @@ impl NavigatedPage for RepetitionsState { deck.general_settings.filter = value; } TryFilter => { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); let deck = self.selected_deck().unwrap(); let count = deck.get_word_list(&state).len(); drop(state); @@ -220,7 +221,7 @@ impl NavigatedPage for RepetitionsState { } let deck = self.selected_deck().unwrap(); - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); Self::append_words(&mut state, &deck.general_settings, adding.into_iter()); } } @@ -253,7 +254,7 @@ impl NavigatedPage for RepetitionsState { impl RepetitionsState { pub(crate) fn new(state: Arc>) -> RepetitionsState { - let state_ = state.lock().unwrap(); + let state_ = state.lock(); let mut map = HashMap::with_capacity(state_.dictionary.len()); state_ @@ -408,7 +409,7 @@ impl RepetitionsState { fn activity_bar(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { const MAX_DAY_COUNT: f32 = 128.0; - let state = self.state.lock().unwrap(); + let state = self.state.lock(); let history = state.activity.get(&deck.id); let mut counts: Vec = vec![0; 30 * 7]; let now = Local::now().date_naive(); diff --git a/src/selector.rs b/src/selector.rs index 8eae3db..3156a77 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -15,7 +15,8 @@ use iced::{Element, Task, alignment}; use iced_core::Alignment::Center; use iced_core::Background; use iced_core::Color; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; pub struct SelectorState { pub set: KanaSet, diff --git a/src/sync.rs b/src/sync.rs index a3aa52b..7c2e4fb 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -9,7 +9,8 @@ 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 std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; use std::time::Duration; #[derive(Clone)] @@ -52,7 +53,7 @@ impl NavigatedPage for SyncState { match message { Back => {} CopyKey => { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); let key = state.sync_data.key.clone().unwrap(); return iced::clipboard::write(key) .map(|_val: String| RootMessage::Sync(KeyCopied)); @@ -69,14 +70,14 @@ impl NavigatedPage for SyncState { if !validate_id(&new_id) { return Task::none(); } - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); state.sync_data.key = Some(new_id.clone()); set_setting("SYNC_KEY".to_string(), new_id, &state.connection); } Send => { self.prepare_to_db_interaction(); - let id = self.state.lock().unwrap().sync_data.key.clone().unwrap(); + let id = self.state.lock().sync_data.key.clone().unwrap(); let tasks = Task::batch([ Task::perform( async { tokio::time::sleep(Duration::from_millis(200)).await }, @@ -88,7 +89,7 @@ impl NavigatedPage for SyncState { return tasks; } GetLast => { - let id = self.state.lock().unwrap().sync_data.key.clone().unwrap(); + let id = self.state.lock().sync_data.key.clone().unwrap(); let tasks = Task::batch([ Task::perform( async { tokio::time::sleep(Duration::from_millis(200)).await }, @@ -116,7 +117,7 @@ impl NavigatedPage for SyncState { let mut updated_state = AppState::new(); fill_state(&mut updated_state); - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); state.sync_data = updated_state.sync_data; state.connection = updated_state.connection; state.decks = updated_state.decks; @@ -125,14 +126,14 @@ impl NavigatedPage for SyncState { } Disable => {} DisableSync => { - let mut state = self.state.lock().unwrap(); + 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); } SwitchAutoSync => { self.auto_fetch = !self.auto_fetch; - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); state.sync_data.auto_web_fetch = self.auto_fetch; set_setting( "AUTO_WEB_FETCH".to_string(), @@ -159,7 +160,7 @@ impl SyncState { pub fn new(state: Arc>) -> SyncState { let auto_fetch; { - auto_fetch = state.lock().unwrap().sync_data.auto_web_fetch; + auto_fetch = state.lock().sync_data.auto_web_fetch; } Self { auto_fetch, @@ -176,7 +177,7 @@ impl SyncState { space().into() }; { - if let Some(key) = self.state.lock().unwrap().sync_data.key.clone() { + if let Some(key) = self.state.lock().sync_data.key.clone() { column![ text!("Ваш ключ синхронизации"), container( @@ -233,7 +234,7 @@ impl SyncState { } self.frozen = true; self.progress = 0.0; - let state = self.state.lock().unwrap(); + let state = self.state.lock(); state .connection .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(())) diff --git a/src/word.rs b/src/word.rs index 84cc90e..8de98e2 100644 --- a/src/word.rs +++ b/src/word.rs @@ -8,7 +8,8 @@ use crate::{AppState, RootMessage}; use iced::widget::button::danger; use iced::widget::{button, column, row, rule, scrollable, text, text_input}; use iced::{Element, Fill, Task}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use parking_lot::Mutex; #[derive(Clone)] pub struct WordState { @@ -31,13 +32,13 @@ impl NavigatedPage for WordState { match message { Back => {} Save => { - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); state.dictionary[self.index] = self.word.clone(); update_word(&mut self.word, &state.connection); return Task::done(RootMessage::Word(Back)); } Delete => { - let mut state = self.state.lock().unwrap(); + let mut state = self.state.lock(); state.dictionary.remove(self.index); delete_word(&self.word, &state.connection); return Task::done(RootMessage::Word(Back)); From ac1b516a881052ce81203e17d54fb8b64c8308f4 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 00:18:16 +0300 Subject: [PATCH 19/30] Fixing Into clippy --- src/lang.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang.rs b/src/lang.rs index f842e8e..5d38442 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -43,9 +43,9 @@ impl From for Id { } } -impl Into for Id { - fn into(self) -> u32 { - self.0 +impl From for u32 { + fn from(value: Id) -> Self { + value.0 } } impl Display for Id { From 3661173bb012296b1b6048f5b857801611c9e58a Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 00:42:06 +0300 Subject: [PATCH 20/30] Final code cleanup --- src/lang.rs | 92 +++++++++++++++++++++++------------------------ src/main.rs | 1 + src/navigation.rs | 2 +- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/lang.rs b/src/lang.rs index 5d38442..921b9ca 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,9 +1,10 @@ -use crate::AppState; use crate::data_provider::card_stats::{delete_stat, load_stats_of_deck, update_stat_score}; -use crate::data_provider::history::{HistoryItem, push_note}; +use crate::data_provider::history::{push_note, HistoryItem}; +use crate::AppState; use chrono::{DateTime, Utc}; -use rand::distr::Distribution; +use parking_lot::Mutex; use rand::distr::weighted::WeightedIndex; +use rand::distr::Distribution; use rand::prelude::SliceRandom; use rand::rng; use rand::rngs::ThreadRng; @@ -11,15 +12,14 @@ use rayon::iter::IndexedParallelIterator; use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rhai::{Engine, Scope}; -use rusqlite::ToSql; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; +use rusqlite::ToSql; use std::cmp::PartialEq; use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::num::ParseIntError; use std::str::FromStr; use std::sync::Arc; -use parking_lot::Mutex; use std::time::Instant; const MAX_HISTORY_LEN: usize = 20; @@ -668,49 +668,47 @@ impl DeckSettings { let engine = Engine::new(); let ast = engine.compile(&self.filter); - if ast.is_err() { - return list; + if let Ok(ast) = ast { + let groups = &state.word_groups; + + list = state + .dictionary + .par_iter() + .enumerate() + .filter(|(_, word)| { + let mut more = rhai::Map::new(); + for iced in &word.additional { + more.insert(iced.0.clone().into(), iced.1.clone().into()); + } + let mut scope = Scope::new(); + scope + .push_constant("id", word.id.0) + .push_constant("key", word.key.clone()) + .push_constant("value", word.value.clone()) + .push_constant("tags", word.tags.clone()) + .push_constant("more", more) + .push_constant( + "group", + groups + .iter() + .find(|g| g.id == word.group_id) + .cloned() + .unwrap() + .name, + ); + + let result = engine.eval_ast_with_scope::(&mut scope, &ast); + result.is_ok() && result.unwrap() + }) + .map(|(index, _)| index) + .collect(); + + println!("Collecting available words is {:?}", time.elapsed()); + + list + } else { + list } - - let ast = ast.unwrap(); - - let groups = &state.word_groups; - - list = state - .dictionary - .par_iter() - .enumerate() - .filter(|(_, word)| { - let mut more = rhai::Map::new(); - for iced in &word.additional { - more.insert(iced.0.clone().into(), iced.1.clone().into()); - } - let mut scope = Scope::new(); - scope - .push_constant("id", word.id.0) - .push_constant("key", word.key.clone()) - .push_constant("value", word.value.clone()) - .push_constant("tags", word.tags.clone()) - .push_constant("more", more) - .push_constant( - "group", - groups - .iter() - .find(|g| g.id == word.group_id) - .cloned() - .unwrap() - .name, - ); - - let result = engine.eval_ast_with_scope::(&mut scope, &ast); - result.is_ok() && result.unwrap() - }) - .map(|(index, _)| index) - .collect(); - - println!("Collecting available words is {:?}", time.elapsed()); - - list } pub fn require_speech(&self) -> bool { self.forward == "speech" || self.backward == "speech" diff --git a/src/main.rs b/src/main.rs index e9706c1..d04e06a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,6 +56,7 @@ fn main() -> iced::Result { .run() } +#[allow(unused_mut)] fn window_settings() -> window::Settings { let mut settings = window::Settings { position: Position::Centered, diff --git a/src/navigation.rs b/src/navigation.rs index 4709400..8f8ed1d 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -132,7 +132,7 @@ impl ScreenState { Ok(v) => v, }; - let id: Id = history_file_name[4..history_file_name.len() - 12] + let id = history_file_name[4..history_file_name.len() - 12] .parse::() .unwrap(); From 44644500b567b587732063481a14da6819161e02 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 00:49:49 +0300 Subject: [PATCH 21/30] Fix repetition display bug and history find bug --- src/dictionary.rs | 2 +- src/history.rs | 12 +++--------- src/import.rs | 2 +- src/lang.rs | 8 ++++---- src/navigation.rs | 2 +- src/repetition.rs | 4 ++-- src/repetition_settings.rs | 3 +-- src/repetitions.rs | 2 +- src/selector.rs | 2 +- src/sync.rs | 2 +- src/word.rs | 2 +- 11 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/dictionary.rs b/src/dictionary.rs index 4b065b5..fc3be4b 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -18,12 +18,12 @@ use iced::widget::text_input::default; use iced::widget::*; use iced::{Border, Color, Shadow, Task}; use iced_core::Length::Fill; +use parking_lot::Mutex; use rand::random_range; use std::fs; use std::ops::Add; use std::path::PathBuf; use std::sync::Arc; -use parking_lot::Mutex; use std::time::{Duration, Instant}; #[derive(Clone)] diff --git a/src/history.rs b/src/history.rs index 7e17885..7399019 100644 --- a/src/history.rs +++ b/src/history.rs @@ -9,8 +9,8 @@ use iced::alignment::Horizontal::Center; use iced::widget::space::horizontal; use iced::widget::*; use iced::{Element, Fill, FillPortion, Task}; -use std::sync::Arc; use parking_lot::Mutex; +use std::sync::Arc; #[derive(Clone)] pub struct HistoryState { @@ -52,14 +52,8 @@ impl HistoryState { let history = get_history_of_set(id); let words = history .iter() - .map(|item| { - state - .dictionary - .iter() - .find(|w| w.id == item.word_id) - .unwrap() - .clone() - }) + .filter_map(|item| state.dictionary.iter().find(|w| w.id == item.word_id)) + .cloned() .collect(); Self { diff --git a/src/import.rs b/src/import.rs index c2aeb04..04c75c9 100644 --- a/src/import.rs +++ b/src/import.rs @@ -16,13 +16,13 @@ use iced::{Element, Task}; use iced_core::Alignment::Center; use iced_core::Length::Fill; use iced_core::Padding; +use parking_lot::Mutex; use rfd::AsyncFileDialog; use rusqlite::Connection; use std::fs::{File, OpenOptions}; use std::io::BufReader; use std::path::PathBuf; use std::sync::Arc; -use parking_lot::Mutex; use tokio::task::spawn_blocking; use zip::ZipArchive; diff --git a/src/lang.rs b/src/lang.rs index 921b9ca..c72276c 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,10 +1,10 @@ -use crate::data_provider::card_stats::{delete_stat, load_stats_of_deck, update_stat_score}; -use crate::data_provider::history::{push_note, HistoryItem}; use crate::AppState; +use crate::data_provider::card_stats::{delete_stat, load_stats_of_deck, update_stat_score}; +use crate::data_provider::history::{HistoryItem, push_note}; use chrono::{DateTime, Utc}; use parking_lot::Mutex; -use rand::distr::weighted::WeightedIndex; use rand::distr::Distribution; +use rand::distr::weighted::WeightedIndex; use rand::prelude::SliceRandom; use rand::rng; use rand::rngs::ThreadRng; @@ -12,8 +12,8 @@ use rayon::iter::IndexedParallelIterator; use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rhai::{Engine, Scope}; -use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::ToSql; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use std::cmp::PartialEq; use std::collections::HashMap; use std::fmt::{Display, Formatter}; diff --git a/src/navigation.rs b/src/navigation.rs index 8f8ed1d..7ef1ca7 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -28,10 +28,10 @@ use chrono::NaiveDate; use hashbrown::HashMap; use iced::keyboard::Event; use iced::{Element, Task}; +use parking_lot::Mutex; use reqwest::Error; use rusqlite::Connection; use std::sync::Arc; -use parking_lot::Mutex; use std::time::Instant; impl Default for ScreenState { diff --git a/src/repetition.rs b/src/repetition.rs index 20ce8b0..41d4ae5 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -11,10 +11,10 @@ use iced::keyboard::key::Physical::Code; use iced::widget::text::Style; 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; use std::sync::Arc; -use parking_lot::Mutex; use tokio::task::spawn_blocking; pub struct RepetitionState { @@ -273,7 +273,7 @@ impl RepetitionState { } } fn draw_description(&self, word: &WordData) -> Element<'_, RepetitionMessage> { - match word.additional.get("context") { + match word.additional.get("description") { None => space().into(), Some(context) => text!("{}", context).size(24).into(), } diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index f7a3a96..4cbfa37 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -11,11 +11,10 @@ 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 std::sync::Arc; use parking_lot::Mutex; +use std::sync::Arc; use std::time::Duration; - pub struct RepetitionSettingsState { set: DeckSettings, index: usize, diff --git a/src/repetitions.rs b/src/repetitions.rs index 3d3c106..7835627 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -22,9 +22,9 @@ use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Tas use iced_core::Padding; use iced_core::border::Radius; use iced_core::svg::Handle; +use parking_lot::Mutex; use std::ops::Deref; use std::sync::Arc; -use parking_lot::Mutex; #[derive(Clone)] pub struct RepetitionsState { diff --git a/src/selector.rs b/src/selector.rs index 3156a77..3be8cac 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -15,8 +15,8 @@ use iced::{Element, Task, alignment}; use iced_core::Alignment::Center; use iced_core::Background; use iced_core::Color; -use std::sync::Arc; use parking_lot::Mutex; +use std::sync::Arc; pub struct SelectorState { pub set: KanaSet, diff --git a/src/sync.rs b/src/sync.rs index 7c2e4fb..de4d134 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -9,8 +9,8 @@ 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 std::sync::Arc; use parking_lot::Mutex; +use std::sync::Arc; use std::time::Duration; #[derive(Clone)] diff --git a/src/word.rs b/src/word.rs index 8de98e2..5daea7d 100644 --- a/src/word.rs +++ b/src/word.rs @@ -8,8 +8,8 @@ use crate::{AppState, RootMessage}; use iced::widget::button::danger; use iced::widget::{button, column, row, rule, scrollable, text, text_input}; use iced::{Element, Fill, Task}; -use std::sync::Arc; use parking_lot::Mutex; +use std::sync::Arc; #[derive(Clone)] pub struct WordState { From 01ba0567f5b53c3292d9dead4761ab1eebb10a06 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 00:55:20 +0300 Subject: [PATCH 22/30] Change version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index edff5b5..5d9afd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2309,7 +2309,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jap_learn" -version = "0.1.2" +version = "0.1.3" dependencies = [ "chrono", "criterion", diff --git a/Cargo.toml b/Cargo.toml index b0d03b1..36e001a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jap_learn" -version = "0.1.2" +version = "0.1.3" edition = "2024" [dependencies] From c5e91b844c8b50d4b3e3aca63bec295d3d0d6959 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 11:17:51 +0300 Subject: [PATCH 23/30] found bug in random srs --- src/data_provider/voice.rs | 16 +++++++--------- src/lang.rs | 13 +------------ src/repetition.rs | 17 +++++++++-------- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/data_provider/voice.rs b/src/data_provider/voice.rs index 45a6833..71c89b4 100644 --- a/src/data_provider/voice.rs +++ b/src/data_provider/voice.rs @@ -4,11 +4,11 @@ use sha2::{Digest, Sha256}; use std::fs::File; use std::io::BufReader; -pub async fn get_voice(text: &str) -> BufReader { +pub async fn get_voice(text: &str) -> Result, ()> { let mut path = app_data_dir(); path.push("voice"); if !path.exists() { - std::fs::create_dir(path.clone()).unwrap(); + std::fs::create_dir(path.clone()).map_err(|_|())?; } let hash = format!("{}.wav", hex::encode(Sha256::digest(text.as_bytes()))); path.push(hash); @@ -20,21 +20,19 @@ pub async fn get_voice(text: &str) -> BufReader { .send() .await; - let query = query.unwrap().text().await.unwrap(); + let query = query.map_err(|_|())?.text().await.map_err(|_|())?; // Synthesis let audio = client .post(format!("{engine_url}/synthesis?speaker=11")) .header("Content-Type", "application/json") .body(query) .send() - .await - .unwrap() + .await.map_err(|_|())? .bytes() - .await - .unwrap(); + .await.map_err(|_|())?; - tokio::fs::write(&path, &audio).await.unwrap(); + tokio::fs::write(&path, &audio).await.map_err(|_|())?; } - BufReader::new(File::open(path).unwrap()) + Ok(BufReader::new(File::open(path).map_err(|_|())?)) } diff --git a/src/lang.rs b/src/lang.rs index c72276c..846cc31 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -364,17 +364,6 @@ impl DeckData { .map(|w| &state_locked.dictionary[*w]) .cloned() .collect(); - let word_ids = last_list.iter().map(|l| l.id).collect::>(); - - let mut index = 0; - for stat in current_set.clone() { - if !word_ids.contains(&stat.word_id) { - delete_stat(&stat, &state_locked.connection); - current_set.remove(index); - } else { - index += 1; - } - } Self { set: current_set, @@ -515,7 +504,7 @@ impl SRSModule for RandomSRSModule { fn init(&mut self, set: &mut DeckData) { self.initialized = true; - self.basket = (0..set.words.len()).collect::>(); + self.basket = (0..set.set.len()).collect::>(); self.basket.shuffle(&mut rand::rng()) } } diff --git a/src/repetition.rs b/src/repetition.rs index 41d4ae5..f75ae84 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -9,8 +9,8 @@ use chrono::Local; use iced::alignment::Horizontal::Center; use iced::keyboard::key::Physical::Code; use iced::widget::text::Style; -use iced::widget::{Column, button, column, container, row, rule, space, text, tooltip}; -use iced::{Element, Fill, Task, Theme, alignment, keyboard}; +use iced::widget::{button, column, container, row, rule, space, text, tooltip, Column}; +use iced::{alignment, keyboard, Element, Fill, Task, Theme}; use parking_lot::Mutex; use rodio::MixerDeviceSink; use std::collections::HashSet; @@ -317,10 +317,11 @@ pub enum RepetitionMessage { } async fn play_sound(sink: Arc, text: String) { - let data = get_voice(text.as_str()).await; - spawn_blocking(move || { - rodio::play(sink.mixer(), data).unwrap().sleep_until_end(); - }) - .await - .unwrap(); + if let Ok(data) = get_voice(text.as_str()).await { + spawn_blocking(move || { + rodio::play(sink.mixer(), data).unwrap().sleep_until_end(); + }) + .await + .unwrap(); + } } From 3f43676580fc50cf7d0d502360cd5678e48bbf51 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 16:22:38 +0300 Subject: [PATCH 24/30] Fix deck data collecting --- src/data_provider/card_stats.rs | 10 ++++++++++ src/lang.rs | 21 +++++++++++---------- src/repetition_settings.rs | 24 ++++++++++++++++++++++-- src/repetitions.rs | 30 ++++++++++++++++++++---------- 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 144b9b4..8249c5c 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -81,3 +81,13 @@ pub fn delete_stat(stat: &CardStatistics, connection: &Connection) { .execute("DELETE FROM card_stats WHERE id = ?1", (&stat.id,)) .unwrap(); } + +pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) { + if stat.is_empty() { + return; + } + let ids = stat.iter().map(|s| s.id.into()).collect::>().join(", "); + connection + .execute(format!("DELETE FROM card_stats WHERE id in {ids}").as_str(), ()) + .unwrap(); +} \ No newline at end of file diff --git a/src/lang.rs b/src/lang.rs index 846cc31..6370164 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -26,7 +26,7 @@ const MAX_HISTORY_LEN: usize = 20; const MAX_HISTORY_LEN_PART: f32 = 0.33; const MAX_SCORE: u8 = 25; const FADE_PER_DAY: f32 = 0.95; -#[derive(Clone, PartialEq, Eq, Debug, Hash, Copy)] +#[derive(Clone, PartialEq, Eq, Debug, Hash, Copy, Ord, PartialOrd)] pub struct Id(u32); pub const INVALID_ID: Id = Id(0); impl FromStr for Id { @@ -69,6 +69,12 @@ impl ToSql for Id { } } +impl From for String { + fn from(value: Id) -> Self { + value.0.to_string() + } +} + impl Id { pub(crate) fn is_valid(self) -> bool { self.0 != 0 @@ -357,17 +363,12 @@ impl DeckData { let state_for = state.clone(); let state_locked = state.lock(); - let mut current_set = load_stats_of_deck(settings, &state_locked.connection); - let last_list: Vec<_> = settings - .get_word_list(&state_locked) - .iter() - .map(|w| &state_locked.dictionary[*w]) - .cloned() - .collect(); + 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()).map(|index| &state_locked.dictionary[index]).cloned().collect(); Self { - set: current_set, - words: last_list, + set: stats, + words, current_word_index: None, state: state_for, order_module: match settings.open_mode { diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index 4cbfa37..5217486 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -1,16 +1,17 @@ -use crate::AppState; use crate::data_provider::card_sets::{delete_set, update_deck}; +use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck}; use crate::lang::DeckSettings; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::repetition_settings::RepetitionSettingsMessage::*; use crate::styling::*; +use crate::AppState; 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::{Padding, Theme, color}; +use iced_core::{color, Padding, Theme}; use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; @@ -21,6 +22,7 @@ pub struct RepetitionSettingsState { state: Arc>, real_delete: bool, correct_filter: bool, + filter_changed: bool, } impl RepetitionSettingsState { @@ -38,6 +40,7 @@ impl RepetitionSettingsState { state, real_delete: false, correct_filter: filter_state, + filter_changed: false, } } } @@ -58,6 +61,9 @@ impl NavigatedPage for RepetitionSettingsState { Save => { let mut state = self.state.lock(); update_deck(&mut self.set, &state.connection); + if self.filter_changed { + update_deck_stats(&self.set, &state); + } state.decks[self.index] = self.set.clone(); } SetName(new) => { @@ -72,6 +78,7 @@ impl NavigatedPage for RepetitionSettingsState { SetFilter(new) => { self.set.filter = new; self.correct_filter = self.set.check_filter(); + self.filter_changed = true; } TryFilter => { let state = self.state.lock(); @@ -163,6 +170,19 @@ impl NavigatedPage for RepetitionSettingsState { } } +fn update_deck_stats(deck: &DeckSettings, state: &AppState) { + let mut db_stats = load_stats_of_deck(deck, &state.connection); + let filtered_words = deck.get_word_list(state); + filtered_words.iter().for_each(|word| { + let word_id = state.dictionary[*word].id; + if let Some(index) = db_stats.iter().position(|stat| stat.word_id == word_id) { + db_stats.remove(index); + } + }); + + delete_stats(db_stats.as_slice(), &state.connection); +} + impl RepetitionSettingsState { fn count_view(&self, set: &DeckSettings) -> Element<'_, RepetitionSettingsMessage> { if let Some(count) = set.count { diff --git a/src/repetitions.rs b/src/repetitions.rs index 7835627..1742a5f 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -87,6 +87,10 @@ impl NavigatedPage for RepetitionsState { drop(state); self.decks.remove(index); self.clear_selection(); + } else { + self.decks[self.selected_deck_index.unwrap()].general_settings = state.decks[index].clone(); + drop(state); + self.fill_deck_word_indices(); } } else { drop(state); @@ -138,16 +142,7 @@ impl NavigatedPage for RepetitionsState { self.select_deck(index); let deck = self.selected_deck().unwrap(); if deck.existing_words_indices.is_none() { - let state = self.state.lock(); - let added: Vec<_> = load_stats_of_deck(deck, &state.connection) - .iter() - .map(|c| self.word_id_index_map[&c.word_id]) - .collect(); - let total = deck.get_word_list(&state); - drop(state); - let deck = self.selected_deck_mut().unwrap(); - deck.existing_words_indices = Some(added); - deck.available_words_indices = Some(total); + self.fill_deck_word_indices(); } } Save => { @@ -618,6 +613,20 @@ impl RepetitionsState { fn clear_selection(&mut self) { self.selected_deck_index = None; } + + fn fill_deck_word_indices(&mut self) { + let state = &self.state.lock(); + let mut deck = self.selected_deck().unwrap().clone(); + let added: Vec<_> = load_stats_of_deck(&deck, &state.connection) + .iter() + .map(|c| self.word_id_index_map[&c.word_id]) + .collect(); + let total = deck.get_word_list(state); + deck.existing_words_indices = Some(added); + deck.available_words_indices = Some(total); + self.decks[self.selected_deck_index.unwrap()] = deck; + + } } #[derive(Clone)] pub enum RepetitionsMessage { @@ -648,6 +657,7 @@ struct DeckViewData { append_mode: AppendMode, index: Option, } + impl Deref for DeckViewData { type Target = DeckSettings; From 878ff3f1e4ab5c4a6ca2d19ab8f596731a360e0a Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 16:23:22 +0300 Subject: [PATCH 25/30] Formatting --- src/data_provider/card_stats.rs | 13 ++++++++++--- src/data_provider/voice.rs | 14 ++++++++------ src/lang.rs | 13 ++++++++++++- src/repetition.rs | 4 ++-- src/repetition_settings.rs | 4 ++-- src/repetitions.rs | 4 ++-- 6 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 8249c5c..728eec3 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -86,8 +86,15 @@ pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) { if stat.is_empty() { return; } - let ids = stat.iter().map(|s| s.id.into()).collect::>().join(", "); + let ids = stat + .iter() + .map(|s| s.id.into()) + .collect::>() + .join(", "); connection - .execute(format!("DELETE FROM card_stats WHERE id in {ids}").as_str(), ()) + .execute( + format!("DELETE FROM card_stats WHERE id in {ids}").as_str(), + (), + ) .unwrap(); -} \ No newline at end of file +} diff --git a/src/data_provider/voice.rs b/src/data_provider/voice.rs index 71c89b4..c24b8fd 100644 --- a/src/data_provider/voice.rs +++ b/src/data_provider/voice.rs @@ -8,7 +8,7 @@ pub async fn get_voice(text: &str) -> Result, ()> { let mut path = app_data_dir(); path.push("voice"); if !path.exists() { - std::fs::create_dir(path.clone()).map_err(|_|())?; + std::fs::create_dir(path.clone()).map_err(|_| ())?; } let hash = format!("{}.wav", hex::encode(Sha256::digest(text.as_bytes()))); path.push(hash); @@ -20,19 +20,21 @@ pub async fn get_voice(text: &str) -> Result, ()> { .send() .await; - let query = query.map_err(|_|())?.text().await.map_err(|_|())?; + let query = query.map_err(|_| ())?.text().await.map_err(|_| ())?; // Synthesis let audio = client .post(format!("{engine_url}/synthesis?speaker=11")) .header("Content-Type", "application/json") .body(query) .send() - .await.map_err(|_|())? + .await + .map_err(|_| ())? .bytes() - .await.map_err(|_|())?; + .await + .map_err(|_| ())?; - tokio::fs::write(&path, &audio).await.map_err(|_|())?; + tokio::fs::write(&path, &audio).await.map_err(|_| ())?; } - Ok(BufReader::new(File::open(path).map_err(|_|())?)) + Ok(BufReader::new(File::open(path).map_err(|_| ())?)) } diff --git a/src/lang.rs b/src/lang.rs index 6370164..4635267 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -364,7 +364,17 @@ impl DeckData { let state_locked = state.lock(); 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()).map(|index| &state_locked.dictionary[index]).cloned().collect(); + let words = stats + .iter() + .map(|stat| { + state_locked + .dictionary + .binary_search_by_key(&stat.word_id, |w| w.id) + .unwrap() + }) + .map(|index| &state_locked.dictionary[index]) + .cloned() + .collect(); Self { set: stats, @@ -381,6 +391,7 @@ impl DeckData { settings: settings.clone(), } } + pub fn next(&mut self) -> (WordData, CardStatistics) { let index = match self.order_module.clone() { OrderModule::SemiRandomSRS(mut module) => { diff --git a/src/repetition.rs b/src/repetition.rs index f75ae84..e5ea97e 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -9,8 +9,8 @@ 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; diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index 5217486..48db074 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -1,3 +1,4 @@ +use crate::AppState; use crate::data_provider::card_sets::{delete_set, update_deck}; use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck}; use crate::lang::DeckSettings; @@ -5,13 +6,12 @@ use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::repetition_settings::RepetitionSettingsMessage::*; use crate::styling::*; -use crate::AppState; 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; diff --git a/src/repetitions.rs b/src/repetitions.rs index 1742a5f..846ee5b 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -88,7 +88,8 @@ impl NavigatedPage for RepetitionsState { self.decks.remove(index); self.clear_selection(); } else { - self.decks[self.selected_deck_index.unwrap()].general_settings = state.decks[index].clone(); + self.decks[self.selected_deck_index.unwrap()].general_settings = + state.decks[index].clone(); drop(state); self.fill_deck_word_indices(); } @@ -625,7 +626,6 @@ impl RepetitionsState { deck.existing_words_indices = Some(added); deck.available_words_indices = Some(total); self.decks[self.selected_deck_index.unwrap()] = deck; - } } #[derive(Clone)] From c9508922f4ff32cc21a78d179bb13d3bb102acd0 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 16:25:41 +0300 Subject: [PATCH 26/30] Gitignore update for macOS --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2a0038a..3d5cb63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -.idea \ No newline at end of file +.idea +.DS_Store \ No newline at end of file From 9456eec2afdab55853f470dba6fa0d9a1f84c00a Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 17:48:03 +0300 Subject: [PATCH 27/30] Rename DictionaryTest to DictionaryQuiz --- src/data_provider/card_stats.rs | 2 +- src/data_provider/history.rs | 2 +- src/data_provider/web_api.rs | 40 +++++++++---------- src/dictionary.rs | 2 +- ...{dictionary_test.rs => dictionary_quiz.rs} | 2 +- src/main.rs | 2 +- src/navigation.rs | 2 +- src/sync.rs | 17 +++++--- 8 files changed, 35 insertions(+), 34 deletions(-) rename src/{dictionary_test.rs => dictionary_quiz.rs} (99%) diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 728eec3..19efeb4 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -93,7 +93,7 @@ pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) { .join(", "); connection .execute( - format!("DELETE FROM card_stats WHERE id in {ids}").as_str(), + format!("DELETE FROM card_stats WHERE id in ({ids})").as_str(), (), ) .unwrap(); diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index d05f485..e4e253b 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -39,7 +39,7 @@ fn parse_history_items(strings: Vec) -> Vec { continue; } items.push(HistoryItem { - timestamp: DateTime::from_timestamp(time.parse().unwrap_or_default(), 0).unwrap(), + 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, diff --git a/src/data_provider/web_api.rs b/src/data_provider/web_api.rs index 2c07ad1..5640474 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -7,10 +7,10 @@ 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/"; -pub async fn send_data(id: String) { +pub async fn send_data(id: String) -> Result<(), ()> { let path = app_data_dir(); let db_file = path.join("data.db"); - let data = tokio::fs::read(db_file).await.unwrap(); + let data = tokio::fs::read(db_file).await.map_err(|_| ())?; let data = compress(data); let api_url = API_URL.to_string(); @@ -21,12 +21,11 @@ pub async fn send_data(id: String) { .post(&id_url) .body(data) .send() - .await - .unwrap() + .await.map_err(|_| ())? .text() - .await - .unwrap(); + .await.map_err(|_| ())?; set_local_version(new_version.parse::().unwrap()).await; + Ok(()) } fn compress(data: Vec) -> Vec { @@ -35,18 +34,16 @@ fn compress(data: Vec) -> Vec { encoder.finish().unwrap() } -pub async fn load_data(id: String, temp: bool) { +pub async fn load_data(id: String, temp: bool) -> Result<(), ()> { 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 - .unwrap() + .await.map_err(|_| ())? .bytes() - .await - .unwrap() + .await.map_err(|_| ())? .to_vec(); let data = decompress(data); @@ -54,18 +51,17 @@ pub async fn load_data(id: String, temp: bool) { 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.unwrap(); + tokio::fs::write(db_file, data).await.map_err(|_| ())?; let version_url = format!("{API_URL}{id}/version"); let version = client .get(&version_url) .send() - .await - .unwrap() + .await.map_err(|_| ())? .text() - .await - .unwrap(); - set_local_version(version.parse::().unwrap()).await; + .await.map_err(|_| ())?; + set_local_version(version.parse::().map_err(|_| ())?).await; + Ok(()) } pub fn decompress(data: Vec) -> Vec { @@ -78,7 +74,7 @@ pub fn decompress(data: Vec) -> Vec { decompressed } -pub async fn first_sync() -> String { +pub async fn first_sync() -> Result { let id_url = API_URL.to_string() + "generate"; let client = reqwest::Client::new(); @@ -86,12 +82,12 @@ pub async fn first_sync() -> String { .get(&id_url) .send() .await - .unwrap() + .map_err(|_| ())? .text() .await - .unwrap(); + .map_err(|_| ())?; println!("id: {}", id); - id + Ok(id) } pub async fn get_web_version(key: &str) -> Result { @@ -112,7 +108,7 @@ pub async fn get_local_version() -> u32 { .read_to_string(&mut data) .await .unwrap(); - return data.parse::().unwrap(); + return data.parse::().unwrap_or_default(); } let mut file = OpenOptions::new() diff --git a/src/dictionary.rs b/src/dictionary.rs index fc3be4b..bcfe3b1 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -1,6 +1,6 @@ use crate::data_provider::words::{delete_group, delete_word, update_group, update_word}; use crate::dictionary::DictionaryMessage::*; -use crate::dictionary_test::DictionaryQuizState; +use crate::dictionary_quiz::DictionaryQuizState; use crate::import::ImportState; use crate::lang::{WordData, WordGroup}; use crate::navigation::Page::{Import, Word}; diff --git a/src/dictionary_test.rs b/src/dictionary_quiz.rs similarity index 99% rename from src/dictionary_test.rs rename to src/dictionary_quiz.rs index cf508fc..c71a740 100644 --- a/src/dictionary_test.rs +++ b/src/dictionary_quiz.rs @@ -1,6 +1,6 @@ use crate::RootMessage; use crate::dictionary::split_with_coma; -use crate::dictionary_test::DictionaryQuizMessage::*; +use crate::dictionary_quiz::DictionaryQuizMessage::*; use crate::lang::WordData; use crate::navigation::Page::PreviousPage; use crate::navigation::*; diff --git a/src/main.rs b/src/main.rs index d04e06a..204e308 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![windows_subsystem = "windows"] mod data_provider; mod dictionary; -mod dictionary_test; +mod dictionary_quiz; mod history; pub mod import; mod lang; diff --git a/src/navigation.rs b/src/navigation.rs index 7ef1ca7..c628b50 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -4,7 +4,7 @@ use crate::data_provider::web_api::{ get_local_version, get_web_version, load_data, set_local_version, }; use crate::dictionary::{DictionaryMessage, DictionaryState, app_data_dir}; -use crate::dictionary_test::{DictionaryQuizMessage, DictionaryQuizState}; +use crate::dictionary_quiz::{DictionaryQuizMessage, DictionaryQuizState}; use crate::history::{HistoryMessage, HistoryState}; use crate::import::{ImportMessage, ImportState}; use crate::lang::Id; diff --git a/src/sync.rs b/src/sync.rs index de4d134..db05690 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -4,7 +4,7 @@ use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page}; use crate::styling::*; use crate::sync::SyncMessage::*; -use crate::{AppState, RootMessage, fill_state}; +use crate::{fill_state, AppState, RootMessage}; use iced::widget::button::danger; use iced::widget::container::rounded_box; use iced::widget::{button, column, container, progress_bar, row, space, text, toggler}; @@ -59,7 +59,12 @@ impl NavigatedPage for SyncState { .map(|_val: String| RootMessage::Sync(KeyCopied)); } InitSync => { - return Task::perform(first_sync(), |id| RootMessage::Sync(IdReceived(id))); + return Task::perform(first_sync(), |id| { + match id { + Ok(id) => RootMessage::Sync(IdReceived(id)), + Err(_) => RootMessage::None + } + }); } GetKey => { return iced::clipboard::read() @@ -218,10 +223,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 c5c1f34e1c1388c3fefa078a0ebfe09c9f5a7f12 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 18:18:14 +0300 Subject: [PATCH 28/30] Fix history bug --- src/data_provider/card_stats.rs | 9 --------- src/data_provider/web_api.rs | 15 ++++++++------- src/history.rs | 23 +++++++++++++++++------ src/import.rs | 2 +- src/lang.rs | 9 +++++---- src/navigation.rs | 6 ++---- src/repetition.rs | 8 +++++--- 7 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 19efeb4..a4d2ad1 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -73,15 +73,6 @@ pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) { println!("Updated stat: {}", time.elapsed().as_millis()); } -pub fn delete_stat(stat: &CardStatistics, connection: &Connection) { - if !stat.id.is_valid() { - return; - } - connection - .execute("DELETE FROM card_stats WHERE id = ?1", (&stat.id,)) - .unwrap(); -} - pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) { if stat.is_empty() { return; diff --git a/src/data_provider/web_api.rs b/src/data_provider/web_api.rs index 5640474..983d54f 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -1,5 +1,6 @@ use crate::dictionary::app_data_dir; use std::io; +use reqwest::Error; use tokio::fs::{File, OpenOptions}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use zstd::{DEFAULT_COMPRESSION_LEVEL, Decoder, Encoder}; @@ -34,16 +35,16 @@ fn compress(data: Vec) -> Vec { encoder.finish().unwrap() } -pub async fn load_data(id: String, temp: bool) -> Result<(), ()> { +pub async fn load_data(id: String, temp: bool) -> Result<(), Error> { 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(|_| ())? + .await? .bytes() - .await.map_err(|_| ())? + .await? .to_vec(); let data = decompress(data); @@ -51,16 +52,16 @@ pub async fn load_data(id: String, temp: bool) -> Result<(), ()> { 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(|_| ())?; + tokio::fs::write(db_file, data).await.unwrap(); let version_url = format!("{API_URL}{id}/version"); let version = client .get(&version_url) .send() - .await.map_err(|_| ())? + .await? .text() - .await.map_err(|_| ())?; - set_local_version(version.parse::().map_err(|_| ())?).await; + .await?; + set_local_version(version.parse::().unwrap_or_default()).await; Ok(()) } diff --git a/src/history.rs b/src/history.rs index 7399019..380477b 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1,9 +1,8 @@ -use crate::data_provider::history::{HistoryItem, get_history_of_set}; -use crate::lang::WordData; +use crate::data_provider::history::{get_history_of_set, HistoryItem}; +use crate::lang::{WordData, INVALID_ID}; use crate::navigation::{NavigatedPage, Page}; use crate::styling::*; use crate::{AppState, RootMessage}; -use HistoryMessage::Back; use chrono::Local; use iced::alignment::Horizontal::Center; use iced::widget::space::horizontal; @@ -11,6 +10,7 @@ use iced::widget::*; use iced::{Element, Fill, FillPortion, Task}; use parking_lot::Mutex; use std::sync::Arc; +use HistoryMessage::Back; #[derive(Clone)] pub struct HistoryState { @@ -52,8 +52,19 @@ impl HistoryState { let history = get_history_of_set(id); let words = history .iter() - .filter_map(|item| state.dictionary.iter().find(|w| w.id == item.word_id)) - .cloned() + .map( + |item| match state.dictionary.iter().find(|w| w.id == item.word_id) { + None => WordData { + id, + key: "Удаленное слово".to_string(), + value: "Удаленное слово".to_string(), + tags: "Удаленное слово".to_string(), + additional: Default::default(), + group_id: INVALID_ID, + }, + Some(word) => word.clone(), + }, + ) .collect(); Self { @@ -64,7 +75,7 @@ impl HistoryState { fn history_lines(&self) -> Column<'_, HistoryMessage> { let mut column = Column::new(); - for (item, index) in self.list.iter().zip(0..self.list.len()) { + for (index, item) in self.list.iter().enumerate() { column = column.push( row![ iced::widget::column![ diff --git a/src/import.rs b/src/import.rs index 04c75c9..0b05ad3 100644 --- a/src/import.rs +++ b/src/import.rs @@ -489,7 +489,7 @@ impl ImportState { add_words(word, connection); index += 1; let mut progress = progress.lock(); - *progress = index as f32 / total_len; + *progress = (index as f32 / total_len).min(1.0); } state.dictionary.append(&mut words_list); diff --git a/src/lang.rs b/src/lang.rs index 4635267..dcf672c 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,10 +1,11 @@ +use crate::data_provider::card_stats::load_stats_of_deck; +use crate::data_provider::card_stats::update_stat_score; +use crate::data_provider::history::{push_note, HistoryItem}; use crate::AppState; -use crate::data_provider::card_stats::{delete_stat, load_stats_of_deck, update_stat_score}; -use crate::data_provider::history::{HistoryItem, push_note}; use chrono::{DateTime, Utc}; use parking_lot::Mutex; -use rand::distr::Distribution; use rand::distr::weighted::WeightedIndex; +use rand::distr::Distribution; use rand::prelude::SliceRandom; use rand::rng; use rand::rngs::ThreadRng; @@ -12,8 +13,8 @@ use rayon::iter::IndexedParallelIterator; use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rhai::{Engine, Scope}; -use rusqlite::ToSql; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; +use rusqlite::ToSql; use std::cmp::PartialEq; use std::collections::HashMap; use std::fmt::{Display, Formatter}; diff --git a/src/navigation.rs b/src/navigation.rs index c628b50..19d9db8 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,8 +1,6 @@ use crate::data_provider::history::{get_history_of_set, history_dir}; 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::data_provider::web_api::{get_local_version, get_web_version, load_data, set_local_version}; use crate::dictionary::{DictionaryMessage, DictionaryState, app_data_dir}; use crate::dictionary_quiz::{DictionaryQuizMessage, DictionaryQuizState}; use crate::history::{HistoryMessage, HistoryState}; @@ -154,7 +152,7 @@ impl ScreenState { println!("{} {}", local, web); if web > local { println!("Web version is newer than local version"); - load_data(string, true).await; + load_data(string, true).await?; set_local_version(web).await; } else { return Ok(false); diff --git a/src/repetition.rs b/src/repetition.rs index e5ea97e..298a3d2 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -9,8 +9,8 @@ use chrono::Local; use iced::alignment::Horizontal::Center; use iced::keyboard::key::Physical::Code; use iced::widget::text::Style; -use iced::widget::{Column, button, column, container, row, rule, space, text, tooltip}; -use iced::{Element, Fill, Task, Theme, alignment, keyboard}; +use iced::widget::{button, column, container, row, rule, space, text, tooltip, Column}; +use iced::{alignment, keyboard, Element, Fill, Task, Theme}; use parking_lot::Mutex; use rodio::MixerDeviceSink; use std::collections::HashSet; @@ -319,7 +319,9 @@ pub enum RepetitionMessage { async fn play_sound(sink: Arc, text: String) { if let Ok(data) = get_voice(text.as_str()).await { spawn_blocking(move || { - rodio::play(sink.mixer(), data).unwrap().sleep_until_end(); + if let Ok(play) = rodio::play(sink.mixer(), data) { + play.sleep_until_end() + } }) .await .unwrap(); From e8c9e37ca17b1ec6cc3b72c877eb704c2ca26039 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 18:27:05 +0300 Subject: [PATCH 29/30] Fix semirandom srs bug --- src/data_provider/sqlite.rs | 4 +++- src/lang.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 3ef4478..493b0c7 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -17,7 +17,9 @@ pub fn create_db() { } } -fn ensure_db_schema(_: &Connection) {} +fn ensure_db_schema(connection: &Connection) { + +} fn create_tables(conn: &Connection) { make_card_set(conn).unwrap(); diff --git a/src/lang.rs b/src/lang.rs index dcf672c..5322791 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -542,7 +542,7 @@ impl SRSModule for SemiRandomSRSModule { fn next(&mut self, set: &mut DeckData) -> usize { let index = self.last_weights.sample(&mut self.generator); - if self.history.contains(&index) { + if self.history.contains(&index) && self.history.len() != 1 { return self.next(set); } From 1406fc8f21532283225c10cada4d0dfa31d6bcc7 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Thu, 20 Aug 2026 18:53:13 +0300 Subject: [PATCH 30/30] Change TEXT to INTEGER in card_stats --- .gitignore | 3 +-- sql/card_stats.sql | 4 ++-- src/data_provider/import.rs | 40 ++++++++++++++++++++----------------- src/data_provider/sqlite.rs | 4 +--- src/repetition.rs | 26 +++++++++++++++--------- 5 files changed, 43 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 3d5cb63..2a0038a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target -.idea -.DS_Store \ No newline at end of file +.idea \ No newline at end of file diff --git a/sql/card_stats.sql b/sql/card_stats.sql index 1655391..fb2b675 100644 --- a/sql/card_stats.sql +++ b/sql/card_stats.sql @@ -5,9 +5,9 @@ create table card_stats word_id INTEGER not null references words on delete cascade, - set_id TEXT not null + set_id INTEGER not null references card_set on delete cascade, score INTEGER default 1 not null, - last_opened integer not null + last_opened INTEGER not null ); \ No newline at end of file diff --git a/src/data_provider/import.rs b/src/data_provider/import.rs index 0e22cb0..432bfa0 100644 --- a/src/data_provider/import.rs +++ b/src/data_provider/import.rs @@ -40,25 +40,29 @@ pub fn load_groups(connection: &Connection) -> ImportData { .collect::>(); for (_, collection) in sets { - let fields = collection["flds"] - .as_array() - .unwrap() - .iter() - .map(|x| x["name"].as_str().unwrap().to_string()) - .collect(); - let mut group = ImportGroup { - id: collection["id"].as_u64().unwrap(), - name: collection["name"].as_str().unwrap().to_string(), - fields, - mapping: Default::default(), - length: 0, - imported: false, - }; - if let Some((_, count)) = counts.iter().find(|(id, _)| *id == group.id as i64) { - group.length = *count as u64 + let fields_property = collection.get("fields"); + if let Some(fields_property) = fields_property { + let fields = fields_property + .as_array() + .unwrap() + .iter() + .map(|x| x["name"].as_str().unwrap().to_string()) + .collect(); + let mut group = ImportGroup { + id: collection["id"].as_u64().unwrap(), + name: collection["name"].as_str().unwrap().to_string(), + fields, + mapping: Default::default(), + length: 0, + imported: false, + }; + if let Some((_, count)) = counts.iter().find(|(id, _)| *id == group.id as i64) { + group.length = *count as u64 + } + println!("{:?}", group); + total_data.0.push(group); } - println!("{:?}", group); - total_data.0.push(group); + } total_data } diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 493b0c7..3ef4478 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -17,9 +17,7 @@ pub fn create_db() { } } -fn ensure_db_schema(connection: &Connection) { - -} +fn ensure_db_schema(_: &Connection) {} fn create_tables(conn: &Connection) { make_card_set(conn).unwrap(); diff --git a/src/repetition.rs b/src/repetition.rs index 298a3d2..7ef3db8 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -25,7 +25,7 @@ pub struct RepetitionState { current_statistic: CardStatistics, open: bool, can_play: bool, - sink: Arc, + sink: Arc>, opened: HashSet, } @@ -100,7 +100,12 @@ impl RepetitionState { pub(crate) fn new(set: DeckSettings, state: Arc>) -> RepetitionState { let mut card_set = DeckData::new(&set, state.clone()); let (word, stat) = card_set.next(); - let sink_handle = rodio::DeviceSinkBuilder::open_default_sink().unwrap(); + + let sink_handle = if set.forward.contains("speech") || set.backward.contains("speech") { + rodio::DeviceSinkBuilder::open_default_sink().ok() + } else { + None + }; RepetitionState { state, @@ -138,11 +143,12 @@ impl RepetitionState { self.current_word = next.0; self.current_statistic = next.1; - if self.settings.require_speech() { - return Task::perform( - play_sound(self.sink.clone(), self.current_word.key.clone()), - |_| RootMessage::Repetition(RepetitionMessage::PlayFinished), - ); + if self.settings.require_speech() && self.sink.is_some() { + let sink = self.sink.clone(); + let task = play_sound(sink, self.current_word.key.clone()); + return Task::perform(task, |_| { + RootMessage::Repetition(RepetitionMessage::PlayFinished) + }); } self.increment_counter(); Task::none() @@ -316,10 +322,12 @@ pub enum RepetitionMessage { PlayFinished, } -async fn play_sound(sink: Arc, text: String) { +async fn play_sound(sink: Arc>, text: String) { if let Ok(data) = get_voice(text.as_str()).await { spawn_blocking(move || { - if let Ok(play) = rodio::play(sink.mixer(), data) { + if let Some(sink) = sink.as_ref() + && let Ok(play) = rodio::play(sink.mixer(), data) + { play.sleep_until_end() } })