diff --git a/Cargo.lock b/Cargo.lock index 379a3a1..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", @@ -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..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] @@ -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/benches/split_bench.rs b/benches/split_bench.rs deleted file mode 100644 index e15b4e7..0000000 --- a/benches/split_bench.rs +++ /dev/null @@ -1,42 +0,0 @@ -use criterion::{Criterion, black_box, 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/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/card_sets.rs b/src/data_provider/card_sets.rs index 46c771f..7e223be 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,8 +29,8 @@ pub fn load_sets(connection: &Connection) -> Vec { buffer } -pub fn add_set(set: &mut CardSetSettings, connection: &Connection) { - let index = connection +pub fn add_set(set: &mut DeckSettings, connection: &Connection) { + let index : u32 = connection .query_row( "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", ( @@ -42,13 +41,13 @@ pub fn add_set(set: &mut CardSetSettings, connection: &Connection) { ), |row| row.get(0) ) - .unwrap_or_else(|e| {println!("{}", e); 0}); + .unwrap(); - set.id = index; + set.id = index.into(); } -pub fn update_card_set(set: &mut CardSetSettings, connection: &Connection) { - if set.id == 0 { +pub fn update_deck(set: &mut DeckSettings, connection: &Connection) { + if !set.id.is_valid() { add_set(set, connection); } else { connection @@ -62,18 +61,15 @@ pub fn update_card_set(set: &mut CardSetSettings, connection: &Connection) { &set.id ), ) - .unwrap_or_else(|e| {println!("{}", e); 0}); + .unwrap(); } } -pub fn delete_set(set: &CardSetSettings, connection: &Connection) { - if set.id == 0 { +pub fn delete_set(set: &DeckSettings, connection: &Connection) { + if !set.id.is_valid() { return; } 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 048c7b0..a4d2ad1 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 rusqlite::Connection; +use crate::lang::{CardStatistics, DeckSettings}; +use rusqlite::{Connection, params}; use std::time::Instant; -pub fn load_stats_of_set(set: &CardSetSettings, 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(); @@ -26,66 +26,40 @@ pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec< buffer } -pub fn add_stat_list(stat: &mut [CardStatistics], connection: &Connection) { +pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) { let time = Instant::now(); - let inserting = stat - .iter() - .map(|stat| { - format!( - "({}, {}, {}, {})", + let tx = connection.transaction().unwrap(); + let count = stats.len(); + + { + let mut stmt = tx + .prepare( + "INSERT INTO card_stats (word_id, set_id, score, last_opened) VALUES (?1, ?2, ?3, ?4)", + ) + .unwrap(); + + for stat in stats.iter() { + stmt.execute(params![ stat.word_id, stat.set_id, stat.score, stat.last_open.timestamp() - ) - }) - .collect::>() - .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; + stats[index].id = id.into(); } - - println!("Added {} cards for {:?}", stat.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()); -// } + println!("Added {} cards for {:?}", stats.len(), time.elapsed()); +} pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) { let time = Instant::now(); @@ -95,21 +69,23 @@ 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()); } -pub fn delete_stat(stat: &CardStatistics, connection: &Connection) { - if stat.id == 0 { +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("DELETE FROM card_stats WHERE id = ?1", (&stat.id,)) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .execute( + 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 7b0a6ec..e4e253b 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(); @@ -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_or_default(), + 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(), }); } } @@ -48,7 +56,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 +92,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/import.rs b/src/data_provider/import.rs index 54e360f..432bfa0 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); @@ -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/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/voice.rs b/src/data_provider/voice.rs index 45a6833..c24b8fd 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,7 +20,7 @@ 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")) @@ -28,13 +28,13 @@ pub async fn get_voice(text: &str) -> BufReader { .body(query) .send() .await - .unwrap() + .map_err(|_| ())? .bytes() .await - .unwrap(); + .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/data_provider/web_api.rs b/src/data_provider/web_api.rs index f73519a..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}; @@ -7,10 +8,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 +22,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 +35,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<(), 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 - .unwrap() + .await? .bytes() - .await - .unwrap() + .await? .to_vec(); let data = decompress(data); @@ -60,12 +58,11 @@ pub async fn load_data(id: String, temp: bool) { let version = client .get(&version_url) .send() - .await - .unwrap() + .await? .text() - .await - .unwrap(); - set_local_version(version.parse::().unwrap()).await; + .await?; + set_local_version(version.parse::().unwrap_or_default()).await; + Ok(()) } pub fn decompress(data: Vec) -> Vec { @@ -78,7 +75,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,19 +83,20 @@ 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 { 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 { @@ -111,7 +109,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/data_provider/words.rs b/src/data_provider/words.rs index a90fe4a..a02ea89 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -3,7 +3,7 @@ use rusqlite::{Connection, params}; 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,13 +14,9 @@ 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; + word.id = index.into(); } pub fn add_words(words: &mut [WordData], connection: &mut Connection) { @@ -46,25 +42,18 @@ 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; 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.is_valid() { add_word(word, connection); } else { connection @@ -78,23 +67,17 @@ pub fn update_word(word: &mut WordData, connection: &Connection) { &word.id, ), ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + .unwrap(); } } pub fn delete_word(word: &WordData, connection: &Connection) { - if word.id == 0 { + if !word.id.is_valid() { return; } 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 { @@ -145,22 +128,19 @@ 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; + group.id = index.into(); } pub fn update_group(group: &mut WordGroup, connection: &Connection) { - if group.id == 0 { + if !group.id.is_valid() { add_group(group, connection); } else { connection @@ -168,21 +148,15 @@ 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(); } } pub fn delete_group(group: &WordGroup, connection: &Connection) { - if group.id == 0 { + if !group.id.is_valid() { return; } 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 06f12e7..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}; @@ -9,6 +9,7 @@ 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}; @@ -17,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 hashbrown::{HashMap, HashSet}; use std::fs; use std::ops::Add; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::{Duration, Instant}; #[derive(Clone)] @@ -65,7 +66,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); } @@ -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,11 +93,11 @@ 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(); } - if word.id != 0 { + if word.id.is_valid() { return Some(Word(WordState::new(word, *index, self.state.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(); @@ -150,12 +151,11 @@ 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); } - dict.get_mut(i).unwrap().tags = v; + dict[i].tags = v; } self.update_tags(); @@ -163,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); @@ -172,9 +172,9 @@ 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(); + let state = self.state.lock(); index = state.word_groups[self.selected_group_index].id; } self.tag_map.insert(t, v); @@ -194,37 +194,33 @@ 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, + id: 0.into(), name: format!("Группа слов {}", random_range(100..1000)), }); } 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 group = &mut state - .word_groups - .get(self.selected_group_index) - .unwrap() - .clone(); + 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; - let index: u32; + let index; { - let state = self.state.lock().unwrap(); + let state = self.state.lock(); index = state.word_groups[i].id; } self.update_words_include(index) @@ -234,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; @@ -277,7 +273,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), ] @@ -294,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], @@ -314,9 +310,9 @@ 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.get(i).unwrap().clone(); + let word = &mut state.dictionary[i].clone(); update_word(word, connection); state.dictionary[i] = word.clone(); @@ -336,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; @@ -423,7 +419,7 @@ impl DictionaryState { let line_button = || { let action = WordAction(index); - if data.id == 0 { + if !data.id.is_valid() { return button("-").on_press(action).style(|_x, _status| Style { background: None, text_color: Color::BLACK, @@ -448,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) @@ -504,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()) @@ -532,7 +528,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() @@ -545,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 @@ -565,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() { @@ -601,6 +597,18 @@ impl DictionaryState { space().into() } } + + fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { + let state = self.state.lock(); + 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 { @@ -614,7 +622,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 @@ -626,6 +634,6 @@ struct WordLineState { key: String, value: String, tags: String, - id: u32, + id: crate::lang::Id, index: usize, } diff --git a/src/dictionary_test.rs b/src/dictionary_quiz.rs similarity index 94% rename from src/dictionary_test.rs rename to src/dictionary_quiz.rs index 49bafa8..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::*; @@ -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, @@ -171,11 +171,14 @@ 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/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/history.rs b/src/history.rs index 28bfb6f..380477b 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1,15 +1,16 @@ -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; use iced::widget::*; use iced::{Element, Fill, FillPortion, Task}; -use std::sync::{Arc, Mutex}; +use parking_lot::Mutex; +use std::sync::Arc; +use HistoryMessage::Back; #[derive(Clone)] pub struct HistoryState { @@ -18,7 +19,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), } @@ -46,19 +47,24 @@ impl NavigatedPage for HistoryState { } impl HistoryState { - pub fn new(id: u32, state: Arc>) -> Self { - let state = state.lock().unwrap(); + pub fn new(id: crate::lang::Id, state: Arc>) -> Self { + let state = state.lock(); 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() - }) + .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 { @@ -69,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 18f7e57..0b05ad3 100644 --- a/src/import.rs +++ b/src/import.rs @@ -16,12 +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, Mutex}; +use std::sync::Arc; use tokio::task::spawn_blocking; use zip::ZipArchive; @@ -62,7 +63,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; } @@ -130,13 +131,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; } } @@ -147,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 { @@ -409,7 +404,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 +413,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() @@ -446,11 +441,11 @@ impl ImportState { let map_indices = Self::get_mapping_indices(&group); let mut group_entity = WordGroup { - id: 0, + id: 0.into(), name: group.name.clone(), }; { - let state = state.lock().unwrap(); + let state = state.lock(); add_group(&mut group_entity, &state.connection); } @@ -486,15 +481,15 @@ 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(); - *progress = index as f32 / total_len; + let mut progress = progress.lock(); + *progress = (index as f32 / total_len).min(1.0); } state.dictionary.append(&mut words_list); @@ -517,14 +512,17 @@ 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(); - let property_index = group.fields.iter().position(|f| f == key).unwrap(); + for (key, endpoint) in &group.mapping { + let property_index = group + .fields + .iter() + .position(|f| f == key) + .expect("Unknown property"); let group_index = result .iter() .position(|(name, _)| name == endpoint) .unwrap(); - result.get_mut(group_index).unwrap().1.push(property_index); + result[group_index].1.push(property_index); } result @@ -538,7 +536,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 1bdc880..5322791 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,9 +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_set, update_stat_score}; -use crate::data_provider::history::{HistoryItem, push_note}; 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,16 +13,75 @@ use rayon::iter::IndexedParallelIterator; use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rhai::{Engine, Scope}; -use serde::{Deserialize, Serialize}; -use std::cmp::min; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; +use rusqlite::ToSql; +use std::cmp::PartialEq; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::fmt::{Display, Formatter}; +use std::num::ParseIntError; +use std::str::FromStr; +use std::sync::Arc; use std::time::Instant; 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, Ord, PartialOrd)] +pub 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 From for Id { + fn from(value: u32) -> Self { + Id(value) + } +} + +impl From for u32 { + fn from(value: Id) -> Self { + value.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)) + } +} + +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 + } +} + #[derive(Clone, Debug)] pub struct KanaSet { name: String, @@ -28,13 +89,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 { @@ -214,7 +273,6 @@ impl KanaSet { current_set } } - impl Default for KanaSet { fn default() -> Self { KanaSet::hiragana() @@ -225,45 +283,40 @@ impl PartialEq for KanaSet { self.name == other.name } } - -#[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: INVALID_ID, 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, } - impl CardStatistics { pub fn update(&mut self, status: WordOpenMode) { match status { @@ -290,7 +343,6 @@ impl CardStatistics { (self.score as f32 * multiplier).max(1.0) } } - #[derive(Clone, Copy)] pub enum WordOpenMode { Easy, @@ -298,68 +350,36 @@ pub enum WordOpenMode { Hard, None, } - #[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(); + let state_locked = state.lock(); - let mut current_set = load_stats_of_set(settings, &state_locked.connection); - let last_list: Vec<_> = settings - .get_word_list(&state_locked) + let stats = load_stats_of_deck(settings, &state_locked.connection); + let words = stats .iter() - .map(|w| state_locked.dictionary.get(*w).unwrap()) + .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 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) { - delete_stat(&stat, &state_locked.connection); - current_set.remove(index); - } else { - index += 1; - } - } Self { - set: current_set, - words: last_list, + set: stats, + words, current_word_index: None, state: state_for, order_module: match settings.open_mode { @@ -404,7 +424,6 @@ impl CardSet { 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; @@ -430,56 +449,50 @@ impl CardSet { 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 { timestamp: Utc::now(), word_id: word.word_id, - mode: WordOpenMode::Easy, + mode: status, before: old_score, after: new_score, }, ) } - pub fn len(&self) -> usize { self.set.len() } } - #[derive(Clone, PartialEq, Copy, Eq)] pub enum OrderMode { Default, TrainWorstFirst, FullRandom, } - -#[derive(Clone, Copy, Eq, PartialEq)] +#[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 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)] struct RandomSRSModule { basket: Vec, initialized: bool, } - impl RandomSRSModule { fn new() -> RandomSRSModule { Self { @@ -488,26 +501,26 @@ impl RandomSRSModule { } } } - impl SRSModule for RandomSRSModule { - fn next(&mut self, set: &mut CardSet) -> usize { - if self.basket.is_empty() { - self.basket = (0..set.words.len()).collect::>(); - self.basket.shuffle(&mut rand::rng()) + fn next(&mut self, set: &mut DeckData) -> usize { + 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) {} - 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 = (0..set.set.len()).collect::>(); self.basket.shuffle(&mut rand::rng()) } } - #[derive(Clone)] struct SemiRandomSRSModule { history: Vec, @@ -515,7 +528,6 @@ struct SemiRandomSRSModule { generator: ThreadRng, initialized: bool, } - impl SemiRandomSRSModule { fn new() -> SemiRandomSRSModule { SemiRandomSRSModule { @@ -526,12 +538,11 @@ 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) { + if self.history.contains(&index) && self.history.len() != 1 { return self.next(set); } @@ -542,15 +553,13 @@ 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(); + .expect("update weights error"); } - - fn init(&mut self, set: &mut CardSet) { + fn init(&mut self, set: &mut DeckData) { self.initialized = true; let weights = set .set @@ -560,16 +569,11 @@ impl SRSModule for SemiRandomSRSModule { self.last_weights = WeightedIndex::new(weights).unwrap(); } } - 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) } } - #[derive(Clone)] struct WorstWordsSRSModule { initialized: bool, @@ -579,29 +583,29 @@ struct WorstWordsSRSModule { queue: Vec, rounds_count: u8, } - 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; } - 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) {} - fn init(&mut self, _: &mut CardSet) { + fn init(&mut self, _: &mut DeckData) { self.initialized = true; } } - impl WorstWordsSRSModule { fn new() -> WorstWordsSRSModule { WorstWordsSRSModule { @@ -613,8 +617,7 @@ impl WorstWordsSRSModule { queue: vec![], } } - - fn fill_pool(&mut self, set: &CardSet) { + fn fill_pool(&mut self, set: &DeckData) { let mut sorted = set .set .clone() @@ -630,10 +633,9 @@ impl WorstWordsSRSModule { self.pool = worst; } } - #[derive(Clone)] -pub struct CardSetSettings { - pub id: u32, +pub struct DeckSettings { + pub id: Id, pub name: String, pub forward: String, pub backward: String, @@ -641,13 +643,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 { - id: 0, +impl DeckSettings { + pub(crate) fn with_name(name: String) -> DeckSettings { + DeckSettings { + id: 0.into(), name, forward: "".to_string(), backward: "".to_string(), @@ -655,67 +655,63 @@ impl CardSetSettings { count: None, worst_words_list: None, open_mode: OrderMode::Default, - append_mode: AppendMode::Manual, } } - - 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 { let time = Instant::now(); let mut list = vec![]; 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) - .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 8cf0b6e..204e308 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ #![windows_subsystem = "windows"] mod data_provider; mod dictionary; -mod dictionary_test; -pub mod helpers; +mod dictionary_quiz; mod history; pub mod import; mod lang; @@ -23,19 +22,19 @@ 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, 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; use iced_core::window::Position; +#[cfg(target_os = "linux")] use iced_core::window::settings::PlatformSpecific; use mimalloc::MiMalloc; use rusqlite::Connection; -use hashbrown::HashMap; - #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; @@ -57,6 +56,7 @@ fn main() -> iced::Result { .run() } +#[allow(unused_mut)] fn window_settings() -> window::Settings { let mut settings = window::Settings { position: Position::Centered, @@ -81,11 +81,11 @@ 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, - pub activity: HashMap>, + pub activity: HashMap>, } impl Default for AppState { @@ -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/navigation.rs b/src/navigation.rs index 5cb84fd..19d9db8 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,12 +1,11 @@ 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_test::{DictionaryQuizMessage, DictionaryQuizState}; +use crate::dictionary_quiz::{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,13 +23,14 @@ 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 parking_lot::Mutex; use reqwest::Error; -use hashbrown::HashMap; -use std::sync::{Arc, Mutex}; -use std::time::Instant; use rusqlite::Connection; +use std::sync::Arc; +use std::time::Instant; impl Default for ScreenState { fn default() -> Self { @@ -60,7 +60,7 @@ pub enum RootMessage { RepetitionSettings(RepetitionSettingsMessage), Import(ImportMessage), Keyboard(Event), - DataLoaded(HashMap>), + DataLoaded(HashMap>), None, UpdateData, } @@ -96,7 +96,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([ @@ -125,9 +125,13 @@ 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 = history_file_name[4..history_file_name.len() - 12] - .parse::() + .parse::() .unwrap(); let history = get_history_of_set(id); @@ -148,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); @@ -164,14 +168,14 @@ 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(); - if cfg!(windows){ + let mut state = self.app_state.lock(); + if cfg!(windows) { state.connection = Connection::open_in_memory().unwrap(); } let path = app_data_dir(); @@ -302,7 +306,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); @@ -330,7 +337,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..7e8be3e 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 { @@ -54,7 +54,7 @@ impl NavigatedPage for QuizState { self.update_showed() } } - Back => todo!(), + Back => {} } Task::none() } 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..7ef3db8 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -1,5 +1,6 @@ use crate::data_provider::voice::get_voice; -use crate::lang::{CardSet, CardSetSettings, 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::*; @@ -8,27 +9,28 @@ 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; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; 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, can_play: bool, - sink: Arc, - opened: HashSet, + sink: Arc>, + opened: HashSet, } 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,10 +97,15 @@ 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(); + + let sink_handle = if set.forward.contains("speech") || set.backward.contains("speech") { + rodio::DeviceSinkBuilder::open_default_sink().ok() + } else { + None + }; RepetitionState { state, @@ -136,18 +143,19 @@ 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() } 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); @@ -271,7 +279,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(), } @@ -314,11 +322,16 @@ pub enum RepetitionMessage { PlayFinished, } -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(); +async fn play_sound(sink: Arc>, text: String) { + if let Ok(data) = get_voice(text.as_str()).await { + spawn_blocking(move || { + if let Some(sink) = sink.as_ref() + && let Ok(play) = rodio::play(sink.mixer(), data) + { + play.sleep_until_end() + } + }) + .await + .unwrap(); + } } diff --git a/src/repetition_settings.rs b/src/repetition_settings.rs index 02169ec..48db074 100644 --- a/src/repetition_settings.rs +++ b/src/repetition_settings.rs @@ -1,43 +1,52 @@ use crate::AppState; -use crate::data_provider::card_sets::{delete_set, update_card_set}; -use crate::lang::CardSetSettings; +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 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; -use std::sync::{Arc, Mutex}; +use iced_core::{Padding, Theme, color}; +use parking_lot::Mutex; +use std::sync::Arc; use std::time::Duration; pub struct RepetitionSettingsState { - set: CardSetSettings, + set: DeckSettings, index: usize, state: Arc>, real_delete: bool, + correct_filter: bool, + filter_changed: bool, } 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(); - set = local_state.card_sets[index].clone(); + let local_state = state.lock(); + set = local_state.decks[index].clone(); } + let filter_state = set.check_filter(); + RepetitionSettingsState { index, set, - state: p1, + state, real_delete: false, + correct_filter: filter_state, + filter_changed: false, } } } impl NavigatedPage for RepetitionSettingsState { - fn navigate(&self, message: &RepetitionSettingsMessage) -> Option { + fn navigate(&mut self, message: &RepetitionSettingsMessage) -> Option { if let Back = message { Some(PreviousPage) } else { @@ -50,9 +59,12 @@ impl NavigatedPage for RepetitionSettingsState { match message { 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(); + 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) => { self.set.name = new; @@ -65,9 +77,11 @@ 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().unwrap(); + let state = self.state.lock(); let count = self.set.get_word_list(&state).len(); self.set.count = Some(count); } @@ -79,9 +93,9 @@ 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.card_sets.remove(self.index); + state.decks.remove(self.index); return Task::done(RootMessage::RepetitionSettings(Back)); } RevertDeleteSet => self.real_delete = false, @@ -111,7 +125,15 @@ 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), @@ -148,8 +170,21 @@ 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: &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..846ee5b 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,7 +1,7 @@ -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::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, CardSetSettings, CardStatistics, 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; @@ -11,53 +11,68 @@ 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}; +use parking_lot::Mutex; +use std::ops::Deref; +use std::sync::Arc; #[derive(Clone)] pub struct RepetitionsState { - selected_set: Option, - correct_filters: Vec, - current_sets_cards_cache: HashMap, Vec)>, - word_id_index_map: HashMap, - pub state: Arc>, + word_id_index_map: HashMap, + selected_deck_index: Option, + decks: Vec, + state: Arc>, } 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; - } - 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.unwrap()].clone(); + + let selected_set = self.selected_deck_mut().unwrap(); + if selected_set.append_mode == AppendMode::Full { + Self::append_all_words(&mut clone.lock(), selected_set); + selected_set.existing_words_indices = selected_set.available_words_indices.clone(); + } else { + if selected_set + .available_words_indices + .as_ref() + .unwrap() + .is_empty() + || selected_set + .existing_words_indices + .as_ref() + .unwrap() + .is_empty() + { + return None; + } } - if card_set.append_mode == AppendMode::Full { - self.append_all_words(&card_set); - } - - 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.unwrap(), + self.selected_deck().unwrap().index.unwrap(), self.state.clone(), ))) } else { @@ -65,109 +80,145 @@ impl NavigatedPage for RepetitionsState { } } fn navigated(&mut self) { - self.selected_set = None; - self.current_sets_cards_cache.clear(); + let index = self.selected_deck_index.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); + 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); + self.decks.remove(index); + self.clear_selection() + } } 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); + 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(), + index: None, + }); } 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); + let index = self.selected_deck_index.unwrap(); + self.clear_selection(); + let deck = self.decks.remove(index); + if deck.id.is_valid() { + let mut state = self.state.lock(); + 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) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.name = value; } 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)); - + self.select_deck(index); + let deck = self.selected_deck().unwrap(); + if deck.existing_words_indices.is_none() { + self.fill_deck_word_indices(); } - 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 mut deck = self.selected_deck().unwrap().clone(); + let mut state = self.state.lock(); + { + update_deck(&mut deck.general_settings, &state.connection); + if let Some(index) = deck.index { + state.decks[index] = deck.general_settings.clone(); + } else { + state.decks.push(deck.general_settings.clone()); + deck.index = Some(state.decks.len() - 1); + self.decks[self.selected_deck_index.unwrap()] = deck.clone(); + } } - - 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; + SetForward(value) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.forward = value; } - SetBackward(new) => { - state.card_sets[self.selected_set.unwrap()].backward = new; + SetBackward(value) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.backward = value; } - SetFilter(new) => { - state.card_sets[self.selected_set.unwrap()].filter = new; + SetFilter(value) => { + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.filter = value; } 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); + let state = self.state.lock(); + 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) => { - state - .card_sets - .get_mut(self.selected_set.unwrap()) - .unwrap() - .open_mode = mode; + let deck = self.selected_deck_mut().unwrap(); + deck.general_settings.open_mode = mode; } SetAppendMode(mode) => { - let set = state.card_sets.get_mut(self.selected_set.unwrap()).unwrap(); - set.append_mode = mode; + let deck = self.selected_deck_mut().unwrap(); + deck.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 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 mut new_current = cache.0.clone(); - new_current.append(&mut adding.clone()); - total_cache.insert(index, (new_current, cache.1.clone())); + let deck = self.selected_deck_mut().unwrap(); + deck.existing_words_indices + .as_mut() + .unwrap() + .append(&mut adding.clone()); } - Self::append_words(&state, set, adding.into_iter()); + let deck = self.selected_deck().unwrap(); + let mut state = self.state.lock(); + Self::append_words(&mut state, &deck.general_settings, adding.into_iter()); } } Task::none() @@ -177,7 +228,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) @@ -185,7 +236,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) @@ -199,60 +250,51 @@ 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 state_ = state.lock(); 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 decks = Self::get_decks_from_state(&state_); drop(state_); RepetitionsState { - selected_set: None, - correct_filters: vec![true; count], - current_sets_cards_cache: Default::default(), word_id_index_map: map, + selected_deck_index: None, + decks, 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 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> { - if set.id != 0 { + fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { + if deck.id.is_valid() { + if deck.append_mode == AppendMode::Manual + && deck.existing_words_indices.as_ref().unwrap().is_empty() + { + return text!("Добавьте карточки или измените фильтр").into(); + } return row![ space().width(Fill), button(text!("Начать повторение").width(200).center()) @@ -263,24 +305,15 @@ 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(); - } + 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), @@ -289,49 +322,49 @@ impl RepetitionsState { ] .spacing(QUARTER_SPACING), { - if set.id == 0 { + if !deck.id.is_valid() { 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), @@ -352,9 +385,10 @@ 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), + .spacing(DEFAULT_SPACING) + .align_y(Center), ] .spacing(DEFAULT_SPACING) .width(Length::FillPortion(2)) @@ -362,18 +396,17 @@ impl RepetitionsState { } space().width(Length::FillPortion(2)).into() } - - fn filled_set_data_view(&self, set: &CardSetSettings) -> 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: &CardSetSettings) -> 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 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(); @@ -382,9 +415,6 @@ impl RepetitionsState { let distance = now - *date; let index = counts.len() as i64 - distance.num_days() - 1; - if index == 210 { - println!("invalid index") - } if index < 0 { continue; } @@ -446,33 +476,31 @@ impl RepetitionsState { }) .into() } - - fn word_append_panel(&self, set: &CardSetSettings) -> 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: &CardSetSettings) -> 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)), @@ -486,25 +514,27 @@ impl RepetitionsState { } fn count_comparator_view(&self) -> Element<'_, RepetitionsMessage> { - let cache = &self.current_sets_cards_cache[&self.selected_set.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: &CardSetSettings) -> 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(); - let sets = &self.state.lock().unwrap().card_sets; - for (i, set) in sets.iter().enumerate() { + for (i, deck) in self.decks.iter().enumerate() { column = column.push( - button(text!("{}", set.name.clone())) + button(text!("{}", &deck.name)) .on_press_with(move || SelectSet(i)) .style(move |_x: &Theme, status: Status| Style { background: if status == Status::Hovered { @@ -512,11 +542,7 @@ impl RepetitionsState { } else { None }, - text_color: if self.correct_filters[i] { - _x.palette().primary - } else { - _x.palette().warning - }, + text_color: _x.palette().primary, border: Border { color: Default::default(), width: 0.0, @@ -532,6 +558,76 @@ impl RepetitionsState { } } +impl RepetitionsState { + 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| { + created_set.insert(c); + }); + 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: &mut AppState, + deck: &DeckSettings, + indices: impl Iterator, + ) { + let words = &state.dictionary; + let stats = &mut indices + .map(|i| { + let word = &words[i]; + CardStatistics { + id: 0.into(), + word_id: word.id, + last_open: Utc::now(), + score: 1, + set_id: deck.id, + } + }) + .collect::>(); + + if !stats.is_empty() { + add_stat_list(stats, &mut state.connection); + } + } + + fn select_deck(&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.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 { Next, @@ -552,3 +648,20 @@ pub enum RepetitionsMessage { GoToSettings, AppendWords(usize), } + +#[derive(Clone)] +struct DeckViewData { + general_settings: DeckSettings, + existing_words_indices: Option>, + available_words_indices: Option>, + append_mode: AppendMode, + index: Option, +} + +impl Deref for DeckViewData { + type Target = DeckSettings; + + fn deref(&self) -> &Self::Target { + &self.general_settings + } +} diff --git a/src/selector.rs b/src/selector.rs index 7faced7..3be8cac 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 parking_lot::Mutex; +use std::sync::Arc; pub struct SelectorState { pub set: KanaSet, @@ -36,7 +37,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..db05690 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -4,12 +4,13 @@ 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}; use iced::{Center, Element, Fill, Font, Length, Task}; -use std::sync::{Arc, Mutex}; +use parking_lot::Mutex; +use std::sync::Arc; use std::time::Duration; #[derive(Clone)] @@ -37,7 +38,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 { @@ -52,13 +53,18 @@ 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)); } 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() @@ -69,14 +75,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 +94,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,23 +122,23 @@ 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.card_sets = updated_state.card_sets; + state.decks = updated_state.decks; state.dictionary = updated_state.dictionary; state.word_groups = updated_state.word_groups; } 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 +165,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 +182,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( @@ -217,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> { @@ -233,7 +239,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 c8f8301..5daea7d 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 parking_lot::Mutex; +use std::sync::Arc; #[derive(Clone)] pub struct WordState { @@ -18,7 +19,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 { @@ -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)); diff --git a/src/writing.rs b/src/writing.rs index 409415d..4f14199 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 { @@ -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, }