History page drawing

This commit is contained in:
2026-07-29 12:57:46 +03:00
parent 848d376cf8
commit b8be482efd
7 changed files with 183 additions and 81 deletions
+78
View File
@@ -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,
}
+1
View File
@@ -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;
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
} }