32 changed files with 262 additions and 931 deletions

No files matched your search

-8
View File
@@ -40,11 +40,3 @@ panic = "abort"
[dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] }
rustc-hash = "2.1.3"
[[bench]]
name = "map_bench"
harness = false
[[bench]]
name = "repo_cache_bench"
harness = false
-126
View File
@@ -1,126 +0,0 @@
use criterion::{Criterion, criterion_group, criterion_main};
use std::collections::HashMap as StdHashMap;
use std::hint::black_box;
use hashbrown::HashMap as BrownHashMap;
const SIZES: [usize; 3] = [5, 10, 50];
const QUERY_COUNT: usize = 4096;
/// Детерминированный ключ ~17 символов, например "key_9e3779b9_0042".
fn make_key(i: u64) -> String {
let h = i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
format!("key_{:08x}_{:04}", h & 0xFFFF_FFFF, i)
}
// Для длинных ключей (пути/URL) замени на:
// fn make_key(i: u64) -> String {
// format!(
// "/api/v2/users/{i}/settings/visibility_{:08x}",
// i.wrapping_mul(0x9E37_79B9)
// )
// }
fn make_vec(n: usize) -> Vec<(String, u64)> {
(0..n as u64).map(|i| (make_key(i), i)).collect()
}
fn make_std_map(data: &[(String, u64)]) -> StdHashMap<String, u64> {
data.iter().map(|(k, v)| (k.clone(), *v)).collect()
}
fn make_brown_map(data: &[(String, u64)]) -> BrownHashMap<String, u64> {
data.iter().map(|(k, v)| (k.clone(), *v)).collect()
}
fn make_queries(n: usize, with_misses: bool) -> Vec<String> {
let keys: Vec<String> = (0..n as u64).map(make_key).collect();
let missing = "__missing_key__".to_string();
(0..QUERY_COUNT)
.map(|i| {
if with_misses && i % 16 == 15 {
missing.clone()
} else {
let idx = i.wrapping_mul(2_654_435_761) % n;
keys[idx].clone()
}
})
.collect()
}
fn lookup_std(map: &StdHashMap<String, u64>, queries: &[String]) -> u64 {
let mut sum = 0u64;
for q in queries {
match map.get(black_box(q.as_str())) {
Some(v) => sum = sum.wrapping_add(*v),
None => sum = sum.wrapping_add(1),
}
}
black_box(sum)
}
fn lookup_brown(map: &BrownHashMap<String, u64>, queries: &[String]) -> u64 {
let mut sum = 0u64;
for q in queries {
match map.get(black_box(q.as_str())) {
Some(v) => sum = sum.wrapping_add(*v),
None => sum = sum.wrapping_add(1),
}
}
black_box(sum)
}
fn lookup_vec(data: &[(String, u64)], queries: &[String]) -> u64 {
let mut sum = 0u64;
for q in queries {
let q = black_box(q);
match data.iter().find(|(k, _)| k == q) {
Some((_, v)) => sum = sum.wrapping_add(*v),
None => sum = sum.wrapping_add(1),
}
}
black_box(sum)
}
fn bench(c: &mut Criterion) {
let mut group = c.benchmark_group("map_vs_vec_str");
for n in SIZES {
let vec_data = make_vec(n);
let std_map = make_std_map(&vec_data);
let brown_map = make_brown_map(&vec_data);
let queries_hit = make_queries(n, false);
let queries_mixed = make_queries(n, true);
// std::collections::HashMap (SipHash)
group.bench_function(format!("std_hashmap_hit/{n}"), |b| {
b.iter(|| lookup_std(black_box(&std_map), black_box(&queries_hit)))
});
group.bench_function(format!("std_hashmap_mixed/{n}"), |b| {
b.iter(|| lookup_std(black_box(&std_map), black_box(&queries_mixed)))
});
// hashbrown (SwissTable + foldhash)
group.bench_function(format!("hashbrown_hit/{n}"), |b| {
b.iter(|| lookup_brown(black_box(&brown_map), black_box(&queries_hit)))
});
group.bench_function(format!("hashbrown_mixed/{n}"), |b| {
b.iter(|| lookup_brown(black_box(&brown_map), black_box(&queries_mixed)))
});
// Vec<(String, u64)>, линейный поиск
group.bench_function(format!("vec_linear_hit/{n}"), |b| {
b.iter(|| lookup_vec(black_box(&vec_data), black_box(&queries_hit)))
});
group.bench_function(format!("vec_linear_mixed/{n}"), |b| {
b.iter(|| lookup_vec(black_box(&vec_data), black_box(&queries_mixed)))
});
}
group.finish();
}
criterion_group!(benches, bench);
criterion_main!(benches);
-325
View File
@@ -1,325 +0,0 @@
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use hashbrown::HashMap as HashbrownMap;
use rustc_hash::FxHashMap;
use std::collections::{BTreeMap, HashMap};
use std::hint::black_box;
// Тестовая структура (аналог вашей сущности)
#[derive(Debug, Clone)]
struct Entity {
id: i64,
data: [u8; 64], // 64 байта данных для реалистичности
}
impl Entity {
fn new(id: i64) -> Self {
Self {
id,
data: [id as u8; 64],
}
}
}
// ========== ТЕСТОВЫЕ ФУНКЦИИ ==========
fn bench_vec_insert_sorted(vec: &mut Vec<Entity>, entities: &[Entity]) {
vec.clear();
for entity in entities {
vec.push(entity.clone());
}
// Vec уже отсортирован, так как ID монотонно возрастают
}
fn bench_vec_search_separated<'a>(ids: &[i64], vec: &'a [Entity], id: i64) -> Option<&'a Entity> {
match ids.binary_search(&id) {
Ok(index) => Some(&vec[index]),
Err(_) => None,
}
}
fn bench_vec_search(vec: &Vec<Entity>, id: i64) -> Option<&Entity> {
match vec.binary_search_by(|probe| probe.id.cmp(&id)) {
Ok(index) => Some(&vec[index]),
Err(_) => None,
}
}
fn bench_btreemap_insert(map: &mut BTreeMap<i64, Entity>, entities: &[Entity]) {
map.clear();
for entity in entities {
map.insert(entity.id, entity.clone());
}
}
fn bench_btreemap_search(map: &BTreeMap<i64, Entity>, id: i64) -> Option<&Entity> {
map.get(&id)
}
fn bench_hashmap_insert(map: &mut HashMap<i64, Entity>, entities: &[Entity]) {
map.clear();
for entity in entities {
map.insert(entity.id, entity.clone());
}
}
fn bench_hashmap_search(map: &HashMap<i64, Entity>, id: i64) -> Option<&Entity> {
map.get(&id)
}
fn bench_hashbrown_insert(map: &mut HashbrownMap<i64, Entity>, entities: &[Entity]) {
map.clear();
for entity in entities {
map.insert(entity.id, entity.clone());
}
}
fn bench_hashbrown_search(map: &HashbrownMap<i64, Entity>, id: i64) -> Option<&Entity> {
map.get(&id)
}
fn bench_fxhashmap_insert(map: &mut FxHashMap<i64, Entity>, entities: &[Entity]) {
map.clear();
for entity in entities {
map.insert(entity.id, entity.clone());
}
}
fn bench_fxhashmap_search(map: &FxHashMap<i64, Entity>, id: i64) -> Option<&Entity> {
map.get(&id)
}
// ========== ГЕНЕРАЦИЯ ДАННЫХ ==========
fn generate_entities(count: usize) -> Vec<Entity> {
(0..count).map(|i| Entity::new(i as i64)).collect()
}
fn generate_search_ids(count: usize, max_id: i64) -> Vec<i64> {
// 70% попаданий, 30% промахов (реалистичный сценарий)
let mut ids = Vec::with_capacity(count);
for i in 0..count {
if i % 10 < 7 {
// Попадание
ids.push((i as i64 * 13) % max_id);
} else {
// Промах (несуществующий ID)
ids.push(max_id + i as i64);
}
}
ids
}
// ========== БЕНЧМАРКИ ==========
fn bench_insert(c: &mut Criterion) {
let mut group = c.benchmark_group("insert");
for size in [10_000, 100_000, 500_000] {
let entities = generate_entities(size);
group.bench_with_input(
BenchmarkId::new("Vec (sorted)", size),
&entities,
|b, entities| {
let mut vec = Vec::with_capacity(size);
b.iter(|| {
bench_vec_insert_sorted(&mut vec, black_box(entities));
});
},
);
group.bench_with_input(
BenchmarkId::new("BTreeMap", size),
&entities,
|b, entities| {
let mut map = BTreeMap::new();
b.iter(|| {
bench_btreemap_insert(&mut map, black_box(entities));
});
},
);
group.bench_with_input(
BenchmarkId::new("HashMap (std)", size),
&entities,
|b, entities| {
let mut map = HashMap::new();
b.iter(|| {
bench_hashmap_insert(&mut map, black_box(entities));
});
},
);
group.bench_with_input(
BenchmarkId::new("HashMap (hashbrown)", size),
&entities,
|b, entities| {
let mut map = HashbrownMap::new();
b.iter(|| {
bench_hashbrown_insert(&mut map, black_box(entities));
});
},
);
group.bench_with_input(
BenchmarkId::new("FxHashMap", size),
&entities,
|b, entities| {
let mut map = FxHashMap::default();
b.iter(|| {
bench_fxhashmap_insert(&mut map, black_box(entities));
});
},
);
}
group.finish();
}
fn bench_search(c: &mut Criterion) {
let mut group = c.benchmark_group("search");
for size in [10_000, 100_000, 500_000] {
let entities = generate_entities(size);
let entity_ids = entities.iter().map(|entity| entity.id).collect::<Vec<_>>();
let search_ids = generate_search_ids(1000, size as i64);
// Подготовка коллекций
let mut vec = Vec::with_capacity(size);
bench_vec_insert_sorted(&mut vec, &entities);
let mut btreemap = BTreeMap::new();
bench_btreemap_insert(&mut btreemap, &entities);
let mut hashmap = HashMap::new();
bench_hashmap_insert(&mut hashmap, &entities);
let mut hashbrown = HashbrownMap::new();
bench_hashbrown_insert(&mut hashbrown, &entities);
let mut fxhashmap = FxHashMap::default();
bench_fxhashmap_insert(&mut fxhashmap, &entities);
group.bench_with_input(
BenchmarkId::new("Vec (binary search)", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_vec_search_separated(&entity_ids, &vec, black_box(id)));
}
});
},
);
group.bench_with_input(
BenchmarkId::new("Vec (binary search) separated", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_vec_search(&vec, black_box(id)));
}
});
},
);
group.bench_with_input(BenchmarkId::new("BTreeMap", size), &search_ids, |b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_btreemap_search(&btreemap, black_box(id)));
}
});
});
group.bench_with_input(
BenchmarkId::new("HashMap (std)", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_hashmap_search(&hashmap, black_box(id)));
}
});
},
);
group.bench_with_input(
BenchmarkId::new("HashMap (hashbrown)", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_hashbrown_search(&hashbrown, black_box(id)));
}
});
},
);
group.bench_with_input(
BenchmarkId::new("FxHashMap", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_fxhashmap_search(&fxhashmap, black_box(id)));
}
});
},
);
}
group.finish();
}
fn bench_memory_efficiency(c: &mut Criterion) {
let mut group = c.benchmark_group("memory_efficiency");
let size = 100_000;
let entities = generate_entities(size);
group.bench_function("Vec memory", |b| {
b.iter(|| {
let mut vec = Vec::with_capacity(size);
bench_vec_insert_sorted(&mut vec, black_box(&entities));
black_box(vec);
});
});
group.bench_function("BTreeMap memory", |b| {
b.iter(|| {
let mut map = BTreeMap::new();
bench_btreemap_insert(&mut map, black_box(&entities));
black_box(map);
});
});
group.bench_function("HashMap (std) memory", |b| {
b.iter(|| {
let mut map = HashMap::new();
bench_hashmap_insert(&mut map, black_box(&entities));
black_box(map);
});
});
group.bench_function("HashMap (hashbrown) memory", |b| {
b.iter(|| {
let mut map = HashbrownMap::new();
bench_hashbrown_insert(&mut map, black_box(&entities));
black_box(map);
});
});
group.bench_function("FxHashMap memory", |b| {
b.iter(|| {
let mut map = FxHashMap::default();
bench_fxhashmap_insert(&mut map, black_box(&entities));
black_box(map);
});
});
group.finish();
}
criterion_group!(benches, bench_insert, bench_search, bench_memory_efficiency);
criterion_main!(benches);
+5 -3
View File
@@ -33,7 +33,9 @@ impl DeckData {
.map(|card_state| EntityHandle::from_id(card_state.word_id))
.collect();
let states = query.handles().collect::<Vec<EntityHandle<CardState>>>();
let fsrs = Fsrs(FsrsModule::new(Self::set_links(states.as_slice(), &states_repo).as_slice()));
let fsrs = Fsrs(FsrsModule::new(
Self::set_links(states.as_slice(), &states_repo).as_slice(),
));
drop(states_repo);
Self {
states,
@@ -61,7 +63,7 @@ impl DeckData {
};
self.current_word_index = Some(index);
(self.words[index].clone(), self.states[index].clone())
(self.words[index], self.states[index])
}
fn set_links<'a>(
@@ -108,7 +110,7 @@ impl DeckData {
timestamp: Utc::now(),
deck_id: self.settings.id,
word_id,
answer: status.clone(),
answer: status,
before: old_score,
after: new_score,
fsrs,
+7 -8
View File
@@ -44,9 +44,7 @@ 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,
}
Self { basket }
}
fn next(&mut self, set: &[&CardState]) -> usize {
@@ -90,16 +88,15 @@ impl SRSModule for SemiRandomSRSModule {
.expect("update weights error");
}
fn new(set: &[&CardState]) -> Self {
let weights = set
.iter()
.map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0)
.collect::<Vec<f32>>();
Self{
Self {
generator: rng(),
last_weights: WeightedIndex::new(weights).expect("Incorrect weights update"),
history: vec![]
last_weights: WeightedIndex::new(weights).expect("Incorrect weights update"),
history: vec![],
}
}
}
@@ -233,7 +230,9 @@ impl SRSModule for FsrsModule {
updated_word.fsrs_open_day = next_day;
updated_word.fsrs_opened = updated_word.last_open.date_naive();
if (updated_word.fsrs_open_day != today && !first_time) || (first_time && status == WordOpenMode::Easy) {
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);
let index = self.current_group.iter().position(|x| *x == index).unwrap();
+7 -13
View File
@@ -1,5 +1,6 @@
use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, WordOpenMode};
use crate::data::{INVALID_ID, Id};
use crate::data_provider::history::HistoryRepositoryInner;
use chrono::{DateTime, Local, NaiveDate, Utc};
use fsrs::MemoryState;
use litemap::LiteMap;
@@ -10,7 +11,6 @@ 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)]
pub struct WordData {
@@ -70,7 +70,7 @@ pub struct HistoryNote {
pub answer: WordOpenMode,
pub before: u8,
pub after: u8,
pub fsrs: bool
pub fsrs: bool,
}
impl EntityData for HistoryNote {
@@ -86,17 +86,16 @@ impl EntityData for HistoryNote {
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 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{
if date == current_date {
count += 1;
}
else {
} else {
counts.push((date, count));
date = current_date;
count = 0;
@@ -209,14 +208,9 @@ pub struct EntityHandle<E> {
}
impl<T> Clone for EntityHandle<T> {
fn clone(&self) -> Self {
EntityHandle {
id: self.id,
index: self.index,
_phantom: Default::default(),
}
}
fn clone(&self) -> Self { *self }
}
impl<T> Copy for EntityHandle<T> {}
impl<T> EntityHandle<T> {
pub fn new(id: Id, index: usize, real: bool) -> Self {
+74 -44
View File
@@ -1,8 +1,9 @@
use crate::data::Id;
use crate::data::entities::{CardState, EntityData, EntityHandle};
use crate::data::entities::{EntityData, EntityHandle};
use crate::data_provider::sqlite::SqliteConnection;
use crate::helpers::into_result;
use crate::notification::{MaybeError, NotificationData};
use log::debug;
use parking_lot::{Mutex, RwLock};
use rayon::iter::IndexedParallelIterator;
use rayon::iter::IntoParallelRefIterator;
@@ -12,7 +13,6 @@ use std::marker::PhantomData;
use std::ops::{Deref, Index, IndexMut};
use std::sync::Arc;
use std::time::Instant;
use log::debug;
pub trait Repository<T, D>
where
@@ -31,13 +31,19 @@ where
fn create_now(&mut self, new: T) -> Result<EntityHandle<T>, NotificationData>;
fn save(&mut self, entity: &mut 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 find<F>(&self, predicate: F) -> Option<EntityHandle<T>>
where
F: Fn(&T) -> bool;
fn init(&mut self) -> MaybeError;
fn start(&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;
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>
@@ -88,9 +94,7 @@ where
.filter(|(_, q)| {
let query = q.lock();
let rule = &query.rule;
return rule(data);
false
rule(data)
})
.map(|(i, _)| i)
.collect()
@@ -118,7 +122,10 @@ where
}
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 DataRepositoryInner<T, D>) -> impl Iterator<Item = &'a T>
pub fn iter<'a>(
&mut self,
data_repository_inner: &'a DataRepositoryInner<T, D>,
) -> impl Iterator<Item = &'a T>
where
T: 'a,
{
@@ -130,7 +137,10 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
.filter_map(move |e| data_repository_inner.get_ref(e))
}
pub fn iter_with_handles<'a>(&mut self, data_repository_inner: &'a DataRepositoryInner<T, D>) -> impl Iterator<Item = (&'a T, EntityHandle<T>)>
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,
{
@@ -139,9 +149,9 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
}
self.cache
.iter_mut()
.filter_map(move |e| Some((data_repository_inner.get_ref(e)?, e.clone())))
.filter_map(move |e| Some((data_repository_inner.get_ref(e)?, *e)))
}
pub fn for_each_mut<F>(&mut self, repo: &mut DataRepositoryInner<T, D>, mut f: F)
where
F: FnMut(&mut T),
@@ -190,19 +200,26 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
.collect::<Vec<_>>(),
);
debug!("Query for {} elements (filtered {}) takes {:?}", data_repository.len(), self.cache.len(), time.elapsed());
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> {
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 {
@@ -210,7 +227,9 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> In
}
}
impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> IndexMut<usize> for Query<T, R, D> {
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]
}
@@ -221,7 +240,10 @@ where
D: BatchedDbLayer<T>,
T: EntityData,
{
fn create_now_many(&mut self, entities: &mut [T]) -> Result<Vec<EntityHandle<T>>, NotificationData>;
fn create_now_many(
&mut self,
entities: &mut [T],
) -> Result<Vec<EntityHandle<T>>, NotificationData>;
fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
fn delete_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
}
@@ -257,10 +279,10 @@ pub struct DataRepositoryInner<T: Clone + EntityData, D: DbLayer<T>> {
shifts: usize,
}
impl<T: Clone + EntityData, D: DbLayer<T> + Default> 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{
Self {
inner: Arc::new(RwLock::new(DataRepositoryInner {
cache: vec![],
temporal: vec![],
next_temporal: 0,
@@ -274,7 +296,6 @@ impl<T: Clone + EntityData, D: DbLayer<T> + Default> DataRepository<T, D> {
}
}
impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
type Target = Arc<RwLock<DataRepositoryInner<T, D>>>;
@@ -283,7 +304,9 @@ impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
}
}
impl<T: EntityData + Clone + std::marker::Send + std::marker::Sync, 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(
&self,
rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
@@ -306,7 +329,7 @@ impl<T: EntityData + Clone + std::marker::Send + std::marker::Sync, D: DbLayer<T
pub fn query(
&self,
rule: Arc<dyn Fn(& T) -> bool + Send + Sync>,
rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
) -> Query<T, DataRepositoryInner<T, D>, D>
where
T: Send + Sync,
@@ -323,9 +346,9 @@ impl<T: EntityData + Clone + std::marker::Send + std::marker::Sync, D: DbLayer<T
query
}
pub fn empty_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query{
Query {
cache: vec![],
rule: Arc::new(|_| false),
parallel: false,
@@ -335,7 +358,7 @@ impl<T: EntityData + Clone + std::marker::Send + std::marker::Sync, D: DbLayer<T
}
pub fn empty_query_par(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query{
Query {
cache: vec![],
rule: Arc::new(|_| false),
parallel: true,
@@ -433,7 +456,7 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
}
fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError {
let original_state = entity.clone();
let original_state = *entity;
if entity.real() {
let data = self.get_ref(entity).unwrap();
self.data_layer
@@ -441,12 +464,11 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
} else {
let index = fast_position_of(self.temporal.as_slice(), entity).unwrap();
let data = &mut self.temporal[index];
self.data_layer
.add_entity(data, &self.connection.lock())?;
self.data_layer.add_entity(data, &self.connection.lock())?;
entity.set_real(true);
entity.id = data.id();
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);
@@ -477,16 +499,16 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>>
where
F: Fn(&T) -> bool
F: Fn(&T) -> bool,
{
if let Some(index) = self.cache.iter().position(|entity| predicate(entity)){
if let Some(index) = self.cache.iter().position(&predicate) {
let value = &self.cache[index];
return Some(EntityHandle::new(value.id(), index, true))
return Some(EntityHandle::new(value.id(), index, true));
}
if let Some(index) = self.temporal.iter().position(|entity| predicate(entity)){
if let Some(index) = self.temporal.iter().position(predicate) {
let value = &self.temporal[index];
return Some(EntityHandle::new(value.id(), index, false))
return Some(EntityHandle::new(value.id(), index, false));
}
None
}
@@ -518,16 +540,16 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
self.shifts
}
fn iter<'a>(&'a self) -> impl Iterator<Item=&'a T>
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T>
where
T: 'a
T: 'a,
{
self.cache.iter().chain(self.temporal.iter())
}
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item=&'a mut T>
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut T>
where
T: 'a
T: 'a,
{
self.cache.iter_mut().chain(self.temporal.iter_mut())
}
@@ -535,10 +557,18 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository<T, D>
for DataRepositoryInner<T, D>
{
fn create_now_many(&mut self, entities: &mut [T]) -> Result<Vec<EntityHandle<T>>, NotificationData> {
self.data_layer.add_many_entities(entities, &mut self.connection.lock())?;
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();
let handles = entities
.iter()
.enumerate()
.map(|(index, entity)| EntityHandle::new(entity.id(), index + len, true))
.collect();
self.cache.extend(entities.to_vec());
Ok(handles)
}
@@ -577,7 +607,7 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
let start = self.cache.len();
for (i, (handle, data)) in temp.into_iter().zip(&mut temp_values).enumerate() {
let old = handle.clone();
let old = *handle;
handle.id = data.id();
handle.set_index(start + i);
handle.set_real(true);
@@ -641,7 +671,7 @@ where
{
let mut temp = Vec::with_capacity(handles.len() / 2);
let mut cached = Vec::with_capacity(handles.len() / 2);
handles.sort_by(|a, b| a.id.cmp(&b.id));
handles.sort_by_key(|a| a.id);
handles.iter().for_each(|handle| {
if handle.real() {
cached.push(getter(handle))
+12 -44
View File
@@ -1,20 +1,14 @@
use std::sync::Arc;
use crate::data::entities::{CardState, DeckSettings};
use crate::data::entities::CardState;
use crate::data::repositories::{DataRepository, DataRepositoryInner, Query};
use crate::data::Id;
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData};
use chrono::NaiveDate;
use fsrs::MemoryState;
use log::info;
use rusqlite::{Connection, Error, Params, Row, params};
use std::time::Instant;
use crate::data::Id;
use rusqlite::{Error, Params, Row};
use std::sync::Arc;
pub type CardStateRepository =
DataRepository<CardState, CardStateDbLayer>;
pub type CardStateRepositoryInner =
DataRepositoryInner<CardState, CardStateDbLayer>;
pub type CardStateRepository = DataRepository<CardState, CardStateDbLayer>;
pub type CardStateRepositoryInner = DataRepositoryInner<CardState, CardStateDbLayer>;
pub type CardStateDbLayer = SqliteDbLayer<CardState, CardStatesQuerySet>;
#[derive(Clone)]
pub struct CardStatesQuerySet;
@@ -73,35 +67,6 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
)
}
}
pub fn load_stats_of_deck(
set: &DeckSettings,
connection: &Connection,
) -> Result<Vec<CardState>, NotificationData> {
let mut stmt = connection
.prepare("SELECT id, word_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states WHERE set_id = ?1")
.expect("Failed to prepare state query");
let iter = stmt
.query_map((set.id,), |row| {
Ok(CardState {
id: row.get(0)?,
word_id: row.get(1)?,
deck_id: set.id,
score: row.get(2)?,
fsrs_opened: NaiveDate::from_epoch_days(row.get::<usize, i32>(4)?)
.expect("Incorrect date encoding"),
fsrs_memory: get_memory(row.get(5)?),
last_open: row.get(3)?,
fsrs_open_day: NaiveDate::from_epoch_days(row.get::<usize, i32>(6)?)
.expect("Incorrect date encoding"),
})
})
.map_err(|_| DbLoadError)?;
let mut buffer = Vec::with_capacity(512);
iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i));
Ok(buffer)
}
fn get_memory(value: i64) -> Option<MemoryState> {
if value == 0 {
@@ -122,7 +87,10 @@ fn memory_to_i64(value: &Option<MemoryState>) -> i64 {
}
impl CardStateRepositoryInner {
pub fn deck_states(&self, deck_id: Id) -> Query<CardState, CardStateRepositoryInner, CardStateDbLayer> {
self.query(Arc::new(move |state| {state.deck_id == deck_id}))
pub fn deck_states(
&self,
deck_id: Id,
) -> Query<CardState, CardStateRepositoryInner, CardStateDbLayer> {
self.query(Arc::new(move |state| state.deck_id == deck_id))
}
}
}
+2 -1
View File
@@ -3,7 +3,8 @@ 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 type CustomFieldRepository =
DataRepository<CustomField, SqliteDbLayer<CustomField, CustomFieldsQuerySet>>;
pub struct CustomFieldsQuerySet;
impl TableAccessQuerySet<WordGroup> for CustomFieldsQuerySet {
+1 -1
View File
@@ -52,4 +52,4 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
entity.id,
)
}
}
}
+3 -3
View File
@@ -77,7 +77,7 @@ impl FromSql for WordOpenMode {
2 => Ok(WordOpenMode::Ok),
3 => Ok(WordOpenMode::Easy),
_ => Ok(WordOpenMode::None),
}
};
}
Ok(WordOpenMode::None)
}
@@ -89,7 +89,7 @@ impl ToSql for WordOpenMode {
WordOpenMode::Easy => 3,
WordOpenMode::Ok => 2,
WordOpenMode::Hard => 1,
WordOpenMode::None => 0
WordOpenMode::None => 0,
}))
}
}
}
+1 -1
View File
@@ -1,10 +1,10 @@
use std::collections::BTreeMap;
use crate::helpers::into_result;
use crate::notification::NotificationData;
use crate::notification::NotificationData::IncorrectAnkiFile;
use log::info;
use rusqlite::Connection;
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone)]
pub struct ImportData(pub(crate) Vec<ImportGroup>);
+6 -2
View File
@@ -67,7 +67,11 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
let time = Instant::now();
let index: u32 = connection
.query_row(format!("{} RETURNING {}", Q::add(), Q::id_field()).as_str(), 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
@@ -104,7 +108,7 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
.prepare(Q::load())
.expect("Failed to prepare state query");
let iter = stmt
.query_map([], |row| Ok(Q::from_sql(row)?))
.query_map([], |row| Q::from_sql(row))
.map_err(|e| {
error!("{}", e);
DbSaveError
+5 -4
View File
@@ -1,7 +1,7 @@
use crate::fs::*;
use log::error;
use rusqlite::Connection;
use std::ops::{Deref, DerefMut};
use log::error;
pub fn create_db() {
let db_file = db_path();
@@ -130,6 +130,7 @@ impl SqliteConnection {
}
impl Drop for SqliteConnection {
fn drop(&mut self) { self.finish(); }
}
fn drop(&mut self) {
self.finish();
}
}
+3 -172
View File
@@ -1,9 +1,7 @@
use crate::data::entities::{WordData, WordGroup};
use crate::data::entities::WordData;
use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData};
use rusqlite::{Connection, Error, Params, Row, params};
use rusqlite::{Error, Params, Row};
pub type WordRepository = DataRepository<WordData, WordDataLayer>;
pub type WordRepositoryInner = DataRepositoryInner<WordData, WordDataLayer>;
@@ -58,171 +56,4 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
entity.id,
)
}
}
pub fn add_word(word: &mut WordData, connection: &Connection) -> MaybeError {
let index : u32 = connection
.query_row(
"INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5)",
(
&word.key,
&word.value,
&word.tags,
serde_json::to_string(&word.additional).unwrap_or_default(),
&word.group_id,
),
|row| row.get(0),
).map_err(|_| DbSaveError)?;
word.id = index.into();
Ok(())
}
pub fn add_words(words: &mut [WordData], connection: &mut Connection) -> MaybeError {
let tx = connection.transaction().map_err(|_| DbLoadError)?;
let count = words.len();
{
let mut stmt = tx
.prepare(
"INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5)",
)
.expect("Failed to prepare statement");
for word in words.iter() {
stmt.execute(params![
word.key,
word.value,
word.tags,
serde_json::to_string(&word.additional).unwrap_or_default(),
word.group_id
])
.map_err(|_| DbSaveError)?;
}
}
let last_index: u32 = tx.last_insert_rowid() as u32;
tx.commit().map_err(|_| DbSaveError)?;
let start_index = last_index - (count as u32) + 1;
for (index, id) in (start_index..=last_index).enumerate() {
words[index].id = id.into();
}
Ok(())
}
pub fn update_word(word: &mut WordData, connection: &Connection) -> MaybeError {
if !word.id.is_valid() {
Ok(add_word(word, connection)?)
} else {
connection
.execute(
"UPDATE words SET key = ?1, value = ?2, tags = ?3, more = ?4 WHERE id = ?5",
(
&word.key,
&word.value,
&word.tags,
serde_json::to_string(&word.additional).map_err(|_| DbSaveError)?,
&word.id,
),
)
.map_err(|_| DbSaveError)?;
Ok(())
}
}
pub fn delete_word(word: &WordData, connection: &Connection) -> MaybeError {
if !word.id.is_valid() {
return Err(DbSaveError);
}
connection
.execute("DELETE FROM words WHERE id = ?1", (&word.id,))
.map_err(|_| DbSaveError)?;
Ok(())
}
pub fn load_words(connection: &Connection) -> Result<Vec<WordData>, NotificationData> {
let mut stmt = connection
.prepare("SELECT id, key, value, tags, more, group_id FROM words")
.expect("Failed to prepare statement");
let word_iter = stmt
.query_map([], |row| {
let additional: String = row.get(4)?;
Ok(WordData {
id: row.get(0)?,
key: row.get(1)?,
value: row.get(2)?,
tags: row.get(3)?,
additional: serde_json::from_str(&additional).unwrap_or_default(),
group_id: row.get(5)?,
})
})
.map_err(|_| DbLoadError)?;
let mut buffer = Vec::with_capacity(1024);
word_iter
.filter_map(|res| res.ok())
.for_each(|i| buffer.push(i));
Ok(buffer)
}
pub fn load_word_groups(connection: &Connection) -> Result<Vec<WordGroup>, NotificationData> {
let mut stmt = connection
.prepare("SELECT id, name FROM word_groups")
.expect("Failed to prepare statement");
let group_iter = stmt
.query_map([], |row| {
Ok(WordGroup {
id: row.get(0)?,
name: row.get(1)?,
})
})
.map_err(|_| DbLoadError)?;
let mut buffer = Vec::with_capacity(8);
group_iter
.filter_map(|res| res.ok())
.for_each(|i| buffer.push(i));
Ok(buffer)
}
pub fn add_group(group: &mut WordGroup, connection: &Connection) -> MaybeError {
let index: u32 = connection
.query_row(
"INSERT INTO word_groups (name) VALUES (?1)",
(&group.name,),
|row| row.get(0),
)
.map_err(|_| DbSaveError)?;
group.id = index.into();
Ok(())
}
pub fn update_group(group: &mut WordGroup, connection: &Connection) -> MaybeError {
if !group.id.is_valid() {
Ok(add_group(group, connection)?)
} else {
connection
.execute(
"UPDATE word_groups SET name = ?1 WHERE id = ?2",
(&group.name, &group.id),
)
.map_err(|_| DbSaveError)?;
Ok(())
}
}
pub fn delete_group(group: &WordGroup, connection: &Connection) -> MaybeError {
if !group.id.is_valid() {
return Err(DbSaveError);
}
connection
.execute("DELETE FROM word_groups WHERE id = ?1", (&group.id,))
.map_err(|_| DbSaveError)?;
Ok(())
}
}
-8
View File
@@ -28,14 +28,6 @@ pub fn db_meta_path() -> PathBuf {
app_data_dir().join("data.meta")
}
pub fn history_dir() -> Result<PathBuf, NotificationData> {
let directory = app_data_dir().join("history");
if !directory.exists() {
fs::create_dir_all(directory.clone()).map_err(|_| PermissionDenied)?;
}
Ok(directory)
}
pub fn voice_dir() -> Result<PathBuf, NotificationData> {
let directory = app_data_dir().join("voice");
if !directory.exists() {
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::app::{fill_state, AppState};
use crate::data_provider::sqlite::{create_db};
use crate::app::{AppState, fill_state};
use crate::data_provider::sqlite::create_db;
use crate::data_provider::web_api::{
get_local_version, get_web_version, load_data, set_local_version,
};
+23 -24
View File
@@ -11,9 +11,9 @@ use crate::pages::dictionary::DictionaryMessage::*;
use crate::pages::dictionary_quiz::DictionaryQuizState;
use crate::pages::import::ImportState;
use crate::pages::word::WordState;
use crate::push_error;
use crate::push_error_task;
use crate::styling::*;
use crate::{jl_button, push_error};
use chrono::{DateTime, TimeDelta, Utc};
use hashbrown::{HashMap, HashSet};
use iced::alignment::Vertical::Center;
@@ -81,7 +81,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let mut words = vec![];
let dict = &self.state.dictionary.read();
let mut query = dict.query(Arc::new(|_| true));
let query = dict.query(Arc::new(|_| true));
let handles = query.handles();
words = handles.filter(|h| self.included.contains(h)).collect();
@@ -257,7 +257,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let mut dictionary = self.state.dictionary.write();
let reload_query = handle.real() == false;
let reload_query = !handle.real();
let _ = dictionary.save(&mut handle);
if reload_query {
self.displayed_words_query.load(&dictionary);
@@ -373,7 +373,7 @@ impl DictionaryState {
key: word.key.clone().into(),
value: word.value.clone().into(),
tags: word.tags.clone().into(),
handle: word_handle.clone(),
handle: *word_handle,
};
let lazy_line = lazy(word_line_data, Self::word_line);
@@ -387,7 +387,7 @@ impl DictionaryState {
fn word_line<'a>(data: &WordLineState) -> Row<'a, DictionaryMessage> {
let mut line = Row::new().width(Fill).align_y(Center);
let handle = data.handle.clone();
let handle = data.handle;
line = line.push(
checkbox(data.is_included)
.label("")
@@ -434,15 +434,13 @@ impl DictionaryState {
if !data.handle.real() {
return button("-")
.on_press(WordAction(data.handle))
.style(
|_x: &iced::Theme, _status: iced::widget::button::Status| Style {
background: None,
text_color: Color::BLACK,
border: Border::default(),
shadow: Shadow::default(),
snap: false,
},
)
.style(|_x: &Theme, _status: button::Status| Style {
background: None,
text_color: Color::BLACK,
border: Border::default(),
shadow: Shadow::default(),
snap: false,
})
.width(15);
}
@@ -512,15 +510,16 @@ impl DictionaryState {
let dict = &self.state.dictionary.write();
let mut words_query = dict.full_query();
let handles = words_query.handles().collect::<Vec<_>>();
words_query.iter(dict).zip(handles).for_each(|(element, handle)| {
let tags = split_with_coma(element.tags.as_str());
tags.iter()
.for_each(|tag| {
words_query
.iter(dict)
.zip(handles)
.for_each(|(element, handle)| {
let tags = split_with_coma(element.tags.as_str());
tags.iter().for_each(|tag| {
tags_list.insert(tag.clone());
});
self.words_tags.push((handle, tags))
});
self.words_tags.push((handle, tags))
});
let current_tags = self
.tag_map
@@ -590,12 +589,12 @@ impl DictionaryState {
}
let groups = self.state.word_groups.read();
let group = groups.get_static(self.selected_group.clone()).unwrap();
let group = groups.get_static(self.selected_group).unwrap();
iced::widget::column![
scrollable(row).width(Fill).horizontal(),
row![
button("⇳").on_press(ChangeDirection).style(jl_button),
jl_button!("⇳").on_press(ChangeDirection),
text_input("Название группы слов", &group.name)
.on_input(EditGroup)
.width(250)
@@ -617,7 +616,7 @@ impl DictionaryState {
}
fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> {
let button = button("Добавить слово").style(jl_button);
let button = jl_button!("Добавить слово");
if self.selected_group.real() {
return button.on_press(NewWord).into();
+17 -16
View File
@@ -1,4 +1,7 @@
use crate::app::AppState;
use crate::data::entities::{EntityHandle, WordData};
use crate::data::repositories::Repository;
use crate::jl_button;
use crate::navigation::Page::PreviousPage;
use crate::navigation::RootMessage;
use crate::navigation::*;
@@ -6,14 +9,12 @@ use crate::pages::dictionary::split_with_coma;
use crate::pages::dictionary_quiz::DictionaryQuizMessage::*;
use crate::pages::quiz::Score;
use crate::styling::*;
use iced::Background::Color;
use iced::border::Radius;
use iced::widget::container::Style;
use iced::widget::{Row, button, container, row, space, text, text_input};
use iced::{Border, Element, Fill, Task, Theme, alignment};
use iced::widget::{container, row, space, text, text_input, Row};
use iced::Background::Color;
use iced::{alignment, Border, Element, Fill, Task, Theme};
use rand::prelude::SliceRandom;
use crate::app::AppState;
use crate::data::repositories::Repository;
pub struct DictionaryQuizState {
state: AppState,
@@ -90,11 +91,8 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
.size(25),
]
.spacing(DEFAULT_SPACING),
row![
button("Закончить").style(jl_button).on_press(Back),
self.appeal_button()
]
.spacing(DEFAULT_SPACING),
row![jl_button!("Закончить").on_press(Back), self.appeal_button()]
.spacing(DEFAULT_SPACING),
]
.spacing(DEFAULT_SPACING)
.align_x(alignment::Horizontal::Center),
@@ -106,8 +104,13 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
}
impl DictionaryQuizState {
pub fn new(words: Vec<EntityHandle<WordData>>, reverse: bool, no_typing: bool, state: AppState) -> DictionaryQuizState {
let state = DictionaryQuizState {
pub fn new(
words: Vec<EntityHandle<WordData>>,
reverse: bool,
no_typing: bool,
state: AppState,
) -> DictionaryQuizState {
Self {
state,
words,
current_set: Vec::new(),
@@ -119,9 +122,7 @@ impl DictionaryQuizState {
reverse,
laps: 0,
no_typing,
};
state
}
}
fn submit(&mut self) {
@@ -218,7 +219,7 @@ impl DictionaryQuizState {
fn appeal_button(&self) -> Element<'_, DictionaryQuizMessage> {
if self.is_help && !self.no_typing {
return button("Апелляция").style(jl_button).on_press(Appeal).into();
return jl_button!("Апелляция").on_press(Appeal).into();
}
space().into()
}
+23 -30
View File
@@ -3,18 +3,16 @@ use crate::data::entities::{HistoryNote, WordData};
use crate::data::repositories::Repository;
use crate::navigation::*;
use crate::styling::*;
use HistoryMessage::Back;
use chrono::Local;
use iced::alignment::Horizontal::Center;
use iced::widget::space::horizontal;
use iced::widget::*;
use iced::{Element, Fill, FillPortion, Task};
use std::sync::Arc;
use HistoryMessage::Back;
#[derive(Clone)]
pub struct HistoryState {
list: Vec<(WordData, HistoryNote)>,
pub state: AppState,
}
impl NavigatedPage<HistoryMessage> for HistoryState {
@@ -37,9 +35,9 @@ impl NavigatedPage<HistoryMessage> for HistoryState {
.width(FillPortion(5)),
horizontal().width(FillPortion(1))
]
.height(Fill)
.width(Fill)
.into(),
.height(Fill)
.width(Fill)
.into(),
Back,
)
}
@@ -50,57 +48,52 @@ impl HistoryState {
let dict = &state.dictionary.read();
let history_repo = &mut state.history.write();
history_repo.init().expect("TODO: panic message");
let id = id.clone();
let mut history = history_repo.query(Arc::new(move |h| h.deck_id == id));
let words = history
.iter(history_repo)
.map(
|item| {let handle = dict.handle_by_id(item.word_id);
let word = dict.get_static(handle).unwrap_or(WordData {
id: Default::default(),
group_id: Default::default(),
key: "Удаленное слово".to_string(),
value: "".to_string(),
tags: "".to_string(),
additional: Default::default(),
});
(word, item.clone())
}
)
.map(|item| {
let handle = dict.handle_by_id(item.word_id);
let word = dict.get_static(handle).unwrap_or(WordData {
id: Default::default(),
group_id: Default::default(),
key: "Удаленное слово".to_string(),
value: "".to_string(),
tags: "".to_string(),
additional: Default::default(),
});
(word, item.clone())
})
.collect();
Self {
list: words,
state: state.clone(),
}
}
fn history_lines(&self) -> Column<'_, HistoryMessage> {
let mut column = Column::new();
for (index, item) in &self.list {
let activity = item;
column = column.push(
row![
iced::widget::column![
text!("{}", index.key)
.width(Fill)
.align_x(Center),
text!("{}", index.value)
.width(Fill)
.align_x(Center),
text!("{}", index.key).width(Fill).align_x(Center),
text!("{}", index.value).width(Fill).align_x(Center),
]
.align_x(Center),
iced::widget::column![
text!("{} ➞ {}", activity.before, activity.after),
text!(
"{}",
activity.timestamp.with_timezone(&Local).format("%d.%m %H:%M")
activity
.timestamp
.with_timezone(&Local)
.format("%d.%m %H:%M")
)
]
.spacing(5)
.align_x(Center)
]
.spacing(5),
.spacing(5),
);
column = column.push(rule::horizontal(1));
}
+5 -5
View File
@@ -1,9 +1,10 @@
use crate::app::AppState;
use crate::data::entities::EntityData;
use crate::data::entities::WordData;
use crate::data::entities::WordGroup;
use crate::data::repositories::{BatchedRepository, Repository};
use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups};
use crate::fs::*;
use crate::jl_button;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::NotificationData::OpenFileError;
use crate::notification::{MaybeError, NotificationData};
@@ -30,11 +31,9 @@ use std::path::PathBuf;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use zip::ZipArchive;
use crate::data::repositories::{BatchedRepository, Repository};
const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"];
#[derive(Clone)]
pub struct ImportState {
state: AppState,
path: Option<PathBuf>,
@@ -155,7 +154,7 @@ impl NavigatedPage<ImportMessage> for ImportState {
scrollable(
column![
row![
button("Выбрать файл").style(jl_button).on_press(SelectFile),
jl_button!("Выбрать файл").on_press(SelectFile),
text!("{}", {
if let Some(path) = &self.path {
path.to_string_lossy().to_string()
@@ -435,7 +434,8 @@ impl ImportState {
async move {
spawn_blocking(move || {
let temp_file_path = temp_import_path();
let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?;
let connection =
Connection::open(&temp_file_path).map_err(|_| OpenFileError)?;
let import_list = get_words_of_group(&connection, group.id)?;
let mut words_list = Vec::with_capacity(import_list.len());
+2 -2
View File
@@ -1,5 +1,6 @@
use crate::app::USER_FONT;
use crate::core::kana_set::KanaSet;
use crate::jl_button;
use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::quiz::QuizMessage::*;
@@ -8,7 +9,6 @@ use iced::widget::*;
use iced::{Element, Fill, Task, alignment};
use rand::seq::SliceRandom;
#[derive(Clone, Debug)]
pub struct QuizState {
kana: String,
current_roman: String,
@@ -82,7 +82,7 @@ impl NavigatedPage<QuizMessage> for QuizState {
.width(150)
.on_input(ContentChanged),
score_display(&self.score),
button("Закончить").style(jl_button).on_press(Back),
jl_button!("Закончить").on_press(Back),
]
.spacing(DEFAULT_SPACING)
.align_x(alignment::Horizontal::Center),
+4 -4
View File
@@ -1,12 +1,12 @@
use crate::jl_button;
use crate::navigation::RootMessage;
use crate::navigation::{NavigatedPage, Page};
use crate::pages::randomizer::RandomizerMessage::{Back, Edit, Start};
use crate::styling::{HALF_SPACING, back_overlay, jl_button};
use crate::styling::{HALF_SPACING, back_overlay};
use iced::Task;
use iced::widget::{button, text_editor};
use iced::widget::text_editor;
use rand::prelude::SliceRandom;
#[derive(Clone, Debug)]
pub struct RandomizerState {
text: text_editor::Content,
list: Vec<String>,
@@ -56,7 +56,7 @@ impl NavigatedPage<RandomizerMessage> for RandomizerState {
.width(400)
.height(400)
.placeholder("Каждый элемент с новой строки"),
button("Перемешать").style(jl_button).on_press(Start),
jl_button!("Перемешать").on_press(Start),
]
.spacing(HALF_SPACING)
.into(),
+2 -2
View File
@@ -5,6 +5,7 @@ use crate::data::Id;
use crate::data::entities::{CardState, WordData};
use crate::data::repositories::Repository;
use crate::data_provider::voice::get_voice;
use crate::jl_button;
use crate::navigation::Page::PreviousPage;
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
use crate::notification::{MaybeError, NotificationData};
@@ -255,8 +256,7 @@ impl RepetitionState {
}
fn draw_voice(&self) -> Element<'_, RepetitionMessage> {
button("Воспроизвести")
.style(jl_button)
jl_button!("Воспроизвести")
.on_press(RepetitionMessage::Play)
.into()
}
+8 -6
View File
@@ -2,6 +2,7 @@ use crate::app::AppState;
use crate::core::deck::DeckData;
use crate::data::entities::{DeckSettings, EntityHandle};
use crate::data::repositories::{BatchedRepository, Repository};
use crate::jl_button;
use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::MaybeError;
@@ -122,7 +123,10 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
}
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<_>>();
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);
}
@@ -163,9 +167,7 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
primary
})
.on_input(SetFilter),
button("Проверить фильтр")
.style(jl_button)
.on_press(TryFilter),
jl_button!("Проверить фильтр").on_press(TryFilter),
self.count_view(),
]
.spacing(QUARTER_SPACING),
@@ -180,7 +182,7 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
)
.height(Fill),
row![
button("Сохранить").style(jl_button).on_press(Save),
jl_button!("Сохранить").on_press(Save),
horizontal(),
button(if self.real_delete_states {
"Вы уверены?"
@@ -211,7 +213,7 @@ fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError {
let deck_id = deck.id;
let mut db_stats = states_repo
.query_par(Arc::new(move |s| s.deck_id == deck_id))
.iter(&states_repo)
.iter(states_repo)
.collect::<Vec<_>>();
let filtered_words = DeckData::list(deck, state)
.iter()
+15 -23
View File
@@ -3,6 +3,7 @@ use crate::core::deck::DeckData;
use crate::core::srs::{AppendMode, OrderMode};
use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData};
use crate::data::repositories::{BatchedRepository, Repository};
use crate::jl_button;
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::MaybeError;
@@ -15,22 +16,21 @@ use crate::push_error_task;
use crate::styling::*;
use chrono::{Days, Local, Utc};
use hashbrown::HashSet;
use iced::widget::button::{danger, Status};
pub use iced::widget::button::{Catalog, Style};
use iced::widget::button::{Status, danger};
use iced::widget::tooltip::Position::Top;
use iced::widget::{
Column, Row, button, column, container, lazy, radio, row, scrollable, space, svg, text,
text_input, tooltip,
button, column, container, lazy, radio, row, scrollable, space, svg, text, text_input, tooltip,
Column, Row,
};
use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme};
use iced_core::Padding;
use iced_core::border::Radius;
use iced_core::svg::Handle;
use iced_core::Padding;
use std::collections::BTreeMap;
use std::ops::Deref;
use std::sync::Arc;
#[derive(Clone)]
pub struct RepetitionsState {
selected_deck_handle: Option<EntityHandle<DeckSettings>>,
decks: BTreeMap<EntityHandle<DeckSettings>, DeckRunData>,
@@ -94,7 +94,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
self.clear_selection();
}
} else {
let _ = self.fill_deck_word_indices();
self.fill_deck_word_indices();
}
}
fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> {
@@ -126,7 +126,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
DeleteSet => {
let index = self.selected_deck_handle.unwrap();
self.clear_selection();
let deck = self.decks.remove(&index);
self.decks.remove(&index);
if index.real() {
let mut repo = self.state.decks.write();
@@ -152,7 +152,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
let mut repo = self.state.decks.write();
repo.set(handle, deck.general_settings.clone());
let old = handle.clone();
let old = *handle;
if let Err(err) = repo.save(handle) {
return push_error_task!(err);
}
@@ -175,7 +175,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
}
TryFilter => {
let deck = self.selected_deck().unwrap();
let count = DeckData::list(&deck, &self.state).len();
let count = DeckData::list(deck, &self.state).len();
let deck = self.selected_deck_mut().unwrap();
deck.count = Some(count);
}
@@ -227,10 +227,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
row![
column![
scrollable(self.decks_list()).height(Fill),
button("Добавить")
.style(jl_button)
.width(Fill)
.on_press(CreateSet),
jl_button!("Добавить").width(Fill).on_press(CreateSet),
]
.spacing(DEFAULT_SPACING)
.width(Length::FillPortion(1)),
@@ -256,9 +253,7 @@ impl RepetitionsState {
}
}
fn get_decks_from_state(
state: &AppState,
) -> BTreeMap<EntityHandle<DeckSettings>, DeckRunData> {
fn get_decks_from_state(state: &AppState) -> BTreeMap<EntityHandle<DeckSettings>, DeckRunData> {
state
.decks
.read()
@@ -331,9 +326,7 @@ impl RepetitionsState {
column![
text!("Фильтр"),
text_input("", &deck.filter).on_input(SetFilter),
button("Проверить фильтр")
.style(jl_button)
.on_press(TryFilter),
jl_button!("Проверить фильтр").on_press(TryFilter),
self.count_view(deck),
]
.spacing(QUARTER_SPACING),
@@ -361,7 +354,7 @@ impl RepetitionsState {
Some(deck.open_mode),
SetOpenMode
),
button("История").style(jl_button).on_press(GoToHistory),
jl_button!("История").on_press(GoToHistory),
]
.spacing(HALF_SPACING)
}
@@ -378,7 +371,7 @@ impl RepetitionsState {
)
.height(Fill),
row![
button("Сохранить").style(jl_button).on_press(Save),
jl_button!("Сохранить").on_press(Save),
self.launch_delete_button(deck)
]
.spacing(DEFAULT_SPACING)
@@ -520,7 +513,7 @@ impl RepetitionsState {
fn decks_list(&self) -> Column<'_, RepetitionsMessage> {
let mut column = Column::new();
for (i, (handle, deck)) in self.decks.iter().enumerate() {
for (handle, deck) in self.decks.iter() {
column = column.push(
button(text!("{}", &deck.name))
.on_press_with(move || SelectSet(*handle))
@@ -566,7 +559,6 @@ impl RepetitionsState {
deck: &DeckSettings,
indices: impl Iterator<Item = EntityHandle<WordData>>,
) -> MaybeError {
let words = &state.dictionary;
let stats = &mut indices
.map(|i| CardState {
id: 0.into(),
+3 -6
View File
@@ -1,5 +1,6 @@
use crate::app::AppState;
use crate::core::kana_set::{KanaSet, KanaType};
use crate::jl_button;
use crate::navigation::Page::*;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::dictionary::DictionaryState;
@@ -12,12 +13,10 @@ use crate::pages::writing::WritingState;
use crate::styling::*;
use iced::widget::button::{Status, Style};
use iced::widget::*;
use iced::{Element, Task, alignment};
use iced::{alignment, Element, Task};
use iced_core::Alignment::Center;
use iced_core::Background;
use iced_core::Color;
use parking_lot::Mutex;
use std::sync::Arc;
pub struct SelectorState {
pub set: KanaSet,
@@ -81,9 +80,7 @@ impl NavigatedPage<SelectorMessage> for SelectorState {
container(
iced::widget::column![
row![
button("あ ↔ ア")
.on_press(SelectorMessage::Change)
.style(jl_button),
jl_button!("あ ↔ ア").on_press(SelectorMessage::Change),
button("Словарь")
.on_press(SelectorMessage::ToDictionary)
.style(Self::nav_style),
+10 -17
View File
@@ -2,6 +2,7 @@ use crate::app::AppState;
use crate::data::repositories::Repository;
use crate::data_provider::settings::{delete_settings, set_setting};
use crate::data_provider::web_api::*;
use crate::jl_button;
use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::{NotificationData, NotificationRequest};
@@ -31,7 +32,6 @@ pub enum SyncMessage {
DisableSync,
SwitchAutoSync,
}
#[derive(Clone)]
pub struct SyncState {
state: AppState,
auto_fetch: bool,
@@ -221,14 +221,13 @@ impl SyncState {
.padding(3)
)
.style(rounded_box),
button("Скопировать в буфер обмена")
.style(jl_button)
jl_button!("Скопировать в буфер обмена")
.on_press(CopyKey)
.width(Fill),
row![
button("↑ Отправить").style(jl_button).on_press(Send),
jl_button!("↑ Отправить").on_press(Send),
space().width(Fill),
button("↓ Скачать").style(jl_button).on_press(GetLast)
jl_button!("↓ Скачать").on_press(GetLast)
]
.spacing(DEFAULT_SPACING),
toggler(self.auto_fetch)
@@ -241,12 +240,8 @@ impl SyncState {
]
} else {
column![
button("Создать сохранение")
.style(jl_button)
.on_press(InitSync),
button("Вставить ключ из буфера")
.style(jl_button)
.on_press(GetKey),
jl_button!("Создать сохранение").on_press(InitSync),
jl_button!("Вставить ключ из буфера").on_press(GetKey),
]
}
}
@@ -273,12 +268,10 @@ impl SyncState {
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
});
connection
.execute("VACUUM;", [])
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
0
});
connection.execute("VACUUM;", []).unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
0
});
connection
.execute("PRAGMA optimize;", [])
.unwrap_or_else(|e| {
+2 -11
View File
@@ -1,19 +1,13 @@
use crate::app::AppState;
use crate::data::entities::{EntityHandle, WordData};
use crate::data_provider::words::{delete_word, update_word};
use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::word::WordMessage::*;
use crate::push_error;
use crate::push_error_task;
use crate::styling::*;
use iced::widget::button::danger;
use iced::widget::{button, column, row, rule, scrollable, space, text, text_input};
use iced::widget::{button, column, row, space, text, text_input};
use iced::{Element, Fill, Task};
use parking_lot::Mutex;
use std::sync::Arc;
#[derive(Clone)]
pub struct WordState {
state: AppState,
handle: EntityHandle<WordData>,
@@ -137,10 +131,7 @@ impl NavigatedPage<WordMessage> for WordState {
impl WordState {
pub fn new(handle: EntityHandle<WordData>, state: AppState) -> Self {
Self{
state,
handle,
}
Self { state, handle }
}
}
+3 -7
View File
@@ -1,4 +1,5 @@
use crate::core::kana_set::KanaSet;
use crate::jl_button;
use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::styling::*;
@@ -6,7 +7,6 @@ use iced::widget::*;
use iced::{Element, Fill, Task, alignment};
use rand::seq::SliceRandom;
#[derive(Clone, Debug)]
pub struct WritingState {
kana: String,
set: Vec<(String, String)>,
@@ -45,12 +45,8 @@ impl NavigatedPage<WritingMessage> for WritingState {
text!("{}", self.kana).size(48),
self.answers(),
row![
button(text!("{}", self.next_text))
.style(jl_button)
.on_press(WritingMessage::Next),
button("Закончить")
.style(jl_button)
.on_press(WritingMessage::Back),
jl_button!(text!("{}", self.next_text)).on_press(WritingMessage::Next),
jl_button!("Закончить").on_press(WritingMessage::Back),
]
.spacing(DEFAULT_SPACING)
]
+13 -9
View File
@@ -1,7 +1,7 @@
use iced::Element;
use iced::border::Radius;
use iced::widget::button::*;
use iced::widget::{button, container};
use iced::widget::container;
use iced_core::Length::Fill;
use iced_core::alignment::Horizontal::Left;
use iced_core::{Border, Theme};
@@ -14,7 +14,7 @@ pub const BUTTON_RADIUS: f32 = 8.0;
pub const SETTINGS_ICON: &[u8] = include_bytes!("../settings_icon.svg");
pub fn jl_button(theme: &Theme, status: Status) -> Style {
pub fn jl_button_style(theme: &Theme, status: Status) -> Style {
let mut style = primary(theme, status);
style.border = Border {
color: Default::default(),
@@ -30,15 +30,19 @@ pub fn cta_button(theme: &Theme, status: Status) -> Style {
style
}
#[macro_export]
macro_rules! jl_button {
($content:expr) => {
iced::widget::button($content).style($crate::styling::jl_button_style)
};
}
pub fn back_overlay<'a, T: Clone + 'a>(content: Element<'a, T>, back_message: T) -> Element<'a, T> {
container(
iced::widget::column![
button("Назад").style(jl_button).on_press(back_message),
content
]
.spacing(DEFAULT_SPACING)
.align_x(Left)
.width(Fill),
iced::widget::column![jl_button!("Назад").on_press(back_message), content]
.spacing(DEFAULT_SPACING)
.align_x(Left)
.width(Fill),
)
.center_x(Fill)
.padding(10)
+4 -4
View File
@@ -73,13 +73,13 @@ mod mod_tests {
#[test]
fn id_0() {
let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true);
assert_eq!(entity.real(), true)
assert!(entity.real())
}
#[test]
fn id_1() {
let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false);
assert_eq!(entity.real(), false)
assert!(!entity.real())
}
#[test]
@@ -97,14 +97,14 @@ mod mod_tests {
fn id_5() {
let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true);
entity.set_real(true);
assert_eq!(entity.real(), true)
assert!(entity.real())
}
#[test]
fn id_6() {
let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false);
entity.set_real(true);
assert_eq!(entity.real(), true)
assert!(entity.real())
}
#[test]