Author SHA1 Message Date
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
23 changed files with 472 additions and 1063 deletions

No files matched your search

Generated
+60 -631
View File
File diff suppressed because it is too large. Load diff
+4 -4
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.26.0", 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"
+1
View File
@@ -103,6 +103,7 @@ pub fn fill_state(state: &mut AppState) {
state.dictionary.write().start().unwrap(); state.dictionary.write().start().unwrap();
state.decks.write().start().unwrap(); state.decks.write().start().unwrap();
state.history.write().start().unwrap(); state.history.write().start().unwrap();
state.card_states.write().start().unwrap();
state.sync_data = Arc::new(RwLock::new(setting)) state.sync_data = Arc::new(RwLock::new(setting))
} }
+74 -94
View File
@@ -1,23 +1,23 @@
use std::sync::Arc;
use std::time::Instant;
use chrono::Utc;
use crate::app::AppState; use crate::app::AppState;
use crate::core::srs::OrderModule::{Fsrs, RandomSRS, SemiRandomSRS, WorstWordsSRS}; use crate::core::srs::OrderModule::{Fsrs, RandomSRS, SemiRandomSRS, WorstWordsSRS};
use crate::core::srs::{ use crate::core::srs::{
OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, FsrsModule, OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule,
WordOpenMode, WorstWordsSRSModule, WordOpenMode, WorstWordsSRSModule,
}; };
use crate::data::entities::*; use crate::data::entities::*;
use crate::data_provider::card_states::{CardStateRepositoryInner}; use crate::data::repositories::Repository;
use crate::notification::{MaybeError, NotificationData}; use crate::data_provider::card_states::CardStateRepositoryInner;
use crate::notification::MaybeError;
use chrono::Utc;
use log::info; use log::info;
use rhai::{Engine, Scope}; use rhai::{Engine, Scope};
use crate::data::repositories::Repository; use std::sync::Arc;
use std::time::Instant;
#[derive(Clone)] #[derive(Clone)]
pub struct DeckData { pub struct DeckData {
pub(crate) words: Vec<EntityHandle<WordData>>, pub(crate) words: Vec<EntityHandle<WordData>>,
pub(crate) set: Vec<EntityHandle<CardState>>, pub(crate) states: Vec<EntityHandle<CardState>>,
current_word_index: Option<usize>, current_word_index: Option<usize>,
state: AppState, state: AppState,
order_module: OrderModule, order_module: OrderModule,
@@ -25,80 +25,58 @@ pub struct DeckData {
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: AppState, let mut query = states_repo.deck_states(settings.id);
) -> Result<Self, NotificationData> { let words = query
todo!() .iter(&states_repo)
// let state_for = state.clone(); .map(|card_state| EntityHandle::from_id(card_state.word_id))
// .collect();
// let stats = state (settings, &state.connection.read())?; let states = query.handles().collect::<Vec<EntityHandle<CardState>>>();
// let words = stats let fsrs = Fsrs(FsrsModule::new(Self::set_links(states.as_slice(), &states_repo).as_slice()));
// .iter() drop(states_repo);
// .map(|stat| { Self {
// state states,
// .dictionary words,
// .binary_search_by_key(&stat.word_id, |w| w.id) current_word_index: None,
// .expect("No word found in dictionary") state,
// }) order_module: fsrs,
// .map(|index| &state.dictionary[index]) open_mode,
// .cloned() settings: settings.clone(),
// .collect(); }
//
// Ok(Self {
// set: stats,
// words,
// current_word_index: None,
// state: state_for,
// order_module: Fsrs(FsrsModule::new()),
// open_mode: settings.open_mode,
// settings: settings.clone(),
// })
} }
pub fn next(&mut self) -> (EntityHandle<WordData>, EntityHandle<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 repo = self.state.card_states.read();
let links = Self::set_links(self.set.as_slice(), &repo); 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(links.as_slice());
}
module.next(links.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(links.as_slice()) Fsrs(ref mut module) => module.next(links.as_slice()),
}
module.next(links.as_slice())
}
WorstWordsSRS(ref mut module) => {
if !module.initialized {
module.init(links.as_slice())
}
module.next(links.as_slice())
}
Fsrs(ref mut module) => {
if !module.initialized {
module.init(links.as_slice())
}
module.next(links.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> { fn set_links<'a>(
set.iter().map(|h| repo.get_ref_static(*h).unwrap()).collect::<Vec<_>>() 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 mut repo = self.state.card_states.write(); let mut repo = self.state.card_states.write();
let card_state_handle = &mut self.set[index]; let card_state_handle = &mut self.states[index];
let card_state = repo.get_mut(card_state_handle).unwrap(); let card_state = repo.get_mut(card_state_handle).unwrap();
let old_score = card_state.score; let old_score = card_state.score;
card_state.update(status); card_state.update(status);
@@ -121,16 +99,6 @@ impl DeckData {
} }
} }
if let Fsrs(ref fsrs) = self.order_module
&& fsrs.finished()
{
self.order_module = match self.open_mode {
OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new()),
OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new()),
OrderMode::FullRandom => RandomSRS(RandomSRSModule::new()),
}
}
let word_id = card_state.word_id; let word_id = card_state.word_id;
repo.save(card_state_handle)?; repo.save(card_state_handle)?;
@@ -145,23 +113,34 @@ impl DeckData {
after: new_score, after: new_score,
fsrs, fsrs,
})?; })?;
todo!("Implement history pushing")
// let _ = push_note(
// self.settings.id,
// HistoryNote {
// timestamp: Utc::now(),
// word_id: word.word_id,
// answer: status,
// before: old_score,
// after: new_score,
// },
// );
} }
Ok(()) 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>> { pub fn list(deck: &DeckSettings, state: &AppState) -> Vec<EntityHandle<WordData>> {
@@ -179,17 +158,18 @@ impl DeckData {
more.insert(iced.0.clone().into(), iced.1.clone().into()); more.insert(iced.0.clone().into(), iced.1.clone().into());
} }
let mut scope = Scope::new(); let mut scope = Scope::new();
let group_name = groups[groups.binary_search_by_key(&word.group_id, |g| g.id).unwrap()].name.clone(); let group_name = groups[groups
.binary_search_by_key(&word.group_id, |g| g.id)
.unwrap()]
.name
.clone();
scope scope
.push_constant("id", word.id.0 as i64) .push_constant("id", word.id.0 as i64)
.push_constant("key", word.key.clone()) .push_constant("key", word.key.clone())
.push_constant("value", word.value.clone()) .push_constant("value", word.value.clone())
.push_constant("tags", word.tags.clone()) .push_constant("tags", word.tags.clone())
.push_constant("more", more) .push_constant("more", more)
.push_constant( .push_constant("group", group_name);
"group",
group_name,
);
let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast); let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast);
if let Ok(result) = result { if let Ok(result) = result {
@@ -199,7 +179,7 @@ impl DeckData {
})); }));
info!("Collecting available words is {:?}", time.elapsed()); info!("Collecting available words is {:?}", time.elapsed());
let handles =query.handles().collect(); let handles = query.handles().collect();
drop(query); drop(query);
handles handles
} else { } else {
+50 -66
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,24 +32,23 @@ pub enum OrderModule {
Fsrs(FsrsModule), Fsrs(FsrsModule),
} }
pub trait SRSModule { pub trait SRSModule {
fn new(set: &[&CardState]) -> Self;
fn next(&mut self, set: &[&CardState]) -> usize; 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 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 { fn next(&mut self, set: &[&CardState]) -> usize {
match self.basket.pop() { match self.basket.pop() {
Some(index) => index, Some(index) => index,
@@ -61,29 +61,12 @@ 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 {
@@ -106,13 +89,18 @@ 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 {
@@ -122,7 +110,6 @@ impl SemiRandomSRSModule {
} }
#[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>,
@@ -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,6 +144,8 @@ impl WorstWordsSRSModule {
queue: vec![], queue: vec![],
} }
} }
}
impl WorstWordsSRSModule {
fn fill_pool(&mut self, set: &[&CardState]) { 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);
@@ -183,10 +166,31 @@ 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 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 { 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,
+24 -41
View File
@@ -11,6 +11,8 @@ use rusqlite::Connection;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{Deref, Index, IndexMut}; 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
@@ -158,6 +160,7 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
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
@@ -186,6 +189,8 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
.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>> { pub fn handles(&self) -> impl Iterator<Item = EntityHandle<T>> {
@@ -278,7 +283,7 @@ impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
} }
} }
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: Arc<dyn Fn(&T) -> bool + Send + Sync>, rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
@@ -286,30 +291,17 @@ impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, 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(
@@ -319,30 +311,17 @@ impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, 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> { pub fn empty_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
@@ -365,6 +344,10 @@ impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, D> {
} }
} }
pub fn full_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
self.query(Arc::new(|_| true))
}
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.cache.len() + self.temporal.len() self.cache.len() + self.temporal.len()
} }
+22 -76
View File
@@ -1,4 +1,6 @@
use std::sync::Arc;
use crate::data::entities::{CardState, DeckSettings}; use crate::data::entities::{CardState, DeckSettings};
use crate::data::repositories::{DataRepository, DataRepositoryInner, Query};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; 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};
@@ -7,10 +9,13 @@ 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::repositories::{DataRepository, DataRepositoryInner}; use crate::data::Id;
pub type CardStateRepository = DataRepository<CardState, SqliteDbLayer<CardState, CardStatesQuerySet>>; pub type CardStateRepository =
pub type CardStateRepositoryInner = DataRepositoryInner<CardState, SqliteDbLayer<CardState, CardStatesQuerySet>>; DataRepository<CardState, CardStateDbLayer>;
pub type CardStateRepositoryInner =
DataRepositoryInner<CardState, CardStateDbLayer>;
pub type CardStateDbLayer = SqliteDbLayer<CardState, CardStatesQuerySet>;
#[derive(Clone)] #[derive(Clone)]
pub struct CardStatesQuerySet; pub struct CardStatesQuerySet;
impl TableAccessQuerySet<CardState> for CardStatesQuerySet { impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
@@ -19,11 +24,11 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
} }
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, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4) RETURNING id" "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 {
@@ -58,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(
@@ -109,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.deck_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(())
}
+1 -1
View File
@@ -16,7 +16,7 @@ impl TableAccessQuerySet<WordGroup> for CustomFieldsQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1) RETURNING id" "INSERT INTO word_groups (name) VALUES (?1)"
} }
fn name() -> &'static str { fn name() -> &'static str {
+1 -1
View File
@@ -17,7 +17,7 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id" "INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4)"
} }
fn name() -> &'static str { fn name() -> &'static str {
+4 -1
View File
@@ -21,13 +21,16 @@ impl TableAccessQuerySet<HistoryNote> for HistoryQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO history (deck_id, state_id, timestamp, answer, before, after, fsrs) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) RETURNING rowid" "INSERT INTO history (deck_id, word_id, timestamp, answer, before, after, fsrs) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
} }
fn name() -> &'static str { fn name() -> &'static str {
"history" "history"
} }
fn id_field() -> &'static str {
"rowid"
}
fn from_sql(row: &Row) -> Result<HistoryNote, Error> { fn from_sql(row: &Row) -> Result<HistoryNote, Error> {
Ok(HistoryNote { Ok(HistoryNote {
id: row.get(0)?, id: row.get(0)?,
+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()
+32 -12
View File
@@ -1,8 +1,8 @@
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{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::{debug, error}; use log::{debug, error};
use rusqlite::{Connection, Error, Params, Row}; use rusqlite::{Connection, Error, Params, Row};
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -25,6 +25,9 @@ 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;
@@ -38,9 +41,9 @@ where
_phantom: PhantomData<(T, Q)>, _phantom: PhantomData<(T, Q)>,
} }
impl<T, Q: TableAccessQuerySet<T>> Default for SqliteDbLayer<T, Q>{ impl<T, Q: TableAccessQuerySet<T>> Default for SqliteDbLayer<T, Q> {
fn default() -> Self { fn default() -> Self {
Self{ Self {
_phantom: PhantomData, _phantom: PhantomData,
} }
} }
@@ -52,7 +55,10 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
connection connection
.execute(Q::save(), Q::update_params(entity)) .execute(Q::save(), Q::update_params(entity))
.map_err(|e| {error!("{}", e); DbSaveError})?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
debug!("Updated in {} for {:?}", Q::name(), time.elapsed()); debug!("Updated in {} for {:?}", Q::name(), time.elapsed());
Ok(()) Ok(())
} }
@@ -61,8 +67,11 @@ 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(|e| {error!("{}", e); DbSaveError})?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
entity.set_id(index.into()); entity.set_id(index.into());
debug!("Added into {} for {:?}", Q::name(), time.elapsed()); debug!("Added into {} for {:?}", Q::name(), time.elapsed());
@@ -80,7 +89,10 @@ 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(|e| {error!("{}", e); DbSaveError})?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
debug!("Deleted from {} for {:?}", Q::name(), time.elapsed()); debug!("Deleted from {} for {:?}", Q::name(), time.elapsed());
Ok(()) Ok(())
} }
@@ -93,7 +105,10 @@ 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(|e| {error!("{}", e); DbSaveError})?; .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));
@@ -123,7 +138,10 @@ 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
})?;
} }
debug!( debug!(
"Deleted {} from {} for {:?}", "Deleted {} from {} for {:?}",
@@ -145,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
})?;
} }
} }
+19
View File
@@ -1,6 +1,7 @@
use crate::fs::*; use crate::fs::*;
use rusqlite::Connection; use rusqlite::Connection;
use std::ops::{Deref, DerefMut}; 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();
@@ -108,9 +109,27 @@ impl Default for SqliteConnection {
impl SqliteConnection { impl SqliteConnection {
pub fn finish(&mut self) { 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; self.0 = None;
} }
pub fn restart(&mut self) { pub fn restart(&mut self) {
self.0 = Some(default_connection()); self.0 = Some(default_connection());
} }
} }
impl Drop for SqliteConnection {
fn drop(&mut self) { self.finish(); }
}
+1 -1
View File
@@ -19,7 +19,7 @@ impl TableAccessQuerySet<WordGroup> for WordGroupQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1) RETURNING id" "INSERT INTO word_groups (name) VALUES (?1)"
} }
fn name() -> &'static str { fn name() -> &'static str {
+3 -3
View File
@@ -20,7 +20,7 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"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)"
} }
fn name() -> &'static str { fn name() -> &'static str {
@@ -63,7 +63,7 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
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,
@@ -193,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),
) )
-1
View File
@@ -9,7 +9,6 @@ mod navigation;
pub mod notification; pub mod notification;
mod pages; mod pages;
mod styling; mod styling;
mod widgets;
pub mod tests; pub mod tests;
use crate::app::start; use crate::app::start;
+104 -77
View File
@@ -2,6 +2,7 @@ use crate::app::AppState;
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{EntityHandle, WordData, WordGroup}; use crate::data::entities::{EntityHandle, WordData, WordGroup};
use crate::data::repositories::{Query, Repository}; 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::WordDataLayer;
use crate::data_provider::words::WordRepositoryInner; use crate::data_provider::words::WordRepositoryInner;
use crate::navigation::Page::{Import, Word}; use crate::navigation::Page::{Import, Word};
@@ -28,7 +29,6 @@ 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};
use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner};
pub struct DictionaryState { pub struct DictionaryState {
state: AppState, state: AppState,
@@ -89,7 +89,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
words, words,
self.reverse, self.reverse,
self.no_typing, self.no_typing,
self.state.clone() self.state.clone(),
))); )));
} }
if let WordAction(handle) = message if let WordAction(handle) = message
@@ -106,6 +106,9 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
fn navigated(&mut self) { fn navigated(&mut self) {
self.included = Default::default(); self.included = Default::default();
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();
} }
@@ -116,6 +119,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let mut word = WordData::new(); let mut word = WordData::new();
word.group_id = self.selected_group.id; word.group_id = self.selected_group.id;
dictionary.create(word); dictionary.create(word);
self.displayed_words_query.load(&dictionary);
} }
SetKey(handle, v) => { SetKey(handle, v) => {
@@ -158,6 +162,8 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
if let Err(err) = dictionary.delete(i) { if let Err(err) = dictionary.delete(i) {
return push_error_task!(err); return push_error_task!(err);
} }
self.displayed_words_query.load(&dictionary);
} }
Include(i, b) => { Include(i, b) => {
if b { if b {
@@ -188,6 +194,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
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 mut groups = self.state.word_groups.write(); let mut groups = self.state.word_groups.write();
@@ -198,14 +205,21 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
} }
} }
SaveGroup => { SaveGroup => {
let mut groups = self.state.word_groups.write(); {
let mut groups = self.state.word_groups.write();
if let Err(err) = groups.save(&mut self.selected_group) { if let Err(err) = groups.save(&mut self.selected_group) {
return push_error_task!(err); return push_error_task!(err);
}
self.groups_query.load(&groups);
} }
self.setup_words_query();
} }
SelectGroup(i) => { SelectGroup(i) => {
self.selected_group = i; self.selected_group = i;
self.setup_words_query();
self.update_words_include(i.id) self.update_words_include(i.id)
} }
DeleteGroup => { DeleteGroup => {
@@ -213,9 +227,10 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
return Task::none(); return Task::none();
} }
{ {
let mut groups = self.state.word_groups.write();
let delete_group = self.selected_group; let delete_group = self.selected_group;
self.selected_group = groups.handle_by_id(Id(1)); self.selected_group = EntityHandle::from_id(1.into());
self.setup_words_query();
let mut groups = self.state.word_groups.write();
if let Err(err) = groups.delete(delete_group) { if let Err(err) = groups.delete(delete_group) {
return push_error_task!(err); return push_error_task!(err);
@@ -224,6 +239,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
if let Err(err) = self.state.dictionary.write().reload() { if let Err(err) = self.state.dictionary.write().reload() {
return push_error_task!(err); return push_error_task!(err);
} }
self.groups_query.load(&groups);
} }
self.update_tags() self.update_tags()
@@ -241,7 +257,11 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let mut dictionary = self.state.dictionary.write(); let mut dictionary = self.state.dictionary.write();
let _ = dictionary.save(&mut handle); let reload_query = handle.real() == false;
let _ = dictionary.save(&mut handle);
if reload_query {
self.displayed_words_query.load(&dictionary);
}
return Task::none(); return Task::none();
} }
} }
@@ -299,11 +319,20 @@ impl DictionaryState {
} }
fn init(&mut self) { fn init(&mut self) {
self.displayed_words_query = self.state.dictionary.read().query_par(Arc::new(|word| word.group_id == Id(1))); self.displayed_words_query = self
.state
.dictionary
.read()
.query(Arc::new(|word| word.group_id == Id(1)));
} }
fn setup_words_query(&mut self, id: Id) { fn setup_words_query(&mut self) {
// self.displayed_words_query = self.state.dictionary.read().query_par(Arc::new(|word| word.group_id == Self::get_id(self))); 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, handle: EntityHandle<WordData>) -> Task<RootMessage> { fn launch_auto_save_offset(&mut self, handle: EntityHandle<WordData>) -> Task<RootMessage> {
@@ -317,63 +346,60 @@ impl DictionaryState {
} }
fn words_list(&self) -> iced::Element<'_, DictionaryMessage> { fn words_list(&self) -> iced::Element<'_, DictionaryMessage> {
todo!("Make new display system") let time = Instant::now();
// let time = Instant::now(); let mut col = Column::new().width(Fill);
// let mut col = Column::new().width(Fill);
// let query = &self.displayed_words_query;
// let query = &self.displayed_words_query; let dictionary = self.state.dictionary.read();
// let dictionary = self.state.dictionary.read();
// for access_index in 0..query.len() {
// let mut i = access_index;
// for access_index in 0..query.len() { if self.reverse_list {
// let mut i = access_index; i = query.len() - access_index - 1;
// if self.reverse_list { }
// i = query.len() - access_index - 1; let word_handle = &query[i];
// } let word = dictionary.get_ref_static(*word_handle).unwrap();
// let word_handle = &query[i];
// let word = dictionary.get_ref_static(*word_handle).unwrap(); if !self.search.is_empty()
// && !word.key.contains(&self.search)
// if !self.search.is_empty() && !word.value.contains(&self.search)
// && !word.key.contains(&self.search) && !word.tags.contains(&self.search)
// && !word.value.contains(&self.search) {
// && !word.tags.contains(&self.search) continue;
// { }
// continue;
// } let word_line_data = WordLineState {
// is_included: self.included.contains(word_handle),
// let word_line_data = WordLineState { key: word.key.clone().into(),
// is_included: self.included.contains(word_handle), value: word.value.clone().into(),
// key: word.key.clone().into(), tags: word.tags.clone().into(),
// value: word.value.clone().into(), handle: word_handle.clone(),
// tags: word.tags.clone().into(), };
// handle: word_handle.clone(),
// }; let lazy_line = lazy(word_line_data, Self::word_line);
//
// col = col.push(lazy_line);
// let lazy_line = lazy(word_line_data, |data: &WordLineState| Self::word_line(data)); }
//
// col = col.push(lazy_line); info!("Words rendering time: {:?}", time.elapsed());
// } scrollable(col).height(Fill).into()
//
// info!("Words rendering time: {:?}", time.elapsed());
// scrollable(col).height(Fill).into()
} }
pub fn word_line(data: &WordLineState) -> Row<'_, DictionaryMessage> 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(data.handle, 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(data.handle, string)) .on_input(move |string| SetKey(handle, string))
.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();
@@ -384,7 +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(data.handle, string)) .on_input(move |string| SetValue(handle, string))
.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();
@@ -396,7 +422,7 @@ impl DictionaryState {
text_input("Теги", data.tags.as_ref()) text_input("Теги", data.tags.as_ref())
.size(ACCENT_FONT_SIZE) .size(ACCENT_FONT_SIZE)
.width(Fill) .width(Fill)
.on_input(move |string| SetTags(data.handle, string)) .on_input(move |string| SetTags(handle, string))
.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();
@@ -408,13 +434,15 @@ impl DictionaryState {
if !data.handle.real() { if !data.handle.real() {
return button("-") return button("-")
.on_press(WordAction(data.handle)) .on_press(WordAction(data.handle))
.style(|_x: &iced::Theme, _status: iced::widget::button::Status| Style { .style(
background: None, |_x: &iced::Theme, _status: iced::widget::button::Status| Style {
text_color: Color::BLACK, background: None,
border: Border::default(), text_color: Color::BLACK,
shadow: Shadow::default(), border: Border::default(),
snap: false, shadow: Shadow::default(),
}) snap: false,
},
)
.width(15); .width(15);
} }
@@ -434,10 +462,7 @@ impl DictionaryState {
.on_input(Search) .on_input(Search)
.width(Fill), .width(Fill),
text!("Всего слов: {}", dict.len()), text!("Всего слов: {}", dict.len()),
text!( text!("Выбрано слов: {}", self.included.iter().count()),
"Выбрано слов: {}",
self.included.iter().count()
),
self.tags_selector(), self.tags_selector(),
toggler(self.no_typing) toggler(self.no_typing)
.label("Без набора") .label("Без набора")
@@ -482,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.dictionary.write(); 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
@@ -529,17 +558,15 @@ impl DictionaryState {
self.included.clear(); self.included.clear();
for (handle, tags) in &mut self.words_tags { for (handle, tags) in &mut self.words_tags {
if let Some(word) = dict.get_ref(handle) { if let Some(word) = dict.get_ref(handle) {
if word.group_id != group_id { if word.group_id != group_id {
continue; continue;
} }
if !tags.is_empty() if !tags.is_empty() && tags.iter().all(|t| include_tags.contains(t)) {
&& tags.iter().all(|t| include_tags.contains(t)) {
self.included.insert(*handle); self.included.insert(*handle);
} }
} } else {
else {
continue; continue;
} }
} }
+6 -5
View File
@@ -2,11 +2,13 @@ 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 iced::alignment::Horizontal::Center; use iced::alignment::Horizontal::Center;
@@ -18,11 +20,10 @@ 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;
use crate::data::repositories::Repository;
pub struct RepetitionState { pub struct RepetitionState {
state: 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,
@@ -102,10 +103,10 @@ impl NavigatedPage<RepetitionMessage> for RepetitionState {
impl RepetitionState { impl RepetitionState {
pub(crate) fn new( pub(crate) fn new(
set: DeckSettings, set: DeckRunData,
state: 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 word = state.dictionary.read().get_static(word).unwrap();
let stat = state.card_states.read().get_static(stat).unwrap(); let stat = state.card_states.read().get_static(stat).unwrap();
+32 -2
View File
@@ -9,6 +9,7 @@ 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};
@@ -22,6 +23,7 @@ pub struct RepetitionSettingsState {
settings_data: DeckSettings, settings_data: DeckSettings,
state: 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>, filtered_words: Option<u32>,
@@ -38,6 +40,7 @@ impl RepetitionSettingsState {
settings_handle: settings, settings_handle: settings,
state, state,
real_delete: false, real_delete: false,
real_delete_states: false,
correct_filter: true, correct_filter: true,
filter_changed: false, filter_changed: false,
filtered_words: None, filtered_words: None,
@@ -105,7 +108,26 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
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()
} }
@@ -159,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 {
@@ -166,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),
] ]
@@ -222,5 +251,6 @@ pub enum RepetitionSettingsMessage {
SetFilter(String), SetFilter(String),
TryFilter, TryFilter,
DeleteSet, DeleteSet,
DeleteStates,
RevertDeleteSet, RevertDeleteSet,
} }
+19 -18
View File
@@ -33,7 +33,7 @@ use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
pub struct RepetitionsState { pub struct RepetitionsState {
selected_deck_handle: Option<EntityHandle<DeckSettings>>, selected_deck_handle: Option<EntityHandle<DeckSettings>>,
decks: BTreeMap<EntityHandle<DeckSettings>, DeckViewData>, decks: BTreeMap<EntityHandle<DeckSettings>, DeckRunData>,
state: AppState, state: AppState,
} }
@@ -63,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(), self.state.clone()) RepetitionState::new(selected_set.clone(), self.state.clone())
{ {
return Some(Repetition(repetition_state)); return Some(Repetition(repetition_state));
} }
@@ -111,7 +111,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
let handle = self.state.decks.write().create(general.clone()); let handle = self.state.decks.write().create(general.clone());
self.decks.insert( self.decks.insert(
handle, handle,
DeckViewData { DeckRunData {
settings_handle: handle, settings_handle: handle,
general_settings: general, general_settings: general,
existing_words_indices: Default::default(), existing_words_indices: Default::default(),
@@ -258,7 +258,7 @@ impl RepetitionsState {
fn get_decks_from_state( fn get_decks_from_state(
state: &AppState, state: &AppState,
) -> BTreeMap<EntityHandle<DeckSettings>, DeckViewData> { ) -> BTreeMap<EntityHandle<DeckSettings>, DeckRunData> {
state state
.decks .decks
.read() .read()
@@ -267,7 +267,7 @@ impl RepetitionsState {
.map(|(index, deck)| { .map(|(index, deck)| {
( (
EntityHandle::new(deck.id, index, true), EntityHandle::new(deck.id, index, true),
DeckViewData { DeckRunData {
settings_handle: EntityHandle::new(deck.id, index, true), 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(),
@@ -284,7 +284,7 @@ impl RepetitionsState {
} }
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();
@@ -468,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![
@@ -491,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![
@@ -511,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: &DeckViewData) -> 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();
} }
@@ -547,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| {
@@ -593,10 +593,10 @@ impl RepetitionsState {
} }
} }
fn selected_deck(&self) -> Option<&DeckViewData> { fn selected_deck(&self) -> Option<&DeckRunData> {
self.decks.get(&self.selected_deck_handle?) 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_handle?) self.decks.get_mut(&self.selected_deck_handle?)
} }
@@ -616,6 +616,7 @@ impl RepetitionsState {
let total = DeckData::list(&deck, &self.state); let total = DeckData::list(&deck, &self.state);
deck.existing_words_indices = added; deck.existing_words_indices = added;
deck.available_words_indices = total; deck.available_words_indices = total;
deck.indices_ready = true;
self.decks.insert(self.selected_deck_handle.unwrap(), deck); self.decks.insert(self.selected_deck_handle.unwrap(), deck);
} }
} }
@@ -641,18 +642,18 @@ pub enum RepetitionsMessage {
} }
#[derive(Clone)] #[derive(Clone)]
struct DeckViewData { pub struct DeckRunData {
settings_handle: EntityHandle<DeckSettings>, pub settings_handle: EntityHandle<DeckSettings>,
general_settings: DeckSettings, pub general_settings: DeckSettings,
existing_words_indices: Vec<EntityHandle<WordData>>, existing_words_indices: Vec<EntityHandle<WordData>>,
available_words_indices: Vec<EntityHandle<WordData>>, available_words_indices: Vec<EntityHandle<WordData>>,
indices_ready: bool, indices_ready: bool,
append_mode: AppendMode, pub append_mode: AppendMode,
count: Option<usize>, count: Option<usize>,
open_mode: OrderMode, 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 {
+12 -1
View File
@@ -273,7 +273,18 @@ impl SyncState {
.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(); connection.finish();
} }
} }
-1
View File
@@ -1 +0,0 @@
pub mod word_display;
-24
View File
@@ -1,24 +0,0 @@
use iced::Renderer;
use iced_core::{Layout, Length, Rectangle, Size, Theme, Widget};
use iced_core::layout::{Limits, Node};
use iced_core::mouse::Cursor;
use iced_core::renderer::Style;
use iced_core::widget::Tree;
pub struct WordDisplay {
}
impl<Message> Widget<Message, Theme, Renderer> for WordDisplay {
fn size(&self) -> Size<Length> {
todo!()
}
fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
todo!()
}
fn draw(&self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, style: &Style, layout: Layout<'_>, cursor: Cursor, viewport: &Rectangle) {
todo!()
}
}