diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs new file mode 100644 index 0000000..fd98c88 --- /dev/null +++ b/src/data_provider/history.rs @@ -0,0 +1,78 @@ +use crate::dictionary::app_data_dir; +use crate::lang::WordOpenMode; +use chrono::{DateTime, Utc}; +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; + +const MAX_HISTORY_LENGTH: usize = 1000; + +pub fn get_history_of_set(id: u32) -> Vec { + let app_dir = app_data_dir(); + let head = app_dir.clone().join(format!("set_{}_history.csv", id)); + let tail = app_dir.clone().join(format!("set_{}_history_tail.csv", id)); + let mut lines = Vec::new(); + + append_lines_if_exists(tail, &mut lines); + append_lines_if_exists(head, &mut lines); + + parse_history_items(lines) +} + +fn append_lines_if_exists(path: PathBuf, lines: &mut Vec) { + if path.exists() { + let reader = BufReader::new(File::open(path).unwrap()); + for line in reader.lines() { + lines.push(line.unwrap()); + } + } +} + +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::>()[..] { + + items.push(HistoryItem { + timestamp: DateTime::from_timestamp(time.parse().unwrap(), 0).unwrap(), + word_id: word.parse::().unwrap(), + mode: match mode.parse::().unwrap() { + 2 => WordOpenMode::Hard, + 3 => WordOpenMode::Ok, + 4 => WordOpenMode::Easy, + _ => WordOpenMode::None, + }, + before: before.parse().unwrap(), + after: after.parse().unwrap(), + }); + } + } + + items +} + +pub fn push_note(set_id: u32, item: HistoryItem) { + let app_dir = app_data_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(); + let line_str = format!("{};{};{};{};{}", item.timestamp.timestamp(), item.word_id, match item.mode { + WordOpenMode::Easy => 4, + WordOpenMode::Ok => 3, + WordOpenMode::Hard => 2, + WordOpenMode::None => 1 + }, + item.before, + item.after); + writeln!(&mut file, "{}", line_str.to_string()).unwrap(); +} + +#[derive(Clone)] +pub struct HistoryItem { + pub timestamp: DateTime, + pub word_id: u32, + pub mode: WordOpenMode, + pub before: u8, + pub after: u8, +} diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index 21972a1..4f8b99f 100644 --- a/src/data_provider/mod.rs +++ b/src/data_provider/mod.rs @@ -4,4 +4,5 @@ pub(crate) mod card_stats; pub(crate) mod voice; pub(crate) mod settings; pub(crate) mod sqlite; +pub(crate) mod history; diff --git a/src/dictionary.rs b/src/dictionary.rs index 75afaf1..a826202 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -550,6 +550,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"); @@ -559,3 +560,13 @@ pub fn app_data_dir() -> PathBuf { dir } + +pub fn app_cache_dir() -> PathBuf { + let mut dir = dirs::cache_dir().unwrap(); + dir.push("jap_learn"); + if !dir.exists() { + fs::create_dir(dir.clone()).unwrap(); + } + + dir +} \ No newline at end of file diff --git a/src/history.rs b/src/history.rs index e3dcf32..e41cfb7 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1,18 +1,18 @@ -use crate::dictionary::app_data_dir; -use crate::lang::{WordData, WordOpenMode}; -use crate::{AppState, NavigatedPage, Page, RootMessage}; -use chrono::{DateTime, Utc}; -use iced::widget::*; -use iced::{Element, Fill, Left, Task}; -use std::fs::File; -use std::io::{BufRead, BufReader}; -use std::sync::{Arc, Mutex}; +use crate::data_provider::history::{HistoryItem, get_history_of_set}; +use crate::lang::WordData; +use crate::{AppState, DEFAULT_SPACING, NavigatedPage, Page, RootMessage}; use HistoryMessage::Back; +use iced::alignment::Horizontal::Center; +use iced::widget::space::horizontal; +use iced::widget::*; +use iced::{Element, Fill, FillPortion, Left, Task}; +use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct HistoryState { - set_id: usize, + set_id: u32, list: Vec, + words: Vec, } impl NavigatedPage for HistoryState { @@ -25,10 +25,25 @@ impl NavigatedPage for HistoryState { } impl HistoryState { - pub fn new(id: usize, state: Arc>) -> Self { + pub fn new(id: u32, state: Arc>) -> Self { + let state = state.lock().unwrap(); + 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() + }) + .collect(); + Self { set_id: id, - list: get_history_of_set(id, &state.lock().unwrap().dictionary), + list: history, + words, } } @@ -39,12 +54,16 @@ impl HistoryState { pub fn view(&self) -> Element<'_, HistoryMessage> { container( iced::widget::column![ - button("Назад").on_press(HistoryMessage::Back), - // column![ - // - // ] - // .height(Fill) - // .width(Fill) + button("Назад").on_press(Back), + iced::widget::row![ + horizontal().width(FillPortion(1)), + scrollable(self.history_lines().padding(DEFAULT_SPACING)) + .height(Fill) + .width(FillPortion(5)), + horizontal().width(FillPortion(1)) + ] + .height(Fill) + .width(Fill) ] .align_x(Left) .width(Fill), @@ -53,66 +72,37 @@ impl HistoryState { .padding(10) .into() } -} -fn get_history_of_set(id: usize, words: &Vec) -> Vec { - let app_dir = app_data_dir(); - let head = app_dir.clone().join(format!("set_{}_history.csv", id)); - let tail = app_dir.clone().join(format!("set_{}_history.csv", id)); - let mut lines = Vec::new(); - if tail.exists() { - let reader = BufReader::new(File::open(tail).unwrap()); - for line in reader.lines() { - lines.push(line.unwrap()); + fn history_lines(&self) -> Column<'_, HistoryMessage> { + let mut column = Column::new(); + for (item, index) in self.list.iter().zip(0..self.list.len()) { + column = column.push( + row![ + iced::widget::column![ + text!("{}", self.words[index].key) + .width(Fill) + .align_x(Center), + text!("{}", self.words[index].value) + .width(Fill) + .align_x(Center), + ] + .align_x(Center), + iced::widget::column![ + text!("{} ➞ {}", item.before, item.after), + text!("{}", item.timestamp.format("%d.%m %H:%M")) + ] + .spacing(5) + .align_x(Center) + ] + .spacing(5), + ); + column = column.push(rule::horizontal(1)); } + column.spacing(5) } - - if head.exists() { - let reader = BufReader::new(File::open(head).unwrap()); - for line in reader.lines() { - lines.push(line.unwrap()); - } - } - - parse_history_items(lines, words) -} - -fn parse_history_items(strings: Vec, words: &Vec) -> Vec { - let mut items = Vec::with_capacity(strings.len()); - for string in strings { - if let [time, word, mode, before, after] = string.split(';').collect::>()[..] { - items.push(HistoryItem { - timestamp: DateTime::from_timestamp(time.parse().unwrap(), 0).unwrap(), - word: words - .iter() - .find(|w| w.id == word.parse::().unwrap()) - .unwrap() - .clone(), - mode: match mode.parse::().unwrap() { - 2 => WordOpenMode::Hard, - 3 => WordOpenMode::Ok, - 4 => WordOpenMode::Easy, - _ => WordOpenMode::None, - }, - before: before.parse().unwrap(), - after: after.parse().unwrap(), - }); - } - } - - items } #[derive(Clone)] pub enum HistoryMessage { Back, } - -#[derive(Clone)] -pub struct HistoryItem { - timestamp: DateTime, - word: WordData, - mode: WordOpenMode, - before: u8, - after: u8, -} diff --git a/src/lang.rs b/src/lang.rs index ead52b3..7a0e296 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -13,10 +13,11 @@ use serde::{Deserialize, Serialize}; use std::cmp::min; use std::collections::HashMap; use std::sync::{Arc, Mutex}; +use crate::data_provider::history::{push_note, HistoryItem}; const MAX_HISTORY_LEN: usize = 20; const MAX_HISTORY_LEN_PART: f32 = 0.33; -const MAX_SCORE: i32 = 25; +const MAX_SCORE: u8 = 25; const FADE_PER_DAY: f32 = 0.95; #[derive(Clone, Debug)] pub struct KanaSet { @@ -266,21 +267,21 @@ pub struct CardStatistics { pub word_id: u32, pub set_id: u32, pub last_open: DateTime, - pub score: i32, + pub score: u8, } impl CardStatistics { pub fn update(&mut self, status: WordOpenMode) { match status { WordOpenMode::Easy => { - self.score = (self.calculated_score() + 5.0).round() as i32; + self.score = (self.calculated_score() + 5.0).round() as u8; } - WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as i32, + WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as u8, WordOpenMode::Hard => { - self.score = (self.calculated_score() * 0.75).round() as i32; + self.score = (self.calculated_score() * 0.75).round() as u8; } WordOpenMode::None => { - self.score = (self.calculated_score() * 0.4) as i32; + self.score = (self.calculated_score() * 0.4) as u8; } } @@ -315,6 +316,7 @@ pub struct CardSet { current_word_index: Option, state: Arc>, order_module: OrderModule, + settings: CardSetSettings } impl CardSet { @@ -364,6 +366,7 @@ impl CardSet { // } SetOrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), }, + settings: settings.clone(), } } @@ -406,7 +409,9 @@ impl CardSet { let index = self.current_word_index.unwrap(); let word = &mut self.set[index]; + let old_score = word.score; word.update(status); + let new_score = word.score; match self.order_module.clone() { OrderModule::SemiRandomSRS(mut module) => { @@ -422,7 +427,15 @@ impl CardSet { // self.order_module = OrderModule::WorstWordsSRS(module); // } } - update_stat_score(word, &self.state.lock().unwrap().connection) + update_stat_score(word, &self.state.lock().unwrap().connection); + push_note(self.settings.id, HistoryItem{ + timestamp: Utc::now(), + word_id: word.word_id, + mode: WordOpenMode::Easy, + before: old_score, + after: new_score, + }) + } pub fn len(&self) -> usize { diff --git a/src/repetitions.rs b/src/repetitions.rs index 0bbc022..2450e65 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -26,7 +26,11 @@ impl NavigatedPage for RepetitionsState { if let RepetitionsMessage::Back = message { Some(PreviousPage) } else if let RepetitionsMessage::GoToHistory = message { - Some(History(HistoryState::new(self.selected_set.unwrap(), self.state.clone()))) + 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()))) } else if let RepetitionsMessage::GoToRepetition = message { let clone = self.state.clone(); let card_set; diff --git a/src/sync.rs b/src/sync.rs index 73e32a8..e427579 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -141,6 +141,7 @@ impl SyncState { } Task::none() } + pub fn view(&self) -> Element<'_, SyncMessage> { container( column![ @@ -172,6 +173,7 @@ impl SyncState { container( container(text!("{}", key).size(20).font(Font::MONOSPACE)).padding(3) ) + .style(rounded_box), button("Скопировать в буффер обмена") .on_press(SyncMessage::CopyKey) @@ -264,7 +266,10 @@ async fn load_data(id: String) { fn decompress(data: Vec) -> Vec { let mut decoder = Decoder::new(&data[..]).unwrap(); let mut decompressed = Vec::new(); - io::copy(&mut decoder, &mut decompressed).unwrap(); + let res = io::copy(&mut decoder, &mut decompressed); + if let Err(e) = res { + println!("{}", e); + } decompressed }