From c74a0eb3119b6fe82caba691c557ed458d421813 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 01:22:00 +0300 Subject: [PATCH] Splitting kana set --- src/core.rs | 1 + src/core/kana_set.rs | 201 ++++++++++++++++++++++++ src/data_provider/history.rs | 28 ++-- src/data_provider/sqlite.rs | 8 +- src/data_provider/voice.rs | 9 +- src/data_provider/web_api.rs | 9 +- src/fs.rs | 45 ++++++ src/lang.rs | 204 +------------------------ src/main.rs | 24 +-- src/navigation.rs | 119 ++++++++------- src/pages.rs | 13 ++ src/{ => pages}/dictionary.rs | 16 +- src/{ => pages}/dictionary_quiz.rs | 8 +- src/{ => pages}/history.rs | 2 +- src/{ => pages}/import.rs | 10 +- src/{ => pages}/quiz.rs | 5 +- src/{ => pages}/randomizer.rs | 4 +- src/{ => pages}/repetition.rs | 0 src/{ => pages}/repetition_settings.rs | 2 +- src/{ => pages}/repetitions.rs | 8 +- src/{ => pages}/selector.rs | 16 +- src/{ => pages}/sync.rs | 2 +- src/{ => pages}/word.rs | 2 +- src/{ => pages}/writing.rs | 6 +- src/styling.rs | 1 - 25 files changed, 389 insertions(+), 354 deletions(-) create mode 100644 src/core.rs create mode 100644 src/core/kana_set.rs create mode 100644 src/fs.rs create mode 100644 src/pages.rs rename src/{ => pages}/dictionary.rs (98%) rename src/{ => pages}/dictionary_quiz.rs (97%) rename src/{ => pages}/history.rs (98%) rename src/{ => pages}/import.rs (98%) rename src/{ => pages}/quiz.rs (98%) rename src/{ => pages}/randomizer.rs (95%) rename src/{ => pages}/repetition.rs (100%) rename src/{ => pages}/repetition_settings.rs (99%) rename src/{ => pages}/repetitions.rs (99%) rename src/{ => pages}/selector.rs (93%) rename src/{ => pages}/sync.rs (99%) rename src/{ => pages}/word.rs (99%) rename src/{ => pages}/writing.rs (97%) diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..bb063eb --- /dev/null +++ b/src/core.rs @@ -0,0 +1 @@ +pub mod kana_set; \ No newline at end of file diff --git a/src/core/kana_set.rs b/src/core/kana_set.rs new file mode 100644 index 0000000..ccd06ed --- /dev/null +++ b/src/core/kana_set.rs @@ -0,0 +1,201 @@ +#[derive(Clone, Debug)] +pub struct KanaSet { + name: String, + pub(crate) chars_type: KanaType, + 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 { + name: "Хирагана".to_string(), + chars_type: KanaType::Hiragana, + dictionary: vec![ + vec![ + (String::from("ぁ"), String::from("a")), + (String::from("ぃ"), String::from("i")), + (String::from("ぅ"), String::from("u")), + (String::from("ぇ"), String::from("e")), + (String::from("ぉ"), String::from("o")), + ], + vec![ + (String::from("さ"), String::from("sa")), + (String::from("し"), String::from("shi")), + (String::from("す"), String::from("su")), + (String::from("せ"), String::from("se")), + (String::from("そ"), String::from("so")), + ], + vec![ + (String::from("か"), String::from("ka")), + (String::from("き"), String::from("ki")), + (String::from("く"), String::from("ku")), + (String::from("け"), String::from("ke")), + (String::from("こ"), String::from("ko")), + ], + // Ряд «та» (たちつてと) + vec![ + (String::from("た"), String::from("ta")), + (String::from("ち"), String::from("chi")), + (String::from("つ"), String::from("tsu")), + (String::from("て"), String::from("te")), + (String::from("と"), String::from("to")), + ], + // Ряд «на» (なにぬねの) + vec![ + (String::from("な"), String::from("na")), + (String::from("に"), String::from("ni")), + (String::from("ぬ"), String::from("nu")), + (String::from("ね"), String::from("ne")), + (String::from("の"), String::from("no")), + ], + // Ряд «ха» (はひふへほ) + vec![ + (String::from("は"), String::from("ha")), + (String::from("ひ"), String::from("hi")), + (String::from("ふ"), String::from("fu")), + (String::from("へ"), String::from("he")), + (String::from("ほ"), String::from("ho")), + ], + // Ряд «ма» (まみむめも) + vec![ + (String::from("ま"), String::from("ma")), + (String::from("み"), String::from("mi")), + (String::from("む"), String::from("mu")), + (String::from("め"), String::from("me")), + (String::from("も"), String::from("mo")), + ], + // Ряд «я» (やゆよ) — только 3 символа + vec![ + (String::from("や"), String::from("ya")), + (String::from("ゆ"), String::from("yu")), + (String::from("よ"), String::from("yo")), + ], + // Ряд «ра» (らりるれろ) + vec![ + (String::from("ら"), String::from("ra")), + (String::from("り"), String::from("ri")), + (String::from("る"), String::from("ru")), + (String::from("れ"), String::from("re")), + (String::from("ろ"), String::from("ro")), + ], + vec![ + (String::from("わ"), String::from("wa")), + (String::from("を"), String::from("wo")), + (String::from("ん"), String::from("n")), + ], + ], + include_map: [true; 10], + } + } + + pub fn katakana() -> Self { + Self { + name: "Катакана".to_string(), + chars_type: KanaType::Katakana, + dictionary: vec![ + vec![ + (String::from("ァ"), String::from("a")), + (String::from("ィ"), String::from("i")), + (String::from("ゥ"), String::from("u")), + (String::from("ェ"), String::from("e")), + (String::from("ォ"), String::from("o")), + ], + vec![ + (String::from("サ"), String::from("sa")), + (String::from("シ"), String::from("shi")), + (String::from("ス"), String::from("su")), + (String::from("セ"), String::from("se")), + (String::from("ソ"), String::from("so")), + ], + vec![ + (String::from("カ"), String::from("ka")), + (String::from("キ"), String::from("ki")), + (String::from("ク"), String::from("ku")), + (String::from("ケ"), String::from("ke")), + (String::from("コ"), String::from("ko")), + ], + // Ряд «та» (タチツテト) + vec![ + (String::from("タ"), String::from("ta")), + (String::from("チ"), String::from("chi")), + (String::from("ツ"), String::from("tsu")), + (String::from("テ"), String::from("te")), + (String::from("ト"), String::from("to")), + ], + // Ряд «на» (ナニヌネノ) + vec![ + (String::from("ナ"), String::from("na")), + (String::from("ニ"), String::from("ni")), + (String::from("ヌ"), String::from("nu")), + (String::from("ネ"), String::from("ne")), + (String::from("ノ"), String::from("no")), + ], + // Ряд «ха» (ハヒフヘホ) + vec![ + (String::from("ハ"), String::from("ha")), + (String::from("ヒ"), String::from("hi")), + (String::from("フ"), String::from("fu")), + (String::from("ヘ"), String::from("he")), + (String::from("ホ"), String::from("ho")), + ], + // Ряд «ма» (マミムメモ) + vec![ + (String::from("マ"), String::from("ma")), + (String::from("ミ"), String::from("mi")), + (String::from("ム"), String::from("mu")), + (String::from("メ"), String::from("me")), + (String::from("モ"), String::from("mo")), + ], + // Ряд «я» (ヤユヨ) — только 3 символа + vec![ + (String::from("ヤ"), String::from("ya")), + (String::from("ユ"), String::from("yu")), + (String::from("ヨ"), String::from("yo")), + ], + // Ряд «ра» (ラリルレロ) + vec![ + (String::from("ラ"), String::from("ra")), + (String::from("リ"), String::from("ri")), + (String::from("ル"), String::from("ru")), + (String::from("レ"), String::from("re")), + (String::from("ロ"), String::from("ro")), + ], + vec![ + (String::from("ワ"), String::from("wa")), + (String::from("ヲ"), String::from("wo")), + (String::from("ン"), String::from("n")), + ], + ], + include_map: [true; 10], + } + } + + pub fn list(&self) -> Vec<(String, String)> { + let mut current_set: Vec<(String, String)> = Vec::new(); + + for i in 0..10 { + if self.include_map[i] { + self.dictionary[i].iter().for_each(|v| { + current_set.push(v.clone()); + }); + } + } + + current_set + } +} +impl Default for KanaSet { + fn default() -> Self { + KanaSet::hiragana() + } +} +impl PartialEq for KanaSet { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} \ No newline at end of file diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index e4e253b..2c04dfc 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -1,20 +1,21 @@ -use crate::dictionary::app_data_dir; +use crate::fs::*; use crate::lang::{Id, WordOpenMode}; use chrono::{DateTime, Utc}; -use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +use crate::notification::NotificationData; +use crate::notification::NotificationData::PermissionDenied; -pub fn get_history_of_set(id: Id) -> Vec { - let app_dir = history_dir(); +pub fn get_history_of_set(id: Id) -> Result, NotificationData> { + let app_dir = history_dir()?; let head = app_dir.clone().join(format!("set_{}_history.csv", id)); let mut lines = Vec::new(); append_lines_if_exists(head, &mut lines); lines.reverse(); - parse_history_items(lines) + Ok(parse_history_items(lines)) } fn append_lines_if_exists(path: PathBuf, lines: &mut Vec) { @@ -56,15 +57,15 @@ fn parse_history_items(strings: Vec) -> Vec { items } -pub fn push_note(set_id: Id, item: HistoryItem) { - let app_dir = history_dir(); +pub fn push_note(set_id: Id, item: HistoryItem) -> Result<(), NotificationData> { + let app_dir = history_dir()?; let path = app_dir.clone().join(format!("set_{}_history.csv", set_id)); let mut file = OpenOptions::new() .create(true) .append(true) .open(path) - .unwrap(); + .map_err(|_| PermissionDenied)?; let line_str = format!( "{};{};{};{};{}", item.timestamp.timestamp(), @@ -78,15 +79,8 @@ pub fn push_note(set_id: Id, item: HistoryItem) { item.before, item.after ); - writeln!(&mut file, "{}", line_str).unwrap(); -} - -pub fn history_dir() -> PathBuf { - let directory = app_data_dir().join("history"); - if !directory.exists() { - fs::create_dir(directory.clone()).unwrap(); - } - directory + writeln!(&mut file, "{}", line_str).map_err(|_| PermissionDenied)?; + Ok(()) } #[derive(Clone)] diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 764d5d6..46b850c 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -1,9 +1,8 @@ -use crate::dictionary::app_data_dir; use rusqlite::Connection; +use crate::fs::*; pub fn create_db() { - let path = app_data_dir(); - let db_file = path.join("data.db"); + let db_file = db_path(); if !db_file.exists() { std::fs::File::create(&db_file).unwrap(); let connection = Connection::open(&db_file).unwrap(); @@ -51,8 +50,7 @@ fn make_card_stats(conn: &Connection) -> Result<(), rusqlite::Error> { } pub fn default_connection() -> Connection { - let path = app_data_dir(); - let db_file = path.join("data.db"); + let db_file = db_path(); let connection = Connection::open(db_file).unwrap(); connection.execute("PRAGMA foreign_keys = ON;", []).unwrap(); connection diff --git a/src/data_provider/voice.rs b/src/data_provider/voice.rs index 92b9cd7..ccfcce4 100644 --- a/src/data_provider/voice.rs +++ b/src/data_provider/voice.rs @@ -1,17 +1,14 @@ -use crate::dictionary::app_data_dir; use reqwest::Client; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::BufReader; +use crate::fs::*; use crate::notification::NotificationData; use crate::notification::NotificationData::*; pub async fn get_voice(text: &str) -> Result, NotificationData> { - let mut path = app_data_dir(); - path.push("voice"); - if !path.exists() { - std::fs::create_dir(path.clone()).map_err(|_| PermissionDenied)?; - } + let mut path = voice_dir()?; + let hash = format!("{}.wav", hex::encode(Sha256::digest(text.as_bytes()))); path.push(hash); if !path.exists() { diff --git a/src/data_provider/web_api.rs b/src/data_provider/web_api.rs index 8a383f1..09b36e2 100644 --- a/src/data_provider/web_api.rs +++ b/src/data_provider/web_api.rs @@ -1,9 +1,9 @@ -use crate::dictionary::app_data_dir; use std::io; use log::info; use tokio::fs::{File, OpenOptions}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use zstd::{DEFAULT_COMPRESSION_LEVEL, Decoder, Encoder}; +use crate::fs::*; use crate::notification::NotificationData; use crate::notification::NotificationData::{PermissionDenied, WebApiUnavailable}; @@ -11,8 +11,7 @@ use crate::notification::NotificationData::{PermissionDenied, WebApiUnavailable} const API_URL: &str = "https://learning.micialware.ru/"; pub async fn send_data(id: String) -> Result<(), NotificationData> { - let path = app_data_dir(); - let db_file = path.join("data.db"); + let db_file = db_path(); let data = tokio::fs::read(db_file).await.map_err(|_| WebApiUnavailable)?; let data = compress(data); @@ -102,7 +101,7 @@ pub async fn get_web_version(key: &str) -> Result { } pub async fn get_local_version() -> u32 { - let file = app_data_dir().join("data.meta"); + let file = db_meta_path(); if file.exists() { let mut data = String::new(); File::open(file.clone()) @@ -126,7 +125,7 @@ pub async fn get_local_version() -> u32 { } pub async fn set_local_version(version: u32) { - let file_path = app_data_dir().join("data.meta"); + let file_path = db_meta_path(); let mut file = OpenOptions::new() .write(true) .create(true) diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..c09e388 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,45 @@ +use std::fs; +use std::path::PathBuf; +use crate::notification::NotificationData; +use crate::notification::NotificationData::PermissionDenied; + +pub fn app_data_dir() -> PathBuf { + let mut dir = dirs::data_dir().unwrap(); + dir.push("jap_learn"); + if !dir.exists() { + fs::create_dir_all(dir.clone()).unwrap(); + } + + dir +} + +pub fn db_path() -> PathBuf { + app_data_dir().join("data.db") +} +pub fn temp_db_path() -> PathBuf { + app_data_dir().join("data.db.tmp") +} + +pub fn temp_import_path() -> PathBuf { + app_data_dir().join("import") +} + +pub fn db_meta_path() -> PathBuf { + app_data_dir().join("data.meta") +} + +pub fn history_dir() -> Result { + let directory = app_data_dir().join("history"); + if !directory.exists() { + fs::create_dir_all(directory.clone()).map_err(|_| PermissionDenied)?; + } + Ok(directory) +} + +pub fn voice_dir() -> Result { + let directory = app_data_dir().join("voice"); + if !directory.exists() { + fs::create_dir_all(directory.clone()).map_err(|_| PermissionDenied)?; + } + Ok(directory) +} \ No newline at end of file diff --git a/src/lang.rs b/src/lang.rs index 961c23a..a22f31f 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -83,207 +83,7 @@ impl Id { } } -#[derive(Clone, Debug)] -pub struct KanaSet { - name: String, - pub(crate) chars_type: KanaType, - 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 { - name: "Хирагана".to_string(), - chars_type: KanaType::Hiragana, - dictionary: vec![ - vec![ - (String::from("ぁ"), String::from("a")), - (String::from("ぃ"), String::from("i")), - (String::from("ぅ"), String::from("u")), - (String::from("ぇ"), String::from("e")), - (String::from("ぉ"), String::from("o")), - ], - vec![ - (String::from("さ"), String::from("sa")), - (String::from("し"), String::from("shi")), - (String::from("す"), String::from("su")), - (String::from("せ"), String::from("se")), - (String::from("そ"), String::from("so")), - ], - vec![ - (String::from("か"), String::from("ka")), - (String::from("き"), String::from("ki")), - (String::from("く"), String::from("ku")), - (String::from("け"), String::from("ke")), - (String::from("こ"), String::from("ko")), - ], - // Ряд «та» (たちつてと) - vec![ - (String::from("た"), String::from("ta")), - (String::from("ち"), String::from("chi")), - (String::from("つ"), String::from("tsu")), - (String::from("て"), String::from("te")), - (String::from("と"), String::from("to")), - ], - // Ряд «на» (なにぬねの) - vec![ - (String::from("な"), String::from("na")), - (String::from("に"), String::from("ni")), - (String::from("ぬ"), String::from("nu")), - (String::from("ね"), String::from("ne")), - (String::from("の"), String::from("no")), - ], - // Ряд «ха» (はひふへほ) - vec![ - (String::from("は"), String::from("ha")), - (String::from("ひ"), String::from("hi")), - (String::from("ふ"), String::from("fu")), - (String::from("へ"), String::from("he")), - (String::from("ほ"), String::from("ho")), - ], - // Ряд «ма» (まみむめも) - vec![ - (String::from("ま"), String::from("ma")), - (String::from("み"), String::from("mi")), - (String::from("む"), String::from("mu")), - (String::from("め"), String::from("me")), - (String::from("も"), String::from("mo")), - ], - // Ряд «я» (やゆよ) — только 3 символа - vec![ - (String::from("や"), String::from("ya")), - (String::from("ゆ"), String::from("yu")), - (String::from("よ"), String::from("yo")), - ], - // Ряд «ра» (らりるれろ) - vec![ - (String::from("ら"), String::from("ra")), - (String::from("り"), String::from("ri")), - (String::from("る"), String::from("ru")), - (String::from("れ"), String::from("re")), - (String::from("ろ"), String::from("ro")), - ], - vec![ - (String::from("わ"), String::from("wa")), - (String::from("を"), String::from("wo")), - (String::from("ん"), String::from("n")), - ], - ], - include_map: [true; 10], - } - } - pub fn katakana() -> Self { - Self { - name: "Катакана".to_string(), - chars_type: KanaType::Katakana, - dictionary: vec![ - vec![ - (String::from("ァ"), String::from("a")), - (String::from("ィ"), String::from("i")), - (String::from("ゥ"), String::from("u")), - (String::from("ェ"), String::from("e")), - (String::from("ォ"), String::from("o")), - ], - vec![ - (String::from("サ"), String::from("sa")), - (String::from("シ"), String::from("shi")), - (String::from("ス"), String::from("su")), - (String::from("セ"), String::from("se")), - (String::from("ソ"), String::from("so")), - ], - vec![ - (String::from("カ"), String::from("ka")), - (String::from("キ"), String::from("ki")), - (String::from("ク"), String::from("ku")), - (String::from("ケ"), String::from("ke")), - (String::from("コ"), String::from("ko")), - ], - // Ряд «та» (タチツテト) - vec![ - (String::from("タ"), String::from("ta")), - (String::from("チ"), String::from("chi")), - (String::from("ツ"), String::from("tsu")), - (String::from("テ"), String::from("te")), - (String::from("ト"), String::from("to")), - ], - // Ряд «на» (ナニヌネノ) - vec![ - (String::from("ナ"), String::from("na")), - (String::from("ニ"), String::from("ni")), - (String::from("ヌ"), String::from("nu")), - (String::from("ネ"), String::from("ne")), - (String::from("ノ"), String::from("no")), - ], - // Ряд «ха» (ハヒフヘホ) - vec![ - (String::from("ハ"), String::from("ha")), - (String::from("ヒ"), String::from("hi")), - (String::from("フ"), String::from("fu")), - (String::from("ヘ"), String::from("he")), - (String::from("ホ"), String::from("ho")), - ], - // Ряд «ма» (マミムメモ) - vec![ - (String::from("マ"), String::from("ma")), - (String::from("ミ"), String::from("mi")), - (String::from("ム"), String::from("mu")), - (String::from("メ"), String::from("me")), - (String::from("モ"), String::from("mo")), - ], - // Ряд «я» (ヤユヨ) — только 3 символа - vec![ - (String::from("ヤ"), String::from("ya")), - (String::from("ユ"), String::from("yu")), - (String::from("ヨ"), String::from("yo")), - ], - // Ряд «ра» (ラリルレロ) - vec![ - (String::from("ラ"), String::from("ra")), - (String::from("リ"), String::from("ri")), - (String::from("ル"), String::from("ru")), - (String::from("レ"), String::from("re")), - (String::from("ロ"), String::from("ro")), - ], - vec![ - (String::from("ワ"), String::from("wa")), - (String::from("ヲ"), String::from("wo")), - (String::from("ン"), String::from("n")), - ], - ], - include_map: [true; 10], - } - } - - pub fn list(&self) -> Vec<(String, String)> { - let mut current_set: Vec<(String, String)> = Vec::new(); - - for i in 0..10 { - if self.include_map[i] { - self.dictionary[i].iter().for_each(|v| { - current_set.push(v.clone()); - }); - } - } - - current_set - } -} -impl Default for KanaSet { - fn default() -> Self { - KanaSet::hiragana() - } -} -impl PartialEq for KanaSet { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - } -} #[derive(Clone, Debug)] pub struct WordData { pub id: Id, @@ -448,7 +248,7 @@ impl DeckData { } } update_stat_score(word, &self.state.lock().connection); - push_note( + let _ = push_note( self.settings.id, HistoryItem { timestamp: Utc::now(), @@ -457,7 +257,7 @@ impl DeckData { before: old_score, after: new_score, }, - ) + ); } pub fn len(&self) -> usize { self.set.len() diff --git a/src/main.rs b/src/main.rs index b8bcf5e..b608b55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,14 @@ #![windows_subsystem = "windows"] mod data_provider; -mod dictionary; -mod dictionary_quiz; -mod history; -pub mod import; -mod lang; -pub mod navigation; -mod quiz; -mod randomizer; -mod repetition; -mod repetition_settings; -mod repetitions; -mod selector; -pub mod styling; -mod sync; -mod word; -mod writing; pub mod notification; pub mod app; - +mod lang; +mod styling; +mod pages; +mod navigation; +mod fs; +mod core; use crate::app::start; -use crate::navigation::RootMessage; use mimalloc::MiMalloc; #[global_allocator] diff --git a/src/navigation.rs b/src/navigation.rs index cb5818a..61862c9 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,39 +1,46 @@ use crate::app::{fill_state, AppState}; -use crate::data_provider::history::{get_history_of_set, history_dir}; +use crate::data_provider::history::get_history_of_set; use crate::data_provider::sqlite::{create_db, default_connection}; -use crate::data_provider::web_api::{get_local_version, get_web_version, load_data, set_local_version}; -use crate::dictionary::{app_data_dir, DictionaryMessage, DictionaryState}; -use crate::dictionary_quiz::{DictionaryQuizMessage, DictionaryQuizState}; -use crate::history::{HistoryMessage, HistoryState}; -use crate::import::{ImportMessage, ImportState}; +use crate::data_provider::web_api::{ + get_local_version, get_web_version, load_data, set_local_version, +}; +use crate::fs::*; use crate::lang::Id; use crate::message_navigation; use crate::navigation::Page::*; -use crate::navigation::RootMessage::{DataLoaded, Keyboard, Notification, PushNotification, UpdateData}; -use crate::notification::{NotificationData, NotificationManager, NotificationMessage, NotificationRequest}; -use crate::quiz::{QuizMessage, QuizState}; -use crate::randomizer::{RandomizerMessage, RandomizerState}; -use crate::repetition::{RepetitionMessage, RepetitionState}; -use crate::repetition_settings::{RepetitionSettingsMessage, RepetitionSettingsState}; -use crate::repetitions::{RepetitionsMessage, RepetitionsState}; -use crate::selector::{SelectorMessage, SelectorState}; +use crate::navigation::RootMessage::{ + DataLoaded, Keyboard, Notification, PushNotification, UpdateData, +}; +use crate::notification::{ + NotificationData, NotificationManager, NotificationMessage, NotificationRequest, +}; +use crate::pages::dictionary::{DictionaryMessage, DictionaryState}; +use crate::pages::dictionary_quiz::{DictionaryQuizMessage, DictionaryQuizState}; +use crate::pages::history::{HistoryMessage, HistoryState}; +use crate::pages::import::{ImportMessage, ImportState}; +use crate::pages::quiz::{QuizMessage, QuizState}; +use crate::pages::randomizer::{RandomizerMessage, RandomizerState}; +use crate::pages::repetition::{RepetitionMessage, RepetitionState}; +use crate::pages::repetition_settings::{RepetitionSettingsMessage, RepetitionSettingsState}; +use crate::pages::repetitions::{RepetitionsMessage, RepetitionsState}; +use crate::pages::selector::{SelectorMessage, SelectorState}; +use crate::pages::sync::{SyncMessage, SyncState}; +use crate::pages::word::{WordMessage, WordState}; +use crate::pages::writing::{WritingMessage, WritingState}; use crate::state_navigate_handle; use crate::state_update; -use crate::sync::{SyncMessage, SyncState}; use crate::view_navigation; -use crate::word::{WordMessage, WordState}; -use crate::writing::{WritingMessage, WritingState}; use chrono::NaiveDate; use hashbrown::HashMap; use iced::keyboard::Event; use iced::widget::stack; use iced::{Element, Task}; use iced_core::Length::Fill; +use log::info; use parking_lot::Mutex; use rusqlite::Connection; use std::sync::Arc; use std::time::Instant; -use log::info; impl Default for ScreenState { fn default() -> Self { @@ -91,7 +98,7 @@ pub enum Page { pub struct ScreenState { stack: Vec, app_state: Arc>, - notification_manager: NotificationManager + notification_manager: NotificationManager, } impl ScreenState { @@ -108,21 +115,16 @@ impl ScreenState { let key = state_guard.sync_data.key.clone().unwrap(); final_task = Task::batch([ reading_additional_task, - Task::perform( - Self::load_web_backup(key), - |result| { - match result { - Ok(update) => { - if update { - UpdateData - }else { - RootMessage::None - } - } - Err(err) => PushNotification(NotificationRequest::Warning(err)) + Task::perform(Self::load_web_backup(key), |result| match result { + Ok(update) => { + if update { + UpdateData + } else { + RootMessage::None } } - ), + Err(err) => PushNotification(NotificationRequest::Warning(err)), + }), ]); } else { final_task = reading_additional_task; @@ -135,28 +137,35 @@ impl ScreenState { async fn load_additional_data() -> RootMessage { let directory = history_dir(); - let mut map = HashMap::new(); - for file in directory.read_dir().unwrap().flatten() { - let mut vec = vec![]; - let history_file_name = match file.file_name().into_string() { - Err(_) => continue, - Ok(v) => v, - }; + if let Ok(directory) = directory { + let mut map = HashMap::new(); + for file in directory.read_dir().unwrap().flatten() { + let mut vec = vec![]; + 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::() - .unwrap(); + let id = history_file_name[4..history_file_name.len() - 12] + .parse::() + .unwrap(); - let history = get_history_of_set(id); - let by_date = history.chunk_by(|x, x1| { - x.timestamp.naive_local().date() == x1.timestamp.naive_local().date() - }); - for group in by_date { - vec.push((group[0].timestamp.naive_local().date(), group.len() as u32)); + let history = get_history_of_set(id); + if let Ok(history) = history { + let by_date = history.chunk_by(|x, x1| { + x.timestamp.naive_local().date() == x1.timestamp.naive_local().date() + }); + for group in by_date { + vec.push((group[0].timestamp.naive_local().date(), group.len() as u32)); + } + map.insert(id, vec); + } else { + return PushNotification(NotificationRequest::Warning(history.err().unwrap())); + } } - map.insert(id, vec); + return DataLoaded(map); } - DataLoaded(map) + PushNotification(NotificationRequest::Warning(directory.err().unwrap())) } async fn load_web_backup(string: String) -> Result { @@ -191,9 +200,8 @@ impl ScreenState { if cfg!(windows) { state.connection = Connection::open_in_memory().unwrap(); } - let path = app_data_dir(); - let db_file = path.join("data.db"); - let temp_db_file = path.join("data.db.tmp"); + let db_file = db_path(); + let temp_db_file = temp_db_path(); std::fs::rename(temp_db_file, &db_file).unwrap(); state.connection = default_connection(); @@ -206,7 +214,7 @@ impl ScreenState { } if let PushNotification(notification) = &message { - return self.notification_manager.push(notification.clone()) + return self.notification_manager.push(notification.clone()); } let task: (Task, bool) = self.navigate_or_update(message); @@ -255,7 +263,6 @@ impl ScreenState { } pub fn view(&self) -> Element<'_, RootMessage> { - info!("View update"); let time = Instant::now(); let view = view_navigation!( @@ -275,7 +282,7 @@ impl ScreenState { Import ); - let notifications = self.notification_manager.draw().map(RootMessage::Notification); + let notifications = self.notification_manager.draw().map(Notification); info!("View took {:?}", time.elapsed()); stack![view, notifications].width(Fill).height(Fill).into() diff --git a/src/pages.rs b/src/pages.rs new file mode 100644 index 0000000..63d28c0 --- /dev/null +++ b/src/pages.rs @@ -0,0 +1,13 @@ +pub mod dictionary; +pub mod dictionary_quiz; +pub mod history; +pub mod import; +pub mod quiz; +pub mod randomizer; +pub mod repetition; +pub mod repetition_settings; +pub mod repetitions; +pub mod selector; +pub mod sync; +pub mod word; +pub mod writing; diff --git a/src/dictionary.rs b/src/pages/dictionary.rs similarity index 98% rename from src/dictionary.rs rename to src/pages/dictionary.rs index 4d030b5..b73e62d 100644 --- a/src/dictionary.rs +++ b/src/pages/dictionary.rs @@ -1,12 +1,12 @@ use crate::data_provider::words::{delete_group, delete_word, update_group, update_word}; -use crate::dictionary::DictionaryMessage::*; -use crate::dictionary_quiz::DictionaryQuizState; -use crate::import::ImportState; +use crate::pages::dictionary::DictionaryMessage::*; +use crate::pages::dictionary_quiz::DictionaryQuizState; +use crate::pages::import::ImportState; use crate::lang::{WordData, WordGroup}; use crate::navigation::Page::{Import, Word}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; -use crate::word::WordState; +use crate::pages::word::WordState; use chrono::{DateTime, TimeDelta, Utc}; use hashbrown::{HashMap, HashSet}; use iced::alignment::Vertical::Center; @@ -619,15 +619,7 @@ pub fn split_with_coma(ts: &str) -> Vec { .collect::>() } -pub fn app_data_dir() -> PathBuf { - let mut dir = dirs::data_dir().unwrap(); - dir.push("jap_learn"); - if !dir.exists() { - fs::create_dir_all(dir.clone()).unwrap(); - } - dir -} #[derive(Hash, PartialEq, Eq)] struct WordLineState { diff --git a/src/dictionary_quiz.rs b/src/pages/dictionary_quiz.rs similarity index 97% rename from src/dictionary_quiz.rs rename to src/pages/dictionary_quiz.rs index ef0a764..2efc286 100644 --- a/src/dictionary_quiz.rs +++ b/src/pages/dictionary_quiz.rs @@ -1,10 +1,10 @@ -use crate::RootMessage; -use crate::dictionary::split_with_coma; -use crate::dictionary_quiz::DictionaryQuizMessage::*; +use crate::navigation::RootMessage; +use crate::pages::dictionary::split_with_coma; +use crate::pages::dictionary_quiz::DictionaryQuizMessage::*; use crate::lang::WordData; use crate::navigation::Page::PreviousPage; use crate::navigation::*; -use crate::quiz::Score; +use crate::pages::quiz::Score; use crate::styling::*; use iced::Background::Color; use iced::border::Radius; diff --git a/src/history.rs b/src/pages/history.rs similarity index 98% rename from src/history.rs rename to src/pages/history.rs index d854854..15dfcef 100644 --- a/src/history.rs +++ b/src/pages/history.rs @@ -49,7 +49,7 @@ impl NavigatedPage for HistoryState { impl HistoryState { pub fn new(id: crate::lang::Id, state: Arc>) -> Self { let state = state.lock(); - let history = get_history_of_set(id); + let history = get_history_of_set(id).unwrap_or_default(); let words = history .iter() .map( diff --git a/src/import.rs b/src/pages/import.rs similarity index 98% rename from src/import.rs rename to src/pages/import.rs index b88c05c..0b84a85 100644 --- a/src/import.rs +++ b/src/pages/import.rs @@ -1,7 +1,6 @@ use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups}; use crate::data_provider::words::{add_group, add_words}; -use crate::dictionary::app_data_dir; -use crate::import::ImportMessage::*; +use crate::pages::import::ImportMessage::*; use crate::lang::{WordData, WordGroup}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::NotificationData::OpenFileError; @@ -28,6 +27,7 @@ use log::info; use tokio::task::spawn_blocking; use zip::ZipArchive; use crate::app::AppState; +use crate::fs::*; const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; @@ -376,7 +376,7 @@ impl ImportState { let file = File::open(path).map_err(|_| OpenFileError)?; let reader = BufReader::new(file); let mut archive = ZipArchive::new(reader).map_err(|_| OpenFileError)?; - let temp_file_path = app_data_dir().join("import"); + let temp_file_path = temp_import_path(); let mut temp_file = OpenOptions::new() .write(true) .create(true) @@ -394,7 +394,7 @@ impl ImportState { } } fn read_import_file() -> Result { - let temp_file_path = app_data_dir().join("import"); + let temp_file_path = temp_import_path(); let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?; let data = load_groups(&connection)?; Ok(data) @@ -431,7 +431,7 @@ impl ImportState { Task::perform( async move { spawn_blocking(move || { - let temp_file_path = app_data_dir().join("import"); + let temp_file_path = temp_import_path(); let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?; let import_list = get_words_of_group(&connection, group.id)?; let mut words_list = Vec::with_capacity(import_list.len()); diff --git a/src/quiz.rs b/src/pages/quiz.rs similarity index 98% rename from src/quiz.rs rename to src/pages/quiz.rs index facc1ab..b94d71f 100644 --- a/src/quiz.rs +++ b/src/pages/quiz.rs @@ -1,12 +1,13 @@ -use crate::lang::KanaSet; + use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; -use crate::quiz::QuizMessage::*; +use crate::pages::quiz::QuizMessage::*; use crate::styling::*; use iced::widget::*; use iced::{Element, Fill, Task, alignment}; use rand::seq::SliceRandom; use crate::app::USER_FONT; +use crate::core::kana_set::KanaSet; #[derive(Clone, Debug)] pub struct QuizState { diff --git a/src/randomizer.rs b/src/pages/randomizer.rs similarity index 95% rename from src/randomizer.rs rename to src/pages/randomizer.rs index 7c3f684..9bb414d 100644 --- a/src/randomizer.rs +++ b/src/pages/randomizer.rs @@ -1,6 +1,6 @@ -use crate::RootMessage; +use crate::navigation::RootMessage; use crate::navigation::{NavigatedPage, Page}; -use crate::randomizer::RandomizerMessage::{Back, Edit, Start}; +use crate::pages::randomizer::RandomizerMessage::{Back, Edit, Start}; use crate::styling::{HALF_SPACING, back_overlay, jl_button}; use iced::Task; use iced::widget::{button, text_editor}; diff --git a/src/repetition.rs b/src/pages/repetition.rs similarity index 100% rename from src/repetition.rs rename to src/pages/repetition.rs diff --git a/src/repetition_settings.rs b/src/pages/repetition_settings.rs similarity index 99% rename from src/repetition_settings.rs rename to src/pages/repetition_settings.rs index fe99dc5..55262cb 100644 --- a/src/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -4,7 +4,7 @@ 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::pages::repetition_settings::RepetitionSettingsMessage::*; use crate::styling::*; use iced::widget::button::danger; use iced::widget::text_input::Catalog; diff --git a/src/repetitions.rs b/src/pages/repetitions.rs similarity index 99% rename from src/repetitions.rs rename to src/pages/repetitions.rs index bf889d3..123c436 100644 --- a/src/repetitions.rs +++ b/src/pages/repetitions.rs @@ -1,12 +1,12 @@ 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::pages::history::HistoryState; use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; -use crate::repetition::RepetitionState; -use crate::repetition_settings::RepetitionSettingsState; -use crate::repetitions::RepetitionsMessage::*; +use crate::pages::repetition::RepetitionState; +use crate::pages::repetition_settings::RepetitionSettingsState; +use crate::pages::repetitions::RepetitionsMessage::*; use crate::styling::*; use chrono::{Days, Local, Utc}; use hashbrown::{HashMap, HashSet}; diff --git a/src/selector.rs b/src/pages/selector.rs similarity index 93% rename from src/selector.rs rename to src/pages/selector.rs index a1eaced..5d70f1e 100644 --- a/src/selector.rs +++ b/src/pages/selector.rs @@ -1,13 +1,12 @@ -use crate::dictionary::DictionaryState; -use crate::lang::{KanaSet, KanaType}; +use crate::pages::dictionary::DictionaryState; use crate::navigation::Page::*; use crate::navigation::{NavigatedPage, Page, RootMessage}; -use crate::randomizer::RandomizerState; -use crate::repetitions::RepetitionsState; -use crate::selector::SelectorMessage::ChangeMode; +use crate::pages::randomizer::RandomizerState; +use crate::pages::repetitions::RepetitionsState; +use crate::pages::selector::SelectorMessage::ChangeMode; use crate::styling::*; -use crate::sync::SyncState; -use crate::writing::WritingState; +use crate::pages::sync::SyncState; +use crate::pages::writing::WritingState; use iced::widget::button::{Status, Style}; use iced::widget::*; use iced::{Element, Task, alignment}; @@ -17,7 +16,8 @@ use iced_core::Color; use parking_lot::Mutex; use std::sync::Arc; use crate::app::AppState; -use crate::quiz::QuizState; +use crate::core::kana_set::{KanaSet, KanaType}; +use crate::pages::quiz::QuizState; pub struct SelectorState { pub set: KanaSet, diff --git a/src/sync.rs b/src/pages/sync.rs similarity index 99% rename from src/sync.rs rename to src/pages/sync.rs index 45c87f1..a04cae5 100644 --- a/src/sync.rs +++ b/src/pages/sync.rs @@ -3,7 +3,7 @@ use crate::data_provider::web_api::*; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; -use crate::sync::SyncMessage::*; +use crate::pages::sync::SyncMessage::*; use iced::widget::button::danger; use iced::widget::container::rounded_box; use iced::widget::{button, column, container, progress_bar, row, space, text, toggler}; diff --git a/src/word.rs b/src/pages/word.rs similarity index 99% rename from src/word.rs rename to src/pages/word.rs index 84ae7fc..7d9778a 100644 --- a/src/word.rs +++ b/src/pages/word.rs @@ -3,7 +3,7 @@ use crate::lang::WordData; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; -use crate::word::WordMessage::*; +use crate::pages::word::WordMessage::*; use iced::widget::button::danger; use iced::widget::{button, column, row, rule, scrollable, text, text_input}; use iced::{Element, Fill, Task}; diff --git a/src/writing.rs b/src/pages/writing.rs similarity index 97% rename from src/writing.rs rename to src/pages/writing.rs index 4f14199..2231197 100644 --- a/src/writing.rs +++ b/src/pages/writing.rs @@ -1,11 +1,11 @@ -use crate::RootMessage; -use crate::lang::KanaSet; + use crate::navigation::Page::PreviousPage; -use crate::navigation::{NavigatedPage, Page}; +use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; use iced::widget::*; use iced::{Element, Fill, Task, alignment}; use rand::seq::SliceRandom; +use crate::core::kana_set::KanaSet; #[derive(Clone, Debug)] pub struct WritingState { diff --git a/src/styling.rs b/src/styling.rs index f7d2fdb..7880b32 100644 --- a/src/styling.rs +++ b/src/styling.rs @@ -1,4 +1,3 @@ -use crate::repetitions::Style; use iced::Element; use iced::border::Radius; use iced::widget::button::*;