Author SHA1 Message Date
micialware c30be42822 reformat custom fields table. will return to this topic later 2026-09-09 15:15:47 +03:00
micialware ace54b10e1 remove clone from states 2026-09-09 15:03:54 +03:00
micialware f66bf69ef5 Code cleanup 2026-09-09 15:00:52 +03:00
micialware 1ad991667b Migrate to tiny macros of button 2026-09-09 14:26:55 +03:00
37 changed files with 302 additions and 967 deletions

No files matched your search

-8
View File
@@ -40,11 +40,3 @@ panic = "abort"
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] } criterion = { version = "0.8.2", features = ["html_reports"] }
rustc-hash = "2.1.3" 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
@@ -1,4 +1,6 @@
create table custom_fields( create table custom_fields
id INTEGER PRIMARY KEY, (
name TEXT not null id integer primary key,
name text not null,
custom_fields text not null
); );
+5 -5
View File
@@ -1,9 +1,9 @@
create table decks create table decks
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
name TEXT not null, name text not null,
forward TEXT not null, forward text not null,
backward TEXT not null, backward text not null,
filter TEXT not null filter text not null
); );
+8 -8
View File
@@ -1,12 +1,12 @@
create table history create table history
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
deck_id INTEGER not null references decks on delete cascade, deck_id integer not null references decks on delete cascade,
word_id INTEGER not null references words on delete cascade, word_id integer not null references words on delete cascade,
timestamp INTEGER not null, timestamp integer not null,
answer INTEGER not null, answer integer not null,
before INTEGER not null, before integer not null,
after INTEGER not null, after integer not null,
fsrs INTEGER not null fsrs integer not null
); );
+8 -8
View File
@@ -1,16 +1,16 @@
create table states create table states
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
word_id INTEGER not null word_id integer not null
references words references words
on delete cascade, on delete cascade,
deck_id INTEGER not null deck_id integer not null
references decks references decks
on delete cascade, on delete cascade,
score INTEGER default 1 not null, score integer default 1 not null,
last_opened INTEGER not null, last_opened integer not null,
fsrs_opened INTEGER not null, fsrs_opened integer not null,
fsrs_memory INTEGER not null, fsrs_memory integer not null,
fsrs_open_day INTEGER not null fsrs_open_day integer not null
); );
+2 -2
View File
@@ -1,6 +1,6 @@
create table word_groups create table word_groups
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
name TEXT not null name text not null
); );
+5 -5
View File
@@ -1,11 +1,11 @@
create table words create table words
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
key TEXT not null, key text not null,
value TEXT not null, value text not null,
tags TEXT not null, tags text not null,
more TEXT, more text,
group_id integer default 1 not null group_id integer default 1 not null
constraint words_word_groups_id_fk constraint words_word_groups_id_fk
references word_groups references word_groups
+5 -3
View File
@@ -33,7 +33,9 @@ impl DeckData {
.map(|card_state| EntityHandle::from_id(card_state.word_id)) .map(|card_state| EntityHandle::from_id(card_state.word_id))
.collect(); .collect();
let states = query.handles().collect::<Vec<EntityHandle<CardState>>>(); 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); drop(states_repo);
Self { Self {
states, states,
@@ -61,7 +63,7 @@ impl DeckData {
}; };
self.current_word_index = Some(index); self.current_word_index = Some(index);
(self.words[index].clone(), self.states[index].clone()) (self.words[index], self.states[index])
} }
fn set_links<'a>( fn set_links<'a>(
@@ -108,7 +110,7 @@ impl DeckData {
timestamp: Utc::now(), timestamp: Utc::now(),
deck_id: self.settings.id, deck_id: self.settings.id,
word_id, word_id,
answer: status.clone(), answer: status,
before: old_score, before: old_score,
after: new_score, after: new_score,
fsrs, fsrs,
+7 -8
View File
@@ -44,9 +44,7 @@ impl SRSModule for RandomSRSModule {
fn new(set: &[&CardState]) -> Self { fn new(set: &[&CardState]) -> Self {
let mut basket = (0..set.len()).collect::<Vec<usize>>(); let mut basket = (0..set.len()).collect::<Vec<usize>>();
basket.shuffle(&mut rand::rng()); basket.shuffle(&mut rand::rng());
Self{ Self { basket }
basket,
}
} }
fn next(&mut self, set: &[&CardState]) -> usize { fn next(&mut self, set: &[&CardState]) -> usize {
@@ -90,16 +88,15 @@ impl SRSModule for SemiRandomSRSModule {
.expect("update weights error"); .expect("update weights error");
} }
fn new(set: &[&CardState]) -> Self { fn new(set: &[&CardState]) -> Self {
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{ Self {
generator: rng(), generator: rng(),
last_weights: WeightedIndex::new(weights).expect("Incorrect weights update"), last_weights: WeightedIndex::new(weights).expect("Incorrect weights update"),
history: vec![] history: vec![],
} }
} }
} }
@@ -233,7 +230,9 @@ impl SRSModule for FsrsModule {
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 && !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); 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();
+8 -13
View File
@@ -1,5 +1,6 @@
use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, WordOpenMode}; use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, WordOpenMode};
use crate::data::{INVALID_ID, Id}; use crate::data::{INVALID_ID, Id};
use crate::data_provider::history::HistoryRepositoryInner;
use chrono::{DateTime, Local, NaiveDate, Utc}; use chrono::{DateTime, Local, NaiveDate, Utc};
use fsrs::MemoryState; use fsrs::MemoryState;
use litemap::LiteMap; use litemap::LiteMap;
@@ -10,7 +11,6 @@ use std::hash::{Hash, Hasher};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use crate::data_provider::history::{HistoryRepository, HistoryRepositoryInner};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct WordData { pub struct WordData {
@@ -70,7 +70,7 @@ pub struct HistoryNote {
pub answer: WordOpenMode, pub answer: WordOpenMode,
pub before: u8, pub before: u8,
pub after: u8, pub after: u8,
pub fsrs: bool pub fsrs: bool,
} }
impl EntityData for HistoryNote { impl EntityData for HistoryNote {
@@ -86,17 +86,16 @@ impl EntityData for HistoryNote {
impl HistoryRepositoryInner { impl HistoryRepositoryInner {
pub fn activity_of_deck(&self, id: Id) -> Vec<(NaiveDate, u32)> { 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 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![]; let mut counts = vec![];
if !values.is_empty() { if !values.is_empty() {
let mut date = values[0].timestamp.with_timezone(&Local).date_naive(); let mut date = values[0].timestamp.with_timezone(&Local).date_naive();
let mut count = 0; let mut count = 0;
for note in values { for note in values {
let current_date = note.timestamp.with_timezone(&Local).date_naive(); let current_date = note.timestamp.with_timezone(&Local).date_naive();
if date == current_date{ if date == current_date {
count += 1; count += 1;
} } else {
else {
counts.push((date, count)); counts.push((date, count));
date = current_date; date = current_date;
count = 0; count = 0;
@@ -200,6 +199,7 @@ impl CardState {
pub struct CustomField { pub struct CustomField {
pub id: Id, pub id: Id,
pub name: String, pub name: String,
pub identifier: String,
} }
pub struct EntityHandle<E> { pub struct EntityHandle<E> {
@@ -209,14 +209,9 @@ pub struct EntityHandle<E> {
} }
impl<T> Clone for EntityHandle<T> { impl<T> Clone for EntityHandle<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self { *self }
EntityHandle {
id: self.id,
index: self.index,
_phantom: Default::default(),
}
}
} }
impl<T> Copy for EntityHandle<T> {} impl<T> Copy for EntityHandle<T> {}
impl<T> EntityHandle<T> { impl<T> EntityHandle<T> {
pub fn new(id: Id, index: usize, real: bool) -> Self { pub fn new(id: Id, index: usize, real: bool) -> Self {
+71 -41
View File
@@ -1,8 +1,9 @@
use crate::data::Id; 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::data_provider::sqlite::SqliteConnection;
use crate::helpers::into_result; use crate::helpers::into_result;
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
use log::debug;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use rayon::iter::IndexedParallelIterator; use rayon::iter::IndexedParallelIterator;
use rayon::iter::IntoParallelRefIterator; use rayon::iter::IntoParallelRefIterator;
@@ -12,7 +13,6 @@ 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 std::time::Instant;
use log::debug;
pub trait Repository<T, D> pub trait Repository<T, D>
where where
@@ -31,13 +31,19 @@ where
fn create_now(&mut self, new: T) -> Result<EntityHandle<T>, NotificationData>; fn create_now(&mut self, new: T) -> Result<EntityHandle<T>, NotificationData>;
fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError; fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError;
fn delete(&mut self, handle: EntityHandle<T>) -> MaybeError; fn delete(&mut self, handle: EntityHandle<T>) -> MaybeError;
fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>> where F : Fn(&T) -> bool; fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>>
where
F: Fn(&T) -> bool;
fn init(&mut self) -> MaybeError; fn init(&mut self) -> MaybeError;
fn start(&mut self) -> MaybeError; fn start(&mut self) -> MaybeError;
fn reload(&mut self) -> MaybeError; fn reload(&mut self) -> MaybeError;
fn shifts(&self) -> usize; fn shifts(&self) -> usize;
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T> where T: 'a; fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T>
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut T> where T: 'a; where
T: 'a;
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut T>
where
T: 'a;
} }
struct QueryManger<T, R, D> struct QueryManger<T, R, D>
@@ -88,9 +94,7 @@ where
.filter(|(_, q)| { .filter(|(_, q)| {
let query = q.lock(); let query = q.lock();
let rule = &query.rule; let rule = &query.rule;
return rule(data); rule(data)
false
}) })
.map(|(i, _)| i) .map(|(i, _)| i)
.collect() .collect()
@@ -118,7 +122,10 @@ where
} }
impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> { impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> {
pub fn iter<'a>(&mut self, data_repository_inner: &'a 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 where
T: 'a, 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)) .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 where
T: 'a, T: 'a,
{ {
@@ -139,7 +149,7 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
} }
self.cache self.cache
.iter_mut() .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) pub fn for_each_mut<F>(&mut self, repo: &mut DataRepositoryInner<T, D>, mut f: F)
@@ -190,7 +200,12 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
.collect::<Vec<_>>(), .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>> { pub fn handles(&self) -> impl Iterator<Item = EntityHandle<T>> {
@@ -202,7 +217,9 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
} }
} }
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>; type Output = EntityHandle<T>;
fn index(&self, index: usize) -> &Self::Output { 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 { fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.cache[index] &mut self.cache[index]
} }
@@ -221,7 +240,10 @@ where
D: BatchedDbLayer<T>, D: BatchedDbLayer<T>,
T: EntityData, T: EntityData,
{ {
fn create_now_many(&mut self, entities: &mut [T]) -> Result<Vec<EntityHandle<T>>, NotificationData>; fn create_now_many(
&mut self,
entities: &mut [T],
) -> Result<Vec<EntityHandle<T>>, NotificationData>;
fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError; fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
fn delete_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError; fn delete_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
} }
@@ -257,10 +279,10 @@ pub struct DataRepositoryInner<T: Clone + EntityData, D: DbLayer<T>> {
shifts: usize, 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> { pub fn new(connection: Arc<Mutex<SqliteConnection>>) -> DataRepository<T, D> {
Self{ Self {
inner: Arc::new(RwLock::new(DataRepositoryInner{ inner: Arc::new(RwLock::new(DataRepositoryInner {
cache: vec![], cache: vec![],
temporal: vec![], temporal: vec![],
next_temporal: 0, 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> { impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
type Target = Arc<RwLock<DataRepositoryInner<T, D>>>; type Target = Arc<RwLock<DataRepositoryInner<T, D>>>;
@@ -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( pub fn query_par(
&self, &self,
rule: Arc<dyn Fn(&T) -> bool + Send + Sync>, 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( pub fn query(
&self, &self,
rule: Arc<dyn Fn(& T) -> bool + Send + Sync>, rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
) -> Query<T, DataRepositoryInner<T, D>, D> ) -> Query<T, DataRepositoryInner<T, D>, D>
where where
T: Send + Sync, T: Send + Sync,
@@ -325,7 +348,7 @@ impl<T: EntityData + Clone + std::marker::Send + std::marker::Sync, D: DbLayer<T
} }
pub fn empty_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> { pub fn empty_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query{ Query {
cache: vec![], cache: vec![],
rule: Arc::new(|_| false), rule: Arc::new(|_| false),
parallel: 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> { pub fn empty_query_par(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query{ Query {
cache: vec![], cache: vec![],
rule: Arc::new(|_| false), rule: Arc::new(|_| false),
parallel: true, 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 { fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError {
let original_state = entity.clone(); let original_state = *entity;
if entity.real() { if entity.real() {
let data = self.get_ref(entity).unwrap(); let data = self.get_ref(entity).unwrap();
self.data_layer self.data_layer
@@ -441,12 +464,11 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
} else { } else {
let index = fast_position_of(self.temporal.as_slice(), entity).unwrap(); let index = fast_position_of(self.temporal.as_slice(), entity).unwrap();
let data = &mut self.temporal[index]; let data = &mut self.temporal[index];
self.data_layer self.data_layer.add_entity(data, &self.connection.lock())?;
.add_entity(data, &self.connection.lock())?;
entity.set_real(true); entity.set_real(true);
entity.id = data.id(); entity.id = data.id();
entity.set_index(self.cache.len()); entity.set_index(self.cache.len());
self.query_manger.handle_save(original_state, entity, &data); self.query_manger.handle_save(original_state, entity, data);
let data = self.temporal.remove(index); let data = self.temporal.remove(index);
self.cache.push(data); 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>> fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>>
where 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]; 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]; let value = &self.temporal[index];
return Some(EntityHandle::new(value.id(), index, false)) return Some(EntityHandle::new(value.id(), index, false));
} }
None None
} }
@@ -518,16 +540,16 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
self.shifts self.shifts
} }
fn iter<'a>(&'a self) -> impl Iterator<Item=&'a T> fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T>
where where
T: 'a T: 'a,
{ {
self.cache.iter().chain(self.temporal.iter()) 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 where
T: 'a T: 'a,
{ {
self.cache.iter_mut().chain(self.temporal.iter_mut()) 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> impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository<T, D>
for DataRepositoryInner<T, D> for DataRepositoryInner<T, D>
{ {
fn create_now_many(&mut self, entities: &mut [T]) -> Result<Vec<EntityHandle<T>>, NotificationData> { fn create_now_many(
self.data_layer.add_many_entities(entities, &mut self.connection.lock())?; &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 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()); self.cache.extend(entities.to_vec());
Ok(handles) Ok(handles)
} }
@@ -577,7 +607,7 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
let start = self.cache.len(); let start = self.cache.len();
for (i, (handle, data)) in temp.into_iter().zip(&mut temp_values).enumerate() { 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.id = data.id();
handle.set_index(start + i); handle.set_index(start + i);
handle.set_real(true); handle.set_real(true);
@@ -641,7 +671,7 @@ where
{ {
let mut temp = Vec::with_capacity(handles.len() / 2); let mut temp = Vec::with_capacity(handles.len() / 2);
let mut cached = Vec::with_capacity(handles.len() / 2); let mut cached = Vec::with_capacity(handles.len() / 2);
handles.sort_by(|a, b| a.id.cmp(&b.id)); handles.sort_by_key(|a| a.id);
handles.iter().for_each(|handle| { handles.iter().for_each(|handle| {
if handle.real() { if handle.real() {
cached.push(getter(handle)) cached.push(getter(handle))
+11 -43
View File
@@ -1,20 +1,14 @@
use std::sync::Arc; use crate::data::entities::CardState;
use crate::data::entities::{CardState, DeckSettings};
use crate::data::repositories::{DataRepository, DataRepositoryInner, Query}; use crate::data::repositories::{DataRepository, DataRepositoryInner, Query};
use crate::data::Id;
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData};
use chrono::NaiveDate; use chrono::NaiveDate;
use fsrs::MemoryState; use fsrs::MemoryState;
use log::info; use rusqlite::{Error, Params, Row};
use rusqlite::{Connection, Error, Params, Row, params}; use std::sync::Arc;
use std::time::Instant;
use crate::data::Id;
pub type CardStateRepository = pub type CardStateRepository = DataRepository<CardState, CardStateDbLayer>;
DataRepository<CardState, CardStateDbLayer>; pub type CardStateRepositoryInner = DataRepositoryInner<CardState, CardStateDbLayer>;
pub type CardStateRepositoryInner =
DataRepositoryInner<CardState, CardStateDbLayer>;
pub type CardStateDbLayer = SqliteDbLayer<CardState, CardStatesQuerySet>; pub type CardStateDbLayer = SqliteDbLayer<CardState, CardStatesQuerySet>;
#[derive(Clone)] #[derive(Clone)]
pub struct CardStatesQuerySet; 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> { fn get_memory(value: i64) -> Option<MemoryState> {
if value == 0 { if value == 0 {
@@ -122,7 +87,10 @@ fn memory_to_i64(value: &Option<MemoryState>) -> i64 {
} }
impl CardStateRepositoryInner { impl CardStateRepositoryInner {
pub fn deck_states(&self, deck_id: Id) -> Query<CardState, CardStateRepositoryInner, CardStateDbLayer> { pub fn deck_states(
self.query(Arc::new(move |state| {state.deck_id == deck_id})) &self,
deck_id: Id,
) -> Query<CardState, CardStateRepositoryInner, CardStateDbLayer> {
self.query(Arc::new(move |state| state.deck_id == deck_id))
} }
} }
+16 -14
View File
@@ -1,40 +1,42 @@
use crate::data::entities::{CustomField, WordGroup}; use crate::data::entities::CustomField;
use crate::data::repositories::DataRepository; use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::{Error, Params, Row}; use rusqlite::{Error, Params, Row};
pub type CustomFieldRepository = DataRepository<CustomField, SqliteDbLayer<CustomField, CustomFieldsQuerySet>>; pub type CustomFieldRepository = DataRepository<CustomField, CustomFieldDbLayer>;
pub type CustomFieldRepositoryInner = DataRepositoryInner<CustomField, CustomFieldDbLayer>;
pub type CustomFieldDbLayer = SqliteDbLayer<CustomField, CustomFieldsQuerySet>;
pub struct CustomFieldsQuerySet; pub struct CustomFieldsQuerySet;
impl TableAccessQuerySet<WordGroup> for CustomFieldsQuerySet { impl TableAccessQuerySet<CustomField> for CustomFieldsQuerySet {
fn load() -> &'static str { fn load() -> &'static str {
"SELECT id, name FROM custom_fields" "SELECT id, name, identifier FROM custom_fields"
} }
fn save() -> &'static str { fn save() -> &'static str {
"UPDATE custom_fields SET name = ?1 WHERE id = ?2" "UPDATE custom_fields SET name = ?1, identifier = ?2 WHERE id = ?3"
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1)" "INSERT INTO custom_fields (name, identifier) VALUES (?1, ?2)"
} }
fn name() -> &'static str { fn name() -> &'static str {
"custom_fields" "custom_fields"
} }
fn from_sql(row: &Row) -> Result<WordGroup, Error> { fn from_sql(row: &Row) -> Result<CustomField, Error> {
Ok(WordGroup { Ok(CustomField {
id: row.get(0)?, id: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
identifier: row.get(2)?,
}) })
} }
fn insert_params(entity: &WordGroup) -> impl Params { fn insert_params(entity: &CustomField) -> impl Params {
(&entity.name,) (&entity.name, &entity.identifier)
} }
fn update_params(entity: &WordGroup) -> impl Params { fn update_params(entity: &CustomField) -> impl Params {
(&entity.name, entity.id) (&entity.name, &entity.identifier, entity.id)
} }
} }
+2 -2
View File
@@ -77,7 +77,7 @@ impl FromSql for WordOpenMode {
2 => Ok(WordOpenMode::Ok), 2 => Ok(WordOpenMode::Ok),
3 => Ok(WordOpenMode::Easy), 3 => Ok(WordOpenMode::Easy),
_ => Ok(WordOpenMode::None), _ => Ok(WordOpenMode::None),
} };
} }
Ok(WordOpenMode::None) Ok(WordOpenMode::None)
} }
@@ -89,7 +89,7 @@ impl ToSql for WordOpenMode {
WordOpenMode::Easy => 3, WordOpenMode::Easy => 3,
WordOpenMode::Ok => 2, WordOpenMode::Ok => 2,
WordOpenMode::Hard => 1, 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::helpers::into_result;
use crate::notification::NotificationData; use crate::notification::NotificationData;
use crate::notification::NotificationData::IncorrectAnkiFile; use crate::notification::NotificationData::IncorrectAnkiFile;
use log::info; use log::info;
use rusqlite::Connection; use rusqlite::Connection;
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone)] #[derive(Clone)]
pub struct ImportData(pub(crate) Vec<ImportGroup>); 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 time = Instant::now();
let index: u32 = connection 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| { .map_err(|e| {
error!("{}", e); error!("{}", e);
DbSaveError DbSaveError
@@ -104,7 +108,7 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
.prepare(Q::load()) .prepare(Q::load())
.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| Q::from_sql(row))
.map_err(|e| { .map_err(|e| {
error!("{}", e); error!("{}", e);
DbSaveError DbSaveError
+4 -3
View File
@@ -1,7 +1,7 @@
use crate::fs::*; use crate::fs::*;
use log::error;
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();
@@ -130,6 +130,7 @@ impl SqliteConnection {
} }
impl Drop for SqliteConnection { impl Drop for SqliteConnection {
fn drop(&mut self) {
fn drop(&mut self) { self.finish(); } self.finish();
}
} }
+2 -171
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::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use rusqlite::{Error, Params, Row};
use crate::notification::{MaybeError, NotificationData};
use rusqlite::{Connection, Error, Params, Row, params};
pub type WordRepository = DataRepository<WordData, WordDataLayer>; pub type WordRepository = DataRepository<WordData, WordDataLayer>;
pub type WordRepositoryInner = DataRepositoryInner<WordData, WordDataLayer>; pub type WordRepositoryInner = DataRepositoryInner<WordData, WordDataLayer>;
@@ -59,170 +57,3 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
) )
} }
} }
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") 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> { pub fn voice_dir() -> Result<PathBuf, NotificationData> {
let directory = app_data_dir().join("voice"); let directory = app_data_dir().join("voice");
if !directory.exists() { if !directory.exists() {
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::app::{fill_state, AppState}; use crate::app::{AppState, fill_state};
use crate::data_provider::sqlite::{create_db}; use crate::data_provider::sqlite::create_db;
use crate::data_provider::web_api::{ use crate::data_provider::web_api::{
get_local_version, get_web_version, load_data, set_local_version, get_local_version, get_web_version, load_data, set_local_version,
}; };
+23 -24
View File
@@ -11,9 +11,9 @@ use crate::pages::dictionary::DictionaryMessage::*;
use crate::pages::dictionary_quiz::DictionaryQuizState; use crate::pages::dictionary_quiz::DictionaryQuizState;
use crate::pages::import::ImportState; use crate::pages::import::ImportState;
use crate::pages::word::WordState; use crate::pages::word::WordState;
use crate::push_error;
use crate::push_error_task; use crate::push_error_task;
use crate::styling::*; use crate::styling::*;
use crate::{jl_button, push_error};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use iced::alignment::Vertical::Center; use iced::alignment::Vertical::Center;
@@ -81,7 +81,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let mut words = vec![]; let mut words = vec![];
let dict = &self.state.dictionary.read(); 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(); let handles = query.handles();
words = handles.filter(|h| self.included.contains(h)).collect(); 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 mut dictionary = self.state.dictionary.write();
let reload_query = handle.real() == false; let reload_query = !handle.real();
let _ = dictionary.save(&mut handle); let _ = dictionary.save(&mut handle);
if reload_query { if reload_query {
self.displayed_words_query.load(&dictionary); self.displayed_words_query.load(&dictionary);
@@ -373,7 +373,7 @@ impl DictionaryState {
key: word.key.clone().into(), key: word.key.clone().into(),
value: word.value.clone().into(), value: word.value.clone().into(),
tags: word.tags.clone().into(), tags: word.tags.clone().into(),
handle: word_handle.clone(), handle: *word_handle,
}; };
let lazy_line = lazy(word_line_data, Self::word_line); 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> { 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(); let handle = data.handle;
line = line.push( line = line.push(
checkbox(data.is_included) checkbox(data.is_included)
.label("") .label("")
@@ -434,15 +434,13 @@ 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( .style(|_x: &Theme, _status: button::Status| Style {
|_x: &iced::Theme, _status: iced::widget::button::Status| Style { background: None,
background: None, text_color: Color::BLACK,
text_color: Color::BLACK, border: Border::default(),
border: Border::default(), shadow: Shadow::default(),
shadow: Shadow::default(), snap: false,
snap: false, })
},
)
.width(15); .width(15);
} }
@@ -512,15 +510,16 @@ impl DictionaryState {
let dict = &self.state.dictionary.write(); let dict = &self.state.dictionary.write();
let mut words_query = dict.full_query(); let mut words_query = dict.full_query();
let handles = words_query.handles().collect::<Vec<_>>(); let handles = words_query.handles().collect::<Vec<_>>();
words_query.iter(dict).zip(handles).for_each(|(element, handle)| { words_query
let tags = split_with_coma(element.tags.as_str()); .iter(dict)
tags.iter() .zip(handles)
.for_each(|tag| { .for_each(|(element, handle)| {
let tags = split_with_coma(element.tags.as_str());
tags.iter().for_each(|tag| {
tags_list.insert(tag.clone()); tags_list.insert(tag.clone());
}); });
self.words_tags.push((handle, tags)) self.words_tags.push((handle, tags))
});
});
let current_tags = self let current_tags = self
.tag_map .tag_map
@@ -590,12 +589,12 @@ impl DictionaryState {
} }
let groups = self.state.word_groups.read(); 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![ iced::widget::column![
scrollable(row).width(Fill).horizontal(), scrollable(row).width(Fill).horizontal(),
row![ row![
button("⇳").on_press(ChangeDirection).style(jl_button), jl_button!("⇳").on_press(ChangeDirection),
text_input("Название группы слов", &group.name) text_input("Название группы слов", &group.name)
.on_input(EditGroup) .on_input(EditGroup)
.width(250) .width(250)
@@ -617,7 +616,7 @@ impl DictionaryState {
} }
fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> {
let button = button("Добавить слово").style(jl_button); let button = jl_button!("Добавить слово");
if self.selected_group.real() { if self.selected_group.real() {
return button.on_press(NewWord).into(); 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::entities::{EntityHandle, WordData};
use crate::data::repositories::Repository;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::RootMessage; use crate::navigation::RootMessage;
use crate::navigation::*; use crate::navigation::*;
@@ -6,14 +9,12 @@ use crate::pages::dictionary::split_with_coma;
use crate::pages::dictionary_quiz::DictionaryQuizMessage::*; use crate::pages::dictionary_quiz::DictionaryQuizMessage::*;
use crate::pages::quiz::Score; use crate::pages::quiz::Score;
use crate::styling::*; use crate::styling::*;
use iced::Background::Color;
use iced::border::Radius; use iced::border::Radius;
use iced::widget::container::Style; use iced::widget::container::Style;
use iced::widget::{Row, button, container, row, space, text, text_input}; use iced::widget::{container, row, space, text, text_input, Row};
use iced::{Border, Element, Fill, Task, Theme, alignment}; use iced::Background::Color;
use iced::{alignment, Border, Element, Fill, Task, Theme};
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use crate::app::AppState;
use crate::data::repositories::Repository;
pub struct DictionaryQuizState { pub struct DictionaryQuizState {
state: AppState, state: AppState,
@@ -90,11 +91,8 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
.size(25), .size(25),
] ]
.spacing(DEFAULT_SPACING), .spacing(DEFAULT_SPACING),
row![ row![jl_button!("Закончить").on_press(Back), self.appeal_button()]
button("Закончить").style(jl_button).on_press(Back), .spacing(DEFAULT_SPACING),
self.appeal_button()
]
.spacing(DEFAULT_SPACING),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.align_x(alignment::Horizontal::Center), .align_x(alignment::Horizontal::Center),
@@ -106,8 +104,13 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
} }
impl DictionaryQuizState { impl DictionaryQuizState {
pub fn new(words: Vec<EntityHandle<WordData>>, reverse: bool, no_typing: bool, state: AppState) -> DictionaryQuizState { pub fn new(
let state = DictionaryQuizState { words: Vec<EntityHandle<WordData>>,
reverse: bool,
no_typing: bool,
state: AppState,
) -> DictionaryQuizState {
Self {
state, state,
words, words,
current_set: Vec::new(), current_set: Vec::new(),
@@ -119,9 +122,7 @@ impl DictionaryQuizState {
reverse, reverse,
laps: 0, laps: 0,
no_typing, no_typing,
}; }
state
} }
fn submit(&mut self) { fn submit(&mut self) {
@@ -218,7 +219,7 @@ impl DictionaryQuizState {
fn appeal_button(&self) -> Element<'_, DictionaryQuizMessage> { fn appeal_button(&self) -> Element<'_, DictionaryQuizMessage> {
if self.is_help && !self.no_typing { 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() space().into()
} }
+23 -30
View File
@@ -3,18 +3,16 @@ use crate::data::entities::{HistoryNote, WordData};
use crate::data::repositories::Repository; use crate::data::repositories::Repository;
use crate::navigation::*; use crate::navigation::*;
use crate::styling::*; use crate::styling::*;
use HistoryMessage::Back;
use chrono::Local; use chrono::Local;
use iced::alignment::Horizontal::Center; use iced::alignment::Horizontal::Center;
use iced::widget::space::horizontal; use iced::widget::space::horizontal;
use iced::widget::*; use iced::widget::*;
use iced::{Element, Fill, FillPortion, Task}; use iced::{Element, Fill, FillPortion, Task};
use std::sync::Arc; use std::sync::Arc;
use HistoryMessage::Back;
#[derive(Clone)]
pub struct HistoryState { pub struct HistoryState {
list: Vec<(WordData, HistoryNote)>, list: Vec<(WordData, HistoryNote)>,
pub state: AppState,
} }
impl NavigatedPage<HistoryMessage> for HistoryState { impl NavigatedPage<HistoryMessage> for HistoryState {
@@ -37,9 +35,9 @@ impl NavigatedPage<HistoryMessage> for HistoryState {
.width(FillPortion(5)), .width(FillPortion(5)),
horizontal().width(FillPortion(1)) horizontal().width(FillPortion(1))
] ]
.height(Fill) .height(Fill)
.width(Fill) .width(Fill)
.into(), .into(),
Back, Back,
) )
} }
@@ -50,57 +48,52 @@ impl HistoryState {
let dict = &state.dictionary.read(); let dict = &state.dictionary.read();
let history_repo = &mut state.history.write(); let history_repo = &mut state.history.write();
history_repo.init().expect("TODO: panic message"); 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 mut history = history_repo.query(Arc::new(move |h| h.deck_id == id));
let words = history let words = history
.iter(history_repo) .iter(history_repo)
.map( .map(|item| {
|item| {let handle = dict.handle_by_id(item.word_id); let handle = dict.handle_by_id(item.word_id);
let word = dict.get_static(handle).unwrap_or(WordData { let word = dict.get_static(handle).unwrap_or(WordData {
id: Default::default(), id: Default::default(),
group_id: Default::default(), group_id: Default::default(),
key: "Удаленное слово".to_string(), key: "Удаленное слово".to_string(),
value: "".to_string(), value: "".to_string(),
tags: "".to_string(), tags: "".to_string(),
additional: Default::default(), additional: Default::default(),
}); });
(word, item.clone()) (word, item.clone())
} })
)
.collect(); .collect();
Self { Self {
list: words, list: words,
state: state.clone(),
} }
} }
fn history_lines(&self) -> Column<'_, HistoryMessage> { fn history_lines(&self) -> Column<'_, HistoryMessage> {
let mut column = Column::new(); let mut column = Column::new();
for (index, item) in &self.list { for (index, item) in &self.list {
let activity = item; let activity = item;
column = column.push( column = column.push(
row![ row![
iced::widget::column![ iced::widget::column![
text!("{}", index.key) text!("{}", index.key).width(Fill).align_x(Center),
.width(Fill) text!("{}", index.value).width(Fill).align_x(Center),
.align_x(Center),
text!("{}", index.value)
.width(Fill)
.align_x(Center),
] ]
.align_x(Center), .align_x(Center),
iced::widget::column![ iced::widget::column![
text!("{} ➞ {}", activity.before, activity.after), text!("{} ➞ {}", activity.before, activity.after),
text!( text!(
"{}", "{}",
activity.timestamp.with_timezone(&Local).format("%d.%m %H:%M") activity
.timestamp
.with_timezone(&Local)
.format("%d.%m %H:%M")
) )
] ]
.spacing(5) .spacing(5)
.align_x(Center) .align_x(Center)
] ]
.spacing(5), .spacing(5),
); );
column = column.push(rule::horizontal(1)); column = column.push(rule::horizontal(1));
} }
+5 -5
View File
@@ -1,9 +1,10 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::EntityData;
use crate::data::entities::WordData; use crate::data::entities::WordData;
use crate::data::entities::WordGroup; 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::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups};
use crate::fs::*; use crate::fs::*;
use crate::jl_button;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::NotificationData::OpenFileError; use crate::notification::NotificationData::OpenFileError;
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
@@ -30,11 +31,9 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use zip::ZipArchive; use zip::ZipArchive;
use crate::data::repositories::{BatchedRepository, Repository};
const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"];
#[derive(Clone)]
pub struct ImportState { pub struct ImportState {
state: AppState, state: AppState,
path: Option<PathBuf>, path: Option<PathBuf>,
@@ -155,7 +154,7 @@ impl NavigatedPage<ImportMessage> for ImportState {
scrollable( scrollable(
column![ column![
row![ row![
button("Выбрать файл").style(jl_button).on_press(SelectFile), jl_button!("Выбрать файл").on_press(SelectFile),
text!("{}", { text!("{}", {
if let Some(path) = &self.path { if let Some(path) = &self.path {
path.to_string_lossy().to_string() path.to_string_lossy().to_string()
@@ -435,7 +434,8 @@ impl ImportState {
async move { async move {
spawn_blocking(move || { spawn_blocking(move || {
let temp_file_path = temp_import_path(); let temp_file_path = temp_import_path();
let connection = 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 import_list = get_words_of_group(&connection, group.id)?;
let mut words_list = Vec::with_capacity(import_list.len()); let mut words_list = Vec::with_capacity(import_list.len());
+2 -2
View File
@@ -1,5 +1,6 @@
use crate::app::USER_FONT; use crate::app::USER_FONT;
use crate::core::kana_set::KanaSet; use crate::core::kana_set::KanaSet;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::quiz::QuizMessage::*; use crate::pages::quiz::QuizMessage::*;
@@ -8,7 +9,6 @@ use iced::widget::*;
use iced::{Element, Fill, Task, alignment}; use iced::{Element, Fill, Task, alignment};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
#[derive(Clone, Debug)]
pub struct QuizState { pub struct QuizState {
kana: String, kana: String,
current_roman: String, current_roman: String,
@@ -82,7 +82,7 @@ impl NavigatedPage<QuizMessage> for QuizState {
.width(150) .width(150)
.on_input(ContentChanged), .on_input(ContentChanged),
score_display(&self.score), score_display(&self.score),
button("Закончить").style(jl_button).on_press(Back), jl_button!("Закончить").on_press(Back),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.align_x(alignment::Horizontal::Center), .align_x(alignment::Horizontal::Center),
+4 -4
View File
@@ -1,12 +1,12 @@
use crate::jl_button;
use crate::navigation::RootMessage; use crate::navigation::RootMessage;
use crate::navigation::{NavigatedPage, Page}; use crate::navigation::{NavigatedPage, Page};
use crate::pages::randomizer::RandomizerMessage::{Back, Edit, Start}; 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::Task;
use iced::widget::{button, text_editor}; use iced::widget::text_editor;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
#[derive(Clone, Debug)]
pub struct RandomizerState { pub struct RandomizerState {
text: text_editor::Content, text: text_editor::Content,
list: Vec<String>, list: Vec<String>,
@@ -56,7 +56,7 @@ impl NavigatedPage<RandomizerMessage> for RandomizerState {
.width(400) .width(400)
.height(400) .height(400)
.placeholder("Каждый элемент с новой строки"), .placeholder("Каждый элемент с новой строки"),
button("Перемешать").style(jl_button).on_press(Start), jl_button!("Перемешать").on_press(Start),
] ]
.spacing(HALF_SPACING) .spacing(HALF_SPACING)
.into(), .into(),
+2 -2
View File
@@ -5,6 +5,7 @@ use crate::data::Id;
use crate::data::entities::{CardState, WordData}; use crate::data::entities::{CardState, WordData};
use crate::data::repositories::Repository; use crate::data::repositories::Repository;
use crate::data_provider::voice::get_voice; use crate::data_provider::voice::get_voice;
use crate::jl_button;
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};
@@ -255,8 +256,7 @@ impl RepetitionState {
} }
fn draw_voice(&self) -> Element<'_, RepetitionMessage> { fn draw_voice(&self) -> Element<'_, RepetitionMessage> {
button("Воспроизвести") jl_button!("Воспроизвести")
.style(jl_button)
.on_press(RepetitionMessage::Play) .on_press(RepetitionMessage::Play)
.into() .into()
} }
+8 -6
View File
@@ -2,6 +2,7 @@ use crate::app::AppState;
use crate::core::deck::DeckData; use crate::core::deck::DeckData;
use crate::data::entities::{DeckSettings, EntityHandle}; use crate::data::entities::{DeckSettings, EntityHandle};
use crate::data::repositories::{BatchedRepository, Repository}; use crate::data::repositories::{BatchedRepository, Repository};
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::MaybeError; use crate::notification::MaybeError;
@@ -122,7 +123,10 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
} }
let mut states = self.state.card_states.write(); let mut states = self.state.card_states.write();
let deck_id = self.settings_handle.id; 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()) { if let Err(e) = states.delete_many(query.as_mut_slice()) {
return push_error_task!(e); return push_error_task!(e);
} }
@@ -163,9 +167,7 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
primary primary
}) })
.on_input(SetFilter), .on_input(SetFilter),
button("Проверить фильтр") jl_button!("Проверить фильтр").on_press(TryFilter),
.style(jl_button)
.on_press(TryFilter),
self.count_view(), self.count_view(),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
@@ -180,7 +182,7 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
) )
.height(Fill), .height(Fill),
row![ row![
button("Сохранить").style(jl_button).on_press(Save), jl_button!("Сохранить").on_press(Save),
horizontal(), horizontal(),
button(if self.real_delete_states { 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 deck_id = deck.id;
let mut db_stats = states_repo let mut db_stats = states_repo
.query_par(Arc::new(move |s| s.deck_id == deck_id)) .query_par(Arc::new(move |s| s.deck_id == deck_id))
.iter(&states_repo) .iter(states_repo)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let filtered_words = DeckData::list(deck, state) let filtered_words = DeckData::list(deck, state)
.iter() .iter()
+15 -23
View File
@@ -3,6 +3,7 @@ use crate::core::deck::DeckData;
use crate::core::srs::{AppendMode, OrderMode}; use crate::core::srs::{AppendMode, OrderMode};
use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData}; use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData};
use crate::data::repositories::{BatchedRepository, Repository}; use crate::data::repositories::{BatchedRepository, Repository};
use crate::jl_button;
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::MaybeError; use crate::notification::MaybeError;
@@ -15,22 +16,21 @@ use crate::push_error_task;
use crate::styling::*; use crate::styling::*;
use chrono::{Days, Local, Utc}; use chrono::{Days, Local, Utc};
use hashbrown::HashSet; use hashbrown::HashSet;
use iced::widget::button::{danger, Status};
pub use iced::widget::button::{Catalog, Style}; pub use iced::widget::button::{Catalog, Style};
use iced::widget::button::{Status, danger};
use iced::widget::tooltip::Position::Top; use iced::widget::tooltip::Position::Top;
use iced::widget::{ use iced::widget::{
Column, Row, button, column, container, lazy, radio, row, scrollable, space, svg, text, button, column, container, lazy, radio, row, scrollable, space, svg, text, text_input, tooltip,
text_input, tooltip, Column, Row,
}; };
use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme}; use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme};
use iced_core::Padding;
use iced_core::border::Radius; use iced_core::border::Radius;
use iced_core::svg::Handle; use iced_core::svg::Handle;
use iced_core::Padding;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone)]
pub struct RepetitionsState { pub struct RepetitionsState {
selected_deck_handle: Option<EntityHandle<DeckSettings>>, selected_deck_handle: Option<EntityHandle<DeckSettings>>,
decks: BTreeMap<EntityHandle<DeckSettings>, DeckRunData>, decks: BTreeMap<EntityHandle<DeckSettings>, DeckRunData>,
@@ -94,7 +94,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
self.clear_selection(); self.clear_selection();
} }
} else { } else {
let _ = self.fill_deck_word_indices(); self.fill_deck_word_indices();
} }
} }
fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> { fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> {
@@ -126,7 +126,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
DeleteSet => { DeleteSet => {
let index = self.selected_deck_handle.unwrap(); let index = self.selected_deck_handle.unwrap();
self.clear_selection(); self.clear_selection();
let deck = self.decks.remove(&index); self.decks.remove(&index);
if index.real() { if index.real() {
let mut repo = self.state.decks.write(); let mut repo = self.state.decks.write();
@@ -152,7 +152,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
let mut repo = self.state.decks.write(); let mut repo = self.state.decks.write();
repo.set(handle, deck.general_settings.clone()); repo.set(handle, deck.general_settings.clone());
let old = handle.clone(); let old = *handle;
if let Err(err) = repo.save(handle) { if let Err(err) = repo.save(handle) {
return push_error_task!(err); return push_error_task!(err);
} }
@@ -175,7 +175,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
} }
TryFilter => { TryFilter => {
let deck = self.selected_deck().unwrap(); 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(); let deck = self.selected_deck_mut().unwrap();
deck.count = Some(count); deck.count = Some(count);
} }
@@ -227,10 +227,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
row![ row![
column![ column![
scrollable(self.decks_list()).height(Fill), scrollable(self.decks_list()).height(Fill),
button("Добавить") jl_button!("Добавить").width(Fill).on_press(CreateSet),
.style(jl_button)
.width(Fill)
.on_press(CreateSet),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.width(Length::FillPortion(1)), .width(Length::FillPortion(1)),
@@ -256,9 +253,7 @@ impl RepetitionsState {
} }
} }
fn get_decks_from_state( fn get_decks_from_state(state: &AppState) -> BTreeMap<EntityHandle<DeckSettings>, DeckRunData> {
state: &AppState,
) -> BTreeMap<EntityHandle<DeckSettings>, DeckRunData> {
state state
.decks .decks
.read() .read()
@@ -331,9 +326,7 @@ impl RepetitionsState {
column![ column![
text!("Фильтр"), text!("Фильтр"),
text_input("", &deck.filter).on_input(SetFilter), text_input("", &deck.filter).on_input(SetFilter),
button("Проверить фильтр") jl_button!("Проверить фильтр").on_press(TryFilter),
.style(jl_button)
.on_press(TryFilter),
self.count_view(deck), self.count_view(deck),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
@@ -361,7 +354,7 @@ impl RepetitionsState {
Some(deck.open_mode), Some(deck.open_mode),
SetOpenMode SetOpenMode
), ),
button("История").style(jl_button).on_press(GoToHistory), jl_button!("История").on_press(GoToHistory),
] ]
.spacing(HALF_SPACING) .spacing(HALF_SPACING)
} }
@@ -378,7 +371,7 @@ impl RepetitionsState {
) )
.height(Fill), .height(Fill),
row![ row![
button("Сохранить").style(jl_button).on_press(Save), jl_button!("Сохранить").on_press(Save),
self.launch_delete_button(deck) self.launch_delete_button(deck)
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
@@ -520,7 +513,7 @@ impl RepetitionsState {
fn decks_list(&self) -> Column<'_, RepetitionsMessage> { fn decks_list(&self) -> Column<'_, RepetitionsMessage> {
let mut column = Column::new(); let mut column = Column::new();
for (i, (handle, deck)) in self.decks.iter().enumerate() { for (handle, deck) in self.decks.iter() {
column = column.push( column = column.push(
button(text!("{}", &deck.name)) button(text!("{}", &deck.name))
.on_press_with(move || SelectSet(*handle)) .on_press_with(move || SelectSet(*handle))
@@ -566,7 +559,6 @@ impl RepetitionsState {
deck: &DeckSettings, deck: &DeckSettings,
indices: impl Iterator<Item = EntityHandle<WordData>>, indices: impl Iterator<Item = EntityHandle<WordData>>,
) -> MaybeError { ) -> MaybeError {
let words = &state.dictionary;
let stats = &mut indices let stats = &mut indices
.map(|i| CardState { .map(|i| CardState {
id: 0.into(), id: 0.into(),
+3 -6
View File
@@ -1,5 +1,6 @@
use crate::app::AppState; use crate::app::AppState;
use crate::core::kana_set::{KanaSet, KanaType}; use crate::core::kana_set::{KanaSet, KanaType};
use crate::jl_button;
use crate::navigation::Page::*; use crate::navigation::Page::*;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::dictionary::DictionaryState; use crate::pages::dictionary::DictionaryState;
@@ -12,12 +13,10 @@ use crate::pages::writing::WritingState;
use crate::styling::*; use crate::styling::*;
use iced::widget::button::{Status, Style}; use iced::widget::button::{Status, Style};
use iced::widget::*; use iced::widget::*;
use iced::{Element, Task, alignment}; use iced::{alignment, Element, Task};
use iced_core::Alignment::Center; use iced_core::Alignment::Center;
use iced_core::Background; use iced_core::Background;
use iced_core::Color; use iced_core::Color;
use parking_lot::Mutex;
use std::sync::Arc;
pub struct SelectorState { pub struct SelectorState {
pub set: KanaSet, pub set: KanaSet,
@@ -81,9 +80,7 @@ impl NavigatedPage<SelectorMessage> for SelectorState {
container( container(
iced::widget::column![ iced::widget::column![
row![ row![
button("あ ↔ ア") jl_button!("あ ↔ ア").on_press(SelectorMessage::Change),
.on_press(SelectorMessage::Change)
.style(jl_button),
button("Словарь") button("Словарь")
.on_press(SelectorMessage::ToDictionary) .on_press(SelectorMessage::ToDictionary)
.style(Self::nav_style), .style(Self::nav_style),
+10 -17
View File
@@ -2,6 +2,7 @@ use crate::app::AppState;
use crate::data::repositories::Repository; use crate::data::repositories::Repository;
use crate::data_provider::settings::{delete_settings, set_setting}; use crate::data_provider::settings::{delete_settings, set_setting};
use crate::data_provider::web_api::*; use crate::data_provider::web_api::*;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::{NotificationData, NotificationRequest}; use crate::notification::{NotificationData, NotificationRequest};
@@ -31,7 +32,6 @@ pub enum SyncMessage {
DisableSync, DisableSync,
SwitchAutoSync, SwitchAutoSync,
} }
#[derive(Clone)]
pub struct SyncState { pub struct SyncState {
state: AppState, state: AppState,
auto_fetch: bool, auto_fetch: bool,
@@ -221,14 +221,13 @@ impl SyncState {
.padding(3) .padding(3)
) )
.style(rounded_box), .style(rounded_box),
button("Скопировать в буфер обмена") jl_button!("Скопировать в буфер обмена")
.style(jl_button)
.on_press(CopyKey) .on_press(CopyKey)
.width(Fill), .width(Fill),
row![ row![
button("↑ Отправить").style(jl_button).on_press(Send), jl_button!("↑ Отправить").on_press(Send),
space().width(Fill), space().width(Fill),
button("↓ Скачать").style(jl_button).on_press(GetLast) jl_button!("↓ Скачать").on_press(GetLast)
] ]
.spacing(DEFAULT_SPACING), .spacing(DEFAULT_SPACING),
toggler(self.auto_fetch) toggler(self.auto_fetch)
@@ -241,12 +240,8 @@ impl SyncState {
] ]
} else { } else {
column![ column![
button("Создать сохранение") jl_button!("Создать сохранение").on_press(InitSync),
.style(jl_button) jl_button!("Вставить ключ из буфера").on_press(GetKey),
.on_press(InitSync),
button("Вставить ключ из буфера")
.style(jl_button)
.on_press(GetKey),
] ]
} }
} }
@@ -273,12 +268,10 @@ impl SyncState {
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}"); error!("Ошибка при checkpoint WAL: {e}");
}); });
connection connection.execute("VACUUM;", []).unwrap_or_else(|e| {
.execute("VACUUM;", []) error!("Ошибка при checkpoint WAL: {e}");
.unwrap_or_else(|e| { 0
error!("Ошибка при checkpoint WAL: {e}"); });
0
});
connection connection
.execute("PRAGMA optimize;", []) .execute("PRAGMA optimize;", [])
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
+2 -11
View File
@@ -1,19 +1,13 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::{EntityHandle, WordData}; use crate::data::entities::{EntityHandle, WordData};
use crate::data_provider::words::{delete_word, update_word};
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::word::WordMessage::*; use crate::pages::word::WordMessage::*;
use crate::push_error;
use crate::push_error_task;
use crate::styling::*; use crate::styling::*;
use iced::widget::button::danger; use iced::widget::button::danger;
use iced::widget::{button, column, row, rule, scrollable, space, text, text_input}; use iced::widget::{button, column, row, space, text, text_input};
use iced::{Element, Fill, Task}; use iced::{Element, Fill, Task};
use parking_lot::Mutex;
use std::sync::Arc;
#[derive(Clone)]
pub struct WordState { pub struct WordState {
state: AppState, state: AppState,
handle: EntityHandle<WordData>, handle: EntityHandle<WordData>,
@@ -137,10 +131,7 @@ impl NavigatedPage<WordMessage> for WordState {
impl WordState { impl WordState {
pub fn new(handle: EntityHandle<WordData>, state: AppState) -> Self { pub fn new(handle: EntityHandle<WordData>, state: AppState) -> Self {
Self{ Self { state, handle }
state,
handle,
}
} }
} }
+3 -7
View File
@@ -1,4 +1,5 @@
use crate::core::kana_set::KanaSet; use crate::core::kana_set::KanaSet;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::styling::*; use crate::styling::*;
@@ -6,7 +7,6 @@ use iced::widget::*;
use iced::{Element, Fill, Task, alignment}; use iced::{Element, Fill, Task, alignment};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
#[derive(Clone, Debug)]
pub struct WritingState { pub struct WritingState {
kana: String, kana: String,
set: Vec<(String, String)>, set: Vec<(String, String)>,
@@ -45,12 +45,8 @@ impl NavigatedPage<WritingMessage> for WritingState {
text!("{}", self.kana).size(48), text!("{}", self.kana).size(48),
self.answers(), self.answers(),
row![ row![
button(text!("{}", self.next_text)) jl_button!(text!("{}", self.next_text)).on_press(WritingMessage::Next),
.style(jl_button) jl_button!("Закончить").on_press(WritingMessage::Back),
.on_press(WritingMessage::Next),
button("Закончить")
.style(jl_button)
.on_press(WritingMessage::Back),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
] ]
+13 -9
View File
@@ -1,7 +1,7 @@
use iced::Element; use iced::Element;
use iced::border::Radius; use iced::border::Radius;
use iced::widget::button::*; use iced::widget::button::*;
use iced::widget::{button, container}; use iced::widget::container;
use iced_core::Length::Fill; use iced_core::Length::Fill;
use iced_core::alignment::Horizontal::Left; use iced_core::alignment::Horizontal::Left;
use iced_core::{Border, Theme}; 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 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); let mut style = primary(theme, status);
style.border = Border { style.border = Border {
color: Default::default(), color: Default::default(),
@@ -30,15 +30,19 @@ pub fn cta_button(theme: &Theme, status: Status) -> Style {
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> { pub fn back_overlay<'a, T: Clone + 'a>(content: Element<'a, T>, back_message: T) -> Element<'a, T> {
container( container(
iced::widget::column![ iced::widget::column![jl_button!("Назад").on_press(back_message), content]
button("Назад").style(jl_button).on_press(back_message), .spacing(DEFAULT_SPACING)
content .align_x(Left)
] .width(Fill),
.spacing(DEFAULT_SPACING)
.align_x(Left)
.width(Fill),
) )
.center_x(Fill) .center_x(Fill)
.padding(10) .padding(10)
+4 -4
View File
@@ -73,13 +73,13 @@ mod mod_tests {
#[test] #[test]
fn id_0() { fn id_0() {
let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true); let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true);
assert_eq!(entity.real(), true) assert!(entity.real())
} }
#[test] #[test]
fn id_1() { fn id_1() {
let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false); let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false);
assert_eq!(entity.real(), false) assert!(!entity.real())
} }
#[test] #[test]
@@ -97,14 +97,14 @@ mod mod_tests {
fn id_5() { fn id_5() {
let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true); let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true);
entity.set_real(true); entity.set_real(true);
assert_eq!(entity.real(), true) assert!(entity.real())
} }
#[test] #[test]
fn id_6() { fn id_6() {
let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false); let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false);
entity.set_real(true); entity.set_real(true);
assert_eq!(entity.real(), true) assert!(entity.real())
} }
#[test] #[test]