diff --git a/Cargo.lock b/Cargo.lock index 48b8829..4fb695a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,19 @@ dependencies = [ "rand_core", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -826,6 +839,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -1201,6 +1226,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "heck" version = "0.5.0" @@ -1225,6 +1259,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "iced" version = "0.14.0" @@ -1437,10 +1495,12 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" name = "jap_learn" version = "0.1.1" dependencies = [ + "chrono", "dirs", "iced", "rand", "rhai", + "rusqlite", "serde", "serde_json", ] @@ -1553,6 +1613,16 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "lilt" version = "0.8.1" @@ -2497,6 +2567,32 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags 2.10.0", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2811,6 +2907,18 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -3125,6 +3233,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 86455d1..b7c7bdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ dirs = "6.0.0" serde = { version = "1.0.144", features = ["derive"] } serde_json = "1.0.149" rhai = "1.24.0" +chrono = "0.4.44" +rusqlite = { version = "0.39.0", features = ["chrono"] } diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs new file mode 100644 index 0000000..00655fc --- /dev/null +++ b/src/data_provider/card_sets.rs @@ -0,0 +1,76 @@ +use std::collections::HashMap; +use iced::time::now; +use rusqlite::Connection; +use crate::lang::{CardSet, DictionaryElement}; +use crate::repetitions::CardSetSettings; + +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 { + id: row.get(0)?, + name: row.get(1)?, + forward: row.get(2)?, + backward: row.get(3)?, + filter: row.get(4)?, + count: None + }) + }).unwrap(); + + let mut buffer = vec![]; + for word in iter { + buffer.push(word.unwrap()); + } + + buffer +} + +pub fn add_set(set: &mut CardSetSettings, connection: &Connection) { + let index = connection + .query_row( + "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", + ( + &set.name, + &set.forward, + &set.backward, + &set.filter, + ), + |row| row.get(0) + ) + .unwrap_or_else(|e| {println!("{}", e); 0}); + + set.id = index; +} + +pub fn update_card_set(set: &mut CardSetSettings, connection: &Connection){ + if set.id == 0 { + add_set(set, &connection); + } + + else { + connection + .execute( + "UPDATE card_set SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5", + ( + &set.name, + &set.forward, + &set.backward, + &set.filter, + &set.id + ), + ) + .unwrap_or_else(|e| {println!("{}", e); 0}); + } +} + +pub fn delete_set(set: &CardSetSettings, connection: &Connection) { + if set.id == 0 { + return; + } + connection + .execute("DELETE FROM card_set WHERE id = ?1", (&set.id,)) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); +} \ No newline at end of file diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs new file mode 100644 index 0000000..1fdda74 --- /dev/null +++ b/src/data_provider/card_stats.rs @@ -0,0 +1,76 @@ +use std::collections::HashMap; +use chrono::{DateTime, Utc}; +use iced::time::now; +use rusqlite::{Connection, ToSql}; +use rusqlite::types::ToSqlOutput; +use crate::lang::{CardSet, CardStatistics, DictionaryElement}; +use crate::repetitions::CardSetSettings; + +pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec { + let mut stmt = connection.prepare("SELECT id, word_id, score, last_opened FROM card_stats WHERE set_id = ?1").unwrap(); + let iter = stmt.query_map((set.id,), |row| { + Ok(CardStatistics { + id: row.get(0)?, + word_id: row.get(1)?, + score: row.get(2)?, + last_open: row.get(2)?, + }) + }).unwrap(); + + let mut buffer = vec![]; + for word in iter { + buffer.push(word.unwrap()); + } + + buffer +} + +pub fn add_set(set: &mut CardSetSettings, connection: &Connection) { + let index = connection + .query_row( + "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", + ( + &set.name, + &set.forward, + &set.backward, + &set.filter, + ), + |row| row.get(0) + ) + .unwrap_or_else(|e| {println!("{}", e); 0}); + + set.id = index; +} + +pub fn update_card_set(set: &mut CardSetSettings, connection: &Connection){ + if set.id == 0 { + add_set(set, &connection); + } + + else { + connection + .execute( + "UPDATE card_set SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5", + ( + &set.name, + &set.forward, + &set.backward, + &set.filter, + &set.id + ), + ) + .unwrap_or_else(|e| {println!("{}", e); 0}); + } +} + +pub fn delete_set(set: &CardSetSettings, connection: &Connection) { + if set.id == 0 { + return; + } + connection + .execute("DELETE FROM card_set WHERE id = ?1", (&set.id,)) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); +} diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs new file mode 100644 index 0000000..34897c4 --- /dev/null +++ b/src/data_provider/mod.rs @@ -0,0 +1,7 @@ +pub(crate) mod words; +pub(crate) mod card_sets; +pub(crate) mod card_stats; + +use crate::lang; +use crate::dictionary; + diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs new file mode 100644 index 0000000..df3dc4f --- /dev/null +++ b/src/data_provider/words.rs @@ -0,0 +1,150 @@ +use crate::dictionary::app_data_dir; +use crate::lang::DictionaryElement; +use rusqlite::Connection; +use std::collections::HashMap; + +pub fn create_db() { + let path = app_data_dir(); + let db_file = path.join("data.db"); + if !db_file.exists() { + std::fs::File::create(&db_file).unwrap(); + let connection = Connection::open(&db_file).unwrap(); + create_words_table(&connection); + create_card_stat_table(&connection); + create_card_set_table(&connection); + } +} + +pub fn add_word(word: &mut DictionaryElement, connection: &Connection) { + let index = connection + .query_row( + "INSERT INTO words (key, value, tags, more) VALUES (?1, ?2, ?3, ?4) RETURNING id", + ( + &word.key, + &word.value, + &word.tags, + serde_json::to_string(&word.additional).unwrap(), + ), + |row| row.get(0), + ) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); + + word.id = index; +} + +pub fn update_word(word: &mut DictionaryElement, connection: &Connection) { + if word.id == 0 { + add_word(word, &connection); + } else { + connection + .execute( + "UPDATE words SET key = ?1, value = ?2, tags = ?3, more = ?4 WHERE id = ?5", + ( + &word.key, + &word.value, + &word.tags, + serde_json::to_string(&word.additional).unwrap(), + &word.id, + ), + ) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); + } +} + +pub fn delete_word(word: &DictionaryElement, connection: &Connection) { + if word.id == 0 { + return; + } + connection + .execute("DELETE FROM words WHERE id = ?1", (&word.id,)) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); +} + +pub fn load_words(connection: &Connection) -> Vec { + let mut stmt = connection + .prepare("SELECT id, key, value, tags, more FROM words") + .unwrap(); + let word_iter = stmt + .query_map([], |row| { + let addinionals: String = row.get(4)?; + Ok(DictionaryElement { + id: row.get(0)?, + key: row.get(1)?, + value: row.get(2)?, + tags: row.get(3)?, + additional: serde_json::from_str::>(&addinionals).unwrap(), + }) + }) + .unwrap(); + + let mut buffer = vec![]; + for word in word_iter { + buffer.push(word.unwrap()); + } + + buffer +} + +fn create_words_table(conn: &Connection) { + conn.execute( + "CREATE TABLE words ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL, + value TEXT NOT NULL, + tags TEXT NOT NULL, + more TEXT + )", + (), + ) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); +} + +fn create_card_stat_table(conn: &Connection) { + conn.execute( + "CREATE TABLE card_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + word_id INTEGER NOT NULL, + set_id TEXT NOT NULL, + score INTEGER NOT NULL DEFAULT 1, + last_opened INTEGER NOT NULL, + FOREIGN KEY (word_id) REFERENCES words (id) ON DELETE CASCADE, + FOREIGN KEY (set_id) REFERENCES sets (id) ON DELETE CASCADE + )", + (), + ) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); +} + +fn create_card_set_table(conn: &Connection) { + conn.execute( + "CREATE TABLE card_set ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + forward TEXT NOT NULL, + backward TEXT NOT NULL, + filter TEXT NOT NULL + )", + (), + ) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); +} + +pub struct Qwerty {} diff --git a/src/dictionary.rs b/src/dictionary.rs index 9835dc9..14a9faa 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -1,11 +1,12 @@ +use crate::data_provider::words::{delete_word, update_word}; use crate::dictionary::DictionaryMessage::Test; use crate::dictionary_test::DictionaryQuizState; +use crate::lang::DictionaryElement; use crate::{AppState, NavigatedPage, Page, RootMessage}; use iced::alignment::Vertical::Center; use iced::widget::button::Style; use iced::widget::*; use iced::{Border, Color, Length, Shadow, Task}; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::PathBuf; @@ -22,31 +23,13 @@ pub struct DictionaryState { no_typing: bool, } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DictionaryElement { - pub key: String, - pub value: String, - pub tags: String, - pub additional: HashMap, -} - -impl DictionaryElement { - fn new() -> Self { - Self { - key: String::new(), - value: String::new(), - tags: String::new(), - additional: Default::default(), - } - } -} - #[derive(Debug, Clone)] pub enum DictionaryMessage { Back, SetTags(usize, String), SetKey(usize, String), SetValue(usize, String), + SubmitWord(usize), Remove(usize), NewWord, Include(usize, bool), @@ -109,6 +92,7 @@ impl DictionaryState { dict.push(DictionaryElement::new()); self.include_map.push(false); } + DictionaryMessage::SetKey(i, v) => { let dict = &mut self.state.lock().unwrap().dictionary; dict[i].key = v @@ -116,7 +100,7 @@ impl DictionaryState { DictionaryMessage::SetValue(i, v) => { let dict = &mut self.state.lock().unwrap().dictionary; dict[i].value = v - }, + } DictionaryMessage::SetTags(i, v) => { { let dict = &mut self.state.lock().unwrap().dictionary; @@ -125,32 +109,45 @@ impl DictionaryState { self.update_tags(); } + DictionaryMessage::Remove(i) => { - let dict = &mut self.state.lock().unwrap().dictionary; - dict.remove(i); + let state = &mut self.state.lock().unwrap(); + let dict = &mut state.dictionary; + let word = dict.remove(i); + delete_word(&word, &state.connection) } DictionaryMessage::Include(i, b) => self.include_map[i] = b, DictionaryMessage::IncludeTag(t, v) => { self.tag_map.insert(t, v); self.update_words_include() } + DictionaryMessage::ResetTags => { self.tag_map.iter_mut().for_each(|(_, v)| *v = false); self.include_map.iter_mut().for_each(|x| *x = false) } + DictionaryMessage::Save => { - let dir = dict_file(); + /* let dir = dict_file(); let dict = &self.state.lock().unwrap().dictionary; let content = serde_json::to_string_pretty(&dict.clone()).unwrap(); fs::write(dir, content.clone()) - .unwrap_or_else(|e| println!("Can't write file: {}", e)); + .unwrap_or_else(|e| println!("Can't write file: {}", e));*/ } DictionaryMessage::SetReverse(v) => self.reverse = v, DictionaryMessage::Search(s) => { self.search = s; } DictionaryMessage::SetTyping(b) => self.no_typing = b, + DictionaryMessage::SubmitWord(i) => { + let state = &mut self.state.lock().unwrap(); + let connection = &state.connection; + let word = &mut state.dictionary.get(i).unwrap().clone(); + + update_word(word, &connection); + state.dictionary[i] = word.clone(); + } _ => {} } @@ -206,19 +203,22 @@ impl DictionaryState { text_input("Ключ", &word.key) .size(20) .width(Length::Fill) - .on_input(move |string| DictionaryMessage::SetKey(i, string)), + .on_input(move |string| DictionaryMessage::SetKey(i, string)) + .on_submit(DictionaryMessage::SubmitWord(i)), ); line = line.push( text_input("Значение", &word.value) .size(20) .width(Length::Fill) - .on_input(move |string| DictionaryMessage::SetValue(i, string)), + .on_input(move |string| DictionaryMessage::SetValue(i, string)) + .on_submit(DictionaryMessage::SubmitWord(i)), ); line = line.push( text_input("Тэги", &word.tags) .size(20) .width(Length::Fill) - .on_input(move |string| DictionaryMessage::SetTags(i, string)), + .on_input(move |string| DictionaryMessage::SetTags(i, string)) + .on_submit(DictionaryMessage::SubmitWord(i)), ); line = line @@ -364,3 +364,13 @@ pub fn dict_file() -> PathBuf { dir.push("dict.json"); dir } + +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(); + } + + dir +} diff --git a/src/dictionary_test.rs b/src/dictionary_test.rs index 5258087..3058711 100644 --- a/src/dictionary_test.rs +++ b/src/dictionary_test.rs @@ -1,4 +1,4 @@ -use crate::dictionary::{split_with_coma, DictionaryElement}; +use crate::dictionary::{split_with_coma}; use crate::quiz::Score; use crate::Page::PreviousPage; use crate::RootMessage; @@ -9,6 +9,8 @@ use iced::widget::{button, container, row, space, text, text_input, Row}; use iced::Background::Color; use iced::{alignment, Border, Element, Fill, Task, Theme}; use rand::prelude::SliceRandom; +use crate::lang::DictionaryElement; + #[derive(Debug, Clone)] pub struct DictionaryQuizState { words: Vec, diff --git a/src/lang.rs b/src/lang.rs index d935a64..b9034ed 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,3 +1,13 @@ +use crate::repetitions::CardSetSettings; +use crate::AppState; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use crate::data_provider::card_stats::load_stats_of_set; + +const MAX_SCORE: u8 = 25_u8; +const FADE_PER_DAY: f32 = 0.95; #[derive(Clone, Debug)] pub struct KanaSet { name: String, @@ -177,7 +187,7 @@ impl KanaSet { } } -/* pub fn next(&mut self) -> (String, String) { + /* pub fn next(&mut self) -> (String, String) { let current_set = self.list(); let mut rand = rand::rng(); @@ -210,3 +220,94 @@ impl PartialEq for KanaSet { self.name == other.name } } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DictionaryElement { + pub id: u32, + pub key: String, + pub value: String, + pub tags: String, + pub additional: HashMap, +} + +impl DictionaryElement { + pub fn new() -> Self { + Self { + id: 0, + key: String::new(), + value: String::new(), + tags: String::new(), + additional: Default::default(), + } + } +} + +#[derive(Clone)] +pub struct CardStatistics { + pub id: u32, + pub word_id: u32, + pub last_open: DateTime, + pub score: u8, +} + +impl CardStatistics { + pub fn open(&mut self, status: WordOpenMode) { + self.last_open = Utc::now(); + match status { + WordOpenMode::Easy => { + self.score += 5; + } + WordOpenMode::Ok => { + self.score += 3; + } + WordOpenMode::Hard => { + self.score -= 1; + } + WordOpenMode::None => { + self.score /= 2; + } + } + + if self.score < 1 { + self.score = 1 + } else if self.score > MAX_SCORE { + self.score = MAX_SCORE + } + } + + pub fn calculated_score(&self) -> f32 { + let time = Utc::now() - self.last_open; + let days = time.num_days(); + let multiplier = FADE_PER_DAY.powi(days as i32); + self.score as f32 * multiplier + } +} + +pub enum WordOpenMode { + Easy, + Ok, + Hard, + None, +} + +#[derive(Clone)] +pub struct CardSet { + set: Vec, + last_weights: Vec, +} + +impl CardSet { + pub fn new(settings: CardSetSettings, state: Arc>) -> Self { + let state_locked = state.lock().unwrap(); + + let current_set = load_stats_of_set(&settings, &state_locked.connection); + let last_list = settings.get_word_list(&state_locked); + + let weights = current_set.iter().map(|x| x.calculated_score()).collect(); + + Self { + set: current_set, + last_weights: weights, + } + } +} diff --git a/src/main.rs b/src/main.rs index 24f1c49..2167bda 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,15 +8,16 @@ mod repetition; mod repetitions; mod selector; mod writing; - -use std::fs::File; -use std::io::Read; -use crate::dictionary::{DictionaryElement, DictionaryMessage, DictionaryState}; +mod data_provider; +use crate::data_provider::card_sets::load_sets; +use crate::data_provider::words::{create_db, load_words}; +use crate::dictionary::{app_data_dir, DictionaryMessage, DictionaryState}; use crate::dictionary_test::{DictionaryQuizMessage, DictionaryQuizState}; +use crate::lang::DictionaryElement; use crate::quiz::*; use crate::randomizer::randomizer::{RandomizerMessage, RandomizerState}; use crate::repetition::{RepetitionMessage, RepetitionState}; -use crate::repetitions::{RepetitionsMessage, RepetitionsState}; +use crate::repetitions::{CardSetSettings, RepetitionsMessage, RepetitionsState}; use crate::selector::*; use crate::writing::{WritingMessage, WritingState}; use crate::Page::{ @@ -25,6 +26,7 @@ use crate::Page::{ use iced::widget::text; use iced::Element; use iced::{Font, Task}; +use rusqlite::Connection; use std::sync::{Arc, Mutex}; fn main() -> iced::Result { @@ -32,7 +34,8 @@ fn main() -> iced::Result { .title("Kana learn app") .font(include_bytes!("../noto.ttf")) .default_font(Font::with_name("Noto Sans JP")) - .run() + . + run() } #[derive(Debug, Clone)] @@ -65,21 +68,19 @@ pub struct ScreenState { pub struct AppState { pub dictionary: Vec, + pub card_sets: Vec, + pub connection: Connection } impl Default for ScreenState { fn default() -> Self { - let mut current_dict = "[]".to_string(); - match File::open(dictionary::dict_file()) { - Ok(mut f) => { - current_dict = String::new(); - f.read_to_string(&mut current_dict).unwrap(); - } - _ => {} - } - - let list: Vec = serde_json::from_str(¤t_dict).unwrap(); - let state = Arc::new(Mutex::new(AppState { dictionary: list })); + let path = app_data_dir(); + let db_file = path.join("data.db"); + let connection = Connection::open(db_file).unwrap(); + let list: Vec = load_words(&connection); + let sets: Vec = load_sets(&connection); + + let state = Arc::new(Mutex::new(AppState { dictionary: list, card_sets: sets, connection })); ScreenState { stack: vec![Selector(SelectorState::new(state.clone()))], } @@ -88,6 +89,7 @@ impl Default for ScreenState { impl ScreenState { pub fn boot() -> (ScreenState, Task) { + create_db(); (ScreenState::default(), Task::none()) } pub fn update(&mut self, message: RootMessage) -> Task { diff --git a/src/repetition.rs b/src/repetition.rs index 39dac2c..7f8c394 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -1,16 +1,15 @@ +use std::sync::{Arc, Mutex}; use crate::Page::PreviousPage; -use crate::{NavigatedPage, Page, RootMessage}; +use crate::{AppState, NavigatedPage, Page, RootMessage}; use iced::widget::{button, container}; use iced::{Element, Fill, Left, Task}; +use crate::lang::CardSet; +use crate::repetitions::CardSetSettings; -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct RepetitionState { -} - -impl Default for RepetitionState { - fn default() -> Self { - RepetitionState::new() - } + pub set: CardSetSettings, + pub state: Arc>, } impl NavigatedPage for RepetitionState { @@ -24,9 +23,10 @@ impl NavigatedPage for RepetitionState { } impl RepetitionState { - pub(crate) fn new() -> RepetitionState { + pub(crate) fn new(set: CardSetSettings, state: Arc>) -> RepetitionState { RepetitionState { - + set, + state } } } diff --git a/src/repetitions.rs b/src/repetitions.rs index 7f0fcb0..8f39ef1 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,7 +1,9 @@ -use crate::dictionary::DictionaryElement; -use crate::Page::PreviousPage; +use crate::data_provider::card_sets::{delete_set, update_card_set}; +use crate::lang::DictionaryElement; +use crate::repetition::RepetitionState; +use crate::Page::{PreviousPage, Repetition}; use crate::{AppState, NavigatedPage, Page, RootMessage}; -use iced::widget::button::{Catalog, Style}; +pub use iced::widget::button::{Catalog, Style}; use iced::widget::{button, column, container, row, scrollable, space, text, text_input, Column}; use iced::Background::Color; use iced::{Border, Center, Element, Fill, Left, Length, Shadow, Task, Theme}; @@ -10,7 +12,6 @@ use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct RepetitionsState { - sets: Vec, selected_set: Option, correct_filters: Vec, pub state: Arc>, @@ -20,6 +21,10 @@ impl NavigatedPage for RepetitionsState { fn navigate(&self, message: &RepetitionsMessage) -> Option { if let RepetitionsMessage::Back = message { Some(PreviousPage) + } + else if let RepetitionsMessage::GoToRepetition = message { + let clone = self.state.clone(); + Some(Repetition(RepetitionState::new(self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].clone(), clone) )) } else { None } @@ -28,10 +33,10 @@ impl NavigatedPage for RepetitionsState { impl RepetitionsState { pub(crate) fn new(state: Arc>) -> RepetitionsState { + let count = state.lock().unwrap().card_sets.len(); RepetitionsState { - sets: Vec::new(), selected_set: None, - correct_filters: vec![], + correct_filters: vec![true; count], state, } } @@ -39,52 +44,54 @@ impl RepetitionsState { impl RepetitionsState { pub fn update(&mut self, message: RepetitionsMessage) -> Task { + let mut state = self.state.lock().unwrap(); match message { RepetitionsMessage::Next => {} RepetitionsMessage::Back => {} RepetitionsMessage::GoToRepetition => {} RepetitionsMessage::CreateSet => { - self.sets.push(CardSet::with_name(format!( - "Card set #{}", - self.sets.len() + 1 - ))); + let index = state.card_sets.len() + 1; + state + .card_sets + .push(CardSetSettings::with_name(format!("Card set #{}", index))); self.correct_filters.push(true); } RepetitionsMessage::DeleteSet => { - self.sets.remove(self.selected_set.unwrap()); + 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); } RepetitionsMessage::SelectSet(index) => { self.selected_set = Some(index); } RepetitionsMessage::SetName(new) => { - self.sets[self.selected_set.unwrap()].name = new; + state.card_sets[self.selected_set.unwrap()].name = new; } RepetitionsMessage::Save => { - for i in 0..self.sets.len() { - self.correct_filters[i] = self.sets[i].check_filter(); + for i in 0..state.card_sets.len() { + self.correct_filters[i] = state.card_sets[i].check_filter(); } - if self.correct_filters.iter().all(|x| *x) { - println!("Saving"); - } else { - println!("Some errors"); - } + let word = &mut state.card_sets[self.selected_set.unwrap()].clone(); + + update_card_set(word, &state.connection); + + state.card_sets[self.selected_set.unwrap()] = word.clone(); } RepetitionsMessage::SetForward(new) => { - self.sets[self.selected_set.unwrap()].forward = new; + state.card_sets[self.selected_set.unwrap()].forward = new; } RepetitionsMessage::SetBackward(new) => { - self.sets[self.selected_set.unwrap()].backward = new; + state.card_sets[self.selected_set.unwrap()].backward = new; } RepetitionsMessage::SetFilter(new) => { - self.sets[self.selected_set.unwrap()].filter = new; + state.card_sets[self.selected_set.unwrap()].filter = new; } RepetitionsMessage::TryFilter => { - let set = &mut self.sets[self.selected_set.unwrap()]; - let count = set.get_word_list(&self.state.lock().unwrap()).len(); - set.count = Some(count); + 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); } } Task::none() @@ -100,9 +107,6 @@ impl RepetitionsState { button("Добавить") .width(Fill) .on_press(RepetitionsMessage::CreateSet), - button("Сохранить") - .width(Fill) - .on_press(RepetitionsMessage::Save), ] .spacing(10) .width(Length::FillPortion(1)), @@ -115,12 +119,12 @@ impl RepetitionsState { .width(Fill) .height(Fill) ] - .align_x(Left) - .width(Fill), + .align_x(Left) + .width(Fill), ) - .center_x(Fill) - .padding(10) - .into() + .center_x(Fill) + .padding(10) + .into() } fn launch_button(&self) -> Element<'_, RepetitionsMessage> { @@ -135,43 +139,48 @@ impl RepetitionsState { fn selected_set_view(&self) -> Element<'_, RepetitionsMessage> { if let Some(index) = self.selected_set { + let sets = &self.state.lock().unwrap().card_sets; + return column![ scrollable( column![ - text_input("Название набора", &self.sets[index].name) + text_input("Название набора", &sets[index].name) .on_input(RepetitionsMessage::SetName), - text_input("Передняя сторона", &self.sets[index].forward) + text_input("Передняя сторона", &sets[index].forward) .on_input(RepetitionsMessage::SetForward), - text_input("Задняя сторона", &self.sets[index].backward) + text_input("Задняя сторона", &sets[index].backward) .on_input(RepetitionsMessage::SetBackward), text!("Фильтр"), - text_input("", &self.sets[index].filter) - .on_input(RepetitionsMessage::SetFilter), + text_input("", &sets[index].filter).on_input(RepetitionsMessage::SetFilter), button("Проверить фильтр").on_press(RepetitionsMessage::TryFilter), - self.count_view() + self.count_view(&sets[index]) ] .spacing(10) ) .height(Fill), - button("Удалить") - .style(|x: &Theme, _status| Style { - background: Some(Color(x.palette().danger)), - text_color: x.palette().text, - border: Default::default(), - shadow: Default::default(), - snap: false, - }) - .on_press(RepetitionsMessage::DeleteSet), + row![ + button("Сохранить").on_press(RepetitionsMessage::Save), + button("Удалить") + .style(|x: &Theme, _status| Style { + background: Some(Color(x.palette().danger)), + text_color: x.palette().text, + border: Border::default().rounded(2), + shadow: Default::default(), + snap: false, + }) + .on_press(RepetitionsMessage::DeleteSet) + ] + .spacing(10), ] - .spacing(10) - .width(Length::FillPortion(2)) - .into(); + .spacing(10) + .width(Length::FillPortion(2)) + .into(); } space().width(Length::FillPortion(2)).into() } - fn count_view(&self) -> Element<'_, RepetitionsMessage> { - if let Some(count) = self.sets[self.selected_set.unwrap()].count { + fn count_view(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { + if let Some(count) = set.count { return text!("Колличество слов: {}", count).into(); } space().into() @@ -180,7 +189,8 @@ impl RepetitionsState { fn sets_list(&self) -> Column<'_, RepetitionsMessage> { let mut column = Column::new(); let mut i = 0; - for set in &self.sets { + let sets = &self.state.lock().unwrap().card_sets; + for set in sets { column = column.push( button(text!("{}", set.name.clone())) .on_press_with(move || RepetitionsMessage::SelectSet(i.clone())) @@ -219,17 +229,19 @@ pub enum RepetitionsMessage { } #[derive(Debug, Clone)] -pub struct CardSet { - name: String, - forward: String, - backward: String, - filter: String, - count: Option, +pub struct CardSetSettings { + pub id: u32, + pub name: String, + pub forward: String, + pub backward: String, + pub filter: String, + pub count: Option, } -impl CardSet { - fn with_name(name: String) -> CardSet { - CardSet { +impl CardSetSettings { + fn with_name(name: String) -> CardSetSettings { + CardSetSettings { + id: 0, name, forward: "".to_string(), backward: "".to_string(), @@ -260,7 +272,8 @@ impl CardSet { more.insert(iced.0.clone().into(), iced.1.clone().into()); } let mut scope = Scope::new(); - scope.push_constant("key", word.key.clone()) + scope + .push_constant("key", word.key.clone()) .push_constant("value", word.value.clone()) .push_constant("tags", word.tags.clone()) .push_constant("more", more);