Rewrite history page

This commit is contained in:
micialware committed 2026-09-04 11:18:23 +03:00
1 parent f19a6f64c3
commit 532640058e
11 files changed
+200 -229

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@ create table history
id INTEGER
primary key autoincrement,
set_id INTEGER not null references decks on delete cascade,
stat_id INTEGER not null references states on delete cascade,
word_id INTEGER not null references words on delete cascade,
timestamp INTEGER not null,
answer INTEGER not null,
before INTEGER not null,
+3 -5
View File
@@ -1,13 +1,10 @@
use std::sync::Arc;
use crate::data::Id;
use crate::data_provider::decks::{DeckRepository};
use crate::data_provider::settings::get_setting;
use crate::data_provider::sqlite::{SqliteConnection};
use crate::data_provider::words::{WordRepository};
use crate::navigation::RootMessage::Keyboard;
use crate::navigation::{AppSettings, RootMessage, ScreenState};
use chrono::NaiveDate;
use hashbrown::HashMap;
use iced::{Subscription, keyboard, window};
use iced_core::window::Position;
#[cfg(target_os = "linux")]
@@ -18,6 +15,7 @@ use parking_lot::RwLock;
use rusqlite::Connection;
use crate::data::repositories::Repository;
use crate::data_provider::card_states::CardStateRepository;
use crate::data_provider::history::HistoryRepository;
use crate::data_provider::word_groups::WordGroupRepository;
pub const USER_FONT: Font = Font::with_name("Noto Sans JP");
@@ -69,7 +67,7 @@ pub struct AppState {
pub card_states: CardStateRepository,
pub connection: Arc<RwLock<SqliteConnection>>,
pub sync_data: Arc<RwLock<AppSettings>>,
pub activity: HashMap<Id, Vec<(NaiveDate, u32)>>,
pub history: HistoryRepository,
}
impl Default for AppState {
@@ -86,11 +84,11 @@ impl AppState {
decks: DeckRepository::new(connection.clone()),
word_groups: WordGroupRepository::new(connection.clone()),
card_states: CardStateRepository::new(connection.clone()),
history: HistoryRepository::new(connection.clone()),
sync_data: Arc::new(RwLock::new(AppSettings {
key: None,
auto_web_fetch: false,
})),
activity: Default::default(),
connection,
}
}
+13 -13
View File
@@ -1,12 +1,11 @@
use crate::app::AppState;
use crate::core::srs::OrderModule::{Fsrs, RandomSRS, SemiRandomSRS, WorstWordsSRS};
use crate::core::srs::{
FsrsModule, OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule,
OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule,
WordOpenMode, WorstWordsSRSModule,
};
use crate::data::entities::*;
use crate::data_provider::card_states::{load_stats_of_deck, update_stat_score};
use crate::data_provider::history::push_note;
use crate::data_provider::card_states::update_stat_score;
use crate::notification::{MaybeError, NotificationData};
use chrono::Utc;
@@ -116,16 +115,17 @@ impl DeckData {
}
}
update_stat_score(word, &self.state.connection.read())?;
let _ = push_note(
self.settings.id,
HistoryItem {
timestamp: Utc::now(),
word_id: word.word_id,
mode: status,
before: old_score,
after: new_score,
},
);
todo!("Implement history pushing")
// let _ = push_note(
// self.settings.id,
// HistoryNote {
// timestamp: Utc::now(),
// word_id: word.word_id,
// answer: status,
// before: old_score,
// after: new_score,
// },
// );
}
Ok(())
+15 -2
View File
@@ -62,12 +62,25 @@ impl WordData {
}
#[derive(Clone)]
pub struct HistoryItem {
pub struct HistoryNote {
pub id: Id,
pub timestamp: DateTime<Utc>,
pub set_id: Id,
pub word_id: Id,
pub mode: WordOpenMode,
pub answer: WordOpenMode,
pub before: u8,
pub after: u8,
pub fsrs: bool
}
impl EntityData for HistoryNote {
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
}
#[derive(Clone)]
+15 -6
View File
@@ -188,7 +188,7 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
);
}
pub fn handles(&self, data_repository: &DataRepositoryInner<T, D>) -> impl Iterator<Item = EntityHandle<T>> {
pub fn handles(&self) -> impl Iterator<Item = EntityHandle<T>> {
self.cache.iter().cloned()
}
@@ -216,6 +216,7 @@ where
D: BatchedDbLayer<T>,
T: EntityData,
{
fn create_now_many(&mut self, entities: &mut [T]) -> Result<Vec<EntityHandle<T>>, NotificationData>;
fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
fn delete_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
}
@@ -236,11 +237,11 @@ where
}
#[derive(Clone)]
pub struct DataRepository<T: std::clone::Clone + EntityData, D: DbLayer<T>> {
pub struct DataRepository<T: Clone + EntityData, D: DbLayer<T>> {
inner: Arc<RwLock<DataRepositoryInner<T, D>>>,
}
pub struct DataRepositoryInner<T: std::clone::Clone + EntityData, D: DbLayer<T>> {
pub struct DataRepositoryInner<T: Clone + EntityData, D: DbLayer<T>> {
cache: Vec<T>,
temporal: Vec<T>,
next_temporal: u32,
@@ -251,7 +252,7 @@ pub struct DataRepositoryInner<T: std::clone::Clone + EntityData, D: DbLayer<T>>
shifts: usize,
}
impl<T: std::clone::Clone + EntityData, D: DbLayer<T> + std::default::Default> DataRepository<T, D> {
impl<T: Clone + EntityData, D: DbLayer<T> + Default> DataRepository<T, D> {
pub fn new(connection: Arc<RwLock<SqliteConnection>>) -> DataRepository<T, D> {
Self{
inner: Arc::new(RwLock::new(DataRepositoryInner{
@@ -269,7 +270,7 @@ impl<T: std::clone::Clone + EntityData, D: DbLayer<T> + std::default::Default>
}
impl<T: std::clone::Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
type Target = Arc<RwLock<DataRepositoryInner<T, D>>>;
fn deref(&self) -> &Self::Target {
@@ -313,7 +314,7 @@ impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, D> {
pub fn query(
&self,
rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
rule: Arc<dyn Fn(& T) -> bool + Send + Sync>,
) -> Query<T, DataRepositoryInner<T, D>, D>
where
T: Send + Sync,
@@ -536,6 +537,14 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository<T, D>
for DataRepositoryInner<T, D>
{
fn create_now_many(&mut self, entities: &mut [T]) -> Result<Vec<EntityHandle<T>>, NotificationData> {
self.data_layer.add_many_entities(entities, &mut self.connection.write())?;
let len = self.cache.len();
let handles = entities.iter().enumerate().map(|(index, entity)| {EntityHandle::new(entity.id(), index + len, true)}).collect();
self.cache.extend(entities.to_vec());
Ok(handles)
}
fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError {
let mut temp = Vec::with_capacity(handles.len() / 2);
let mut cached = Vec::with_capacity(handles.len() / 2);
+80 -85
View File
@@ -1,97 +1,92 @@
use crate::core::srs::WordOpenMode;
use crate::data::Id;
use crate::data::entities::HistoryItem;
use crate::fs::*;
use crate::notification::NotificationData::PermissionDenied;
use crate::notification::{MaybeError, NotificationData};
use chrono::{DateTime, NaiveDate};
use rusqlite::Connection;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use crate::data::entities::HistoryNote;
use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::{Error, Params, Row, ToSql};
pub fn get_open_count_of_set(
id: Id,
connection: &Connection,
) -> Result<Vec<(NaiveDate, u32)>, NotificationData> {
todo!()
}
pub type HistoryRepository = DataRepository<HistoryNote, HistoryDataLayer>;
pub type HistoryRepositoryInner = DataRepositoryInner<HistoryNote, HistoryDataLayer>;
pub type HistoryDataLayer = SqliteDbLayer<HistoryNote, HistoryQuerySet>;
pub fn get_history_of_set(id: Id) -> Result<Vec<HistoryItem>, 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();
Ok(parse_history_items(lines))
}
fn append_lines_if_exists(path: PathBuf, lines: &mut Vec<String>) -> MaybeError {
if path.exists() {
let reader = BufReader::new(File::open(path).map_err(|_| PermissionDenied)?);
reader
.lines()
.map_while(Result::ok)
.for_each(|i| lines.push(i));
#[derive(Clone)]
pub struct HistoryQuerySet;
impl TableAccessQuerySet<HistoryNote> for HistoryQuerySet {
fn load() -> &'static str {
"SELECT rowid, set_id, state_id, timestamp, answer, before, after, fsrs FROM history"
}
fn save() -> &'static str {
todo!("History is readonly")
}
fn add() -> &'static str {
"INSERT INTO history (set_id, state_id, timestamp, answer, before, after, fsrs) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
}
fn name() -> &'static str {
"history"
}
fn from_sql(row: &Row) -> Result<HistoryNote, Error> {
Ok(HistoryNote {
id: row.get(0)?,
timestamp: row.get(3)?,
set_id: row.get(1)?,
word_id: row.get(2)?,
answer: row.get(4)?,
before: row.get(5)?,
after: row.get(6)?,
fsrs: row.get(7)?,
})
}
fn insert_params(entity: &HistoryNote) -> impl Params {
(
entity.set_id,
entity.word_id,
entity.timestamp,
entity.answer,
entity.before,
entity.after,
entity.fsrs,
)
}
fn update_params(entity: &HistoryNote) -> impl Params {
(
entity.set_id,
entity.word_id,
entity.timestamp,
entity.answer,
entity.before,
entity.after,
entity.fsrs,
)
}
Ok(())
}
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>>()[..] {
let id = word.parse::<Id>();
if id.is_err() {
continue;
impl FromSql for WordOpenMode {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
if let ValueRef::Integer(s) = value {
return match s {
1 => Ok(WordOpenMode::Hard),
2 => Ok(WordOpenMode::Ok),
3 => Ok(WordOpenMode::Easy),
_ => Ok(WordOpenMode::None),
}
let id = id.unwrap_or_default();
if !id.is_valid() {
continue;
}
items.push(HistoryItem {
timestamp: DateTime::from_timestamp(time.parse().unwrap_or_default(), 0)
.unwrap_or_default(),
word_id: id,
mode: match mode.parse::<u8>().unwrap_or_default() {
2 => WordOpenMode::Hard,
3 => WordOpenMode::Ok,
4 => WordOpenMode::Easy,
_ => WordOpenMode::None,
},
before: before.parse().unwrap_or_default(),
after: after.parse().unwrap_or_default(),
});
}
Ok(WordOpenMode::None)
}
items
}
pub fn push_note(set_id: Id, item: HistoryItem) -> MaybeError {
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)
.map_err(|_| PermissionDenied)?;
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).map_err(|_| PermissionDenied)?;
Ok(())
impl ToSql for WordOpenMode {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(ToSqlOutput::from(match self {
WordOpenMode::Easy => 3,
WordOpenMode::Ok => 2,
WordOpenMode::Hard => 1,
WordOpenMode::None => 0
}))
}
}
+4 -53
View File
@@ -1,15 +1,12 @@
use crate::app::{AppState, fill_state};
use crate::app::{fill_state, AppState};
use crate::data::Id;
use crate::data_provider::history::get_history_of_set;
use crate::data_provider::sqlite::{create_db, default_connection, SqliteConnection};
use crate::data_provider::sqlite::{create_db, SqliteConnection};
use crate::data_provider::web_api::{
get_local_version, get_web_version, load_data, set_local_version,
};
use crate::fs::*;
use crate::navigation::Page::*;
use crate::navigation::RootMessage::{
DataLoaded, Keyboard, Notification, PushNotification, UpdateData,
};
use crate::navigation::RootMessage::{Keyboard, Notification, PushNotification, UpdateData};
use crate::notification::NotificationData::PermissionDenied;
use crate::notification::{
NotificationData, NotificationManager, NotificationMessage, NotificationRequest,
@@ -31,16 +28,11 @@ use crate::state_navigate_handle;
use crate::state_update;
use crate::view_navigation;
use crate::{message_navigation, push_error, push_error_task};
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::{debug, info};
use parking_lot::Mutex;
use rusqlite::Connection;
use std::sync::Arc;
use std::time::Instant;
impl Default for ScreenState {
@@ -71,7 +63,6 @@ pub enum RootMessage {
RepetitionSettings(RepetitionSettingsMessage),
Import(ImportMessage),
Keyboard(Event),
DataLoaded(HashMap<Id, Vec<(NaiveDate, u32)>>),
PushNotification(NotificationRequest),
Notification(NotificationMessage),
None,
@@ -108,11 +99,8 @@ impl ScreenState {
create_db();
info!("Db created");
let state = ScreenState::default();
let reading_additional_task = Task::perform(Self::load_additional_data(), |data| {
data.unwrap_or_else(|err| push_error!(err))
});
let mut final_task = reading_additional_task;
let mut final_task = Task::none();
let state_guard = &state.app_state;
if state_guard.sync_data.read().auto_web_fetch
&& let Some(key) = state_guard.sync_data.read().key.clone()
@@ -136,38 +124,6 @@ impl ScreenState {
(state, final_task)
}
async fn load_additional_data() -> Result<RootMessage, NotificationData> {
let directory = history_dir()?;
let mut map = HashMap::new();
for file in directory
.read_dir()
.map_err(|_| PermissionDenied)?
.filter_map(Result::ok)
{
let mut vec = vec![];
let history_file_name = match file.file_name().into_string() {
Err(_) => continue,
Ok(v) => v,
};
if let Ok(id) = history_file_name.split('_').collect::<Vec<_>>()[1].parse::<Id>()
&& let Ok(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));
}
map.insert(id, vec);
} else {
continue;
}
}
Ok(DataLoaded(map))
}
async fn load_web_backup(string: String) -> Result<bool, NotificationData> {
let local = get_local_version().await?;
let web = get_web_version(string.as_str()).await?;
@@ -189,11 +145,6 @@ impl ScreenState {
return self.handle_keyboard(e);
}
if let DataLoaded(data) = message {
self.app_state.activity = data;
return Task::none();
}
if let UpdateData = message {
info!("Loading data");
let state = &mut self.app_state;
+2 -2
View File
@@ -81,7 +81,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let dict = &self.state.dictionary.read();
let mut query = dict.query(Arc::new(|_| true));
let handles = query.handles(dict);
let handles = query.handles();
words = handles.filter(|h| self.included.contains(h)).collect();
return Some(Page::DictionaryQuiz(DictionaryQuizState::new(
@@ -552,7 +552,7 @@ impl DictionaryState {
let groups = &self.state.word_groups.read();
for mut group in self.groups_query.handles(groups) {
for mut group in self.groups_query.handles() {
let group_name = groups.get(&mut group).unwrap().name.clone();
row = row.push(
button(text!("{}", group_name))
+33 -31
View File
@@ -1,22 +1,20 @@
use crate::app::AppState;
use crate::data::INVALID_ID;
use crate::data::entities::{HistoryItem, WordData};
use crate::data_provider::history::get_history_of_set;
use crate::data::entities::{HistoryNote, WordData};
use crate::data::repositories::Repository;
use crate::navigation::*;
use crate::styling::*;
use HistoryMessage::Back;
use chrono::Local;
use iced::alignment::Horizontal::Center;
use iced::widget::space::horizontal;
use iced::widget::*;
use iced::{Element, Fill, FillPortion, Task};
use parking_lot::Mutex;
use std::sync::Arc;
use HistoryMessage::Back;
#[derive(Clone)]
pub struct HistoryState {
list: Vec<HistoryItem>,
words: Vec<WordData>,
list: Vec<(WordData, HistoryNote)>,
pub state: AppState,
}
impl NavigatedPage<HistoryMessage> for HistoryState {
@@ -39,9 +37,9 @@ impl NavigatedPage<HistoryMessage> for HistoryState {
.width(FillPortion(5)),
horizontal().width(FillPortion(1))
]
.height(Fill)
.width(Fill)
.into(),
.height(Fill)
.width(Fill)
.into(),
Back,
)
}
@@ -49,56 +47,60 @@ impl NavigatedPage<HistoryMessage> for HistoryState {
impl HistoryState {
pub fn new(id: crate::data::Id, state: AppState) -> Self {
let state = state.lock();
let history = get_history_of_set(id).unwrap_or_default();
let dict = &state.dictionary.read();
let history_repo = &mut state.history.write();
history_repo.init().expect("TODO: panic message");
let id = id.clone();
let mut history = history_repo.query(Arc::new(move |h| h.set_id == id));
let words = history
.iter()
.iter(history_repo)
.map(
|item| match state.dictionary.iter().find(|w| w.id == item.word_id) {
None => WordData {
id,
|item| {let handle = dict.handle_by_id(item.word_id);
let word = dict.get_static(handle).unwrap_or(WordData {
id: Default::default(),
group_id: Default::default(),
key: "Удаленное слово".to_string(),
value: "Удаленное слово".to_string(),
tags: "Удаленное слово".to_string(),
value: "".to_string(),
tags: "".to_string(),
additional: Default::default(),
group_id: INVALID_ID,
},
Some(word) => word.clone(),
},
});
(word, item.clone())
}
)
.collect();
Self {
list: history,
words,
list: words,
state: state.clone(),
}
}
fn history_lines(&self) -> Column<'_, HistoryMessage> {
let mut column = Column::new();
for (index, item) in self.list.iter().enumerate() {
for (index, item) in &self.list {
let activity = item;
column = column.push(
row![
iced::widget::column![
text!("{}", self.words[index].key)
text!("{}", index.key)
.width(Fill)
.align_x(Center),
text!("{}", self.words[index].value)
text!("{}", index.value)
.width(Fill)
.align_x(Center),
]
.align_x(Center),
iced::widget::column![
text!("{} ➞ {}", item.before, item.after),
text!("{} ➞ {}", activity.before, activity.after),
text!(
"{}",
item.timestamp.with_timezone(&Local).format("%d.%m %H:%M")
activity.timestamp.with_timezone(&Local).format("%d.%m %H:%M")
)
]
.spacing(5)
.align_x(Center)
]
.spacing(5),
.spacing(5),
);
column = column.push(rule::horizontal(1));
}
+33 -30
View File
@@ -1,5 +1,5 @@
use crate::app::AppState;
use crate::data::entities::{WordData, WordGroup};
use crate::data::entities::{EntityData, WordData, WordGroup};
use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups};
use crate::data_provider::words::{add_group, add_words};
use crate::fs::*;
@@ -29,6 +29,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use zip::ZipArchive;
use crate::data::repositories::{BatchedRepository, Repository};
const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"];
@@ -431,29 +432,18 @@ impl ImportState {
info!("{}", group.name);
Task::perform(
async move {
spawn_blocking(move || {
let words_list = spawn_blocking(move || {
let temp_file_path = temp_import_path();
let connection =
Connection::open(&temp_file_path).map_err(|_| OpenFileError)?;
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());
let map_indices = Self::get_mapping_indices(&group);
let mut group_entity = WordGroup {
id: 0.into(),
name: group.name.clone(),
};
{
let state = state.lock();
add_group(&mut group_entity, &state.connection)?;
}
let group_id = group_entity.id;
for import in import_list {
let mut word = WordData::new();
word.tags = import.tags.trim().replace(" ", ", ");
word.group_id = group_id;
for (dest, indices) in &map_indices {
let collected_string = Self::collect_strings(
&import.fields,
@@ -480,24 +470,37 @@ impl ImportState {
words_list.push(word);
}
let mut state = state.lock();
let connection = &mut state.connection;
let mut index = 0;
let total_len = words_list.len() as f32 / 1024.0;
for word in &mut words_list.chunks_mut(1024) {
add_words(word, connection)?;
index += 1;
let mut progress = progress.lock();
*progress = (index as f32 / total_len).min(1.0);
}
state.dictionary.append(&mut words_list);
state.word_groups.push(group_entity);
Ok(())
Ok::<_, NotificationData>(words_list)
})
.await
.unwrap()
.unwrap()?;
let group_handle = {
let group_entity = WordGroup {
id: 0.into(),
name: group.name.clone(),
};
let mut groups_repo = state.word_groups.write();
groups_repo.create_now(group_entity)?
};
let group_id = group_handle.id;
let mut words_list = words_list;
for word in &mut words_list {
word.group_id = group_id;
}
let mut words_repo = state.dictionary.write();
let mut index = 0;
let total_len = words_list.len() as f32 / 1024.0;
for words in &mut words_list.chunks_mut(1024) {
words_repo.create_now_many(words)?;
index += 1;
let mut progress = progress.lock();
*progress = (index as f32 / total_len).min(1.0);
}
Ok(())
},
|r: Result<(), NotificationData>| match r {
Ok(_) => RootMessage::Import(ImportFinished),
+1 -1
View File
@@ -167,7 +167,7 @@ impl RepetitionState {
}
fn increment_counter(&mut self) {
let mut state = self.state.lock();
let mut state = self.state;
let id = self.settings.id;
let activity = state.activity.get_mut(&id);