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