41 Commits
Author SHA1 Message Date
micialware 444352c007 Merge pull request 'Repo integration' (#15) from repo-integration into master
Reviewed-on: https://gitea.micialware.ru/micialware/jap_learn/pulls/15
2026-09-09 12:47:42 +03:00
micialware 2069bdf5b3 Fix already updated cache change 2026-09-09 12:47:22 +03:00
micialware 28730c0fda Delete saved card states 2026-09-09 12:35:00 +03:00
micialware 7715aaff3b Fixing tag selection 2026-09-09 12:12:44 +03:00
micialware a20c72eb9d Optimize groups query 2026-09-09 11:23:51 +03:00
micialware 58bdaac7fe Remove word display widget base 2026-09-09 11:13:50 +03:00
micialware 4a71f31e6f Fix import 2026-09-09 10:53:03 +03:00
micialware 6e3f58d21c Version update 2026-09-09 10:43:26 +03:00
micialware 3663765284 Db saving cases 2026-09-09 10:33:21 +03:00
micialware 294f0e8b98 Query time and database optimization 2026-09-09 10:19:44 +03:00
micialware 2e04450c76 Working repetitions 2026-09-09 10:08:52 +03:00
micialware 242b56fc21 Fixes for sqlite data layer 2026-09-08 00:09:13 +03:00
micialware 7f9aa11d6e More fixes for dictionary 2026-09-07 18:19:54 +03:00
micialware 92b019b072 Temporal dictionary display fix 2026-09-07 18:00:50 +03:00
micialware 9c93ca4408 Merge remote-tracking branch 'origin/word-display' into repo-integration 2026-09-07 17:17:18 +03:00
micialware 6387afba93 Incorrect data display fix 2026-09-07 16:21:40 +03:00
micialware 42c38d55e1 Fix more bugs 2026-09-07 16:10:10 +03:00
micialware 60dae212ec Fix incorrect filter light in repetition page 2026-09-07 15:49:39 +03:00
micialware b3b5785fd7 Fix some bugs in repo and repetitions page 2026-09-07 15:48:10 +03:00
micialware bdf71fa0b7 Small history table change 2026-09-07 10:59:55 +03:00
micialware f9c4bf9c03 App is building 2026-09-07 10:53:09 +03:00
micialware 7e1bb4bc22 temporal solving word 2026-09-06 23:58:41 +03:00
micialware 1afb7493f6 Sync solved 2026-09-06 23:55:50 +03:00
micialware e2f367f3aa WordDisplay base 2026-09-06 10:13:33 +03:00
micialware 22c05250aa Repetition settings remake 2026-09-04 22:34:36 +03:00
micialware a68e56e861 Solve repetition 2026-09-04 17:26:59 +03:00
micialware 08ab55f283 Fix deck data 2026-09-04 13:00:41 +03:00
micialware 9a7578727b Fix data access method and solve import page 2026-09-04 11:37:19 +03:00
micialware 532640058e Rewrite history page 2026-09-04 11:18:23 +03:00
micialware f19a6f64c3 Dictionary partial rewritten and dictionary quiz rewritten 2026-09-03 00:36:15 +03:00
micialware f2035ca749 Partial dictionary rewrite 2026-09-02 00:19:58 +03:00
micialware 1509947cb0 Temporal todo 2026-08-31 14:50:21 +03:00
micialware 55bdc5a7ad New repository methods 2026-08-31 14:35:11 +03:00
micialware 11d5144db5 Added repositories into AppState 2026-08-31 12:12:14 +03:00
micialware 6c20561ede Repository types defined 2026-08-31 11:52:54 +03:00
micialware 0c4aec3dc0 Partial AppState rewrite 2026-08-31 11:46:35 +03:00
micialware 29a90d48d9 Reformat code 2026-08-31 11:26:05 +03:00
micialware 2fc58fcc2d Custom fields table 2026-08-31 11:25:41 +03:00
micialware 6d363c8148 Custom fields table 2026-08-31 11:23:41 +03:00
micialware ba834ac9b1 Impl Id convert methods 2026-08-31 11:21:01 +03:00
micialware 97e301b0d6 Merge pull request 'Db layers' (#14) from db_layers into master
Reviewed-on: https://gitea.micialware.ru/micialware/jap_learn/pulls/14
2026-08-30 23:38:33 +03:00
31 changed files with 1633 additions and 1987 deletions

No files matched your search

Generated
+67 -638
View File
File diff suppressed because it is too large. Load diff
+6 -6
View File
@@ -4,20 +4,20 @@ version = "0.1.3"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
iced = { version = "0.14.0", features = ["tokio", "svg", "lazy"]} iced = { version = "0.14.0", features = ["tokio", "svg", "lazy", "crisp", "tiny-skia", "wayland", "wgpu"], default-features = false}
iced_core = "0.14.0" iced_core = "0.14.0"
rand = "0.10.2" rand = "0.10.2"
dirs = "6.0.0" dirs = "7.0.0"
serde_json = "1.0.151" serde_json = "1.0.151"
rhai = { version = "1.25.1", features = ["sync"] } rhai = { version = "1.26.0", features = ["sync"] }
chrono = "0.4.45" chrono = "0.4.45"
rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] } rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] }
reqwest = { version = "0.13.4", features = ["json", "stream", "multipart"] } reqwest = { version = "0.13.5", features = ["json", "stream", "multipart"] }
rodio = "0.22.2" rodio = "0.22.2"
sha2 = "0.11.0" sha2 = "0.11.0"
tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros"] }
hex = "0.4.3" hex = "0.4.3"
zstd = "0.13.3" zstd = "0.14.0"
zip = "8.6.0" zip = "8.6.0"
rfd = "0.17.2" rfd = "0.17.2"
mimalloc = "0.1.52" mimalloc = "0.1.52"
@@ -26,7 +26,7 @@ rayon = "1.12.0"
parking_lot = "0.12.5" parking_lot = "0.12.5"
env_logger = "0.11.11" env_logger = "0.11.11"
log = "0.4.34" log = "0.4.34"
fsrs = "6.6.1" fsrs = "6.6.2"
litemap = { version = "0.8.3", features = ["serde"] } litemap = { version = "0.8.3", features = ["serde"] }
[profile.super-release] [profile.super-release]
+4
View File
@@ -0,0 +1,4 @@
create table custom_fields(
id INTEGER PRIMARY KEY,
name TEXT not null
);
+2 -2
View File
@@ -2,8 +2,8 @@ create table history
( (
id INTEGER id INTEGER
primary key autoincrement, primary key autoincrement,
set_id INTEGER not null references decks on delete cascade, deck_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,
+1 -1
View File
@@ -5,7 +5,7 @@ create table states
word_id INTEGER not null word_id INTEGER not null
references words references words
on delete cascade, on delete cascade,
set_id INTEGER not null deck_id INTEGER not null
references decks references decks
on delete cascade, on delete cascade,
score INTEGER default 1 not null, score INTEGER default 1 not null,
+37 -29
View File
@@ -1,21 +1,23 @@
use crate::data::Id; use crate::data::repositories::Repository;
use crate::data::entities::DeckSettings; use crate::data_provider::card_states::CardStateRepository;
use crate::data::entities::{WordData, WordGroup}; use crate::data_provider::decks::DeckRepository;
use crate::data_provider::decks::load_sets; use crate::data_provider::history::HistoryRepository;
use crate::data_provider::settings::get_setting; use crate::data_provider::settings::get_setting;
use crate::data_provider::sqlite::default_connection; use crate::data_provider::sqlite::SqliteConnection;
use crate::data_provider::words::{load_word_groups, load_words}; use crate::data_provider::word_groups::WordGroupRepository;
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")]
use iced_core::window::settings::PlatformSpecific; use iced_core::window::settings::PlatformSpecific;
use iced_core::{Font, Size, Theme}; use iced_core::{Font, Size, Theme};
use log::info; use log::info;
use parking_lot::{Mutex, RwLock};
use rusqlite::Connection; use rusqlite::Connection;
use std::sync::Arc;
pub const USER_FONT: Font = Font::with_name("Noto Sans JP"); pub const USER_FONT: Font = Font::with_name("Noto Sans JP");
pub fn start() -> iced::Result { pub fn start() -> iced::Result {
@@ -57,13 +59,15 @@ fn subscription(_state: &ScreenState) -> Subscription<RootMessage> {
keyboard::listen().map(Keyboard) keyboard::listen().map(Keyboard)
} }
#[derive(Clone)]
pub struct AppState { pub struct AppState {
pub dictionary: Vec<WordData>, pub dictionary: WordRepository,
pub decks: Vec<DeckSettings>, pub decks: DeckRepository,
pub word_groups: Vec<WordGroup>, pub word_groups: WordGroupRepository,
pub connection: Connection, pub card_states: CardStateRepository,
pub sync_data: AppSettings, pub connection: Arc<Mutex<SqliteConnection>>,
pub activity: HashMap<Id, Vec<(NaiveDate, u32)>>, pub sync_data: Arc<RwLock<AppSettings>>,
pub history: HistoryRepository,
} }
impl Default for AppState { impl Default for AppState {
@@ -74,30 +78,34 @@ impl Default for AppState {
impl AppState { impl AppState {
pub fn new() -> Self { pub fn new() -> Self {
let connection = Arc::new(Mutex::new(SqliteConnection::default()));
Self { Self {
dictionary: vec![], dictionary: WordRepository::new(connection.clone()),
decks: vec![], decks: DeckRepository::new(connection.clone()),
word_groups: vec![], word_groups: WordGroupRepository::new(connection.clone()),
connection: default_connection(), card_states: CardStateRepository::new(connection.clone()),
sync_data: AppSettings { history: HistoryRepository::new(connection.clone()),
sync_data: Arc::new(RwLock::new(AppSettings {
key: None, key: None,
auto_web_fetch: false, auto_web_fetch: false,
}, })),
activity: Default::default(), connection,
} }
} }
} }
pub fn fill_state(state: &mut AppState) { pub fn fill_state(state: &mut AppState) {
let list = load_words(&state.connection); let connection = state.connection.lock();
let sets = load_sets(&state.connection);
let groups = load_word_groups(&state.connection);
let setting = load_settings(&state.connection);
state.dictionary = list.unwrap_or_default(); let setting = load_settings(&connection);
state.decks = sets.unwrap_or_default(); drop(connection);
state.word_groups = groups.unwrap_or_default(); state.word_groups.write().start().unwrap();
state.sync_data = setting state.dictionary.write().start().unwrap();
state.decks.write().start().unwrap();
state.history.write().start().unwrap();
state.card_states.write().start().unwrap();
state.sync_data = Arc::new(RwLock::new(setting))
} }
fn load_settings(connection: &Connection) -> AppSettings { fn load_settings(connection: &Connection) -> AppSettings {
+135 -84
View File
@@ -5,134 +5,185 @@ use crate::core::srs::{
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::repositories::Repository;
use crate::data_provider::history::push_note; use crate::data_provider::card_states::CardStateRepositoryInner;
use crate::notification::{MaybeError, NotificationData}; use crate::notification::MaybeError;
use chrono::Utc; use chrono::Utc;
use parking_lot::Mutex; use log::info;
use rhai::{Engine, Scope};
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant;
#[derive(Clone)] #[derive(Clone)]
pub struct DeckData { pub struct DeckData {
pub(crate) words: Vec<WordData>, pub(crate) words: Vec<EntityHandle<WordData>>,
pub(crate) set: Vec<CardState>, pub(crate) states: Vec<EntityHandle<CardState>>,
current_word_index: Option<usize>, current_word_index: Option<usize>,
state: Arc<Mutex<AppState>>, state: AppState,
order_module: OrderModule, order_module: OrderModule,
open_mode: OrderMode, open_mode: OrderMode,
settings: DeckSettings, settings: DeckSettings,
} }
impl DeckData { impl DeckData {
pub fn new( pub fn new(settings: &DeckSettings, open_mode: OrderMode, state: AppState) -> Self {
settings: &DeckSettings, let states_repo = state.card_states.read();
state: Arc<Mutex<AppState>>, let mut query = states_repo.deck_states(settings.id);
) -> Result<Self, NotificationData> { let words = query
let state_for = state.clone(); .iter(&states_repo)
let state_locked = state.lock(); .map(|card_state| EntityHandle::from_id(card_state.word_id))
let stats = load_stats_of_deck(settings, &state_locked.connection)?;
let words = stats
.iter()
.map(|stat| {
state_locked
.dictionary
.binary_search_by_key(&stat.word_id, |w| w.id)
.expect("No word found in dictionary")
})
.map(|index| &state_locked.dictionary[index])
.cloned()
.collect(); .collect();
let states = query.handles().collect::<Vec<EntityHandle<CardState>>>();
Ok(Self { let fsrs = Fsrs(FsrsModule::new(Self::set_links(states.as_slice(), &states_repo).as_slice()));
set: stats, drop(states_repo);
Self {
states,
words, words,
current_word_index: None, current_word_index: None,
state: state_for, state,
order_module: Fsrs(FsrsModule::new()), order_module: fsrs,
open_mode: settings.open_mode, open_mode,
settings: settings.clone(), settings: settings.clone(),
}) }
} }
pub fn next(&mut self) -> (WordData, CardState) { pub fn next(&mut self) -> (EntityHandle<WordData>, EntityHandle<CardState>) {
if self.fsrs_finished() {
self.switch_order_engine()
}
let repo = self.state.card_states.read();
let links = Self::set_links(self.states.as_slice(), &repo);
let index = match self.order_module { let index = match self.order_module {
SemiRandomSRS(ref mut module) => { SemiRandomSRS(ref mut module) => module.next(links.as_slice()),
if !module.initialized {
module.init(self.set.as_slice())
}
module.next(self.set.as_slice())
}
RandomSRS(ref mut module) => { RandomSRS(ref mut module) => module.next(links.as_slice()),
if !module.initialized { WorstWordsSRS(ref mut module) => module.next(links.as_slice()),
module.init(self.set.as_slice()) Fsrs(ref mut module) => module.next(links.as_slice()),
}
module.next(self.set.as_slice())
}
WorstWordsSRS(ref mut module) => {
if !module.initialized {
module.init(self.set.as_slice())
}
module.next(self.set.as_slice())
}
Fsrs(ref mut module) => {
if !module.initialized {
module.init(self.set.as_slice())
}
module.next(self.set.as_slice())
}
}; };
self.current_word_index = Some(index); self.current_word_index = Some(index);
(self.words[index].clone(), self.set[index].clone()) (self.words[index].clone(), self.states[index].clone())
} }
fn set_links<'a>(
set: &[EntityHandle<CardState>],
repo: &'a CardStateRepositoryInner,
) -> Vec<&'a CardState> {
set.iter()
.map(|h| repo.get_ref_static(*h).unwrap())
.collect::<Vec<_>>()
}
pub fn open(&mut self, status: WordOpenMode) -> MaybeError { pub fn open(&mut self, status: WordOpenMode) -> MaybeError {
if let Some(index) = self.current_word_index { if let Some(index) = self.current_word_index {
let word: &mut CardState = &mut self.set[index]; let mut repo = self.state.card_states.write();
let old_score = word.score; let card_state_handle = &mut self.states[index];
word.update(status); let card_state = repo.get_mut(card_state_handle).unwrap();
let new_score = word.score; let old_score = card_state.score;
card_state.update(status);
let new_score = card_state.score;
let mut fsrs = false;
match self.order_module { match self.order_module {
SemiRandomSRS(ref mut module) => { SemiRandomSRS(ref mut module) => {
module.open(status, index, word); module.open(status, index, card_state);
} }
RandomSRS(ref mut module) => { RandomSRS(ref mut module) => {
module.open(status, index, word); module.open(status, index, card_state);
} }
WorstWordsSRS(ref mut module) => { WorstWordsSRS(ref mut module) => {
module.open(status, index, word); module.open(status, index, card_state);
} }
Fsrs(ref mut module) => { Fsrs(ref mut module) => {
module.open(status, index, word); module.open(status, index, card_state);
fsrs = true;
} }
} }
if let Fsrs(ref fsrs) = self.order_module let word_id = card_state.word_id;
&& fsrs.finished() repo.save(card_state_handle)?;
{
self.order_module = match self.open_mode { let mut history = self.state.history.write();
OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new()), history.create_now(HistoryNote {
OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new()), id: Default::default(),
OrderMode::FullRandom => RandomSRS(RandomSRSModule::new()),
}
}
update_stat_score(word, &self.state.lock().connection)?;
let _ = push_note(
self.settings.id,
HistoryItem {
timestamp: Utc::now(), timestamp: Utc::now(),
word_id: word.word_id, deck_id: self.settings.id,
mode: status, word_id,
answer: status.clone(),
before: old_score, before: old_score,
after: new_score, after: new_score,
}, fsrs,
); })?;
} }
Ok(()) Ok(())
} }
fn switch_order_engine(&mut self) {
let repo = self.state.card_states.read();
let links = Self::set_links(self.states.as_slice(), &repo);
self.order_module = match self.open_mode {
OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new(links.as_slice())),
OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new(links.as_slice())),
OrderMode::FullRandom => RandomSRS(RandomSRSModule::new(links.as_slice())),
}
}
fn fsrs_finished(&self) -> bool {
if let Fsrs(ref fsrs) = self.order_module
&& fsrs.finished()
{
info!("Fsrs finished. Infinite mode enabled");
return true;
}
false
}
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.set.len() self.states.len()
}
pub fn list(deck: &DeckSettings, state: &AppState) -> Vec<EntityHandle<WordData>> {
let time = Instant::now();
let list = vec![];
let engine = Engine::new();
let ast = engine.compile(&deck.filter);
if let Ok(ast) = ast {
let groups = state.word_groups.read().iter().cloned().collect::<Vec<_>>();
let dictionary = &state.dictionary.read();
let query = dictionary.query_par(Arc::new(move |word| {
let mut more = rhai::Map::new();
for iced in &word.additional {
more.insert(iced.0.clone().into(), iced.1.clone().into());
}
let mut scope = Scope::new();
let group_name = groups[groups
.binary_search_by_key(&word.group_id, |g| g.id)
.unwrap()]
.name
.clone();
scope
.push_constant("id", word.id.0 as i64)
.push_constant("key", word.key.clone())
.push_constant("value", word.value.clone())
.push_constant("tags", word.tags.clone())
.push_constant("more", more)
.push_constant("group", group_name);
let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast);
if let Ok(result) = result {
return result;
}
false
}));
info!("Collecting available words is {:?}", time.elapsed());
let handles = query.handles().collect();
drop(query);
handles
} else {
list
}
} }
} }
+57 -73
View File
@@ -1,6 +1,7 @@
use crate::data::entities::CardState; use crate::data::entities::CardState;
use chrono::{Local, TimeDelta}; use chrono::{Local, TimeDelta};
use fsrs::FSRS; use fsrs::FSRS;
use log::{debug, info};
use rand::distr::weighted::WeightedIndex; use rand::distr::weighted::WeightedIndex;
use rand::prelude::ThreadRng; use rand::prelude::ThreadRng;
use rand::prelude::{Distribution, SliceRandom}; use rand::prelude::{Distribution, SliceRandom};
@@ -31,25 +32,24 @@ pub enum OrderModule {
Fsrs(FsrsModule), Fsrs(FsrsModule),
} }
pub trait SRSModule { pub trait SRSModule {
fn next(&mut self, set: &[CardState]) -> usize; fn new(set: &[&CardState]) -> Self;
fn next(&mut self, set: &[&CardState]) -> usize;
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &mut CardState); fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &mut CardState);
fn init(&mut self, set: &[CardState]);
} }
#[derive(Clone)] #[derive(Clone)]
pub struct RandomSRSModule { pub struct RandomSRSModule {
basket: Vec<usize>, basket: Vec<usize>,
pub(crate) initialized: bool,
}
impl RandomSRSModule {
pub(crate) fn new() -> RandomSRSModule {
Self {
basket: vec![],
initialized: false,
}
}
} }
impl SRSModule for RandomSRSModule { impl SRSModule for RandomSRSModule {
fn next(&mut self, set: &[CardState]) -> usize { fn new(set: &[&CardState]) -> Self {
let mut basket = (0..set.len()).collect::<Vec<usize>>();
basket.shuffle(&mut rand::rng());
Self{
basket,
}
}
fn next(&mut self, set: &[&CardState]) -> usize {
match self.basket.pop() { match self.basket.pop() {
Some(index) => index, Some(index) => index,
None => { None => {
@@ -61,32 +61,15 @@ impl SRSModule for RandomSRSModule {
} }
fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {} fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {}
fn init(&mut self, set: &[CardState]) {
self.initialized = true;
self.basket = (0..set.len()).collect::<Vec<usize>>();
self.basket.shuffle(&mut rand::rng())
}
} }
#[derive(Clone)] #[derive(Clone)]
pub struct SemiRandomSRSModule { pub struct SemiRandomSRSModule {
history: Vec<usize>, history: Vec<usize>,
last_weights: WeightedIndex<f32>, last_weights: WeightedIndex<f32>,
generator: ThreadRng, generator: ThreadRng,
pub(crate) initialized: bool,
}
impl SemiRandomSRSModule {
pub(crate) fn new() -> SemiRandomSRSModule {
SemiRandomSRSModule {
history: vec![],
last_weights: WeightedIndex::new([1.0]).unwrap(),
generator: rng(),
initialized: false,
}
}
} }
impl SRSModule for SemiRandomSRSModule { impl SRSModule for SemiRandomSRSModule {
fn next(&mut self, set: &[CardState]) -> usize { fn next(&mut self, set: &[&CardState]) -> usize {
let index = self.last_weights.sample(&mut self.generator); let index = self.last_weights.sample(&mut self.generator);
if self.history.contains(&index) && self.history.len() != 1 { if self.history.contains(&index) && self.history.len() != 1 {
@@ -106,23 +89,27 @@ impl SRSModule for SemiRandomSRSModule {
.update_weights(&[(index, &new_weight)]) .update_weights(&[(index, &new_weight)])
.expect("update weights error"); .expect("update weights error");
} }
fn init(&mut self, set: &[CardState]) { fn new(set: &[&CardState]) -> Self {
self.initialized = true;
let weights = set let weights = set
.iter() .iter()
.map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0) .map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0)
.collect::<Vec<f32>>(); .collect::<Vec<f32>>();
self.last_weights = WeightedIndex::new(weights).expect("Incorrect weights update");
Self{
generator: rng(),
last_weights: WeightedIndex::new(weights).expect("Incorrect weights update"),
history: vec![]
}
} }
} }
impl SemiRandomSRSModule { impl SemiRandomSRSModule {
fn history_len(&self, set: &[CardState]) -> usize { fn history_len(&self, set: &[&CardState]) -> usize {
((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN) ((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN)
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct WorstWordsSRSModule { pub struct WorstWordsSRSModule {
pub(crate) initialized: bool,
pool_size: usize, pool_size: usize,
rounds_remaining: u8, rounds_remaining: u8,
pool: Vec<usize>, pool: Vec<usize>,
@@ -130,7 +117,7 @@ pub struct WorstWordsSRSModule {
rounds_count: u8, rounds_count: u8,
} }
impl SRSModule for WorstWordsSRSModule { impl SRSModule for WorstWordsSRSModule {
fn next(&mut self, set: &[CardState]) -> usize { fn next(&mut self, set: &[&CardState]) -> usize {
if self.rounds_remaining == 0 { if self.rounds_remaining == 0 {
self.fill_pool(set); self.fill_pool(set);
self.rounds_remaining = self.rounds_count; self.rounds_remaining = self.rounds_count;
@@ -148,14 +135,8 @@ impl SRSModule for WorstWordsSRSModule {
fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {} fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {}
fn init(&mut self, _: &[CardState]) { fn new(_: &[&CardState]) -> Self {
self.initialized = true;
}
}
impl WorstWordsSRSModule {
pub(crate) fn new() -> WorstWordsSRSModule {
WorstWordsSRSModule { WorstWordsSRSModule {
initialized: false,
pool_size: PARTIAL_POOL_SIZE, pool_size: PARTIAL_POOL_SIZE,
rounds_count: 2, rounds_count: 2,
rounds_remaining: 0, rounds_remaining: 0,
@@ -163,7 +144,9 @@ impl WorstWordsSRSModule {
queue: vec![], queue: vec![],
} }
} }
fn fill_pool(&mut self, set: &[CardState]) { }
impl WorstWordsSRSModule {
fn fill_pool(&mut self, set: &[&CardState]) {
let mut sorted = set.iter().enumerate().collect::<Vec<_>>(); let mut sorted = set.iter().enumerate().collect::<Vec<_>>();
sorted.sort_by_key(|c| c.1.score); sorted.sort_by_key(|c| c.1.score);
let mut worst = sorted[0..self.pool_size] let mut worst = sorted[0..self.pool_size]
@@ -183,11 +166,32 @@ pub struct FsrsModule {
queue: Vec<usize>, queue: Vec<usize>,
rng: ThreadRng, rng: ThreadRng,
fsrs: FSRS, fsrs: FSRS,
pub(crate) initialized: bool,
} }
impl SRSModule for FsrsModule { impl SRSModule for FsrsModule {
fn next(&mut self, _: &[CardState]) -> usize { fn new(set: &[&CardState]) -> Self {
let today = Local::now().date_naive();
let mut new = Self {
today_set: set
.iter()
.enumerate()
.filter(|(_, c)| c.fsrs_open_day <= today)
.map(|(index, _)| index)
.collect(),
current_group: vec![],
queue: vec![],
solved: vec![],
rng: rng(),
fsrs: FSRS::default(),
};
new.today_set.shuffle(&mut new.rng);
new.fill_partial_pool();
new
}
fn next(&mut self, _: &[&CardState]) -> usize {
if self.current_group.is_empty() { if self.current_group.is_empty() {
self.fill_partial_pool(); self.fill_partial_pool();
} }
@@ -222,45 +226,25 @@ impl SRSModule for FsrsModule {
let offset = ((rng().random::<f32>() - 0.5) * 3.0).round() as i32; let offset = ((rng().random::<f32>() - 0.5) * 3.0).round() as i32;
next_day += TimeDelta::days(offset as i64); next_day += TimeDelta::days(offset as i64);
} }
let first_time = updated_word.fsrs_memory.is_none();
updated_word.fsrs_memory = Some(next.memory); updated_word.fsrs_memory = Some(next.memory);
updated_word.fsrs_open_day = next_day; updated_word.fsrs_open_day = next_day;
updated_word.fsrs_opened = updated_word.last_open.date_naive(); updated_word.fsrs_opened = updated_word.last_open.date_naive();
if updated_word.fsrs_open_day != today { if (updated_word.fsrs_open_day != today && !first_time) || (first_time && status == WordOpenMode::Easy) {
info!("For today word on index {} solved", index);
self.solved.push(index); self.solved.push(index);
let index = self.current_group.iter().position(|x| *x == index).unwrap(); let index = self.current_group.iter().position(|x| *x == index).unwrap();
self.current_group.remove(index); self.current_group.remove(index);
} }
} }
fn init(&mut self, set: &[CardState]) {
self.rng = rng();
let today = Local::now().date_naive();
self.today_set = set
.iter()
.enumerate()
.filter(|(_, c)| c.fsrs_open_day <= today)
.map(|(index, _)| index)
.collect();
self.today_set.shuffle(&mut self.rng);
self.fill_partial_pool();
self.initialized = true;
}
} }
impl FsrsModule { impl FsrsModule {
pub fn new() -> Self {
Self {
today_set: vec![],
current_group: vec![],
queue: vec![],
solved: vec![],
rng: Default::default(),
fsrs: FSRS::default(),
initialized: false,
}
}
fn fill_partial_pool(&mut self) { fn fill_partial_pool(&mut self) {
debug!("Filling partial pool");
self.current_group.extend( self.current_group.extend(
self.today_set self.today_set
.iter() .iter()
@@ -281,7 +265,7 @@ impl FsrsModule {
} }
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum WordOpenMode { pub enum WordOpenMode {
Easy, Easy,
Ok, Ok,
+14 -2
View File
@@ -3,7 +3,7 @@ pub mod repositories;
use rusqlite::ToSql; use rusqlite::ToSql;
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
use std::fmt::{write, Display, Formatter}; use std::fmt::{Display, Formatter};
use std::num::ParseIntError; use std::num::ParseIntError;
use std::str::FromStr; use std::str::FromStr;
@@ -63,8 +63,20 @@ macro_rules! id_impl {
self.0 != 0 self.0 != 0
} }
} }
} };
} }
id_impl!(u32, Id, INVALID_ID); id_impl!(u32, Id, INVALID_ID);
id_impl!(u16, SmallId, INVALID_SMALL_ID); id_impl!(u16, SmallId, INVALID_SMALL_ID);
impl From<SmallId> for Id {
fn from(value: SmallId) -> Self {
Self(value.0 as u32)
}
}
impl From<Id> for SmallId {
fn from(value: Id) -> Self {
Self(value.0 as u16)
}
}
+79 -68
View File
@@ -1,15 +1,16 @@
use crate::app::AppState; use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, WordOpenMode};
use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, OrderMode, WordOpenMode};
use crate::data::{INVALID_ID, Id}; use crate::data::{INVALID_ID, Id};
use chrono::{DateTime, NaiveDate, Utc}; use chrono::{DateTime, Local, NaiveDate, Utc};
use fsrs::MemoryState; use fsrs::MemoryState;
use log::info;
use rayon::prelude::*;
use rhai::{Engine, Scope};
use std::cmp::Ordering;
use std::marker::PhantomData;
use std::time::Instant;
use litemap::LiteMap; use litemap::LiteMap;
use log::info;
use rhai::Engine;
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Instant;
use crate::data_provider::history::{HistoryRepository, HistoryRepositoryInner};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct WordData { pub struct WordData {
@@ -61,12 +62,49 @@ impl WordData {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct HistoryItem { pub struct HistoryNote {
pub id: Id,
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,
pub deck_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;
}
}
impl HistoryRepositoryInner {
pub fn activity_of_deck(&self, id: Id) -> Vec<(NaiveDate, u32)> {
let mut query = self.query(Arc::new(move |note| note.deck_id == id));
let values = query.iter(&self).collect::<Vec<_>>();
let mut counts = vec![];
if !values.is_empty() {
let mut date = values[0].timestamp.with_timezone(&Local).date_naive();
let mut count = 0;
for note in values {
let current_date = note.timestamp.with_timezone(&Local).date_naive();
if date == current_date{
count += 1;
}
else {
counts.push((date, count));
date = current_date;
count = 0;
}
}
}
counts
}
} }
#[derive(Clone)] #[derive(Clone)]
@@ -76,9 +114,18 @@ pub struct DeckSettings {
pub forward: String, pub forward: String,
pub backward: String, pub backward: String,
pub filter: String, pub filter: String,
pub count: Option<usize>,
pub open_mode: OrderMode,
} }
impl EntityData for DeckSettings {
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
}
impl DeckSettings { impl DeckSettings {
pub(crate) fn with_name(name: String) -> DeckSettings { pub(crate) fn with_name(name: String) -> DeckSettings {
DeckSettings { DeckSettings {
@@ -87,8 +134,6 @@ impl DeckSettings {
forward: "".to_string(), forward: "".to_string(),
backward: "".to_string(), backward: "".to_string(),
filter: "true".to_string(), filter: "true".to_string(),
count: None,
open_mode: OrderMode::Default,
} }
} }
pub fn check_filter(&self) -> bool { pub fn check_filter(&self) -> bool {
@@ -98,57 +143,6 @@ impl DeckSettings {
info!("Compile time is {:?}", time.elapsed()); info!("Compile time is {:?}", time.elapsed());
ast.is_ok() ast.is_ok()
} }
pub fn get_word_list(&self, state: &AppState) -> Vec<usize> {
let time = Instant::now();
let mut list = vec![];
let engine = Engine::new();
let ast = engine.compile(&self.filter);
if let Ok(ast) = ast {
let groups = &state.word_groups;
list = state
.dictionary
.par_iter()
.enumerate()
.filter(|(_, word)| {
let mut more = rhai::Map::new();
for iced in &word.additional {
more.insert(iced.0.clone().into(), iced.1.clone().into());
}
let mut scope = Scope::new();
scope
.push_constant("id", word.id.0 as i64)
.push_constant("key", word.key.clone())
.push_constant("value", word.value.clone())
.push_constant("tags", word.tags.clone())
.push_constant("more", more)
.push_constant(
"group",
groups
.iter()
.find(|g| g.id == word.group_id)
.cloned()
.expect("No group id")
.name,
);
let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast);
if let Ok(result) = result {
return result;
}
false
})
.map(|(index, _)| index)
.collect();
info!("Collecting available words is {:?}", time.elapsed());
list
} else {
list
}
}
pub fn require_speech(&self) -> bool { pub fn require_speech(&self) -> bool {
self.forward == "speech" || self.backward == "speech" self.forward == "speech" || self.backward == "speech"
} }
@@ -158,7 +152,7 @@ impl DeckSettings {
pub struct CardState { pub struct CardState {
pub id: Id, pub id: Id,
pub word_id: Id, pub word_id: Id,
pub set_id: Id, pub deck_id: Id,
pub last_open: DateTime<Utc>, pub last_open: DateTime<Utc>,
pub score: u8, pub score: u8,
pub fsrs_opened: NaiveDate, pub fsrs_opened: NaiveDate,
@@ -203,7 +197,11 @@ impl CardState {
} }
} }
#[derive(Copy)] pub struct CustomField {
pub id: Id,
pub name: String,
}
pub struct EntityHandle<E> { pub struct EntityHandle<E> {
pub(crate) id: Id, pub(crate) id: Id,
index: u32, index: u32,
@@ -219,6 +217,7 @@ impl<T> Clone for EntityHandle<T> {
} }
} }
} }
impl<T> Copy for EntityHandle<T> {}
impl<T> EntityHandle<T> { impl<T> EntityHandle<T> {
pub fn new(id: Id, index: usize, real: bool) -> Self { pub fn new(id: Id, index: usize, real: bool) -> Self {
Self { Self {
@@ -227,6 +226,11 @@ impl<T> EntityHandle<T> {
_phantom: Default::default(), _phantom: Default::default(),
} }
} }
pub fn from_id(id: Id) -> Self {
Self::new(id, id.0 as usize, true)
}
pub fn index(&self) -> usize { pub fn index(&self) -> usize {
(self.index << 1 >> 1) as usize (self.index << 1 >> 1) as usize
} }
@@ -270,6 +274,13 @@ impl<T> Ord for EntityHandle<T> {
} }
} }
impl<T> Hash for EntityHandle<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.real().hash(state);
}
}
pub trait EntityData { pub trait EntityData {
fn id(&self) -> Id; fn id(&self) -> Id;
fn set_id(&mut self, id: Id); fn set_id(&mut self, id: Id);
+213 -65
View File
@@ -1,5 +1,6 @@
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{EntityData, EntityHandle}; use crate::data::entities::{CardState, EntityData, EntityHandle};
use crate::data_provider::sqlite::SqliteConnection;
use crate::helpers::into_result; use crate::helpers::into_result;
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
@@ -8,8 +9,10 @@ use rayon::iter::IntoParallelRefIterator;
use rayon::iter::ParallelIterator; use rayon::iter::ParallelIterator;
use rusqlite::Connection; use rusqlite::Connection;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::Deref; use std::ops::{Deref, Index, IndexMut};
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant;
use log::debug;
pub trait Repository<T, D> pub trait Repository<T, D>
where where
@@ -18,14 +21,23 @@ where
fn get(&self, handle: &mut EntityHandle<T>) -> Option<T>; fn get(&self, handle: &mut EntityHandle<T>) -> Option<T>;
fn get_ref(&self, handle: &mut EntityHandle<T>) -> Option<&T>; fn get_ref(&self, handle: &mut EntityHandle<T>) -> Option<&T>;
fn get_mut(&mut self, handle: &mut EntityHandle<T>) -> Option<&mut T>; fn get_mut(&mut self, handle: &mut EntityHandle<T>) -> Option<&mut T>;
fn get_static(&self, handle: EntityHandle<T>) -> Option<T>;
fn get_ref_static(&self, handle: EntityHandle<T>) -> Option<&T>;
fn get_mut_static(&mut self, handle: EntityHandle<T>) -> Option<&mut T>;
fn set(&mut self, handle: &mut EntityHandle<T>, value: T);
fn set_static(&mut self, handle: EntityHandle<T>, value: T);
fn handle_by_id(&self, id: Id) -> EntityHandle<T>; fn handle_by_id(&self, id: Id) -> EntityHandle<T>;
fn create(&mut self, new: T) -> EntityHandle<T>; fn create(&mut self, new: T) -> EntityHandle<T>;
fn create_now(&mut self, new: T) -> Result<EntityHandle<T>, NotificationData>; fn create_now(&mut self, new: T) -> Result<EntityHandle<T>, NotificationData>;
fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError; fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError;
fn delete(&mut self, handle: EntityHandle<T>) -> MaybeError; fn delete(&mut self, handle: EntityHandle<T>) -> MaybeError;
fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>> where F : Fn(&T) -> bool;
fn init(&mut self) -> MaybeError; fn init(&mut self) -> MaybeError;
fn start(&mut self) -> MaybeError; fn start(&mut self) -> MaybeError;
fn reload(&mut self) -> MaybeError; fn reload(&mut self) -> MaybeError;
fn shifts(&self) -> usize;
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T> where T: 'a;
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut T> where T: 'a;
} }
struct QueryManger<T, R, D> struct QueryManger<T, R, D>
@@ -91,32 +103,52 @@ where
} }
} }
} }
#[derive(Clone)]
pub struct Query<T, R, D> pub struct Query<T, R, D>
where where
R: Repository<T, D>, R: Repository<T, D>,
D: DbLayer<T>, D: DbLayer<T>,
{ {
cache: Vec<EntityHandle<T>>, cache: Vec<EntityHandle<T>>,
rule: Box<dyn Fn(&T) -> bool + Send + Sync>, rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
parallel: bool, parallel: bool,
_marker: PhantomData<(R, D)>, _marker: PhantomData<(R, D)>,
shift_stamp: u16, shift_stamp: usize,
} }
impl<T: EntityData + Clone, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> { impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> {
pub fn iter<'a>(&mut self, data_repository_inner: &'a R) -> impl Iterator<Item = &'a T> pub fn iter<'a>(&mut self, data_repository_inner: &'a DataRepositoryInner<T, D>) -> impl Iterator<Item = &'a T>
where where
T: 'a, T: 'a,
{ {
if self.shift_stamp != data_repository_inner.shifts() {
self.load(data_repository_inner);
}
self.cache self.cache
.iter_mut() .iter_mut()
.filter_map(move |e| data_repository_inner.get_ref(e)) .filter_map(move |e| data_repository_inner.get_ref(e))
} }
pub fn for_each_mut<F>(&mut self, repo: &mut R, mut f: F) pub fn iter_with_handles<'a>(&mut self, data_repository_inner: &'a DataRepositoryInner<T, D>) -> impl Iterator<Item = (&'a T, EntityHandle<T>)>
where
T: 'a,
{
if self.shift_stamp != data_repository_inner.shifts() {
self.load(data_repository_inner);
}
self.cache
.iter_mut()
.filter_map(move |e| Some((data_repository_inner.get_ref(e)?, e.clone())))
}
pub fn for_each_mut<F>(&mut self, repo: &mut DataRepositoryInner<T, D>, mut f: F)
where where
F: FnMut(&mut T), F: FnMut(&mut T),
{ {
if self.shift_stamp != repo.shifts() {
self.load(repo);
}
for handle in &mut self.cache { for handle in &mut self.cache {
if let Some(entity) = repo.get_mut(handle) { if let Some(entity) = repo.get_mut(handle) {
f(entity); f(entity);
@@ -128,13 +160,14 @@ impl<T: EntityData + Clone, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> {
where where
T: Send + Sync, T: Send + Sync,
{ {
let time = Instant::now();
self.cache.clear(); self.cache.clear();
if self.parallel { if self.parallel {
self.cache = data_repository self.cache = data_repository
.cache .cache
.par_iter() .par_iter()
.enumerate() .enumerate()
.filter(|(_, entity)| (self.rule)(entity)) .filter(|(_, entity)| self.rule.as_ref()(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, true)) .map(|(i, entity)| EntityHandle::new(entity.id(), i, true))
.collect(); .collect();
} else { } else {
@@ -156,6 +189,30 @@ impl<T: EntityData + Clone, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> {
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false)) .map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
debug!("Query for {} elements (filtered {}) takes {:?}", data_repository.len(), self.cache.len(), time.elapsed());
}
pub fn handles(&self) -> impl Iterator<Item = EntityHandle<T>> {
self.cache.iter().cloned()
}
pub fn len(&self) -> usize {
self.cache.len()
}
}
impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Index<usize> for Query<T, R, D> {
type Output = EntityHandle<T>;
fn index(&self, index: usize) -> &Self::Output {
&self.cache[index]
}
}
impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> IndexMut<usize> for Query<T, R, D> {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.cache[index]
} }
} }
@@ -164,6 +221,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;
} }
@@ -184,22 +242,40 @@ 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,
connection: Arc<RwLock<Connection>>, connection: Arc<Mutex<SqliteConnection>>,
data_layer: D, data_layer: D,
query_manger: QueryManger<T, Self, D>, query_manger: QueryManger<T, Self, D>,
version: u16, version: u16,
shifts: u16, shifts: usize,
} }
impl<T: std::clone::Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> { impl<T: Clone + EntityData, D: DbLayer<T> + Default> DataRepository<T, D> {
pub fn new(connection: Arc<Mutex<SqliteConnection>>) -> DataRepository<T, D> {
Self{
inner: Arc::new(RwLock::new(DataRepositoryInner{
cache: vec![],
temporal: vec![],
next_temporal: 0,
connection,
data_layer: Default::default(),
query_manger: QueryManger { queries: vec![] },
version: 0,
shifts: 0,
})),
}
}
}
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 {
@@ -207,71 +283,73 @@ impl<T: std::clone::Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<
} }
} }
impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, D> { impl<T: EntityData + Clone + std::marker::Send + std::marker::Sync, D: DbLayer<T>> DataRepositoryInner<T, D> {
pub fn query_par( pub fn query_par(
&self, &self,
rule: Box<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,
{ {
let mut cache: Vec<EntityHandle<T>> = self let mut query = Query {
.cache cache: vec![],
.par_iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, true))
.collect();
cache.extend(
self.temporal
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(),
);
Query {
cache,
rule, rule,
parallel: true, parallel: true,
_marker: PhantomData, _marker: PhantomData,
shift_stamp: self.shifts, shift_stamp: self.shifts,
} };
query.load(self);
query
} }
pub fn query( pub fn query(
&self, &self,
rule: Box<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,
{ {
let mut cache: Vec<EntityHandle<T>> = self let mut query = Query {
.cache cache: vec![],
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, true))
.collect();
cache.extend(
self.temporal
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(),
);
Query {
cache,
rule, rule,
parallel: false, parallel: false,
_marker: PhantomData, _marker: PhantomData,
shift_stamp: self.shifts, shift_stamp: self.shifts,
};
query.load(self);
query
} }
pub fn empty_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query{
cache: vec![],
rule: Arc::new(|_| false),
parallel: false,
_marker: Default::default(),
shift_stamp: 0,
}
}
pub fn empty_query_par(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query{
cache: vec![],
rule: Arc::new(|_| false),
parallel: true,
_marker: Default::default(),
shift_stamp: 0,
}
}
pub fn full_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
self.query(Arc::new(|_| true))
}
pub fn len(&self) -> usize {
self.cache.len() + self.temporal.len()
} }
} }
@@ -306,6 +384,32 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
Some(&mut slice[index]) Some(&mut slice[index])
} }
fn get_static(&self, mut handle: EntityHandle<T>) -> Option<T> {
self.get(&mut handle)
}
fn get_ref_static(&self, mut handle: EntityHandle<T>) -> Option<&T> {
self.get_ref(&mut handle)
}
fn get_mut_static(&mut self, mut handle: EntityHandle<T>) -> Option<&mut T> {
self.get_mut(&mut handle)
}
fn set(&mut self, handle: &mut EntityHandle<T>, value: T) {
let slice = if handle.real() {
self.cache.as_mut_slice()
} else {
self.temporal.as_mut_slice()
};
let index = fast_position_of(slice, handle).unwrap();
slice[index] = value;
}
fn set_static(&mut self, mut handle: EntityHandle<T>, value: T) {
self.set(&mut handle, value)
}
fn handle_by_id(&self, id: Id) -> EntityHandle<T> { fn handle_by_id(&self, id: Id) -> EntityHandle<T> {
EntityHandle::new(id, id.0 as usize, true) EntityHandle::new(id, id.0 as usize, true)
} }
@@ -320,7 +424,8 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
} }
fn create_now(&mut self, mut new: T) -> Result<EntityHandle<T>, NotificationData> { fn create_now(&mut self, mut new: T) -> Result<EntityHandle<T>, NotificationData> {
self.data_layer.add_entity(&mut new, &self.connection.read())?; self.data_layer
.add_entity(&mut new, &self.connection.lock())?;
let index = self.cache.len(); let index = self.cache.len();
let handle = EntityHandle::new(new.id(), index, true); let handle = EntityHandle::new(new.id(), index, true);
self.cache.push(new); self.cache.push(new);
@@ -332,17 +437,18 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
if entity.real() { if entity.real() {
let data = self.get_ref(entity).unwrap(); let data = self.get_ref(entity).unwrap();
self.data_layer self.data_layer
.write_entity(data, &self.connection.read())?; .write_entity(data, &self.connection.lock())?;
} else { } else {
let index = fast_position_of(self.temporal.as_slice(), entity).unwrap(); let index = fast_position_of(self.temporal.as_slice(), entity).unwrap();
let mut data = self.temporal.remove(index); let data = &mut self.temporal[index];
self.data_layer self.data_layer
.add_entity(&mut data, &self.connection.read())?; .add_entity(data, &self.connection.lock())?;
entity.set_real(true); entity.set_real(true);
entity.id = data.id(); entity.id = data.id();
entity.set_index(self.cache.len()); entity.set_index(self.cache.len());
self.query_manger.handle_save(original_state, entity, &data); self.query_manger.handle_save(original_state, entity, &data);
let data = self.temporal.remove(index);
self.cache.push(data); self.cache.push(data);
self.shifts += 1; self.shifts += 1;
} }
@@ -353,7 +459,7 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
fn delete(&mut self, mut handle: EntityHandle<T>) -> MaybeError { fn delete(&mut self, mut handle: EntityHandle<T>) -> MaybeError {
let vec = if handle.real() { let vec = if handle.real() {
self.data_layer self.data_layer
.delete_entity(handle.id, &self.connection.read())?; .delete_entity(handle.id, &self.connection.lock())?;
&mut self.cache &mut self.cache
} else { } else {
&mut self.temporal &mut self.temporal
@@ -369,16 +475,32 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
Ok(()) Ok(())
} }
fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>>
where
F: Fn(&T) -> bool
{
if let Some(index) = self.cache.iter().position(|entity| predicate(entity)){
let value = &self.cache[index];
return Some(EntityHandle::new(value.id(), index, true))
}
if let Some(index) = self.temporal.iter().position(|entity| predicate(entity)){
let value = &self.temporal[index];
return Some(EntityHandle::new(value.id(), index, false))
}
None
}
fn init(&mut self) -> MaybeError { fn init(&mut self) -> MaybeError {
self.data_layer self.data_layer
.load_all(&mut self.cache, &self.connection.read())?; .load_all(&mut self.cache, &self.connection.lock())?;
self.version += 1; self.version += 1;
Ok(()) Ok(())
} }
fn start(&mut self) -> MaybeError { fn start(&mut self) -> MaybeError {
if self.version != 0 { if self.version != 0 {
return Ok(()) return Ok(());
} }
self.init()?; self.init()?;
@@ -391,10 +513,36 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
self.init()?; self.init()?;
Ok(()) Ok(())
} }
fn shifts(&self) -> usize {
self.shifts
}
fn iter<'a>(&'a self) -> impl Iterator<Item=&'a T>
where
T: 'a
{
self.cache.iter().chain(self.temporal.iter())
}
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item=&'a mut T>
where
T: 'a
{
self.cache.iter_mut().chain(self.temporal.iter_mut())
}
} }
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.lock())?;
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);
@@ -424,7 +572,7 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
}); });
self.data_layer self.data_layer
.add_many_entities(&mut temp_values, &mut self.connection.write())?; .add_many_entities(&mut temp_values, &mut self.connection.lock())?;
let start = self.cache.len(); let start = self.cache.len();
@@ -442,7 +590,7 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
let h = handle; let h = handle;
let idx = fast_position_of(self.cache.as_slice(), h).unwrap(); let idx = fast_position_of(self.cache.as_slice(), h).unwrap();
self.data_layer self.data_layer
.write_entity(&self.cache[idx], &self.connection.read())?; .write_entity(&self.cache[idx], &self.connection.lock())?;
} }
self.shifts += 1; self.shifts += 1;
@@ -466,7 +614,7 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
!remove !remove
}); });
self.data_layer self.data_layer
.delete_many_entities(cached.as_slice(), &mut self.connection.write())?; .delete_many_entities(cached.as_slice(), &mut self.connection.lock())?;
let len = cached.len(); let len = cached.len();
cell = 0; cell = 0;
self.cache.retain(|x| { self.cache.retain(|x| {
+27 -77
View File
@@ -1,5 +1,7 @@
use std::sync::Arc;
use crate::data::entities::{CardState, DeckSettings}; use crate::data::entities::{CardState, DeckSettings};
use crate::data_provider::TableAccessQuerySet; use crate::data::repositories::{DataRepository, DataRepositoryInner, Query};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
use chrono::NaiveDate; use chrono::NaiveDate;
@@ -7,19 +9,26 @@ use fsrs::MemoryState;
use log::info; use log::info;
use rusqlite::{Connection, Error, Params, Row, params}; use rusqlite::{Connection, Error, Params, Row, params};
use std::time::Instant; use std::time::Instant;
use crate::data::Id;
pub type CardStateRepository =
DataRepository<CardState, CardStateDbLayer>;
pub type CardStateRepositoryInner =
DataRepositoryInner<CardState, CardStateDbLayer>;
pub type CardStateDbLayer = SqliteDbLayer<CardState, CardStatesQuerySet>;
#[derive(Clone)]
pub struct CardStatesQuerySet; pub struct CardStatesQuerySet;
impl TableAccessQuerySet<CardState> for CardStatesQuerySet { impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
fn load() -> &'static str { fn load() -> &'static str {
"SELECT id, word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states" "SELECT id, word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states"
} }
fn save() -> &'static str { fn save() -> &'static str {
"UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3" "UPDATE states SET score = ?1, last_opened = ?2, fsrs_opened = ?3, fsrs_memory = ?4, fsrs_open_day = ?5 WHERE id = ?6"
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)" "INSERT INTO states (word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
} }
fn name() -> &'static str { fn name() -> &'static str {
@@ -30,7 +39,7 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
Ok(CardState { Ok(CardState {
id: row.get(0)?, id: row.get(0)?,
word_id: row.get(1)?, word_id: row.get(1)?,
set_id: row.get(2)?, deck_id: row.get(2)?,
score: row.get(3)?, score: row.get(3)?,
fsrs_opened: NaiveDate::from_epoch_days(row.get::<usize, i32>(5)?) fsrs_opened: NaiveDate::from_epoch_days(row.get::<usize, i32>(5)?)
.expect("Incorrect date encoding"), .expect("Incorrect date encoding"),
@@ -44,7 +53,7 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
fn insert_params(entity: &CardState) -> impl Params { fn insert_params(entity: &CardState) -> impl Params {
( (
entity.word_id, entity.word_id,
entity.set_id, entity.deck_id,
entity.score, entity.score,
entity.last_open.timestamp(), entity.last_open.timestamp(),
entity.fsrs_opened.to_epoch_days(), entity.fsrs_opened.to_epoch_days(),
@@ -54,7 +63,14 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
} }
fn update_params(entity: &CardState) -> impl Params { fn update_params(entity: &CardState) -> impl Params {
(entity.score, entity.last_open.timestamp(), entity.id) (
entity.score,
entity.last_open.timestamp(),
entity.fsrs_opened.to_epoch_days(),
memory_to_i64(&entity.fsrs_memory),
entity.fsrs_open_day.to_epoch_days(),
entity.id,
)
} }
} }
pub fn load_stats_of_deck( pub fn load_stats_of_deck(
@@ -69,7 +85,7 @@ pub fn load_stats_of_deck(
Ok(CardState { Ok(CardState {
id: row.get(0)?, id: row.get(0)?,
word_id: row.get(1)?, word_id: row.get(1)?,
set_id: set.id, deck_id: set.id,
score: row.get(2)?, score: row.get(2)?,
fsrs_opened: NaiveDate::from_epoch_days(row.get::<usize, i32>(4)?) fsrs_opened: NaiveDate::from_epoch_days(row.get::<usize, i32>(4)?)
.expect("Incorrect date encoding"), .expect("Incorrect date encoding"),
@@ -105,74 +121,8 @@ fn memory_to_i64(value: &Option<MemoryState>) -> i64 {
value.stability.to_bits() as i64 | ((value.difficulty.to_bits() as i64) << 32) value.stability.to_bits() as i64 | ((value.difficulty.to_bits() as i64) << 32)
} }
pub fn add_stat_list(stats: &mut [CardState], connection: &mut Connection) -> MaybeError { impl CardStateRepositoryInner {
let time = Instant::now(); pub fn deck_states(&self, deck_id: Id) -> Query<CardState, CardStateRepositoryInner, CardStateDbLayer> {
let tx = connection self.query(Arc::new(move |state| {state.deck_id == deck_id}))
.transaction()
.expect("Failed to create transaction");
let count = stats.len();
{
let mut stmt = tx
.prepare(
"INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)",
)
.expect("Failed to prepare state query");
for stat in stats.iter() {
stmt.execute(params![
stat.word_id,
stat.set_id,
stat.score,
stat.last_open.timestamp(),
stat.fsrs_opened.to_epoch_days(),
memory_to_i64(&stat.fsrs_memory),
stat.fsrs_open_day.to_epoch_days()
])
.map_err(|_| DbSaveError)?;
} }
} }
let last_index: u32 = tx.last_insert_rowid() as u32;
tx.commit().expect("Failed to commit");
let start_index = last_index - (count as u32) + 1;
for (index, id) in (start_index..=last_index).enumerate() {
stats[index].id = id.into();
}
info!("Added {} cards for {:?}", stats.len(), time.elapsed());
Ok(())
}
pub fn update_stat_score(stat: &CardState, connection: &Connection) -> MaybeError {
let time = Instant::now();
connection
.execute(
"UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3",
(&stat.score, &stat.last_open.timestamp(), &stat.id),
)
.map_err(|_| DbSaveError)?;
info!("Updated stat: {}", time.elapsed().as_millis());
Ok(())
}
pub fn delete_stats(stat: &[CardState], connection: &Connection) -> MaybeError {
if stat.is_empty() {
return Ok(());
}
let ids = stat
.iter()
.map(|s| s.id.into())
.collect::<Vec<String>>()
.join(", ");
connection
.execute(
format!("DELETE FROM states WHERE id in ({ids})").as_str(),
(),
)
.map_err(|_| DbSaveError)?;
Ok(())
}
+40
View File
@@ -0,0 +1,40 @@
use crate::data::entities::{CustomField, WordGroup};
use crate::data::repositories::DataRepository;
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::{Error, Params, Row};
pub type CustomFieldRepository = DataRepository<CustomField, SqliteDbLayer<CustomField, CustomFieldsQuerySet>>;
pub struct CustomFieldsQuerySet;
impl TableAccessQuerySet<WordGroup> for CustomFieldsQuerySet {
fn load() -> &'static str {
"SELECT id, name FROM custom_fields"
}
fn save() -> &'static str {
"UPDATE custom_fields SET name = ?1 WHERE id = ?2"
}
fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1)"
}
fn name() -> &'static str {
"custom_fields"
}
fn from_sql(row: &Row) -> Result<WordGroup, Error> {
Ok(WordGroup {
id: row.get(0)?,
name: row.get(1)?,
})
}
fn insert_params(entity: &WordGroup) -> impl Params {
(&entity.name,)
}
fn update_params(entity: &WordGroup) -> impl Params {
(&entity.name, entity.id)
}
}
+13 -80
View File
@@ -1,11 +1,11 @@
use crate::core::srs::OrderMode; use crate::data::entities::DeckSettings;
use crate::data::entities::{CardState, DeckSettings}; use crate::data::repositories::DataRepository;
use crate::data_provider::TableAccessQuerySet; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use rusqlite::{Error, Params, Row};
use crate::notification::{MaybeError, NotificationData};
use chrono::NaiveDate;
use rusqlite::{Connection, Error, Params, Row};
pub type DeckRepository = DataRepository<DeckSettings, SqliteDbLayer<DeckSettings, DeckQuerySet>>;
#[derive(Clone)]
pub struct DeckQuerySet; pub struct DeckQuerySet;
impl TableAccessQuerySet<DeckSettings> for DeckQuerySet { impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
fn load() -> &'static str { fn load() -> &'static str {
@@ -17,7 +17,7 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)" "INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4)"
} }
fn name() -> &'static str { fn name() -> &'static str {
@@ -31,8 +31,6 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
forward: row.get(2)?, forward: row.get(2)?,
backward: row.get(3)?, backward: row.get(3)?,
filter: row.get(4)?, filter: row.get(4)?,
count: None,
open_mode: OrderMode::Default,
}) })
} }
@@ -46,77 +44,12 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
} }
fn update_params(entity: &DeckSettings) -> impl Params { fn update_params(entity: &DeckSettings) -> impl Params {
(&entity.name, &entity.forward, &entity.backward, &entity.filter, entity.id)
}
}
pub fn load_sets(connection: &Connection) -> Result<Vec<DeckSettings>, NotificationData> {
let mut stmt = connection
.prepare("SELECT id, name, forward, backward, filter FROM decks")
.expect("Failed to prepare state query");
let iter = stmt
.query_map([], |row| {
Ok(DeckSettings {
id: row.get(0)?,
name: row.get(1)?,
forward: row.get(2)?,
backward: row.get(3)?,
filter: row.get(4)?,
count: None,
open_mode: OrderMode::Default,
})
})
.map_err(|_| DbLoadError)?;
let mut buffer = Vec::with_capacity(16);
iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i));
Ok(buffer)
}
pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> MaybeError {
let index : u32 = connection
.query_row(
"INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id",
( (
&set.name, &entity.name,
&set.forward, &entity.forward,
&set.backward, &entity.backward,
&set.filter, &entity.filter,
), entity.id,
|row| row.get(0)
) )
.map_err(|_| DbSaveError)?;
set.id = index.into();
Ok(())
} }
pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> MaybeError {
if !set.id.is_valid() {
add_set(set, connection)?;
} else {
connection
.execute(
"UPDATE decks SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5",
(
&set.name,
&set.forward,
&set.backward,
&set.filter,
&set.id
),
)
.map_err(|_| DbSaveError)?;
}
Ok(())
}
pub fn delete_set(set: &DeckSettings, connection: &Connection) -> MaybeError {
if !set.id.is_valid() {
return Err(DbSaveError);
}
connection
.execute("DELETE FROM decks WHERE id = ?1", (&set.id,))
.map_err(|_| DbSaveError)?;
Ok(())
} }
+79 -81
View File
@@ -1,97 +1,95 @@
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!() #[derive(Clone)]
pub struct HistoryQuerySet;
impl TableAccessQuerySet<HistoryNote> for HistoryQuerySet {
fn load() -> &'static str {
"SELECT rowid, deck_id, word_id, timestamp, answer, before, after, fsrs FROM history"
} }
pub fn get_history_of_set(id: Id) -> Result<Vec<HistoryItem>, NotificationData> { fn save() -> &'static str {
let app_dir = history_dir()?; todo!("History is readonly")
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 { fn add() -> &'static str {
if path.exists() { "INSERT INTO history (deck_id, word_id, timestamp, answer, before, after, fsrs) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
let reader = BufReader::new(File::open(path).map_err(|_| PermissionDenied)?);
reader
.lines()
.map_while(Result::ok)
.for_each(|i| lines.push(i));
}
Ok(())
} }
fn parse_history_items(strings: Vec<String>) -> Vec<HistoryItem> { fn name() -> &'static str {
let mut items = Vec::with_capacity(strings.len()); "history"
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;
} }
let id = id.unwrap_or_default();
if !id.is_valid() { fn id_field() -> &'static str {
continue; "rowid"
} }
items.push(HistoryItem { fn from_sql(row: &Row) -> Result<HistoryNote, Error> {
timestamp: DateTime::from_timestamp(time.parse().unwrap_or_default(), 0) Ok(HistoryNote {
.unwrap_or_default(), id: row.get(0)?,
word_id: id, timestamp: row.get(3)?,
mode: match mode.parse::<u8>().unwrap_or_default() { deck_id: row.get(1)?,
2 => WordOpenMode::Hard, word_id: row.get(2)?,
3 => WordOpenMode::Ok, answer: row.get(4)?,
4 => WordOpenMode::Easy, before: row.get(5)?,
_ => WordOpenMode::None, after: row.get(6)?,
}, fsrs: row.get(7)?,
before: before.parse().unwrap_or_default(), })
after: after.parse().unwrap_or_default(), }
});
fn insert_params(entity: &HistoryNote) -> impl Params {
(
entity.deck_id,
entity.word_id,
entity.timestamp,
entity.answer,
entity.before,
entity.after,
entity.fsrs,
)
}
fn update_params(entity: &HistoryNote) -> impl Params {
(
entity.deck_id,
entity.word_id,
entity.timestamp,
entity.answer,
entity.before,
entity.after,
entity.fsrs,
)
} }
} }
items 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),
}
}
Ok(WordOpenMode::None)
}
} }
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(())
} }
+3 -3
View File
@@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use crate::helpers::into_result; use crate::helpers::into_result;
use crate::notification::NotificationData; use crate::notification::NotificationData;
use crate::notification::NotificationData::IncorrectAnkiFile; use crate::notification::NotificationData::IncorrectAnkiFile;
use hashbrown::HashMap;
use log::info; use log::info;
use rusqlite::Connection; use rusqlite::Connection;
use serde_json::Value; use serde_json::Value;
@@ -15,7 +15,7 @@ pub struct ImportGroup {
pub name: String, pub name: String,
pub fields: Vec<String>, pub fields: Vec<String>,
pub length: u64, pub length: u64,
pub mapping: HashMap<String, String>, pub mapping: BTreeMap<String, String>,
pub imported: bool, pub imported: bool,
} }
@@ -45,7 +45,7 @@ pub fn load_groups(connection: &Connection) -> Result<ImportData, NotificationDa
.collect::<Vec<(i64, i64)>>(); .collect::<Vec<(i64, i64)>>();
for (_, collection) in sets { for (_, collection) in sets {
let fields_property = collection.get("fields"); let fields_property = collection.get("flds");
if let Some(fields_property) = fields_property { if let Some(fields_property) = fields_property {
let fields = into_result(fields_property.as_array(), IncorrectAnkiFile)? let fields = into_result(fields_property.as_array(), IncorrectAnkiFile)?
.iter() .iter()
+49 -30
View File
@@ -1,14 +1,15 @@
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{CardState, EntityData}; use crate::data::entities::EntityData;
use crate::data::repositories::{BatchedDbLayer, DbLayer}; use crate::data::repositories::{BatchedDbLayer, DbLayer};
use crate::notification::MaybeError; use crate::notification::MaybeError;
use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::NotificationData::DbSaveError;
use log::info; use log::{debug, error};
use rusqlite::{Connection, Error, Params, Row}; use rusqlite::{Connection, Error, Params, Row};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::time::Instant; use std::time::Instant;
pub(crate) mod card_states; pub(crate) mod card_states;
pub mod custom_fields;
pub(crate) mod decks; pub(crate) mod decks;
pub(crate) mod history; pub(crate) mod history;
pub(crate) mod import; pub(crate) mod import;
@@ -16,6 +17,7 @@ pub(crate) mod settings;
pub(crate) mod sqlite; pub(crate) mod sqlite;
pub(crate) mod voice; pub(crate) mod voice;
pub(crate) mod web_api; pub(crate) mod web_api;
pub mod word_groups;
pub(crate) mod words; pub(crate) mod words;
pub trait TableAccessQuerySet<T> { pub trait TableAccessQuerySet<T> {
@@ -23,11 +25,15 @@ pub trait TableAccessQuerySet<T> {
fn save() -> &'static str; fn save() -> &'static str;
fn add() -> &'static str; fn add() -> &'static str;
fn name() -> &'static str; fn name() -> &'static str;
fn id_field() -> &'static str {
"id"
}
fn from_sql(row: &Row) -> Result<T, Error>; fn from_sql(row: &Row) -> Result<T, Error>;
fn insert_params(entity: &T) -> impl Params; fn insert_params(entity: &T) -> impl Params;
fn update_params(entity: &T) -> impl Params; fn update_params(entity: &T) -> impl Params;
} }
#[derive(Clone)]
pub struct SqliteDbLayer<T, Q> pub struct SqliteDbLayer<T, Q>
where where
Q: TableAccessQuerySet<T>, Q: TableAccessQuerySet<T>,
@@ -35,18 +41,25 @@ where
_phantom: PhantomData<(T, Q)>, _phantom: PhantomData<(T, Q)>,
} }
impl<T, Q: TableAccessQuerySet<T>> Default for SqliteDbLayer<T, Q> {
fn default() -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q> { impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q> {
fn write_entity(&self, entity: &T, connection: &Connection) -> MaybeError { fn write_entity(&self, entity: &T, connection: &Connection) -> MaybeError {
let time = Instant::now(); let time = Instant::now();
connection connection
.execute(Q::load(), Q::update_params(entity)) .execute(Q::save(), Q::update_params(entity))
.map_err(|_| DbSaveError)?; .map_err(|e| {
info!( error!("{}", e);
"Updated in {} for {:?}", DbSaveError
Q::name(), })?;
time.elapsed() debug!("Updated in {} for {:?}", Q::name(), time.elapsed());
);
Ok(()) Ok(())
} }
@@ -54,15 +67,14 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
let time = Instant::now(); let time = Instant::now();
let index: u32 = connection let index: u32 = connection
.query_row(Q::add(), Q::insert_params(entity), |row| row.get(0)) .query_row(format!("{} RETURNING {}", Q::add(), Q::id_field()).as_str(), Q::insert_params(entity), |row| row.get(0))
.map_err(|_| DbSaveError)?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
entity.set_id(index.into()); entity.set_id(index.into());
info!( debug!("Added into {} for {:?}", Q::name(), time.elapsed());
"Added into {} for {:?}",
Q::name(),
time.elapsed()
);
Ok(()) Ok(())
} }
@@ -77,12 +89,11 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
format!("DELETE FROM {} WHERE id = ?1", Q::name()).as_str(), format!("DELETE FROM {} WHERE id = ?1", Q::name()).as_str(),
(&id,), (&id,),
) )
.map_err(|_| DbSaveError)?; .map_err(|e| {
info!( error!("{}", e);
"Deleted from {} for {:?}", DbSaveError
Q::name(), })?;
time.elapsed() debug!("Deleted from {} for {:?}", Q::name(), time.elapsed());
);
Ok(()) Ok(())
} }
@@ -94,11 +105,14 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
.expect("Failed to prepare state query"); .expect("Failed to prepare state query");
let iter = stmt let iter = stmt
.query_map([], |row| Ok(Q::from_sql(row)?)) .query_map([], |row| Ok(Q::from_sql(row)?))
.map_err(|_| DbLoadError)?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
iter.filter_map(|res| res.ok()) iter.filter_map(|res| res.ok())
.for_each(|i| storage.push(i)); .for_each(|i| storage.push(i));
info!( debug!(
"Loaded {} from {} for {:?}", "Loaded {} from {} for {:?}",
storage.len(), storage.len(),
Q::name(), Q::name(),
@@ -124,9 +138,12 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> BatchedDbLayer<T> for SqliteDbLay
format!("DELETE FROM {} WHERE id in ({})", Q::name(), stringed).as_str(), format!("DELETE FROM {} WHERE id in ({})", Q::name(), stringed).as_str(),
(), (),
) )
.map_err(|_| DbSaveError)?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
} }
info!( debug!(
"Deleted {} from {} for {:?}", "Deleted {} from {} for {:?}",
ids.len(), ids.len(),
Q::name(), Q::name(),
@@ -146,8 +163,10 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> BatchedDbLayer<T> for SqliteDbLay
{ {
let mut stmt = tx.prepare(Q::add()).expect("Failed to prepare state query"); let mut stmt = tx.prepare(Q::add()).expect("Failed to prepare state query");
for entity in entities.iter() { for entity in entities.iter() {
stmt.execute(Q::insert_params(entity)) stmt.execute(Q::insert_params(entity)).map_err(|e| {
.map_err(|_| DbSaveError)?; error!("{}", e);
DbSaveError
})?;
} }
} }
@@ -160,7 +179,7 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> BatchedDbLayer<T> for SqliteDbLay
entities[index].set_id(id.into()); entities[index].set_id(id.into());
} }
info!( debug!(
"Added {} into {} for {:?}", "Added {} into {} for {:?}",
entities.len(), entities.len(),
Q::name(), Q::name(),
+56
View File
@@ -1,5 +1,7 @@
use crate::fs::*; use crate::fs::*;
use rusqlite::Connection; use rusqlite::Connection;
use std::ops::{Deref, DerefMut};
use log::error;
pub fn create_db() { pub fn create_db() {
let db_file = db_path(); let db_file = db_path();
@@ -21,6 +23,7 @@ fn create_tables(conn: &Connection) {
make_words(conn).expect("Failed to init table"); make_words(conn).expect("Failed to init table");
make_card_stats(conn).expect("Failed to init table"); make_card_stats(conn).expect("Failed to init table");
make_history(conn).expect("Failed to init table"); make_history(conn).expect("Failed to init table");
make_custom_fields(conn).expect("Failed to init table");
} }
fn make_settings(conn: &Connection) -> Result<(), rusqlite::Error> { fn make_settings(conn: &Connection) -> Result<(), rusqlite::Error> {
@@ -58,6 +61,12 @@ fn make_history(conn: &Connection) -> Result<(), rusqlite::Error> {
Ok(()) Ok(())
} }
fn make_custom_fields(conn: &Connection) -> Result<(), rusqlite::Error> {
let query = include_str!("../../sql/custom_fields.sql");
conn.execute(query, ())?;
Ok(())
}
pub fn default_connection() -> Connection { pub fn default_connection() -> Connection {
let db_file = db_path(); let db_file = db_path();
let connection = Connection::open(db_file).expect("Failed to open database"); let connection = Connection::open(db_file).expect("Failed to open database");
@@ -77,3 +86,50 @@ pub fn default_connection() -> Connection {
.expect("Failed to set PRAGMA"); .expect("Failed to set PRAGMA");
connection connection
} }
pub struct SqliteConnection(Option<Connection>);
impl Deref for SqliteConnection {
type Target = Connection;
fn deref(&self) -> &Self::Target {
self.0.as_ref().unwrap()
}
}
impl DerefMut for SqliteConnection {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().unwrap()
}
}
impl Default for SqliteConnection {
fn default() -> Self {
SqliteConnection(Some(default_connection()))
}
}
impl SqliteConnection {
pub fn finish(&mut self) {
let connection = self.0.take().unwrap();
connection
.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
});
connection
.execute("PRAGMA optimize", [])
.unwrap_or_else(|e| {
error!("Ошибка при оптимизации БД: {e}");
0
});
self.0 = None;
}
pub fn restart(&mut self) {
self.0 = Some(default_connection());
}
}
impl Drop for SqliteConnection {
fn drop(&mut self) { self.finish(); }
}
+43
View File
@@ -0,0 +1,43 @@
use crate::data::entities::WordGroup;
use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::{Error, Params, Row};
pub type WordGroupRepository = DataRepository<WordGroup, WordGroupDataLayer>;
pub type WordGroupRepositoryInner = DataRepositoryInner<WordGroup, WordGroupDataLayer>;
pub type WordGroupDataLayer = SqliteDbLayer<WordGroup, WordGroupQuerySet>;
#[derive(Clone)]
pub struct WordGroupQuerySet;
impl TableAccessQuerySet<WordGroup> for WordGroupQuerySet {
fn load() -> &'static str {
"SELECT id, name FROM word_groups"
}
fn save() -> &'static str {
"UPDATE word_groups SET name = ?1 WHERE id = ?2"
}
fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1)"
}
fn name() -> &'static str {
"word_groups"
}
fn from_sql(row: &Row) -> Result<WordGroup, Error> {
Ok(WordGroup {
id: row.get(0)?,
name: row.get(1)?,
})
}
fn insert_params(entity: &WordGroup) -> impl Params {
(&entity.name,)
}
fn update_params(entity: &WordGroup) -> impl Params {
(&entity.name, entity.id)
}
}
+10 -43
View File
@@ -1,11 +1,14 @@
use crate::data::entities::{WordData, WordGroup}; use crate::data::entities::{WordData, WordGroup};
use crate::data_provider::TableAccessQuerySet; use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
use rusqlite::{Connection, Error, Params, Row, params}; use rusqlite::{Connection, Error, Params, Row, params};
use std::collections::HashMap;
use litemap::LiteMap;
pub type WordRepository = DataRepository<WordData, WordDataLayer>;
pub type WordRepositoryInner = DataRepositoryInner<WordData, WordDataLayer>;
pub type WordDataLayer = SqliteDbLayer<WordData, WordQuerySet>;
#[derive(Clone)]
pub struct WordQuerySet; pub struct WordQuerySet;
impl TableAccessQuerySet<WordData> for WordQuerySet { impl TableAccessQuerySet<WordData> for WordQuerySet {
fn load() -> &'static str { fn load() -> &'static str {
@@ -31,8 +34,7 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
key: row.get(1)?, key: row.get(1)?,
value: row.get(2)?, value: row.get(2)?,
tags: row.get(3)?, tags: row.get(3)?,
additional: serde_json::from_str(&additional) additional: serde_json::from_str(&additional).unwrap_or_default(),
.unwrap_or_default(),
group_id: row.get(5)?, group_id: row.get(5)?,
}) })
} }
@@ -58,44 +60,10 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
} }
} }
pub struct WordGroupQuerySet;
impl TableAccessQuerySet<WordGroup> for WordGroupQuerySet {
fn load() -> &'static str {
"SELECT id, name FROM word_groups"
}
fn save() -> &'static str {
"UPDATE word_groups SET name = ?1 WHERE id = ?2"
}
fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1)"
}
fn name() -> &'static str {
"word_groups"
}
fn from_sql(row: &Row) -> Result<WordGroup, Error> {
Ok(WordGroup {
id: row.get(0)?,
name: row.get(1)?,
})
}
fn insert_params(entity: &WordGroup) -> impl Params {
(&entity.name,)
}
fn update_params(entity: &WordGroup) -> impl Params {
(&entity.name, entity.id)
}
}
pub fn add_word(word: &mut WordData, connection: &Connection) -> MaybeError { pub fn add_word(word: &mut WordData, connection: &Connection) -> MaybeError {
let index : u32 = connection let index : u32 = connection
.query_row( .query_row(
"INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id", "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5)",
( (
&word.key, &word.key,
&word.value, &word.value,
@@ -187,8 +155,7 @@ pub fn load_words(connection: &Connection) -> Result<Vec<WordData>, Notification
key: row.get(1)?, key: row.get(1)?,
value: row.get(2)?, value: row.get(2)?,
tags: row.get(3)?, tags: row.get(3)?,
additional: serde_json::from_str(&additional) additional: serde_json::from_str(&additional).unwrap_or_default(),
.unwrap_or_default(),
group_id: row.get(5)?, group_id: row.get(5)?,
}) })
}) })
@@ -226,7 +193,7 @@ pub fn load_word_groups(connection: &Connection) -> Result<Vec<WordGroup>, Notif
pub fn add_group(group: &mut WordGroup, connection: &Connection) -> MaybeError { pub fn add_group(group: &mut WordGroup, connection: &Connection) -> MaybeError {
let index: u32 = connection let index: u32 = connection
.query_row( .query_row(
"INSERT INTO word_groups (name) VALUES (?1) RETURNING id", "INSERT INTO word_groups (name) VALUES (?1)",
(&group.name,), (&group.name,),
|row| row.get(0), |row| row.get(0),
) )
+15 -67
View File
@@ -1,15 +1,11 @@
use crate::app::{AppState, fill_state}; use crate::app::{fill_state, AppState};
use crate::data::Id; use crate::data_provider::sqlite::{create_db};
use crate::data_provider::history::get_history_of_set;
use crate::data_provider::sqlite::{create_db, default_connection};
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,23 +27,17 @@ 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::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 {
fn default() -> Self { fn default() -> Self {
let mut state = AppState::new(); let mut state = AppState::new();
fill_state(&mut state); fill_state(&mut state);
let state = Arc::new(Mutex::new(state));
ScreenState { ScreenState {
stack: vec![Selector(SelectorState::new(state.clone()))], stack: vec![Selector(SelectorState::new(state.clone()))],
app_state: state.clone(), app_state: state.clone(),
@@ -72,7 +62,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,
@@ -99,7 +88,7 @@ pub enum Page {
pub struct ScreenState { pub struct ScreenState {
stack: Vec<Page>, stack: Vec<Page>,
app_state: Arc<Mutex<AppState>>, app_state: AppState,
notification_manager: NotificationManager, notification_manager: NotificationManager,
} }
@@ -109,14 +98,11 @@ 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.lock(); let state_guard = &state.app_state;
if state_guard.sync_data.auto_web_fetch if state_guard.sync_data.read().auto_web_fetch
&& let Some(key) = state_guard.sync_data.key.clone() && let Some(key) = state_guard.sync_data.read().key.clone()
{ {
final_task = Task::batch([ final_task = Task::batch([
final_task, final_task,
@@ -133,43 +119,10 @@ impl ScreenState {
]); ]);
} }
drop(state_guard);
info!("Boot finished"); info!("Boot finished");
(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?;
@@ -191,16 +144,11 @@ impl ScreenState {
return self.handle_keyboard(e); return self.handle_keyboard(e);
} }
if let DataLoaded(data) = message {
self.app_state.lock().activity = data;
return Task::none();
}
if let UpdateData = message { if let UpdateData = message {
info!("Loading data"); info!("Loading data");
let mut state = self.app_state.lock(); let state = &mut self.app_state;
if cfg!(windows) { if cfg!(windows) {
state.connection = Connection::open_in_memory().unwrap(); state.connection.lock().finish();
} }
let db_file = db_path(); let db_file = db_path();
let temp_db_file = temp_db_path(); let temp_db_file = temp_db_path();
@@ -208,8 +156,8 @@ impl ScreenState {
return push_error_task!(PermissionDenied); return push_error_task!(PermissionDenied);
} }
state.connection = default_connection(); state.connection = Default::default();
fill_state(&mut state); fill_state(state);
return Task::none(); return Task::none();
} }
@@ -241,7 +189,7 @@ impl ScreenState {
Import Import
); );
} }
info!("Update took {:?}", time.elapsed()); debug!("Update took {:?}", time.elapsed());
task.0 task.0
} }
@@ -288,7 +236,7 @@ impl ScreenState {
let notifications = self.notification_manager.draw().map(Notification); let notifications = self.notification_manager.draw().map(Notification);
info!("View took {:?}", time.elapsed()); debug!("View took {:?}", time.elapsed());
stack![view, notifications].width(Fill).height(Fill).into() stack![view, notifications].width(Fill).height(Fill).into()
} }
+216 -216
View File
@@ -1,7 +1,10 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{WordData, WordGroup}; use crate::data::entities::{EntityHandle, WordData, WordGroup};
use crate::data_provider::words::{delete_group, delete_word, update_group, update_word}; use crate::data::repositories::{Query, Repository};
use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner};
use crate::data_provider::words::WordDataLayer;
use crate::data_provider::words::WordRepositoryInner;
use crate::navigation::Page::{Import, Word}; use crate::navigation::Page::{Import, Word};
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::dictionary::DictionaryMessage::*; use crate::pages::dictionary::DictionaryMessage::*;
@@ -22,37 +25,37 @@ use iced::widget::*;
use iced::{Border, Color, Shadow, Task}; use iced::{Border, Color, Shadow, Task};
use iced_core::Length::Fill; use iced_core::Length::Fill;
use log::info; use log::info;
use parking_lot::Mutex;
use rand::random_range; use rand::random_range;
use std::ops::Add; use std::ops::Add;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
#[derive(Clone)]
pub struct DictionaryState { pub struct DictionaryState {
state: Arc<Mutex<AppState>>, state: AppState,
include_map: Vec<bool>, included: HashSet<EntityHandle<WordData>>,
tag_map: HashMap<String, bool>, tag_map: HashMap<String, bool>,
words_tags: Vec<(EntityHandle<WordData>, Vec<String>)>,
reverse: bool, reverse: bool,
search: String, search: String,
no_typing: bool, no_typing: bool,
selected_group_index: usize, selected_group: EntityHandle<WordGroup>,
reverse_list: bool, reverse_list: bool,
auto_save_queue: HashMap<usize, DateTime<Utc>>, auto_save_queue: HashMap<EntityHandle<WordData>, DateTime<Utc>>,
displayed_words_query: Query<WordData, WordRepositoryInner, WordDataLayer>,
groups_query: Query<WordGroup, WordGroupRepositoryInner, WordGroupDataLayer>,
} }
#[derive(Debug, Clone)] #[derive(Clone)]
pub enum DictionaryMessage { pub enum DictionaryMessage {
Back, Back,
SetTags(usize, String), SetTags(EntityHandle<WordData>, String),
SetKey(usize, String), SetKey(EntityHandle<WordData>, String),
SetValue(usize, String), SetValue(EntityHandle<WordData>, String),
SubmitWord(usize), WordAction(EntityHandle<WordData>),
WordAction(usize),
NewWord, NewWord,
Include(usize, bool), Include(EntityHandle<WordData>, bool),
IncludeTag(String, bool), IncludeTag(String, bool),
Test, GotoTest,
ResetTags, ResetTags,
SetReverse(bool), SetReverse(bool),
Search(String), Search(String),
@@ -60,11 +63,11 @@ pub enum DictionaryMessage {
CreateGroup, CreateGroup,
EditGroup(String), EditGroup(String),
SaveGroup, SaveGroup,
SelectGroup(usize), SelectGroup(EntityHandle<WordGroup>),
DeleteGroup, DeleteGroup,
ChangeDirection, ChangeDirection,
TrySave(usize), TrySave(EntityHandle<WordData>),
ToImport, GotoImport,
} }
impl NavigatedPage<DictionaryMessage> for DictionaryState { impl NavigatedPage<DictionaryMessage> for DictionaryState {
@@ -72,82 +75,71 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
if let Back = message { if let Back = message {
return Some(Page::PreviousPage); return Some(Page::PreviousPage);
} }
if let Test = message if let GotoTest = message
&& self.include_map.iter().any(|x| *x) && !self.included.is_empty()
{ {
let mut words = vec![]; let mut words = vec![];
let dict = &self.state.lock().dictionary; let dict = &self.state.dictionary.read();
words = self let mut query = dict.query(Arc::new(|_| true));
.include_map let handles = query.handles();
.iter() words = handles.filter(|h| self.included.contains(h)).collect();
.zip(0..self.include_map.len())
.filter(|(flag, _)| **flag)
.map(|(_, index)| dict[index].clone())
.collect();
return Some(Page::DictionaryQuiz(DictionaryQuizState::new( return Some(Page::DictionaryQuiz(DictionaryQuizState::new(
words, words,
self.reverse, self.reverse,
self.no_typing, self.no_typing,
self.state.clone(),
))); )));
} }
if let WordAction(index) = message { if let WordAction(handle) = message
let word: WordData; && handle.real()
{ {
let state = self.state.lock(); return Some(Word(WordState::new(*handle, self.state.clone())));
let dict = &state.dictionary;
word = dict[*index].clone();
} }
if word.id.is_valid() { if let GotoImport = message {
return Some(Word(WordState::new(word, *index, self.state.clone())));
}
}
if let ToImport = message {
return Some(Import(ImportState::new(self.state.clone()))); return Some(Import(ImportState::new(self.state.clone())));
} }
None None
} }
fn navigated(&mut self) { fn navigated(&mut self) {
let len = self.state.lock().dictionary.len(); self.included = Default::default();
self.include_map = vec![false; len];
self.tag_map = Default::default(); self.tag_map = Default::default();
let groups = self.state.word_groups.read();
self.groups_query.load(&groups);
drop(groups);
self.update_tags(); self.update_tags();
} }
fn update(&mut self, message: DictionaryMessage) -> Task<RootMessage> { fn update(&mut self, message: DictionaryMessage) -> Task<RootMessage> {
match message { match message {
NewWord => { NewWord => {
let mut state = self.state.lock(); let mut dictionary = self.state.dictionary.write();
let mut word = WordData::new(); let mut word = WordData::new();
word.group_id = state.word_groups[self.selected_group_index].id; word.group_id = self.selected_group.id;
dictionary.create(word);
let dict = &mut state.dictionary; self.displayed_words_query.load(&dictionary);
dict.push(word);
self.include_map.push(false);
} }
SetKey(i, v) => { SetKey(handle, v) => {
{ let mut dictionary = self.state.dictionary.write();
let dict = &mut self.state.lock().dictionary; dictionary.get_mut_static(handle).unwrap().key = v;
dict[i].key = v; drop(dictionary);
return self.launch_auto_save_offset(handle);
} }
return self.launch_auto_save_offset(i); SetValue(handle, v) => {
} let mut dictionary = self.state.dictionary.write();
SetValue(i, v) => { dictionary.get_mut_static(handle).unwrap().value = v;
{ drop(dictionary);
let dict = &mut self.state.lock().dictionary; return self.launch_auto_save_offset(handle);
dict[i].value = v
}
return self.launch_auto_save_offset(i);
} }
SetTags(i, mut v) => { SetTags(i, mut v) => {
{ {
let dict = &mut self.state.lock().dictionary; let mut dictionary = self.state.dictionary.write();
let current_tags_value = dict[i].tags.clone(); let word = dictionary.get_mut_static(i).unwrap();
let current_tags_value = word.tags.clone();
if v.ends_with(", ") && v.len() < current_tags_value.len() { if v.ends_with(", ") && v.len() < current_tags_value.len() {
v = v[..v.len() - 2].to_string() v = v[..v.len() - 2].to_string()
@@ -157,7 +149,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
v.remove(index); v.remove(index);
} }
dict[i].tags = v; word.tags = v;
} }
self.update_tags(); self.update_tags();
@@ -165,94 +157,89 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
} }
WordAction(i) => { WordAction(i) => {
let state = &mut self.state.lock(); let mut dictionary = self.state.dictionary.write();
let dict = &mut state.dictionary;
let word = dict.remove(i); if let Err(err) = dictionary.delete(i) {
self.include_map.remove(i);
self.auto_save_queue.remove(&i);
if let Err(err) = delete_word(&word, &state.connection) {
return push_error_task!(err); return push_error_task!(err);
} }
self.displayed_words_query.load(&dictionary);
}
Include(i, b) => {
if b {
self.included.insert(i);
} else {
self.included.remove(&i);
}
} }
Include(i, b) => self.include_map[i] = b,
IncludeTag(t, v) => { IncludeTag(t, v) => {
let index;
{
let state = self.state.lock();
index = state.word_groups[self.selected_group_index].id;
}
self.tag_map.insert(t, v); self.tag_map.insert(t, v);
self.update_words_include(index) self.update_words_include(self.selected_group.id)
} }
ResetTags => { ResetTags => {
self.tag_map.iter_mut().for_each(|(_, v)| *v = false); self.tag_map.iter_mut().for_each(|(_, v)| *v = false);
self.include_map.iter_mut().for_each(|x| *x = false) self.included.clear();
} }
SetReverse(v) => self.reverse = v, SetReverse(v) => self.reverse = v,
Search(s) => { Search(s) => {
self.search = s; self.search = s;
} }
SetTyping(b) => self.no_typing = b, SetTyping(b) => self.no_typing = b,
SubmitWord(i) => return self.save_word(i),
Back => {} Back => {}
Test => {} GotoTest => {}
CreateGroup => { CreateGroup => {
let state = &mut self.state.lock(); let mut groups = self.state.word_groups.write();
groups.create(WordGroup {
state.word_groups.push(WordGroup {
id: 0.into(), id: 0.into(),
name: format!("Группа слов {}", random_range(100..1000)), name: format!("Группа слов {}", random_range(100..1000)),
}); });
self.groups_query.load(&groups);
} }
EditGroup(new) => { EditGroup(new) => {
let state = &mut self.state.lock(); let mut groups = self.state.word_groups.write();
let group = state.word_groups.get_mut(self.selected_group_index);
let group = groups.get_mut(&mut self.selected_group);
if let Some(group) = group { if let Some(group) = group {
group.name = new.clone(); group.name = new;
} }
} }
SaveGroup => { SaveGroup => {
let state = &mut self.state.lock(); {
let connection = &state.connection; let mut groups = self.state.word_groups.write();
let mut group = state.word_groups[self.selected_group_index].clone();
if let Err(err) = update_group(&mut group, connection) { if let Err(err) = groups.save(&mut self.selected_group) {
return push_error_task!(err); return push_error_task!(err);
} }
state.word_groups[self.selected_group_index] = group; self.groups_query.load(&groups);
}
self.setup_words_query();
} }
SelectGroup(i) => { SelectGroup(i) => {
self.selected_group_index = i; self.selected_group = i;
let index; self.setup_words_query();
{ self.update_words_include(i.id)
let state = self.state.lock();
index = state.word_groups[i].id;
}
self.update_words_include(index)
} }
DeleteGroup => { DeleteGroup => {
if self.selected_group_index == 0 { if self.selected_group.id.0 == 1 {
return Task::none(); return Task::none();
} }
{ {
let state = &mut self.state.lock(); let delete_group = self.selected_group;
self.selected_group = EntityHandle::from_id(1.into());
self.setup_words_query();
let mut groups = self.state.word_groups.write();
if let Some(group) = state.word_groups.get(self.selected_group_index) { if let Err(err) = groups.delete(delete_group) {
let remove_group_id = group.id;
let connection = &state.connection;
if let Err(err) = delete_group(group, connection) {
return push_error_task!(err); return push_error_task!(err);
} }
state.word_groups.remove(self.selected_group_index); if let Err(err) = self.state.dictionary.write().reload() {
state return push_error_task!(err);
.dictionary
.retain(|word| word.group_id != remove_group_id);
self.selected_group_index = 0;
} }
self.groups_query.load(&groups);
} }
self.update_tags() self.update_tags()
@@ -260,18 +247,25 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
ChangeDirection => { ChangeDirection => {
self.reverse_list = !self.reverse_list; self.reverse_list = !self.reverse_list;
} }
TrySave(word_index) => { TrySave(mut handle) => {
let now = Utc::now(); let now = Utc::now();
if !self.auto_save_queue.contains_key(&word_index) { if !self.auto_save_queue.contains_key(&handle) {
return Task::none(); return Task::none();
} }
if self.auto_save_queue[&word_index] <= now { if self.auto_save_queue[&handle] <= now {
self.auto_save_queue.remove(&word_index); self.auto_save_queue.remove(&handle);
return self.save_word(word_index); let mut dictionary = self.state.dictionary.write();
let reload_query = handle.real() == false;
let _ = dictionary.save(&mut handle);
if reload_query {
self.displayed_words_query.load(&dictionary);
}
return Task::none();
} }
} }
ToImport => {} GotoImport => {}
} }
Task::none() Task::none()
@@ -286,7 +280,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
row![ row![
self.add_word_button(), self.add_word_button(),
horizontal().width(Fill), horizontal().width(Fill),
button("Импорт").style(text).on_press(ToImport), button("Импорт").style(text).on_press(GotoImport),
] ]
] ]
.spacing(5), .spacing(5),
@@ -300,43 +294,51 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
} }
impl DictionaryState { impl DictionaryState {
pub fn new(state: Arc<Mutex<AppState>>) -> Self { pub fn new(state: AppState) -> Self {
let len = state.lock().dictionary.len(); let words_query = state.dictionary.read().empty_query_par();
let groups_query = state.word_groups.read().query(Arc::new(|_| true));
let mut result = DictionaryState { let mut result = DictionaryState {
include_map: vec![false; len], included: Default::default(),
selected_group_index: 0, selected_group: EntityHandle::new(Id(1), 0, true),
state, state,
tag_map: HashMap::new(), tag_map: HashMap::new(),
words_tags: vec![],
reverse: false, reverse: false,
search: "".to_string(), search: "".to_string(),
no_typing: true, no_typing: true,
reverse_list: true, reverse_list: true,
auto_save_queue: HashMap::new(), auto_save_queue: HashMap::new(),
displayed_words_query: words_query,
groups_query,
}; };
result.init();
result.update_tags(); result.update_tags();
result result
} }
fn save_word(&mut self, i: usize) -> Task<RootMessage> { fn init(&mut self) {
let state = &mut self.state.lock(); self.displayed_words_query = self
let connection = &state.connection; .state
let word = &mut state.dictionary[i].clone(); .dictionary
.read()
if let Err(err) = update_word(word, connection) { .query(Arc::new(|word| word.group_id == Id(1)));
return push_error_task!(err);
} }
state.dictionary[i] = word.clone(); fn setup_words_query(&mut self) {
Task::none() let id = self.selected_group.id;
self.displayed_words_query = self
.state
.dictionary
.read()
.query(Arc::new(move |word| word.group_id == id));
} }
fn launch_auto_save_offset(&mut self, index: usize) -> Task<RootMessage> { fn launch_auto_save_offset(&mut self, handle: EntityHandle<WordData>) -> Task<RootMessage> {
let save_time = Utc::now().add(TimeDelta::milliseconds(900)); let save_time = Utc::now().add(TimeDelta::milliseconds(900));
self.auto_save_queue.insert(index, save_time); self.auto_save_queue.insert(handle, save_time);
let message = RootMessage::Dictionary(TrySave(index)); let message = RootMessage::Dictionary(TrySave(handle));
Task::perform( Task::perform(
async { tokio::time::sleep(Duration::from_secs(1)).await }, async { tokio::time::sleep(Duration::from_secs(1)).await },
|_| message, |_| message,
@@ -347,22 +349,16 @@ impl DictionaryState {
let time = Instant::now(); let time = Instant::now();
let mut col = Column::new().width(Fill); let mut col = Column::new().width(Fill);
let state = self.state.lock(); let query = &self.displayed_words_query;
let group_id = state.word_groups[self.selected_group_index].id; let dictionary = self.state.dictionary.read();
let dict = &state.dictionary; for access_index in 0..query.len() {
let mut index = 0;
for access_index in 0..dict.len() {
let mut i = access_index; let mut i = access_index;
if self.reverse_list { if self.reverse_list {
i = dict.len() - access_index - 1; i = query.len() - access_index - 1;
}
let word = &dict[i];
if word.group_id != group_id {
continue;
} }
let word_handle = &query[i];
let word = dictionary.get_ref_static(*word_handle).unwrap();
if !self.search.is_empty() if !self.search.is_empty()
&& !word.key.contains(&self.search) && !word.key.contains(&self.search)
@@ -373,32 +369,37 @@ impl DictionaryState {
} }
let word_line_data = WordLineState { let word_line_data = WordLineState {
is_included: self.include_map[i], is_included: self.included.contains(word_handle),
key: word.key.clone(), key: word.key.clone().into(),
value: word.value.clone(), value: word.value.clone().into(),
tags: word.tags.clone(), tags: word.tags.clone().into(),
id: word.id, handle: word_handle.clone(),
index: i,
}; };
index += 1; let lazy_line = lazy(word_line_data, Self::word_line);
let lazy_line = lazy(word_line_data, move |data| { col = col.push(lazy_line);
let index = data.index; }
info!("Words rendering time: {:?}", time.elapsed());
scrollable(col).height(Fill).into()
}
fn word_line<'a>(data: &WordLineState) -> Row<'a, DictionaryMessage> {
let mut line = Row::new().width(Fill).align_y(Center); let mut line = Row::new().width(Fill).align_y(Center);
let handle = data.handle.clone();
line = line.push( line = line.push(
checkbox(data.is_included) checkbox(data.is_included)
.label("") .label("")
.spacing(15) .spacing(15)
.on_toggle(move |b| Include(index, b)), .on_toggle(move |b| Include(handle, b)),
); );
line = line.push( line = line.push(
text_input("Слово", &data.key) text_input("Слово", &data.key)
.size(ACCENT_FONT_SIZE) .size(ACCENT_FONT_SIZE)
.width(Fill) .width(Fill)
.on_input(move |string| SetKey(index, string)) .on_input(move |string| SetKey(handle, string))
.on_submit(SubmitWord(index))
.style(|x, status| { .style(|x, status| {
let mut default_style = default(x, status); let mut default_style = default(x, status);
default_style.border.radius = 0.0.into(); default_style.border.radius = 0.0.into();
@@ -409,8 +410,7 @@ impl DictionaryState {
text_input("Перевод", &data.value) text_input("Перевод", &data.value)
.size(ACCENT_FONT_SIZE) .size(ACCENT_FONT_SIZE)
.width(Fill) .width(Fill)
.on_input(move |string| SetValue(index, string)) .on_input(move |string| SetValue(handle, string))
.on_submit(SubmitWord(index))
.style(|x, status| { .style(|x, status| {
let mut default_style = default(x, status); let mut default_style = default(x, status);
default_style.border.radius = 0.0.into(); default_style.border.radius = 0.0.into();
@@ -419,11 +419,10 @@ impl DictionaryState {
); );
line = line.push( line = line.push(
text_input("Теги", &data.tags) text_input("Теги", data.tags.as_ref())
.size(ACCENT_FONT_SIZE) .size(ACCENT_FONT_SIZE)
.width(Fill) .width(Fill)
.on_input(move |string| SetTags(index, string)) .on_input(move |string| SetTags(handle, string))
.on_submit(SubmitWord(index))
.style(|x, status| { .style(|x, status| {
let mut default_style = default(x, status); let mut default_style = default(x, status);
default_style.border.radius = 0.0.into(); default_style.border.radius = 0.0.into();
@@ -432,44 +431,38 @@ impl DictionaryState {
); );
let line_button = || { let line_button = || {
let action = WordAction(index); if !data.handle.real() {
return button("-")
if !data.id.is_valid() { .on_press(WordAction(data.handle))
return button("-").on_press(action).style(|_x, _status| Style { .style(
|_x: &iced::Theme, _status: iced::widget::button::Status| Style {
background: None, background: None,
text_color: Color::BLACK, text_color: Color::BLACK,
border: Border::default(), border: Border::default(),
shadow: Shadow::default(), shadow: Shadow::default(),
snap: false, snap: false,
}); },
)
.width(15);
} }
button("").on_press(WordAction(index)).width(15) button("")
.on_press(WordAction(data.handle))
.style(button::primary)
.width(15)
}; };
line = line.push(line_button()).push(space().width(10)); line.push(line_button()).push(space().width(10))
line
});
col = col.push(lazy_line);
}
info!("Drawing {index} lines");
info!("Words rendering time: {:?}", time.elapsed());
scrollable(col).height(Fill).into()
} }
fn filters(&self) -> iced::Element<'_, DictionaryMessage> { fn filters(&self) -> iced::Element<'_, DictionaryMessage> {
let dict = &self.state.lock().dictionary; let dict = &self.state.dictionary.read();
iced::widget::column![ iced::widget::column![
text_input("Поиск", &self.search) text_input("Поиск", &self.search)
.on_input(Search) .on_input(Search)
.width(Fill), .width(Fill),
text!("Всего слов: {}", dict.len()), text!("Всего слов: {}", dict.len()),
text!( text!("Выбрано слов: {}", self.included.iter().count()),
"Выбрано слов: {}",
self.include_map.iter().filter(|i| **i).count()
),
self.tags_selector(), self.tags_selector(),
toggler(self.no_typing) toggler(self.no_typing)
.label("Без набора") .label("Без набора")
@@ -479,7 +472,7 @@ impl DictionaryState {
.on_toggle(SetReverse), .on_toggle(SetReverse),
button(text!("Тест").center().width(Fill)) button(text!("Тест").center().width(Fill))
.style(cta_button) .style(cta_button)
.on_press(Test) .on_press(GotoTest)
.width(Fill), .width(Fill),
] ]
.width(250) .width(250)
@@ -514,15 +507,19 @@ impl DictionaryState {
} }
fn update_tags(&mut self) { fn update_tags(&mut self) {
self.words_tags.clear();
let mut tags_list: HashSet<String> = HashSet::new(); let mut tags_list: HashSet<String> = HashSet::new();
let dict = &self.state.lock().dictionary; let dict = &self.state.dictionary.write();
let mut words_query = dict.full_query();
dict.iter().for_each(|element| { let handles = words_query.handles().collect::<Vec<_>>();
split_with_coma(element.tags.as_str()) words_query.iter(dict).zip(handles).for_each(|(element, handle)| {
.iter() let tags = split_with_coma(element.tags.as_str());
tags.iter()
.for_each(|tag| { .for_each(|tag| {
tags_list.insert(tag.clone()); tags_list.insert(tag.clone());
}); });
self.words_tags.push((handle, tags))
}); });
let current_tags = self let current_tags = self
@@ -552,22 +549,27 @@ impl DictionaryState {
.collect::<Vec<String>>(); .collect::<Vec<String>>();
if include_tags.is_empty() { if include_tags.is_empty() {
self.include_map.iter_mut().for_each(|x| *x = false); self.included.clear();
return; return;
} }
let dict = &self.state.lock().dictionary; let dict = &self.state.dictionary.read();
let time = Instant::now(); let time = Instant::now();
self.include_map = dict self.included.clear();
.iter() for (handle, tags) in &mut self.words_tags {
.map(|word| (split_with_coma(word.tags.as_str()), word.group_id)) if let Some(word) = dict.get_ref(handle) {
.map(|(tags, word_group_id)| { if word.group_id != group_id {
!tags.is_empty() continue;
&& tags.iter().all(|t| include_tags.contains(t)) }
&& word_group_id == group_id
}) if !tags.is_empty() && tags.iter().all(|t| include_tags.contains(t)) {
.collect(); self.included.insert(*handle);
}
} else {
continue;
}
}
info!("Time {}", time.elapsed().as_micros()); info!("Time {}", time.elapsed().as_micros());
} }
@@ -576,18 +578,19 @@ impl DictionaryState {
let mut row = Row::new(); let mut row = Row::new();
row = row.push(button("+").style(text).on_press(CreateGroup)); row = row.push(button("+").style(text).on_press(CreateGroup));
let state = &self.state.lock(); let groups = &self.state.word_groups.read();
let groups = &state.word_groups;
for (index, group) in groups.iter().enumerate() { for mut group in self.groups_query.handles() {
let group_name = groups.get(&mut group).unwrap().name.clone();
row = row.push( row = row.push(
button(text!("{}", group.name.clone())) button(text!("{}", group_name))
.style(text) .style(text)
.on_press(SelectGroup(index)), .on_press(SelectGroup(group)),
); );
} }
let group = state.word_groups[self.selected_group_index].clone(); let groups = self.state.word_groups.read();
let group = groups.get_static(self.selected_group.clone()).unwrap();
iced::widget::column![ iced::widget::column![
scrollable(row).width(Fill).horizontal(), scrollable(row).width(Fill).horizontal(),
@@ -606,7 +609,7 @@ impl DictionaryState {
} }
fn group_delete_button(&self) -> iced::Element<'_, DictionaryMessage> { fn group_delete_button(&self) -> iced::Element<'_, DictionaryMessage> {
if self.selected_group_index != 0 { if self.selected_group.real() && self.selected_group.id != 1.into() {
button("Удалить").style(danger).on_press(DeleteGroup).into() button("Удалить").style(danger).on_press(DeleteGroup).into()
} else { } else {
space().into() space().into()
@@ -614,11 +617,9 @@ impl DictionaryState {
} }
fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> {
let state = self.state.lock();
let group_id = state.word_groups[self.selected_group_index].id;
let button = button("Добавить слово").style(jl_button); let button = button("Добавить слово").style(jl_button);
if group_id.is_valid() { if self.selected_group.real() {
return button.on_press(NewWord).into(); return button.on_press(NewWord).into();
} }
@@ -633,12 +634,11 @@ pub fn split_with_coma(ts: &str) -> Vec<String> {
.collect::<Vec<String>>() .collect::<Vec<String>>()
} }
#[derive(Hash, PartialEq, Eq)] #[derive(Clone, Hash, PartialEq, Eq)]
struct WordLineState { struct WordLineState {
is_included: bool, is_included: bool,
key: String, key: Box<str>,
value: String, value: Box<str>,
tags: String, tags: Box<str>,
id: Id, handle: EntityHandle<WordData>,
index: usize,
} }
+19 -12
View File
@@ -1,4 +1,4 @@
use crate::data::entities::WordData; use crate::data::entities::{EntityHandle, WordData};
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::RootMessage; use crate::navigation::RootMessage;
use crate::navigation::*; use crate::navigation::*;
@@ -12,11 +12,13 @@ use iced::widget::container::Style;
use iced::widget::{Row, button, container, row, space, text, text_input}; use iced::widget::{Row, button, container, row, space, text, text_input};
use iced::{Border, Element, Fill, Task, Theme, alignment}; use iced::{Border, Element, Fill, Task, Theme, alignment};
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use crate::app::AppState;
use crate::data::repositories::Repository;
#[derive(Debug, Clone)]
pub struct DictionaryQuizState { pub struct DictionaryQuizState {
words: Vec<WordData>, state: AppState,
current_set: Vec<WordData>, words: Vec<EntityHandle<WordData>>,
current_set: Vec<EntityHandle<WordData>>,
answer: String, answer: String,
view: String, view: String,
correct: String, correct: String,
@@ -104,8 +106,9 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
} }
impl DictionaryQuizState { impl DictionaryQuizState {
pub fn new(words: Vec<WordData>, reverse: bool, no_typing: bool) -> DictionaryQuizState { pub fn new(words: Vec<EntityHandle<WordData>>, reverse: bool, no_typing: bool, state: AppState) -> DictionaryQuizState {
DictionaryQuizState { let state = DictionaryQuizState {
state,
words, words,
current_set: Vec::new(), current_set: Vec::new(),
answer: "".to_string(), answer: "".to_string(),
@@ -116,7 +119,9 @@ impl DictionaryQuizState {
reverse, reverse,
laps: 0, laps: 0,
no_typing, no_typing,
} };
state
} }
fn submit(&mut self) { fn submit(&mut self) {
@@ -172,7 +177,7 @@ impl DictionaryQuizState {
self.is_help = false; self.is_help = false;
self.answer = String::new(); self.answer = String::new();
let next: WordData = match self.current_set.pop() { let next: EntityHandle<WordData> = match self.current_set.pop() {
Some(word) => word, Some(word) => word,
None => { None => {
self.update_set(); self.update_set();
@@ -180,12 +185,14 @@ impl DictionaryQuizState {
} }
}; };
let repo = self.state.dictionary.read();
let next = repo.get_static(next).unwrap();
if self.reverse { if self.reverse {
self.view = next.value.clone(); self.view = next.value;
self.correct = next.key.clone(); self.correct = next.key;
} else { } else {
self.view = next.key.clone(); self.view = next.key;
self.correct = next.value.clone(); self.correct = next.value;
} }
} }
+30 -28
View File
@@ -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 {
@@ -48,51 +46,55 @@ impl NavigatedPage<HistoryMessage> for HistoryState {
} }
impl HistoryState { impl HistoryState {
pub fn new(id: crate::data::Id, state: Arc<Mutex<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.deck_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)
+16 -18
View File
@@ -1,7 +1,8 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::{WordData, WordGroup}; use crate::data::entities::EntityData;
use crate::data::entities::WordData;
use crate::data::entities::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::fs::*; use crate::fs::*;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::NotificationData::OpenFileError; use crate::notification::NotificationData::OpenFileError;
@@ -29,12 +30,13 @@ 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"];
#[derive(Clone)] #[derive(Clone)]
pub struct ImportState { pub struct ImportState {
state: Arc<Mutex<AppState>>, state: AppState,
path: Option<PathBuf>, path: Option<PathBuf>,
import_data: Option<ImportData>, import_data: Option<ImportData>,
selected_index: usize, selected_index: usize,
@@ -318,7 +320,7 @@ impl ImportState {
} }
impl ImportState { impl ImportState {
pub fn new(state: Arc<Mutex<AppState>>) -> ImportState { pub fn new(state: AppState) -> ImportState {
ImportState { ImportState {
state, state,
path: None, path: None,
@@ -433,22 +435,22 @@ impl ImportState {
async move { async move {
spawn_blocking(move || { 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 { let group_handle;
{
let group_entity = WordGroup {
id: 0.into(), id: 0.into(),
name: group.name.clone(), name: group.name.clone(),
}; };
{ let mut groups_repo = state.word_groups.write();
let state = state.lock(); group_handle = groups_repo.create_now(group_entity)?;
add_group(&mut group_entity, &state.connection)?;
} }
let group_id = group_entity.id; let group_id = group_handle.id;
for import in import_list { for import in import_list {
let mut word = WordData::new(); let mut word = WordData::new();
@@ -480,20 +482,16 @@ impl ImportState {
words_list.push(word); words_list.push(word);
} }
let mut state = state.lock(); let mut words_repo = state.dictionary.write();
let connection = &mut state.connection;
let mut index = 0; let mut index = 0;
let total_len = words_list.len() as f32 / 1024.0; let total_len = words_list.len() as f32 / 1024.0;
for word in &mut words_list.chunks_mut(1024) { for words in &mut words_list.chunks_mut(1024) {
add_words(word, connection)?; words_repo.create_now_many(words)?;
index += 1; index += 1;
let mut progress = progress.lock(); let mut progress = progress.lock();
*progress = (index as f32 / total_len).min(1.0); *progress = (index as f32 / total_len).min(1.0);
} }
state.dictionary.append(&mut words_list);
state.word_groups.push(group_entity);
Ok(()) Ok(())
}) })
.await .await
+15 -32
View File
@@ -2,28 +2,28 @@ use crate::app::AppState;
use crate::core::deck::DeckData; use crate::core::deck::DeckData;
use crate::core::srs::WordOpenMode; use crate::core::srs::WordOpenMode;
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{CardState, DeckSettings, WordData}; use crate::data::entities::{CardState, WordData};
use crate::data::repositories::Repository;
use crate::data_provider::voice::get_voice; use crate::data_provider::voice::get_voice;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage}; use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
use crate::pages::repetitions::DeckRunData;
use crate::styling::*; use crate::styling::*;
use crate::{push_error, push_error_task}; use crate::{push_error, push_error_task};
use chrono::Local;
use iced::alignment::Horizontal::Center; use iced::alignment::Horizontal::Center;
use iced::keyboard::key::Physical::Code; use iced::keyboard::key::Physical::Code;
use iced::widget::text::Style; use iced::widget::text::Style;
use iced::widget::{Column, button, column, container, row, rule, space, text, tooltip}; use iced::widget::{Column, button, column, container, row, rule, space, text, tooltip};
use iced::{Element, Fill, Task, Theme, alignment, keyboard}; use iced::{Element, Fill, Task, Theme, alignment, keyboard};
use parking_lot::Mutex;
use rodio::MixerDeviceSink; use rodio::MixerDeviceSink;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
pub struct RepetitionState { pub struct RepetitionState {
state: Arc<Mutex<AppState>>, state: AppState,
pub settings: DeckSettings, pub settings: DeckRunData,
pub set: DeckData, pub set: DeckData,
current_word: WordData, current_word: WordData,
current_statistic: CardState, current_statistic: CardState,
@@ -103,12 +103,13 @@ impl NavigatedPage<RepetitionMessage> for RepetitionState {
impl RepetitionState { impl RepetitionState {
pub(crate) fn new( pub(crate) fn new(
set: DeckSettings, set: DeckRunData,
state: Arc<Mutex<AppState>>, state: AppState,
) -> Result<RepetitionState, NotificationData> { ) -> Result<RepetitionState, NotificationData> {
let mut card_set = DeckData::new(&set, state.clone())?; let mut card_set = DeckData::new(&set, set.open_mode, state.clone());
let (word, stat) = card_set.next(); let (word, stat) = card_set.next();
let word = state.dictionary.read().get_static(word).unwrap();
let stat = state.card_states.read().get_static(stat).unwrap();
let sink_handle = if set.forward.contains("speech") || set.backward.contains("speech") { let sink_handle = if set.forward.contains("speech") || set.backward.contains("speech") {
rodio::DeviceSinkBuilder::open_default_sink().ok() rodio::DeviceSinkBuilder::open_default_sink().ok()
} else { } else {
@@ -150,9 +151,11 @@ impl RepetitionState {
self.open = false; self.open = false;
self.opened.insert(self.current_word.id); self.opened.insert(self.current_word.id);
let next = self.set.next(); let (word, state) = self.set.next();
self.current_word = next.0; let word = self.state.dictionary.read().get_static(word).unwrap();
self.current_statistic = next.1; let stat = self.state.card_states.read().get_static(state).unwrap();
self.current_word = word;
self.current_statistic = stat;
if self.settings.require_speech() && self.sink.is_some() { if self.settings.require_speech() && self.sink.is_some() {
let sink = self.sink.clone(); let sink = self.sink.clone();
@@ -162,29 +165,9 @@ impl RepetitionState {
Err(err) => push_error!(err), Err(err) => push_error!(err),
}); });
} }
self.increment_counter();
Task::none() Task::none()
} }
fn increment_counter(&mut self) {
let mut state = self.state.lock();
let id = self.settings.id;
let activity = state.activity.get_mut(&id);
let now = Local::now().date_naive();
if let Some(activity) = activity {
let (last_day, counter) = activity.last_mut().unwrap();
if *last_day == now {
*counter += 1;
} else {
activity.push((now, 1));
}
} else {
state.activity.insert(id, vec![(now, 1)]);
}
}
fn draw_forward(&self) -> Element<'_, RepetitionMessage> { fn draw_forward(&self) -> Element<'_, RepetitionMessage> {
self.draw_card_view(self.settings.forward.as_str()) self.draw_card_view(self.settings.forward.as_str())
} }
+88 -48
View File
@@ -1,7 +1,7 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::DeckSettings; use crate::core::deck::DeckData;
use crate::data_provider::card_states::{delete_stats, load_stats_of_deck}; use crate::data::entities::{DeckSettings, EntityHandle};
use crate::data_provider::decks::{delete_set, update_deck}; use crate::data::repositories::{BatchedRepository, Repository};
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::MaybeError; use crate::notification::MaybeError;
@@ -9,40 +9,41 @@ use crate::pages::repetition_settings::RepetitionSettingsMessage::*;
use crate::styling::*; use crate::styling::*;
use crate::{push_error, push_error_task}; use crate::{push_error, push_error_task};
use iced::widget::button::danger; use iced::widget::button::danger;
use iced::widget::space::horizontal;
use iced::widget::text_input::Catalog; use iced::widget::text_input::Catalog;
use iced::widget::{button, column, row, scrollable, space, text, text_input}; use iced::widget::{button, column, row, scrollable, space, text, text_input};
use iced::{Element, Task}; use iced::{Element, Task};
use iced_core::Length::Fill; use iced_core::Length::Fill;
use iced_core::{Padding, Theme, color}; use iced_core::{Padding, Theme, color};
use parking_lot::Mutex;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
pub struct RepetitionSettingsState { pub struct RepetitionSettingsState {
set: DeckSettings, settings_handle: EntityHandle<DeckSettings>,
index: usize, settings_data: DeckSettings,
state: Arc<Mutex<AppState>>, state: AppState,
real_delete: bool, real_delete: bool,
real_delete_states: bool,
correct_filter: bool, correct_filter: bool,
filter_changed: bool, filter_changed: bool,
filtered_words: Option<u32>,
} }
impl RepetitionSettingsState { impl RepetitionSettingsState {
pub(crate) fn new(index: usize, state: Arc<Mutex<AppState>>) -> RepetitionSettingsState { pub(crate) fn new(
let set; settings: EntityHandle<DeckSettings>,
{ state: AppState,
let local_state = state.lock(); ) -> RepetitionSettingsState {
set = local_state.decks[index].clone(); let data = state.decks.read().get_static(settings).unwrap();
}
let filter_state = set.check_filter();
RepetitionSettingsState { RepetitionSettingsState {
index, settings_data: data,
set, settings_handle: settings,
state, state,
real_delete: false, real_delete: false,
correct_filter: filter_state, real_delete_states: false,
correct_filter: true,
filter_changed: false, filter_changed: false,
filtered_words: None,
} }
} }
} }
@@ -61,35 +62,36 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
match message { match message {
Back => {} Back => {}
Save => { Save => {
let mut state = self.state.lock(); let mut decks = self.state.decks.write();
if let Err(e) = update_deck(&mut self.set, &state.connection) { decks.set(&mut self.settings_handle, self.settings_data.clone());
if let Err(e) = decks.save(&mut self.settings_handle) {
return push_error_task!(e); return push_error_task!(e);
} }
drop(decks);
if self.filter_changed if self.filter_changed
&& let Err(e) = update_deck_stats(&self.set, &state) && let Err(e) = update_deck_stats(&self.settings_data, &self.state)
{ {
return push_error_task!(e); return push_error_task!(e);
} }
state.decks[self.index] = self.set.clone();
} }
SetName(new) => { SetName(new) => {
self.set.name = new; self.settings_data.name = new;
} }
SetForward(new) => { SetForward(new) => {
self.set.forward = new; self.settings_data.forward = new;
} }
SetBackward(backward) => { SetBackward(backward) => {
self.set.backward = backward; self.settings_data.backward = backward;
} }
SetFilter(new) => { SetFilter(new) => {
self.set.filter = new; self.settings_data.filter = new;
self.correct_filter = self.set.check_filter(); self.correct_filter = self.settings_data.check_filter();
self.filter_changed = true; self.filter_changed = true;
} }
TryFilter => { TryFilter => {
let state = self.state.lock(); let list = DeckData::list(&self.settings_data, &self.state);
let count = self.set.get_word_list(&state).len(); self.filtered_words = Some(list.len() as u32);
self.set.count = Some(count);
} }
DeleteSet => { DeleteSet => {
if !self.real_delete { if !self.real_delete {
@@ -99,15 +101,33 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
RootMessage::RepetitionSettings(RevertDeleteSet) RootMessage::RepetitionSettings(RevertDeleteSet)
}); });
} }
let mut state = self.state.lock(); let mut decks = self.state.decks.write();
if let Err(e) = delete_set(&self.set, &state.connection) { if let Err(e) = decks.delete(self.settings_handle) {
return push_error_task!(e); return push_error_task!(e);
} }
state.decks.remove(self.index);
return Task::done(RootMessage::RepetitionSettings(Back)); return Task::done(RootMessage::RepetitionSettings(Back));
} }
RevertDeleteSet => self.real_delete = false, RevertDeleteSet => {
self.real_delete = false;
self.real_delete_states = true;
}
DeleteStates => {
if !self.real_delete_states {
self.real_delete_states = true;
return Task::future(async {
tokio::time::sleep(Duration::from_millis(3000)).await;
RootMessage::RepetitionSettings(RevertDeleteSet)
});
}
let mut states = self.state.card_states.write();
let deck_id = self.settings_handle.id;
let mut query = states.query(Arc::new(move |state| {state.deck_id == deck_id})).handles().collect::<Vec<_>>();
if let Err(e) = states.delete_many(query.as_mut_slice()) {
return push_error_task!(e);
}
self.real_delete_states = false;
}
} }
Task::none() Task::none()
} }
@@ -119,22 +139,22 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
column![ column![
column![ column![
text!("Название набора"), text!("Название набора"),
text_input("", &self.set.name).on_input(SetName), text_input("", &self.settings_data.name).on_input(SetName),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
column![ column![
text!("Передняя сторона"), text!("Передняя сторона"),
text_input("", &self.set.forward).on_input(SetForward), text_input("", &self.settings_data.forward).on_input(SetForward),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
column![ column![
text!("Задняя сторона"), text!("Задняя сторона"),
text_input("", &self.set.backward).on_input(SetBackward), text_input("", &self.settings_data.backward).on_input(SetBackward),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
column![ column![
text!("Фильтр"), text!("Фильтр"),
text_input("", &self.set.filter) text_input("", &self.settings_data.filter)
.style(|x, status| { .style(|x, status| {
let mut primary = Theme::default()(x, status); let mut primary = Theme::default()(x, status);
if !self.correct_filter { if !self.correct_filter {
@@ -146,7 +166,7 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
button("Проверить фильтр") button("Проверить фильтр")
.style(jl_button) .style(jl_button)
.on_press(TryFilter), .on_press(TryFilter),
self.count_view(&self.set), self.count_view(),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
] ]
@@ -161,6 +181,14 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
.height(Fill), .height(Fill),
row![ row![
button("Сохранить").style(jl_button).on_press(Save), button("Сохранить").style(jl_button).on_press(Save),
horizontal(),
button(if self.real_delete_states {
"Вы уверены?"
} else {
"Удалить прогресс?"
})
.style(danger)
.on_press(DeleteStates),
button(if self.real_delete { button(if self.real_delete {
"Вы уверены?" "Вы уверены?"
} else { } else {
@@ -168,7 +196,6 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
}) })
.style(danger) .style(danger)
.on_press(DeleteSet), .on_press(DeleteSet),
space().width(Fill),
] ]
.spacing(DEFAULT_SPACING), .spacing(DEFAULT_SPACING),
] ]
@@ -180,22 +207,34 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
} }
fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError { fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError {
let mut db_stats = load_stats_of_deck(deck, &state.connection).expect("Wrong db operation"); let states_repo = &mut state.card_states.write();
let filtered_words = deck.get_word_list(state); let deck_id = deck.id;
filtered_words.iter().for_each(|word| { let mut db_stats = states_repo
let word_id = state.dictionary[*word].id; .query_par(Arc::new(move |s| s.deck_id == deck_id))
if let Some(index) = db_stats.iter().position(|stat| stat.word_id == word_id) { .iter(&states_repo)
.collect::<Vec<_>>();
let filtered_words = DeckData::list(deck, state)
.iter()
.map(|w| w.id)
.collect::<Vec<_>>();
filtered_words.iter().for_each(|word_id| {
if let Some(index) = db_stats.iter().position(|stat_id| stat_id.id == *word_id) {
db_stats.remove(index); db_stats.remove(index);
} }
}); });
delete_stats(db_stats.as_slice(), &state.connection)?; let mut delete_operation = db_stats
.iter()
.map(|state| EntityHandle::new(state.id, state.id.0 as usize, true))
.collect::<Vec<_>>();
states_repo.delete_many(delete_operation.as_mut_slice())?;
Ok(()) Ok(())
} }
impl RepetitionSettingsState { impl RepetitionSettingsState {
fn count_view(&self, set: &DeckSettings) -> Element<'_, RepetitionSettingsMessage> { fn count_view(&self) -> Element<'_, RepetitionSettingsMessage> {
if let Some(count) = set.count { if let Some(count) = self.filtered_words {
return text!("Количество слов: {}", count).into(); return text!("Количество слов: {}", count).into();
} }
space().into() space().into()
@@ -212,5 +251,6 @@ pub enum RepetitionSettingsMessage {
SetFilter(String), SetFilter(String),
TryFilter, TryFilter,
DeleteSet, DeleteSet,
DeleteStates,
RevertDeleteSet, RevertDeleteSet,
} }
+130 -143
View File
@@ -1,12 +1,11 @@
use crate::app::AppState; use crate::app::AppState;
use crate::core::deck::DeckData;
use crate::core::srs::{AppendMode, OrderMode}; use crate::core::srs::{AppendMode, OrderMode};
use crate::data::Id; use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData};
use crate::data::entities::{CardState, DeckSettings}; use crate::data::repositories::{BatchedRepository, Repository};
use crate::data_provider::card_states::{add_stat_list, load_stats_of_deck};
use crate::data_provider::decks::update_deck;
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::{MaybeError, NotificationRequest}; use crate::notification::MaybeError;
use crate::pages::history::HistoryState; use crate::pages::history::HistoryState;
use crate::pages::repetition::RepetitionState; use crate::pages::repetition::RepetitionState;
use crate::pages::repetition_settings::RepetitionSettingsState; use crate::pages::repetition_settings::RepetitionSettingsState;
@@ -15,7 +14,7 @@ use crate::push_error;
use crate::push_error_task; use crate::push_error_task;
use crate::styling::*; use crate::styling::*;
use chrono::{Days, Local, Utc}; use chrono::{Days, Local, Utc};
use hashbrown::{HashMap, HashSet}; use hashbrown::HashSet;
pub use iced::widget::button::{Catalog, Style}; pub use iced::widget::button::{Catalog, Style};
use iced::widget::button::{Status, danger}; use iced::widget::button::{Status, danger};
use iced::widget::tooltip::Position::Top; use iced::widget::tooltip::Position::Top;
@@ -27,16 +26,15 @@ use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Tas
use iced_core::Padding; use iced_core::Padding;
use iced_core::border::Radius; use iced_core::border::Radius;
use iced_core::svg::Handle; use iced_core::svg::Handle;
use parking_lot::Mutex; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
pub struct RepetitionsState { pub struct RepetitionsState {
word_id_index_map: HashMap<Id, usize>, selected_deck_handle: Option<EntityHandle<DeckSettings>>,
selected_deck_index: Option<usize>, decks: BTreeMap<EntityHandle<DeckSettings>, DeckRunData>,
decks: Vec<DeckViewData>, state: AppState,
state: Arc<Mutex<AppState>>,
} }
impl NavigatedPage<RepetitionsMessage> for RepetitionsState { impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
@@ -49,11 +47,10 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
self.state.clone(), self.state.clone(),
))) )))
} else if let GoToRepetition = message { } else if let GoToRepetition = message {
let clone = self.state.clone(); let mut state_clone = self.state.clone();
let selected_set = self.selected_deck_mut().unwrap(); let selected_set = self.selected_deck_mut().unwrap();
if selected_set.append_mode == AppendMode::Full { if selected_set.append_mode == AppendMode::Full {
if let Err(_err) = Self::append_all_words(&mut clone.lock(), selected_set) { if let Err(_err) = Self::append_all_words(&mut state_clone, selected_set) {
return None; return None;
} }
selected_set.existing_words_indices = selected_set.available_words_indices.clone(); selected_set.existing_words_indices = selected_set.available_words_indices.clone();
@@ -66,7 +63,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
} }
if let Ok(repetition_state) = if let Ok(repetition_state) =
RepetitionState::new(selected_set.general_settings.clone(), clone) RepetitionState::new(selected_set.clone(), self.state.clone())
{ {
return Some(Repetition(repetition_state)); return Some(Repetition(repetition_state));
} }
@@ -74,7 +71,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
None None
} else if let GoToSettings = message { } else if let GoToSettings = message {
Some(RepetitionSettings(RepetitionSettingsState::new( Some(RepetitionSettings(RepetitionSettingsState::new(
self.selected_deck().unwrap().index.unwrap(), self.selected_deck_handle.unwrap(),
self.state.clone(), self.state.clone(),
))) )))
} else { } else {
@@ -82,24 +79,23 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
} }
} }
fn navigated(&mut self) { fn navigated(&mut self) {
if let Some(index) = self.selected_deck_index { if let Some(index) = self.selected_deck_handle {
let state = self.state.lock(); let decks = self.state.decks.read();
if let Some(global_deck) = state.decks.get(index) { if let Some(deck) = decks.get_static(index) {
if global_deck.id != self.selected_deck().unwrap().id { {
drop(state); let stored = self.decks.get_mut(&index).unwrap();
self.decks.remove(index); stored.general_settings = deck;
}
drop(decks);
self.fill_deck_word_indices();
} else {
self.decks.remove(&index);
drop(decks);
self.clear_selection(); self.clear_selection();
}
} else { } else {
self.decks[index].general_settings = state.decks[index].clone();
drop(state);
let _ = self.fill_deck_word_indices(); let _ = self.fill_deck_word_indices();
} }
} else {
drop(state);
self.decks.remove(index);
self.clear_selection()
}
}
} }
fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> { fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> {
match message { match message {
@@ -111,31 +107,32 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
CreateSet => { CreateSet => {
let index = self.decks.len() + 1; let index = self.decks.len() + 1;
self.decks.push(DeckViewData { let general = DeckSettings::with_name(format!("Колода #{}", index));
general_settings: DeckSettings::with_name(format!("Колода #{}", index)), let handle = self.state.decks.write().create(general.clone());
self.decks.insert(
handle,
DeckRunData {
settings_handle: handle,
general_settings: general,
existing_words_indices: Default::default(), existing_words_indices: Default::default(),
available_words_indices: Default::default(), available_words_indices: Default::default(),
indices_ready: false, indices_ready: false,
append_mode: Default::default(), append_mode: Default::default(),
index: None, count: None,
}); open_mode: OrderMode::Default,
},
);
} }
DeleteSet => { DeleteSet => {
let index = self.selected_deck_index.unwrap(); let index = self.selected_deck_handle.unwrap();
self.clear_selection(); self.clear_selection();
let deck = self.decks.remove(index); let deck = self.decks.remove(&index);
if deck.id.is_valid() {
let mut state = self.state.lock(); if index.real() {
state.decks.remove(index); let mut repo = self.state.decks.write();
self.decks = self if let Err(err) = repo.delete(index) {
.decks return push_error_task!(err);
.clone() }
.into_iter()
.map(|mut deck| {
deck.index = state.decks.iter().position(|d| d.id == deck.id);
deck
})
.collect();
} }
} }
SetName(value) => { SetName(value) => {
@@ -145,29 +142,24 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
SelectSet(index) => { SelectSet(index) => {
self.select_deck(index); self.select_deck(index);
let deck = self.selected_deck().unwrap(); let deck = self.selected_deck().unwrap();
if !deck.indices_ready if !deck.indices_ready {
&& let Err(err) = self.fill_deck_word_indices() self.fill_deck_word_indices()
{
return push_error_task!(err);
} }
} }
Save => { Save => {
let mut deck = self.selected_deck().unwrap().clone(); let deck = self.selected_deck().unwrap().clone();
let mut state = self.state.lock(); let handle = self.selected_deck_handle.as_mut().unwrap();
{ let mut repo = self.state.decks.write();
if let Err(e) = update_deck(&mut deck.general_settings, &state.connection) {
return Task::done(RootMessage::PushNotification( repo.set(handle, deck.general_settings.clone());
NotificationRequest::Error(e), let old = handle.clone();
)); if let Err(err) = repo.save(handle) {
} return push_error_task!(err);
if let Some(index) = deck.index {
state.decks[index] = deck.general_settings.clone();
} else {
state.decks.push(deck.general_settings.clone());
deck.index = Some(state.decks.len() - 1);
self.decks[self.selected_deck_index.unwrap()] = deck.clone();
}
} }
let mut deck = self.decks.remove(&old).unwrap();
deck.settings_handle = *handle;
self.decks.insert(*handle, deck);
} }
SetForward(value) => { SetForward(value) => {
let deck = self.selected_deck_mut().unwrap(); let deck = self.selected_deck_mut().unwrap();
@@ -182,23 +174,21 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
deck.general_settings.filter = value; deck.general_settings.filter = value;
} }
TryFilter => { TryFilter => {
let state = self.state.lock();
let deck = self.selected_deck().unwrap(); let deck = self.selected_deck().unwrap();
let count = deck.get_word_list(&state).len(); let count = DeckData::list(&deck, &self.state).len();
drop(state);
let deck = self.selected_deck_mut().unwrap(); let deck = self.selected_deck_mut().unwrap();
deck.general_settings.count = Some(count); deck.count = Some(count);
} }
SetOpenMode(mode) => { SetOpenMode(mode) => {
let deck = self.selected_deck_mut().unwrap(); let deck = self.selected_deck_mut().unwrap();
deck.general_settings.open_mode = mode; deck.open_mode = mode;
} }
SetAppendMode(mode) => { SetAppendMode(mode) => {
let deck = self.selected_deck_mut().unwrap(); let deck = self.selected_deck_mut().unwrap();
deck.append_mode = mode; deck.append_mode = mode;
} }
AppendWords(count) => { AppendWords(count) => {
let adding: Vec<usize>; let adding: Vec<EntityHandle<WordData>>;
{ {
{ {
let deck = self.selected_deck().unwrap(); let deck = self.selected_deck().unwrap();
@@ -220,10 +210,10 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
deck.existing_words_indices.append(&mut adding.clone()); deck.existing_words_indices.append(&mut adding.clone());
} }
let deck = self.selected_deck().unwrap(); let deck = self.selected_deck().unwrap().clone();
let mut state = self.state.lock(); let state = &mut self.state;
if let Err(err) = if let Err(err) =
Self::append_words(&mut state, &deck.general_settings, adding.into_iter()) Self::append_words(state, &deck.general_settings, adding.into_iter())
{ {
return push_error_task!(err); return push_error_task!(err);
} }
@@ -257,47 +247,44 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
} }
impl RepetitionsState { impl RepetitionsState {
pub(crate) fn new(state: Arc<Mutex<AppState>>) -> RepetitionsState { pub(crate) fn new(state: AppState) -> RepetitionsState {
let state_ = state.lock(); let decks = Self::get_decks_from_state(&state);
let mut map = HashMap::with_capacity(state_.dictionary.len());
state_
.dictionary
.iter()
.enumerate()
.for_each(|(index, word)| {
map.insert(word.id, index);
});
let decks = Self::get_decks_from_state(&state_);
drop(state_);
RepetitionsState { RepetitionsState {
word_id_index_map: map, selected_deck_handle: None,
selected_deck_index: None,
decks, decks,
state, state,
} }
} }
fn get_decks_from_state(state: &AppState) -> Vec<DeckViewData> { fn get_decks_from_state(
state: &AppState,
) -> BTreeMap<EntityHandle<DeckSettings>, DeckRunData> {
state state
.decks .decks
.read()
.iter() .iter()
.enumerate() .enumerate()
.map(|(index, deck)| DeckViewData { .map(|(index, deck)| {
(
EntityHandle::new(deck.id, index, true),
DeckRunData {
settings_handle: EntityHandle::new(deck.id, index, true),
general_settings: deck.clone(), general_settings: deck.clone(),
existing_words_indices: Default::default(), existing_words_indices: Default::default(),
available_words_indices: Default::default(), available_words_indices: Default::default(),
indices_ready: false, indices_ready: false,
append_mode: Default::default(), append_mode: Default::default(),
index: Some(index), count: None,
open_mode: OrderMode::Default,
},
)
}) })
.collect() .collect::<BTreeMap<_, _>>()
} }
} }
impl RepetitionsState { impl RepetitionsState {
fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { fn launch_delete_button(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
if deck.id.is_valid() { if deck.id.is_valid() {
if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.is_empty() { if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.is_empty() {
return text!("Добавьте карточки или измените фильтр").into(); return text!("Добавьте карточки или измените фильтр").into();
@@ -329,7 +316,7 @@ impl RepetitionsState {
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
{ {
if !deck.id.is_valid() { if !deck.settings_handle.real() {
column![ column![
column![ column![
text!("Передняя сторона"), text!("Передняя сторона"),
@@ -412,22 +399,20 @@ impl RepetitionsState {
fn activity_bar(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { fn activity_bar(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> {
const MAX_DAY_COUNT: f32 = 128.0; const MAX_DAY_COUNT: f32 = 128.0;
let state = self.state.lock(); let history = self.state.history.read();
let history = state.activity.get(&deck.id); let history = history.activity_of_deck(deck.id);
let mut counts: Vec<u32> = vec![0; 30 * 7]; let mut counts: Vec<u32> = vec![0; 30 * 7];
let now = Local::now().date_naive(); let now = Local::now().date_naive();
if let Some(history) = history {
for (date, activity) in history { for (date, activity) in history {
let distance = now - *date; let distance = now - date;
let index = counts.len() as i64 - distance.num_days() - 1; let index = counts.len() as i64 - distance.num_days() - 1;
if index < 0 { if index < 0 {
continue; continue;
} }
counts[index as usize] = *activity; counts[index as usize] = activity;
}
} }
lazy((counts, now), |(counts, now)| { lazy((counts, now), |(counts, now)| {
@@ -483,7 +468,7 @@ impl RepetitionsState {
}) })
.into() .into()
} }
fn word_append_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { fn word_append_panel(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
column![ column![
text!("Режим добавления карточек"), text!("Режим добавления карточек"),
row![ row![
@@ -506,7 +491,7 @@ impl RepetitionsState {
.spacing(HALF_SPACING) .spacing(HALF_SPACING)
.into() .into()
} }
fn adder_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { fn adder_panel(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
match deck.append_mode { match deck.append_mode {
AppendMode::Full => space().into(), AppendMode::Full => space().into(),
AppendMode::Manual => row![ AppendMode::Manual => row![
@@ -526,7 +511,7 @@ impl RepetitionsState {
let available = selected_deck.available_words_indices.len(); let available = selected_deck.available_words_indices.len();
text!("{} слова добавлено из {}", now, available).into() text!("{} слова добавлено из {}", now, available).into()
} }
fn count_view(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { fn count_view(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
if let Some(count) = deck.count { if let Some(count) = deck.count {
return text!("Количество слов: {}", count).into(); return text!("Количество слов: {}", count).into();
} }
@@ -535,10 +520,10 @@ impl RepetitionsState {
fn decks_list(&self) -> Column<'_, RepetitionsMessage> { fn decks_list(&self) -> Column<'_, RepetitionsMessage> {
let mut column = Column::new(); let mut column = Column::new();
for (i, deck) in self.decks.iter().enumerate() { for (i, (handle, deck)) in self.decks.iter().enumerate() {
column = column.push( column = column.push(
button(text!("{}", &deck.name)) button(text!("{}", &deck.name))
.on_press_with(move || SelectSet(i)) .on_press_with(move || SelectSet(*handle))
.style(move |_x: &Theme, status: Status| Style { .style(move |_x: &Theme, status: Status| Style {
background: if status == Status::Hovered { background: if status == Status::Hovered {
Some(Background::Color(Color::WHITE.scale_alpha(0.2))) Some(Background::Color(Color::WHITE.scale_alpha(0.2)))
@@ -562,7 +547,7 @@ impl RepetitionsState {
} }
impl RepetitionsState { impl RepetitionsState {
fn append_all_words(state: &mut AppState, deck: &DeckViewData) -> MaybeError { fn append_all_words(state: &mut AppState, deck: &DeckRunData) -> MaybeError {
let existing = &deck.existing_words_indices; let existing = &deck.existing_words_indices;
let mut created_set = HashSet::with_capacity(existing.len()); let mut created_set = HashSet::with_capacity(existing.len());
existing.iter().for_each(|c| { existing.iter().for_each(|c| {
@@ -579,60 +564,60 @@ impl RepetitionsState {
fn append_words( fn append_words(
state: &mut AppState, state: &mut AppState,
deck: &DeckSettings, deck: &DeckSettings,
indices: impl Iterator<Item = usize>, indices: impl Iterator<Item = EntityHandle<WordData>>,
) -> MaybeError { ) -> MaybeError {
let words = &state.dictionary; let words = &state.dictionary;
let stats = &mut indices let stats = &mut indices
.map(|i| { .map(|i| CardState {
let word = &words[i];
CardState {
id: 0.into(), id: 0.into(),
word_id: word.id, word_id: i.id,
last_open: Utc::now(), last_open: Utc::now(),
score: 1, score: 1,
fsrs_opened: Utc::now().date_naive(), fsrs_opened: Utc::now().date_naive(),
fsrs_memory: None, fsrs_memory: None,
set_id: deck.id, deck_id: deck.id,
fsrs_open_day: Utc::now().date_naive(), fsrs_open_day: Utc::now().date_naive(),
}
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !stats.is_empty() { if !stats.is_empty() {
add_stat_list(stats, &mut state.connection)?; let mut repo = state.card_states.write();
repo.create_now_many(stats.as_mut_slice())?;
} }
Ok(()) Ok(())
} }
fn select_deck(&mut self, index: usize) { fn select_deck(&mut self, index: EntityHandle<DeckSettings>) {
if self.decks.get(index).is_some() { if self.decks.contains_key(&index) {
self.selected_deck_index = Some(index); self.selected_deck_handle = Some(index);
} }
} }
fn selected_deck(&self) -> Option<&DeckViewData> { fn selected_deck(&self) -> Option<&DeckRunData> {
self.decks.get(self.selected_deck_index?) self.decks.get(&self.selected_deck_handle?)
} }
fn selected_deck_mut(&mut self) -> Option<&mut DeckViewData> { fn selected_deck_mut(&mut self) -> Option<&mut DeckRunData> {
self.decks.get_mut(self.selected_deck_index?) self.decks.get_mut(&self.selected_deck_handle?)
} }
fn clear_selection(&mut self) { fn clear_selection(&mut self) {
self.selected_deck_index = None; self.selected_deck_handle = None;
} }
fn fill_deck_word_indices(&mut self) -> MaybeError { fn fill_deck_word_indices(&mut self) {
let state = &self.state.lock(); let card_states = self.state.card_states.read();
let deck_id = self.selected_deck().unwrap().id;
let added = card_states
.query_par(Arc::new(move |state| state.deck_id == deck_id))
.iter(&card_states)
.map(|state| EntityHandle::from_id(state.word_id))
.collect::<Vec<_>>();
let mut deck = self.selected_deck().unwrap().clone(); let mut deck = self.selected_deck().unwrap().clone();
let added: Vec<_> = load_stats_of_deck(&deck, &state.connection)? let total = DeckData::list(&deck, &self.state);
.iter()
.map(|c| self.word_id_index_map[&c.word_id])
.collect();
let total = deck.get_word_list(state);
deck.existing_words_indices = added; deck.existing_words_indices = added;
deck.available_words_indices = total; deck.available_words_indices = total;
self.decks[self.selected_deck_index.unwrap()] = deck; deck.indices_ready = true;
Ok(()) self.decks.insert(self.selected_deck_handle.unwrap(), deck);
} }
} }
#[derive(Clone)] #[derive(Clone)]
@@ -643,7 +628,7 @@ pub enum RepetitionsMessage {
CreateSet, CreateSet,
DeleteSet, DeleteSet,
SetName(String), SetName(String),
SelectSet(usize), SelectSet(EntityHandle<DeckSettings>),
Save, Save,
SetForward(String), SetForward(String),
SetBackward(String), SetBackward(String),
@@ -657,16 +642,18 @@ pub enum RepetitionsMessage {
} }
#[derive(Clone)] #[derive(Clone)]
struct DeckViewData { pub struct DeckRunData {
general_settings: DeckSettings, pub settings_handle: EntityHandle<DeckSettings>,
existing_words_indices: Vec<usize>, pub general_settings: DeckSettings,
available_words_indices: Vec<usize>, existing_words_indices: Vec<EntityHandle<WordData>>,
available_words_indices: Vec<EntityHandle<WordData>>,
indices_ready: bool, indices_ready: bool,
append_mode: AppendMode, pub append_mode: AppendMode,
index: Option<usize>, count: Option<usize>,
pub open_mode: OrderMode,
} }
impl Deref for DeckViewData { impl Deref for DeckRunData {
type Target = DeckSettings; type Target = DeckSettings;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
+2 -2
View File
@@ -22,7 +22,7 @@ use std::sync::Arc;
pub struct SelectorState { pub struct SelectorState {
pub set: KanaSet, pub set: KanaSet,
is_writing: bool, is_writing: bool,
state: Arc<Mutex<AppState>>, state: AppState,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -114,7 +114,7 @@ impl NavigatedPage<SelectorMessage> for SelectorState {
} }
impl SelectorState { impl SelectorState {
pub fn new(state: Arc<Mutex<AppState>>) -> Self { pub fn new(state: AppState) -> Self {
Self { Self {
set: Default::default(), set: Default::default(),
is_writing: false, is_writing: false,
+60 -35
View File
@@ -1,4 +1,5 @@
use crate::app::{AppState, fill_state}; use crate::app::AppState;
use crate::data::repositories::Repository;
use crate::data_provider::settings::{delete_settings, set_setting}; use crate::data_provider::settings::{delete_settings, set_setting};
use crate::data_provider::web_api::*; use crate::data_provider::web_api::*;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
@@ -12,8 +13,6 @@ use iced::widget::container::rounded_box;
use iced::widget::{button, column, container, progress_bar, row, space, text, toggler}; use iced::widget::{button, column, container, progress_bar, row, space, text, toggler};
use iced::{Center, Element, Fill, Font, Length, Task}; use iced::{Center, Element, Fill, Font, Length, Task};
use log::error; use log::error;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
#[derive(Clone)] #[derive(Clone)]
@@ -34,7 +33,7 @@ pub enum SyncMessage {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct SyncState { pub struct SyncState {
state: Arc<Mutex<AppState>>, state: AppState,
auto_fetch: bool, auto_fetch: bool,
frozen: bool, frozen: bool,
progress: f32, progress: f32,
@@ -56,8 +55,7 @@ impl NavigatedPage<SyncMessage> for SyncState {
match message { match message {
Back => {} Back => {}
CopyKey => { CopyKey => {
let state = self.state.lock(); let key = self.state.sync_data.read().key.clone().unwrap();
let key = state.sync_data.key.clone().unwrap();
return iced::clipboard::write(key) return iced::clipboard::write(key)
.map(|_val: String| RootMessage::Sync(KeyCopied)); .map(|_val: String| RootMessage::Sync(KeyCopied));
} }
@@ -76,16 +74,20 @@ impl NavigatedPage<SyncMessage> for SyncState {
if !validate_id(&new_id) { if !validate_id(&new_id) {
return Task::none(); return Task::none();
} }
let mut state = self.state.lock(); let mut settings = self.state.sync_data.write();
state.sync_data.key = Some(new_id.clone()); settings.key = Some(new_id.clone());
if let Err(err) = set_setting("SYNC_KEY".to_string(), new_id, &state.connection) { if let Err(err) = set_setting(
"SYNC_KEY".to_string(),
new_id,
&self.state.connection.lock(),
) {
return push_error_task!(err); return push_error_task!(err);
} }
} }
Send => { Send => {
self.prepare_to_db_interaction(); self.prepare_to_db_interaction();
let id = self.state.lock().sync_data.key.clone().unwrap(); let id = self.state.sync_data.read().key.clone().unwrap();
let tasks = Task::batch([ let tasks = Task::batch([
Task::perform( Task::perform(
async { tokio::time::sleep(Duration::from_millis(200)).await }, async { tokio::time::sleep(Duration::from_millis(200)).await },
@@ -97,7 +99,7 @@ impl NavigatedPage<SyncMessage> for SyncState {
return tasks; return tasks;
} }
GetLast => { GetLast => {
let id = self.state.lock().sync_data.key.clone().unwrap(); let id = self.state.sync_data.read().key.clone().unwrap();
let tasks = Task::batch([ let tasks = Task::batch([
Task::perform( Task::perform(
async { tokio::time::sleep(Duration::from_millis(200)).await }, async { tokio::time::sleep(Duration::from_millis(200)).await },
@@ -122,26 +124,27 @@ impl NavigatedPage<SyncMessage> for SyncState {
} }
NetworkFinished => { NetworkFinished => {
self.frozen = false; self.frozen = false;
let mut updated_state = AppState::new(); {
fill_state(&mut updated_state); let mut connection = self.state.connection.lock();
connection.restart();
}
if let Err(err) = self.reload_data() {
return push_error_task!(err);
}
let mut state = self.state.lock();
state.sync_data = updated_state.sync_data;
state.connection = updated_state.connection;
state.decks = updated_state.decks;
state.dictionary = updated_state.dictionary;
state.word_groups = updated_state.word_groups;
return Task::done(RootMessage::PushNotification(NotificationRequest::Info( return Task::done(RootMessage::PushNotification(NotificationRequest::Info(
NotificationData::DataLoaded, NotificationData::DataLoaded,
))); )));
} }
Disable => {} Disable => {}
DisableSync => { DisableSync => {
let mut state = self.state.lock(); let mut sync_data = self.state.sync_data.write();
state.sync_data.key = None; let connection = self.state.connection.lock();
sync_data.key = None;
let save_result = || { let save_result = || {
delete_settings("SYNC_KEY".to_string(), &state.connection)?; delete_settings("SYNC_KEY".to_string(), &connection)?;
delete_settings("AUTO_WEB_FETCH".to_string(), &state.connection)?; delete_settings("AUTO_WEB_FETCH".to_string(), &connection)?;
Ok::<(), NotificationData>(()) Ok::<(), NotificationData>(())
}; };
@@ -151,12 +154,14 @@ impl NavigatedPage<SyncMessage> for SyncState {
} }
SwitchAutoSync => { SwitchAutoSync => {
self.auto_fetch = !self.auto_fetch; self.auto_fetch = !self.auto_fetch;
let mut state = self.state.lock(); let mut sync_data = self.state.sync_data.write();
state.sync_data.auto_web_fetch = self.auto_fetch; let connection = self.state.connection.lock();
sync_data.auto_web_fetch = self.auto_fetch;
if let Err(err) = set_setting( if let Err(err) = set_setting(
"AUTO_WEB_FETCH".to_string(), "AUTO_WEB_FETCH".to_string(),
self.auto_fetch.to_string(), self.auto_fetch.to_string(),
&state.connection, &connection,
) { ) {
return push_error_task!(err); return push_error_task!(err);
} }
@@ -177,11 +182,9 @@ impl NavigatedPage<SyncMessage> for SyncState {
} }
impl SyncState { impl SyncState {
pub fn new(state: Arc<Mutex<AppState>>) -> SyncState { pub fn new(state: AppState) -> SyncState {
let auto_fetch; let auto_fetch = state.sync_data.read().auto_web_fetch;
{
auto_fetch = state.lock().sync_data.auto_web_fetch;
}
Self { Self {
auto_fetch, auto_fetch,
state, state,
@@ -190,6 +193,15 @@ impl SyncState {
} }
} }
fn reload_data(&mut self) -> Result<(), NotificationData> {
self.state.card_states.write().reload()?;
self.state.word_groups.write().reload()?;
self.state.decks.write().reload()?;
self.state.dictionary.write().reload()?;
self.state.history.write().reload()?;
Ok(())
}
fn sync_column(&self) -> Element<'_, SyncMessage> { fn sync_column(&self) -> Element<'_, SyncMessage> {
let network_view: Element<'_, SyncMessage> = if self.frozen { let network_view: Element<'_, SyncMessage> = if self.frozen {
progress_bar(0.0..=5.0, self.progress).into() progress_bar(0.0..=5.0, self.progress).into()
@@ -197,7 +209,7 @@ impl SyncState {
space().into() space().into()
}; };
{ {
if let Some(key) = self.state.lock().sync_data.key.clone() { if let Some(key) = self.state.sync_data.read().key.as_ref() {
column![ column![
text!("Ваш ключ синхронизации"), text!("Ваш ключ синхронизации"),
container( container(
@@ -254,13 +266,26 @@ impl SyncState {
} }
self.frozen = true; self.frozen = true;
self.progress = 0.0; self.progress = 0.0;
let state = self.state.lock(); let mut connection = self.state.connection.lock();
state
.connection connection
.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(())) .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}"); error!("Ошибка при checkpoint WAL: {e}");
}); });
connection
.execute("VACUUM;", [])
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
0
});
connection
.execute("PRAGMA optimize;", [])
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
0
});
connection.finish();
} }
} }
+107 -104
View File
@@ -1,5 +1,5 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::WordData; use crate::data::entities::{EntityHandle, WordData};
use crate::data_provider::words::{delete_word, update_word}; use crate::data_provider::words::{delete_word, update_word};
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
@@ -8,16 +8,15 @@ use crate::push_error;
use crate::push_error_task; use crate::push_error_task;
use crate::styling::*; use crate::styling::*;
use iced::widget::button::danger; use iced::widget::button::danger;
use iced::widget::{button, column, row, rule, scrollable, text, text_input}; use iced::widget::{button, column, row, rule, scrollable, space, text, text_input};
use iced::{Element, Fill, Task}; use iced::{Element, Fill, Task};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
pub struct WordState { pub struct WordState {
state: Arc<Mutex<AppState>>, state: AppState,
index: usize, handle: EntityHandle<WordData>,
word: WordData,
} }
impl NavigatedPage<WordMessage> for WordState { impl NavigatedPage<WordMessage> for WordState {
@@ -31,113 +30,117 @@ impl NavigatedPage<WordMessage> for WordState {
fn navigated(&mut self) {} fn navigated(&mut self) {}
fn update(&mut self, message: WordMessage) -> Task<RootMessage> { fn update(&mut self, message: WordMessage) -> Task<RootMessage> {
match message { // match message {
Back => {} // Back => {}
Save => { // Save => {
let mut state = self.state.lock(); // let mut state = self.state.lock();
state.dictionary[self.index] = self.word.clone(); // state.dictionary[self.index] = self.word.clone();
//
if let Err(err) = update_word(&mut self.word, &state.connection) { // if let Err(err) = update_word(&mut self.word, &state.connection) {
return push_error_task!(err); // return push_error_task!(err);
} // }
return Task::done(RootMessage::Word(Back)); // return Task::done(RootMessage::Word(Back));
} // }
Delete => { // Delete => {
let mut state = self.state.lock(); // let mut state = self.state.lock();
state.dictionary.remove(self.index); // state.dictionary.remove(self.index);
if let Err(err) = delete_word(&self.word, &state.connection) { // if let Err(err) = delete_word(&self.word, &state.connection) {
return push_error_task!(err); // return push_error_task!(err);
} // }
return Task::done(RootMessage::Word(Back)); // return Task::done(RootMessage::Word(Back));
} // }
SetTags(n) => self.word.tags = n, // SetTags(n) => self.word.tags = n,
SetKey(n) => { // SetKey(n) => {
self.word.key = n; // self.word.key = n;
} // }
SetValue(n) => { // SetValue(n) => {
self.word.value = n; // self.word.value = n;
} // }
SetAdditional(key, value) => { // SetAdditional(key, value) => {
self.word.additional.insert(key, value.clone()); // self.word.additional.insert(key, value.clone());
} // }
AddAdditional(key) => { // AddAdditional(key) => {
self.word.additional.insert(key, "".to_string()); // self.word.additional.insert(key, "".to_string());
} // }
RemoveAdditional(key) => { // RemoveAdditional(key) => {
self.word.additional.remove(key.as_str()); // self.word.additional.remove(key.as_str());
} // }
} // }
Task::none() Task::none()
} }
fn view(&self) -> Element<'_, WordMessage> { fn view(&self) -> Element<'_, WordMessage> {
let mut fast_add = row![]; // let mut fast_add = row![];
if !self.word.additional.contains_key("reading") { // if !self.word.additional.contains_key("reading") {
fast_add = fast_add.push( // fast_add = fast_add.push(
button("Чтение") // button("Чтение")
.style(button::text) // .style(button::text)
.on_press(AddAdditional("reading".to_string())), // .on_press(AddAdditional("reading".to_string())),
); // );
} // }
if !self.word.additional.contains_key("description") { // if !self.word.additional.contains_key("description") {
fast_add = fast_add.push( // fast_add = fast_add.push(
button("Описание") // button("Описание")
.style(button::text) // .style(button::text)
.on_press(AddAdditional("description".to_string())), // .on_press(AddAdditional("description".to_string())),
); // );
} // }
//
if !self.word.additional.contains_key("context") { // if !self.word.additional.contains_key("context") {
fast_add = fast_add.push( // fast_add = fast_add.push(
button("В контексте") // button("В контексте")
.style(button::text) // .style(button::text)
.on_press(AddAdditional("context".to_string())), // .on_press(AddAdditional("context".to_string())),
); // );
} // }
//
let mut col = iced::widget::column![ // let mut col = iced::widget::column![
column![ // column![
text!("Ключ"), // text!("Ключ"),
text_input("key", &self.word.key).on_input(SetKey), // text_input("key", &self.word.key).on_input(SetKey),
] // ]
.spacing(QUARTER_SPACING), // .spacing(QUARTER_SPACING),
column![ // column![
text!("Значение"), // text!("Значение"),
text_input("value", &self.word.value).on_input(SetValue), // text_input("value", &self.word.value).on_input(SetValue),
] // ]
.spacing(QUARTER_SPACING), // .spacing(QUARTER_SPACING),
column![ // column![
text!("Теги"), // text!("Теги"),
text_input("tags", &self.word.tags).on_input(SetTags), // text_input("tags", &self.word.tags).on_input(SetTags),
] // ]
.spacing(QUARTER_SPACING), // .spacing(QUARTER_SPACING),
rule::horizontal(2), // rule::horizontal(2),
scrollable(fast_add), // scrollable(fast_add),
]; // ];
//
for more in &self.word.additional { // for more in &self.word.additional {
col = col.push(self.get_view_for_more(more)); // col = col.push(self.get_view_for_more(more));
} // }
//
back_overlay( // back_overlay(
column![ // column![
col.spacing(DEFAULT_SPACING / 2.0).width(Fill).height(Fill), // col.spacing(DEFAULT_SPACING / 2.0).width(Fill).height(Fill),
row![ // row![
button("Сохранить").style(jl_button).on_press(Save), // button("Сохранить").style(jl_button).on_press(Save),
button("Удалить").style(danger).on_press(Delete), // button("Удалить").style(danger).on_press(Delete),
] // ]
.spacing(DEFAULT_SPACING) // .spacing(DEFAULT_SPACING)
] // ]
.spacing(DEFAULT_SPACING) // .spacing(DEFAULT_SPACING)
.into(), // .into(),
Back, // Back,
) // )
space().into()
} }
} }
impl WordState { impl WordState {
pub(crate) fn new(word: WordData, index: usize, state: Arc<Mutex<AppState>>) -> WordState { pub fn new(handle: EntityHandle<WordData>, state: AppState) -> Self {
WordState { state, index, word } Self{
state,
handle,
}
} }
} }