Add anki21 format and fix multi import bug

This commit is contained in:
2026-08-15 20:16:48 +03:00
parent e676898135
commit 8f789facd7
11 changed files with 214 additions and 167 deletions
Generated
+17
View File
@@ -2320,6 +2320,7 @@ dependencies = [
"iced_core", "iced_core",
"mimalloc", "mimalloc",
"rand", "rand",
"rayon",
"reqwest", "reqwest",
"rfd", "rfd",
"rhai", "rhai",
@@ -2792,6 +2793,15 @@ dependencies = [
"jni-sys 0.3.0", "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]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.6" version = "0.4.6"
@@ -3995,6 +4005,7 @@ checksum = "dd4dd0f8c36625202a4ba553c416c19b719947cd2a31d1bda06126e4a5727daf"
dependencies = [ dependencies = [
"ahash", "ahash",
"bitflags 2.10.0", "bitflags 2.10.0",
"no-std-compat",
"num-traits", "num-traits",
"once_cell", "once_cell",
"rhai_codegen", "rhai_codegen",
@@ -4591,6 +4602,12 @@ dependencies = [
"x11rb", "x11rb",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "spirv" name = "spirv"
version = "0.3.0+sdk-1.3.268.0" version = "0.3.0+sdk-1.3.268.0"
+2 -1
View File
@@ -10,7 +10,7 @@ rand = "0.10.2"
dirs = "6.0.0" dirs = "6.0.0"
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151" serde_json = "1.0.151"
rhai = "1.25.1" rhai = { version = "1.25.1", features = ["sync"] }
chrono = "0.4.45" chrono = "0.4.45"
rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] } rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] }
reqwest = { version = "0.13.4", features = ["json", "stream", "multipart"] } reqwest = { version = "0.13.4", features = ["json", "stream", "multipart"] }
@@ -23,6 +23,7 @@ zip = "8.6.0"
rfd = "0.17.2" rfd = "0.17.2"
mimalloc = "0.1.52" mimalloc = "0.1.52"
hashbrown = "0.17.1" hashbrown = "0.17.1"
rayon = "1.12.0"
[profile.super-release] [profile.super-release]
inherits = "release" inherits = "release"
+4 -3
View File
@@ -1,5 +1,5 @@
use crate::lang::SetOrderMode; use crate::lang::{AppendMode, CardSetSettings, OrderMode};
use crate::repetitions::CardSetSettings;
use rusqlite::Connection; use rusqlite::Connection;
pub fn load_sets(connection: &Connection) -> Vec<CardSetSettings> { pub fn load_sets(connection: &Connection) -> Vec<CardSetSettings> {
@@ -16,7 +16,8 @@ pub fn load_sets(connection: &Connection) -> Vec<CardSetSettings> {
filter: row.get(4)?, filter: row.get(4)?,
count: None, count: None,
worst_words_list: None, worst_words_list: None,
open_mode: SetOrderMode::Default, open_mode: OrderMode::Default,
append_mode: AppendMode::Full,
}) })
}) })
.unwrap(); .unwrap();
+1 -2
View File
@@ -1,5 +1,4 @@
use crate::lang::CardStatistics; use crate::lang::{CardSetSettings, CardStatistics};
use crate::repetitions::CardSetSettings;
use rusqlite::Connection; use rusqlite::Connection;
use std::time::Instant; use std::time::Instant;
View File
+13 -7
View File
@@ -80,10 +80,12 @@ impl NavigatedPage<ImportMessage> for ImportState {
Back => {} Back => {}
UpdateFile(path) => { UpdateFile(path) => {
self.path = Some(path); self.path = Some(path);
self.import_data = None;
return self.load_package(); return self.load_package();
} }
SelectFile => return Self::select_import_file(), SelectFile => return Self::select_import_file(),
UpdateImport(import) => { UpdateImport(import) => {
self.selected_index = 0;
self.import_data = Some(import); self.import_data = Some(import);
} }
NextGroup => { NextGroup => {
@@ -129,7 +131,8 @@ impl NavigatedPage<ImportMessage> for ImportState {
ImportFinished => { ImportFinished => {
self.progress = None; self.progress = None;
let group = self let group = self
.import_data.as_mut() .import_data
.as_mut()
.unwrap() .unwrap()
.0 .0
.get_mut(self.selected_index) .get_mut(self.selected_index)
@@ -200,7 +203,9 @@ impl ImportState {
fn import_button(&self, group: &ImportGroup) -> Element<'_, ImportMessage> { fn import_button(&self, group: &ImportGroup) -> Element<'_, ImportMessage> {
if group.imported { if group.imported {
return container(text!("Группа слов успешно импортирована")).padding(HALF_SPACING).align_x(Center) return container(text!("Группа слов успешно импортирована"))
.padding(HALF_SPACING)
.align_x(Center)
.style(success) .style(success)
.width(Fill) .width(Fill)
.into(); .into();
@@ -379,8 +384,6 @@ impl ImportState {
let file = File::open(path).map_err(|_| ())?; let file = File::open(path).map_err(|_| ())?;
let reader = BufReader::new(file); let reader = BufReader::new(file);
let mut archive = ZipArchive::new(reader).map_err(|_| ())?; 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 temp_file_path = app_data_dir().join("import");
let mut temp_file = OpenOptions::new() let mut temp_file = OpenOptions::new()
.write(true) .write(true)
@@ -388,6 +391,10 @@ impl ImportState {
.truncate(true) .truncate(true)
.open(&temp_file_path) .open(&temp_file_path)
.map_err(|_| ())?; .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(|_| ())?; std::io::copy(&mut file, &mut temp_file).map_err(|_| ())?;
Ok(()) Ok(())
} else { } else {
@@ -476,15 +483,14 @@ impl ImportState {
} }
} }
println!("{word:?}");
words_list.push(word); words_list.push(word);
} }
let mut state = state.lock().unwrap(); let mut state = state.lock().unwrap();
let connection = &mut state.connection; let connection = &mut state.connection;
let mut index = 0; let mut index = 0;
let total_len = words_list.len() as f32 / 256.0; let total_len = words_list.len() as f32 / 1024.0;
for word in &mut words_list.chunks_mut(256) { for word in &mut words_list.chunks_mut(1024) {
add_words(word, connection); add_words(word, connection);
index += 1; index += 1;
let mut progress = progress.lock().unwrap(); let mut progress = progress.lock().unwrap();
+147 -31
View File
@@ -1,8 +1,7 @@
use crate::data_provider::card_stats::{ 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::data_provider::history::{push_note, HistoryItem};
use crate::repetitions::CardSetSettings;
use crate::AppState; use crate::AppState;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use rand::distr::weighted::WeightedIndex; use rand::distr::weighted::WeightedIndex;
@@ -10,6 +9,9 @@ use rand::distr::Distribution;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use rand::rng; use rand::rng;
use rand::rngs::ThreadRng; use rand::rngs::ThreadRng;
use rayon::iter::IntoParallelRefIterator;
use rayon::iter::ParallelIterator;
use rhai::{Engine, Scope};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::min; use std::cmp::min;
use std::collections::HashMap; use std::collections::HashMap;
@@ -315,33 +317,32 @@ impl CardSet {
let mut current_set = load_stats_of_set(settings, &state_locked.connection); let mut current_set = load_stats_of_set(settings, &state_locked.connection);
let last_list = settings.get_word_list(&state_locked); let last_list = settings.get_word_list(&state_locked);
let saved_ids = current_set.iter().map(|l| l.word_id).collect::<Vec<u32>>();
let word_ids = last_list.iter().map(|l| l.id).collect::<Vec<u32>>(); let word_ids = last_list.iter().map(|l| l.id).collect::<Vec<u32>>();
let new_stats: &mut Vec<CardStatistics> = &mut last_list // let new_stats: &mut Vec<CardStatistics> = &mut last_list
.iter() // .iter()
.filter(|word| !saved_ids.contains(&word.id)) // .filter(|word| !saved_ids.contains(&word.id))
.map(|word| CardStatistics { // .map(|word| CardStatistics {
id: 0, // id: 0,
word_id: word.id, // word_id: word.id,
last_open: Utc::now(), // last_open: Utc::now(),
score: 1, // score: 1,
set_id: settings.id, // set_id: settings.id,
}) // })
.collect(); // .collect();
//
let time = Instant::now(); // let time = Instant::now();
//
if !new_stats.is_empty() { // if !new_stats.is_empty() {
add_stat_list(new_stats, &state_locked.connection); // add_stat_list(new_stats, &state_locked.connection);
current_set.append(new_stats); // current_set.append(new_stats);
} // }
//
println!( // println!(
"Added {} stats: {}", // "Added {} stats: {}",
new_stats.len(), // new_stats.len(),
time.elapsed().as_millis() // time.elapsed().as_millis()
); // );
let mut index = 0; let mut index = 0;
for stat in current_set.clone() { for stat in current_set.clone() {
if !word_ids.contains(&stat.word_id) { if !word_ids.contains(&stat.word_id) {
@@ -358,11 +359,11 @@ impl CardSet {
current_word_index: None, current_word_index: None,
state: state_for, state: state_for,
order_module: match settings.open_mode { order_module: match settings.open_mode {
SetOrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()), OrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()),
SetOrderMode::TrainWorstFirst => { OrderMode::TrainWorstFirst => {
OrderModule::WorstWordsSRS(WorstWordsSRSModule::new()) OrderModule::WorstWordsSRS(WorstWordsSRSModule::new())
} }
SetOrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()),
}, },
settings: settings.clone(), settings: settings.clone(),
} }
@@ -444,12 +445,18 @@ impl CardSet {
} }
#[derive(Clone, PartialEq, Copy, Eq)] #[derive(Clone, PartialEq, Copy, Eq)]
pub enum SetOrderMode { pub enum OrderMode {
Default, Default,
TrainWorstFirst, TrainWorstFirst,
FullRandom, FullRandom,
} }
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum AppendMode{
Full,
Manual
}
#[derive(Clone)] #[derive(Clone)]
enum OrderModule { enum OrderModule {
SemiRandomSRS(SemiRandomSRSModule), SemiRandomSRS(SemiRandomSRSModule),
@@ -619,3 +626,112 @@ impl WorstWordsSRSModule {
self.pool = worst; 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<usize>,
pub worst_words_list: Option<Vec<WordData>>,
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<WordData> {
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::<bool>(&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::<f32>() / stats.len() as f32;
let avg = avg * 0.7;
let bad: Vec<WordData> = 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());
}
}
+2 -2
View File
@@ -16,15 +16,15 @@ mod word;
mod writing; mod writing;
mod repetition_settings; mod repetition_settings;
pub mod import; pub mod import;
pub mod helpers;
use crate::data_provider::card_sets::load_sets; use crate::data_provider::card_sets::load_sets;
use crate::data_provider::settings::get_setting; use crate::data_provider::settings::get_setting;
use crate::data_provider::sqlite::default_connection; use crate::data_provider::sqlite::default_connection;
use crate::data_provider::words::{load_word_groups, load_words}; 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::navigation::{AppSettings, RootMessage, ScreenState};
use crate::quiz::*; use crate::quiz::*;
use crate::repetitions::CardSetSettings;
use crate::RootMessage::Keyboard; use crate::RootMessage::Keyboard;
use chrono::NaiveDate; use chrono::NaiveDate;
use iced::{keyboard, Subscription, Theme}; use iced::{keyboard, Subscription, Theme};
+1 -2
View File
@@ -1,8 +1,7 @@
use crate::data_provider::voice::get_voice; 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::Page::PreviousPage;
use crate::navigation::{KeyPressedPage, NavigatedPage, Page}; use crate::navigation::{KeyPressedPage, NavigatedPage, Page};
use crate::repetitions::CardSetSettings;
use crate::styling::*; use crate::styling::*;
use crate::{AppState, RootMessage}; use crate::{AppState, RootMessage};
use chrono::Local; use chrono::Local;
+1 -1
View File
@@ -2,7 +2,6 @@ use crate::data_provider::card_sets::{delete_set, update_card_set};
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::repetition_settings::RepetitionSettingsMessage::*; use crate::repetition_settings::RepetitionSettingsMessage::*;
use crate::repetitions::CardSetSettings;
use crate::styling::*; use crate::styling::*;
use crate::AppState; use crate::AppState;
use iced::widget::button::danger; use iced::widget::button::danger;
@@ -12,6 +11,7 @@ use iced_core::Length::Fill;
use iced_core::Padding; use iced_core::Padding;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
use crate::lang::CardSetSettings;
pub struct RepetitionSettingsState { pub struct RepetitionSettingsState {
set: CardSetSettings, set: CardSetSettings,
+19 -111
View File
@@ -1,7 +1,6 @@
use crate::data_provider::card_sets::{delete_set, update_card_set}; 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::history::HistoryState;
use crate::lang::{SetOrderMode, WordData}; use crate::lang::{AppendMode, CardSetSettings, OrderMode};
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
use crate::navigation::{NavigatedPage, Page}; use crate::navigation::{NavigatedPage, Page};
use crate::repetition::RepetitionState; 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::border::Radius;
use iced_core::svg::Handle; use iced_core::svg::Handle;
use iced_core::Padding; use iced_core::Padding;
use rhai::{Engine, Scope};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[derive(Clone)] #[derive(Clone)]
@@ -121,6 +119,10 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
.unwrap() .unwrap()
.open_mode = mode; .open_mode = mode;
} }
SetAppendMode(mode) => {
let set = state.card_sets.get_mut(self.selected_set.unwrap()).unwrap();
set.append_mode = mode;
}
} }
Task::none() Task::none()
} }
@@ -230,11 +232,12 @@ impl RepetitionsState {
}, },
radio( radio(
"Обычный режим", "Обычный режим",
SetOrderMode::Default, OrderMode::Default,
Some(set.open_mode), Some(set.open_mode),
SetOpenMode SetOpenMode
), ),
self.words_words_view(&set), // self.words_words_view(&set),
self.word_append_panel(&set),
button("История").style(jl_button).on_press(GoToHistory), button("История").style(jl_button).on_press(GoToHistory),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
@@ -342,13 +345,13 @@ impl RepetitionsState {
container(scrollable(self.worst_words_list(set)).height(200)).style(bordered_box), container(scrollable(self.worst_words_list(set)).height(200)).style(bordered_box),
radio( radio(
"Начать с плохих слов", "Начать с плохих слов",
SetOrderMode::TrainWorstFirst, OrderMode::TrainWorstFirst,
Some(set.open_mode), Some(set.open_mode),
SetOpenMode SetOpenMode
), ),
radio( radio(
"Полностью случайно", "Полностью случайно",
SetOrderMode::FullRandom, OrderMode::FullRandom,
Some(set.open_mode), Some(set.open_mode),
SetOpenMode SetOpenMode
) )
@@ -357,6 +360,13 @@ impl RepetitionsState {
.into() .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> { fn worst_words_list(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> {
let mut column = Column::new(); let mut column = Column::new();
@@ -421,111 +431,9 @@ pub enum RepetitionsMessage {
SetBackward(String), SetBackward(String),
SetFilter(String), SetFilter(String),
TryFilter, TryFilter,
SetOpenMode(SetOrderMode), SetOpenMode(OrderMode),
SetAppendMode(AppendMode),
GoToHistory, GoToHistory,
GoToSettings, GoToSettings,
} }
#[derive(Clone)]
pub struct CardSetSettings {
pub id: u32,
pub name: String,
pub forward: String,
pub backward: String,
pub filter: String,
pub count: Option<usize>,
pub worst_words_list: Option<Vec<WordData>>,
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<WordData> {
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::<bool>(&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::<f32>() / stats.len() as f32;
let avg = avg * 0.7;
let bad: Vec<WordData> = 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());
}
}