From 8f789facd7d705dc108ec1d7cfe54d81df8e59ec Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Sat, 15 Aug 2026 20:16:48 +0300 Subject: [PATCH] Add anki21 format and fix multi import bug --- Cargo.lock | 17 +++ Cargo.toml | 3 +- src/data_provider/card_sets.rs | 7 +- src/data_provider/card_stats.rs | 3 +- src/helpers.rs | 0 src/import.rs | 34 +++--- src/lang.rs | 178 ++++++++++++++++++++++++++------ src/main.rs | 4 +- src/repetition.rs | 3 +- src/repetition_settings.rs | 2 +- src/repetitions.rs | 130 ++++------------------- 11 files changed, 214 insertions(+), 167 deletions(-) create mode 100644 src/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 6f41fa7..379a3a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2320,6 +2320,7 @@ dependencies = [ "iced_core", "mimalloc", "rand", + "rayon", "reqwest", "rfd", "rhai", @@ -2792,6 +2793,15 @@ dependencies = [ "jni-sys 0.3.0", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +dependencies = [ + "spin", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3995,6 +4005,7 @@ checksum = "dd4dd0f8c36625202a4ba553c416c19b719947cd2a31d1bda06126e4a5727daf" dependencies = [ "ahash", "bitflags 2.10.0", + "no-std-compat", "num-traits", "once_cell", "rhai_codegen", @@ -4591,6 +4602,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" diff --git a/Cargo.toml b/Cargo.toml index df9e21f..2f57537 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ rand = "0.10.2" dirs = "6.0.0" serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" -rhai = "1.25.1" +rhai = { version = "1.25.1", features = ["sync"] } chrono = "0.4.45" rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] } reqwest = { version = "0.13.4", features = ["json", "stream", "multipart"] } @@ -23,6 +23,7 @@ zip = "8.6.0" rfd = "0.17.2" mimalloc = "0.1.52" hashbrown = "0.17.1" +rayon = "1.12.0" [profile.super-release] inherits = "release" diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 8a4f701..eed91c5 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,5 +1,5 @@ -use crate::lang::SetOrderMode; -use crate::repetitions::CardSetSettings; +use crate::lang::{AppendMode, CardSetSettings, OrderMode}; + use rusqlite::Connection; pub fn load_sets(connection: &Connection) -> Vec { @@ -16,7 +16,8 @@ pub fn load_sets(connection: &Connection) -> Vec { filter: row.get(4)?, count: None, worst_words_list: None, - open_mode: SetOrderMode::Default, + open_mode: OrderMode::Default, + append_mode: AppendMode::Full, }) }) .unwrap(); diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index d7ae457..89b5cbb 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,5 +1,4 @@ -use crate::lang::CardStatistics; -use crate::repetitions::CardSetSettings; +use crate::lang::{CardSetSettings, CardStatistics}; use rusqlite::Connection; use std::time::Instant; diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/import.rs b/src/import.rs index 664cedb..18f7e57 100644 --- a/src/import.rs +++ b/src/import.rs @@ -80,10 +80,12 @@ impl NavigatedPage for ImportState { Back => {} UpdateFile(path) => { self.path = Some(path); + self.import_data = None; return self.load_package(); } SelectFile => return Self::select_import_file(), UpdateImport(import) => { + self.selected_index = 0; self.import_data = Some(import); } NextGroup => { @@ -129,7 +131,8 @@ impl NavigatedPage for ImportState { ImportFinished => { self.progress = None; let group = self - .import_data.as_mut() + .import_data + .as_mut() .unwrap() .0 .get_mut(self.selected_index) @@ -200,7 +203,9 @@ impl ImportState { fn import_button(&self, group: &ImportGroup) -> Element<'_, ImportMessage> { if group.imported { - return container(text!("Группа слов успешно импортирована")).padding(HALF_SPACING).align_x(Center) + return container(text!("Группа слов успешно импортирована")) + .padding(HALF_SPACING) + .align_x(Center) .style(success) .width(Fill) .into(); @@ -379,15 +384,17 @@ impl ImportState { let file = File::open(path).map_err(|_| ())?; let reader = BufReader::new(file); let mut archive = ZipArchive::new(reader).map_err(|_| ())?; - let db_file = archive.by_name("collection.anki2"); - if let Ok(mut file) = db_file { - let temp_file_path = app_data_dir().join("import"); - let mut temp_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&temp_file_path) - .map_err(|_| ())?; + let temp_file_path = app_data_dir().join("import"); + let mut temp_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&temp_file_path) + .map_err(|_| ())?; + if let Ok(mut file) = archive.by_name("collection.anki21") { + std::io::copy(&mut file, &mut temp_file).map_err(|_| ())?; + Ok(()) + } else if let Ok(mut file) = archive.by_name("collection.anki2") { std::io::copy(&mut file, &mut temp_file).map_err(|_| ())?; Ok(()) } else { @@ -476,15 +483,14 @@ impl ImportState { } } - println!("{word:?}"); words_list.push(word); } let mut state = state.lock().unwrap(); let connection = &mut state.connection; let mut index = 0; - let total_len = words_list.len() as f32 / 256.0; - for word in &mut words_list.chunks_mut(256) { + 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(); diff --git a/src/lang.rs b/src/lang.rs index 6e09572..3cc28d4 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,8 +1,7 @@ use crate::data_provider::card_stats::{ - add_stat_list, delete_stat, load_stats_of_set, update_stat_score, + delete_stat, load_stats_of_set, update_stat_score, }; use crate::data_provider::history::{push_note, HistoryItem}; -use crate::repetitions::CardSetSettings; use crate::AppState; use chrono::{DateTime, Utc}; use rand::distr::weighted::WeightedIndex; @@ -10,6 +9,9 @@ use rand::distr::Distribution; use rand::prelude::SliceRandom; use rand::rng; use rand::rngs::ThreadRng; +use rayon::iter::IntoParallelRefIterator; +use rayon::iter::ParallelIterator; +use rhai::{Engine, Scope}; use serde::{Deserialize, Serialize}; use std::cmp::min; use std::collections::HashMap; @@ -315,33 +317,32 @@ impl CardSet { let mut current_set = load_stats_of_set(settings, &state_locked.connection); let last_list = settings.get_word_list(&state_locked); - let saved_ids = current_set.iter().map(|l| l.word_id).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 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) { @@ -358,11 +359,11 @@ impl CardSet { current_word_index: None, state: state_for, order_module: match settings.open_mode { - SetOrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()), - SetOrderMode::TrainWorstFirst => { + OrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()), + OrderMode::TrainWorstFirst => { OrderModule::WorstWordsSRS(WorstWordsSRSModule::new()) } - SetOrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), + OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), }, settings: settings.clone(), } @@ -444,12 +445,18 @@ impl CardSet { } #[derive(Clone, PartialEq, Copy, Eq)] -pub enum SetOrderMode { +pub enum OrderMode { Default, TrainWorstFirst, FullRandom, } +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum AppendMode{ + Full, + Manual +} + #[derive(Clone)] enum OrderModule { SemiRandomSRS(SemiRandomSRSModule), @@ -619,3 +626,112 @@ impl WorstWordsSRSModule { self.pool = worst; } } + +#[derive(Clone)] +pub struct CardSetSettings { + pub id: u32, + pub name: String, + pub forward: String, + pub backward: String, + pub filter: String, + 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 { + id: 0, + name, + forward: "".to_string(), + backward: "".to_string(), + filter: "true".to_string(), + count: None, + worst_words_list: None, + open_mode: OrderMode::Default, + append_mode: AppendMode::Full + } + } + + 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![]; + let engine = Engine::new(); + + let ast = engine.compile(&self.filter); + if ast.is_err() { + return list; + } + + let ast = ast.unwrap(); + + let groups = &state.word_groups; + + list = state.dictionary.par_iter().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) + .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() + }).cloned().collect(); + + println!("Collecting available words is {:?}", time.elapsed()); + + list + } + + pub fn require_speech(&self) -> bool { + self.forward == "speech" || self.backward == "speech" + } + + pub(crate) fn update_worst_words(&mut self, state: &AppState) { + if self.worst_words_list.is_some() { + return; + } + + let connection = &state.connection; + let mut stats = load_stats_of_set(self, connection); + stats.sort_by_key(|s| s.calculated_score() as i32); + let avg = stats.iter().map(|s| s.calculated_score()).sum::() / stats.len() as f32; + let avg = avg * 0.7; + let bad: Vec = stats + .iter() + .take_while(|word| word.calculated_score() < avg) + .map(|stat| { + state.dictionary[state + .dictionary + .binary_search_by_key(&stat.word_id, |x| x.id) + .unwrap()] + .clone() + }) + .collect(); + + self.worst_words_list = Some(bad.clone()); + } +} diff --git a/src/main.rs b/src/main.rs index 40d26ac..01ec997 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,15 +16,15 @@ mod word; mod writing; mod repetition_settings; pub mod import; +pub mod helpers; 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::{WordData, WordGroup}; +use crate::lang::{CardSetSettings, WordData, WordGroup}; use crate::navigation::{AppSettings, RootMessage, ScreenState}; use crate::quiz::*; -use crate::repetitions::CardSetSettings; use crate::RootMessage::Keyboard; use chrono::NaiveDate; use iced::{keyboard, Subscription, Theme}; diff --git a/src/repetition.rs b/src/repetition.rs index 00e19b9..1b6e283 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -1,8 +1,7 @@ use crate::data_provider::voice::get_voice; -use crate::lang::{CardSet, CardStatistics, WordData, WordOpenMode}; +use crate::lang::{CardSet, CardSetSettings, CardStatistics, WordData, WordOpenMode}; use crate::navigation::Page::PreviousPage; use crate::navigation::{KeyPressedPage, NavigatedPage, Page}; -use crate::repetitions::CardSetSettings; use crate::styling::*; use crate::{AppState, RootMessage}; use chrono::Local; diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index 1ad0c4d..6450e5a 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -2,7 +2,6 @@ use crate::data_provider::card_sets::{delete_set, update_card_set}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::repetition_settings::RepetitionSettingsMessage::*; -use crate::repetitions::CardSetSettings; use crate::styling::*; use crate::AppState; use iced::widget::button::danger; @@ -12,6 +11,7 @@ use iced_core::Length::Fill; use iced_core::Padding; use std::sync::{Arc, Mutex}; use std::time::Duration; +use crate::lang::CardSetSettings; pub struct RepetitionSettingsState { set: CardSetSettings, diff --git a/src/repetitions.rs b/src/repetitions.rs index 5ff2620..9839f1b 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,7 +1,6 @@ use crate::data_provider::card_sets::{delete_set, update_card_set}; -use crate::data_provider::card_stats::load_stats_of_set; use crate::history::HistoryState; -use crate::lang::{SetOrderMode, WordData}; +use crate::lang::{AppendMode, CardSetSettings, OrderMode}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page}; use crate::repetition::RepetitionState; @@ -19,7 +18,6 @@ use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Tas use iced_core::border::Radius; use iced_core::svg::Handle; use iced_core::Padding; -use rhai::{Engine, Scope}; use std::sync::{Arc, Mutex}; #[derive(Clone)] @@ -121,6 +119,10 @@ impl NavigatedPage for RepetitionsState { .unwrap() .open_mode = mode; } + SetAppendMode(mode) => { + let set = state.card_sets.get_mut(self.selected_set.unwrap()).unwrap(); + set.append_mode = mode; + } } Task::none() } @@ -230,11 +232,12 @@ impl RepetitionsState { }, radio( "Обычный режим", - SetOrderMode::Default, + OrderMode::Default, Some(set.open_mode), SetOpenMode ), - self.words_words_view(&set), + // self.words_words_view(&set), + self.word_append_panel(&set), button("История").style(jl_button).on_press(GoToHistory), ] .spacing(DEFAULT_SPACING) @@ -342,13 +345,13 @@ impl RepetitionsState { container(scrollable(self.worst_words_list(set)).height(200)).style(bordered_box), radio( "Начать с плохих слов", - SetOrderMode::TrainWorstFirst, + OrderMode::TrainWorstFirst, Some(set.open_mode), SetOpenMode ), radio( "Полностью случайно", - SetOrderMode::FullRandom, + OrderMode::FullRandom, Some(set.open_mode), SetOpenMode ) @@ -357,6 +360,13 @@ impl RepetitionsState { .into() } + fn word_append_panel(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { + column![ + text!("Режим добавления карточек"), + row![radio("Добавлять все доступные", AppendMode::Full, Some(set.append_mode), SetAppendMode), radio("Добавлять вручную", AppendMode::Manual, Some(set.append_mode), SetAppendMode)].spacing(HALF_SPACING), + ].spacing(HALF_SPACING).into() + } + fn worst_words_list(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { let mut column = Column::new(); @@ -421,111 +431,9 @@ pub enum RepetitionsMessage { SetBackward(String), SetFilter(String), TryFilter, - SetOpenMode(SetOrderMode), + SetOpenMode(OrderMode), + SetAppendMode(AppendMode), GoToHistory, GoToSettings, } -#[derive(Clone)] -pub struct CardSetSettings { - pub id: u32, - pub name: String, - pub forward: String, - pub backward: String, - pub filter: String, - pub count: Option, - pub worst_words_list: Option>, - pub open_mode: SetOrderMode, -} - -impl CardSetSettings { - fn with_name(name: String) -> CardSetSettings { - CardSetSettings { - id: 0, - name, - forward: "".to_string(), - backward: "".to_string(), - filter: "true".to_string(), - count: None, - worst_words_list: None, - open_mode: SetOrderMode::Default, - } - } - - 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 mut list = vec![]; - let engine = Engine::new(); - let ast = engine.compile(&self.filter); - if ast.is_err() { - return list; - } - - let ast = ast.unwrap(); - - let groups = &state.word_groups; - - for word in &state.dictionary { - 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("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); - if result.is_ok() && result.unwrap() { - list.push(word.clone()); - } - } - - list - } - - pub fn require_speech(&self) -> bool { - self.forward == "speech" || self.backward == "speech" - } - - fn update_worst_words(&mut self, state: &AppState) { - if self.worst_words_list.is_some() { - return; - } - - let connection = &state.connection; - let mut stats = load_stats_of_set(self, connection); - stats.sort_by_key(|s| s.calculated_score() as i32); - let avg = stats.iter().map(|s| s.calculated_score()).sum::() / stats.len() as f32; - let avg = avg * 0.7; - let bad: Vec = stats - .iter() - .take_while(|word| word.calculated_score() < avg) - .map(|stat| { - state.dictionary[state - .dictionary - .binary_search_by_key(&stat.word_id, |x| x.id) - .unwrap()] - .clone() - }) - .collect(); - - self.worst_words_list = Some(bad.clone()); - } -}