History page drawing
This commit is contained in:
@@ -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<HistoryItem> {
|
||||||
|
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<String>) {
|
||||||
|
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<String>) -> Vec<HistoryItem> {
|
||||||
|
let mut items = Vec::with_capacity(strings.len());
|
||||||
|
for string in strings {
|
||||||
|
if let [time, word, mode, before, after] = string.split(';').collect::<Vec<&str>>()[..] {
|
||||||
|
|
||||||
|
items.push(HistoryItem {
|
||||||
|
timestamp: DateTime::from_timestamp(time.parse().unwrap(), 0).unwrap(),
|
||||||
|
word_id: word.parse::<u32>().unwrap(),
|
||||||
|
mode: match mode.parse::<u8>().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<Utc>,
|
||||||
|
pub word_id: u32,
|
||||||
|
pub mode: WordOpenMode,
|
||||||
|
pub before: u8,
|
||||||
|
pub after: u8,
|
||||||
|
}
|
||||||
@@ -4,4 +4,5 @@ pub(crate) mod card_stats;
|
|||||||
pub(crate) mod voice;
|
pub(crate) mod voice;
|
||||||
pub(crate) mod settings;
|
pub(crate) mod settings;
|
||||||
pub(crate) mod sqlite;
|
pub(crate) mod sqlite;
|
||||||
|
pub(crate) mod history;
|
||||||
|
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ pub fn split_with_coma(ts: &str) -> Vec<String> {
|
|||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn app_data_dir() -> PathBuf {
|
pub fn app_data_dir() -> PathBuf {
|
||||||
let mut dir = dirs::data_dir().unwrap();
|
let mut dir = dirs::data_dir().unwrap();
|
||||||
dir.push("jap_learn");
|
dir.push("jap_learn");
|
||||||
@@ -559,3 +560,13 @@ pub fn app_data_dir() -> PathBuf {
|
|||||||
|
|
||||||
dir
|
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
|
||||||
|
}
|
||||||
+62
-72
@@ -1,18 +1,18 @@
|
|||||||
use crate::dictionary::app_data_dir;
|
use crate::data_provider::history::{HistoryItem, get_history_of_set};
|
||||||
use crate::lang::{WordData, WordOpenMode};
|
use crate::lang::WordData;
|
||||||
use crate::{AppState, NavigatedPage, Page, RootMessage};
|
use crate::{AppState, DEFAULT_SPACING, 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 HistoryMessage::Back;
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct HistoryState {
|
pub struct HistoryState {
|
||||||
set_id: usize,
|
set_id: u32,
|
||||||
list: Vec<HistoryItem>,
|
list: Vec<HistoryItem>,
|
||||||
|
words: Vec<WordData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NavigatedPage<HistoryMessage> for HistoryState {
|
impl NavigatedPage<HistoryMessage> for HistoryState {
|
||||||
@@ -25,10 +25,25 @@ impl NavigatedPage<HistoryMessage> for HistoryState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryState {
|
impl HistoryState {
|
||||||
pub fn new(id: usize, state: Arc<Mutex<AppState>>) -> Self {
|
pub fn new(id: u32, state: Arc<Mutex<AppState>>) -> 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 {
|
Self {
|
||||||
set_id: id,
|
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> {
|
pub fn view(&self) -> Element<'_, HistoryMessage> {
|
||||||
container(
|
container(
|
||||||
iced::widget::column![
|
iced::widget::column![
|
||||||
button("Назад").on_press(HistoryMessage::Back),
|
button("Назад").on_press(Back),
|
||||||
// column![
|
iced::widget::row![
|
||||||
//
|
horizontal().width(FillPortion(1)),
|
||||||
// ]
|
scrollable(self.history_lines().padding(DEFAULT_SPACING))
|
||||||
// .height(Fill)
|
.height(Fill)
|
||||||
// .width(Fill)
|
.width(FillPortion(5)),
|
||||||
|
horizontal().width(FillPortion(1))
|
||||||
|
]
|
||||||
|
.height(Fill)
|
||||||
|
.width(Fill)
|
||||||
]
|
]
|
||||||
.align_x(Left)
|
.align_x(Left)
|
||||||
.width(Fill),
|
.width(Fill),
|
||||||
@@ -53,66 +72,37 @@ impl HistoryState {
|
|||||||
.padding(10)
|
.padding(10)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn get_history_of_set(id: usize, words: &Vec<WordData>) -> Vec<HistoryItem> {
|
fn history_lines(&self) -> Column<'_, HistoryMessage> {
|
||||||
let app_dir = app_data_dir();
|
let mut column = Column::new();
|
||||||
let head = app_dir.clone().join(format!("set_{}_history.csv", id));
|
for (item, index) in self.list.iter().zip(0..self.list.len()) {
|
||||||
let tail = app_dir.clone().join(format!("set_{}_history.csv", id));
|
column = column.push(
|
||||||
let mut lines = Vec::new();
|
row![
|
||||||
if tail.exists() {
|
iced::widget::column![
|
||||||
let reader = BufReader::new(File::open(tail).unwrap());
|
text!("{}", self.words[index].key)
|
||||||
for line in reader.lines() {
|
.width(Fill)
|
||||||
lines.push(line.unwrap());
|
.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<String>, words: &Vec<WordData>) -> Vec<HistoryItem> {
|
|
||||||
let mut items = Vec::with_capacity(strings.len());
|
|
||||||
for string in strings {
|
|
||||||
if let [time, word, mode, before, after] = string.split(';').collect::<Vec<&str>>()[..] {
|
|
||||||
items.push(HistoryItem {
|
|
||||||
timestamp: DateTime::from_timestamp(time.parse().unwrap(), 0).unwrap(),
|
|
||||||
word: words
|
|
||||||
.iter()
|
|
||||||
.find(|w| w.id == word.parse::<u32>().unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.clone(),
|
|
||||||
mode: match mode.parse::<u8>().unwrap() {
|
|
||||||
2 => WordOpenMode::Hard,
|
|
||||||
3 => WordOpenMode::Ok,
|
|
||||||
4 => WordOpenMode::Easy,
|
|
||||||
_ => WordOpenMode::None,
|
|
||||||
},
|
|
||||||
before: before.parse().unwrap(),
|
|
||||||
after: after.parse().unwrap(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum HistoryMessage {
|
pub enum HistoryMessage {
|
||||||
Back,
|
Back,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct HistoryItem {
|
|
||||||
timestamp: DateTime<Utc>,
|
|
||||||
word: WordData,
|
|
||||||
mode: WordOpenMode,
|
|
||||||
before: u8,
|
|
||||||
after: u8,
|
|
||||||
}
|
|
||||||
|
|||||||
+20
-7
@@ -13,10 +13,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use crate::data_provider::history::{push_note, HistoryItem};
|
||||||
|
|
||||||
const MAX_HISTORY_LEN: usize = 20;
|
const MAX_HISTORY_LEN: usize = 20;
|
||||||
const MAX_HISTORY_LEN_PART: f32 = 0.33;
|
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;
|
const FADE_PER_DAY: f32 = 0.95;
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct KanaSet {
|
pub struct KanaSet {
|
||||||
@@ -266,21 +267,21 @@ pub struct CardStatistics {
|
|||||||
pub word_id: u32,
|
pub word_id: u32,
|
||||||
pub set_id: u32,
|
pub set_id: u32,
|
||||||
pub last_open: DateTime<Utc>,
|
pub last_open: DateTime<Utc>,
|
||||||
pub score: i32,
|
pub score: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CardStatistics {
|
impl CardStatistics {
|
||||||
pub fn update(&mut self, status: WordOpenMode) {
|
pub fn update(&mut self, status: WordOpenMode) {
|
||||||
match status {
|
match status {
|
||||||
WordOpenMode::Easy => {
|
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 => {
|
WordOpenMode::Hard => {
|
||||||
self.score = (self.calculated_score() * 0.75).round() as i32;
|
self.score = (self.calculated_score() * 0.75).round() as u8;
|
||||||
}
|
}
|
||||||
WordOpenMode::None => {
|
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<usize>,
|
current_word_index: Option<usize>,
|
||||||
state: Arc<Mutex<AppState>>,
|
state: Arc<Mutex<AppState>>,
|
||||||
order_module: OrderModule,
|
order_module: OrderModule,
|
||||||
|
settings: CardSetSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CardSet {
|
impl CardSet {
|
||||||
@@ -364,6 +366,7 @@ impl CardSet {
|
|||||||
// }
|
// }
|
||||||
SetOrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()),
|
SetOrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()),
|
||||||
},
|
},
|
||||||
|
settings: settings.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +409,9 @@ impl CardSet {
|
|||||||
let index = self.current_word_index.unwrap();
|
let index = self.current_word_index.unwrap();
|
||||||
|
|
||||||
let word = &mut self.set[index];
|
let word = &mut self.set[index];
|
||||||
|
let old_score = word.score;
|
||||||
word.update(status);
|
word.update(status);
|
||||||
|
let new_score = word.score;
|
||||||
|
|
||||||
match self.order_module.clone() {
|
match self.order_module.clone() {
|
||||||
OrderModule::SemiRandomSRS(mut module) => {
|
OrderModule::SemiRandomSRS(mut module) => {
|
||||||
@@ -422,7 +427,15 @@ impl CardSet {
|
|||||||
// self.order_module = OrderModule::WorstWordsSRS(module);
|
// 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 {
|
pub fn len(&self) -> usize {
|
||||||
|
|||||||
+5
-1
@@ -26,7 +26,11 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
|
|||||||
if let RepetitionsMessage::Back = message {
|
if let RepetitionsMessage::Back = message {
|
||||||
Some(PreviousPage)
|
Some(PreviousPage)
|
||||||
} else if let RepetitionsMessage::GoToHistory = message {
|
} 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 {
|
} else if let RepetitionsMessage::GoToRepetition = message {
|
||||||
let clone = self.state.clone();
|
let clone = self.state.clone();
|
||||||
let card_set;
|
let card_set;
|
||||||
|
|||||||
+6
-1
@@ -141,6 +141,7 @@ impl SyncState {
|
|||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self) -> Element<'_, SyncMessage> {
|
pub fn view(&self) -> Element<'_, SyncMessage> {
|
||||||
container(
|
container(
|
||||||
column![
|
column![
|
||||||
@@ -172,6 +173,7 @@ impl SyncState {
|
|||||||
container(
|
container(
|
||||||
container(text!("{}", key).size(20).font(Font::MONOSPACE)).padding(3)
|
container(text!("{}", key).size(20).font(Font::MONOSPACE)).padding(3)
|
||||||
)
|
)
|
||||||
|
|
||||||
.style(rounded_box),
|
.style(rounded_box),
|
||||||
button("Скопировать в буффер обмена")
|
button("Скопировать в буффер обмена")
|
||||||
.on_press(SyncMessage::CopyKey)
|
.on_press(SyncMessage::CopyKey)
|
||||||
@@ -264,7 +266,10 @@ async fn load_data(id: String) {
|
|||||||
fn decompress(data: Vec<u8>) -> Vec<u8> {
|
fn decompress(data: Vec<u8>) -> Vec<u8> {
|
||||||
let mut decoder = Decoder::new(&data[..]).unwrap();
|
let mut decoder = Decoder::new(&data[..]).unwrap();
|
||||||
let mut decompressed = Vec::new();
|
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
|
decompressed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user