Author SHA1 Message Date
micialware f4903725aa deck list code 2026-10-04 16:47:16 +03:00
micialware 936efd0e99 Fsrs pools change 2026-09-23 18:05:58 +03:00
micialware 8603e9b981 Fix editor bug 2026-09-23 14:37:42 +03:00
micialware 824a868d73 Small database sync update 2026-09-22 21:57:29 +03:00
micialware 8b673e52da Change selection type on extended editing 2026-09-22 15:48:18 +03:00
micialware b210b2013d Fix saving bug 2026-09-22 15:27:39 +03:00
micialware 48cd68d1c7 Fix incorrect states clearing 2026-09-21 11:53:07 +03:00
micialware a5526dc5e9 Migrate to String::new() 2026-09-20 22:24:42 +03:00
micialware 1d4b37e381 Update version, remove unused features 2026-09-20 22:18:59 +03:00
micialware 96a58c61bc Rollback default control radius 2026-09-15 22:11:38 +03:00
micialware 1847d4f44f apply new style 2026-09-15 22:09:06 +03:00
micialware b1902b35cb style and macros for text input 2026-09-15 21:54:24 +03:00
micialware 5d12f3b02a Fixing incorrect history reading bug 2026-09-14 19:41:02 +03:00
micialware 0e867a33ce Merge pull request 'Updated word display' (#19) from updated-word-display into master
Reviewed-on: #19
2026-09-13 17:42:58 +03:00
micialware ccfb302770 Prepare for new anki format 2026-09-13 12:44:51 +03:00
micialware 0de809fcce Compressing word fields 2026-09-11 22:08:50 +03:00
micialware e5622c70de Fix deck filter 2026-09-11 14:02:57 +03:00
micialware cbf49412ec additional fields encoding and decoding 2026-09-11 13:56:26 +03:00
micialware f61f93717c Custom fields repo into AppState 2026-09-11 13:28:23 +03:00
micialware be64b1da2d Fix group delete 2026-09-11 13:12:45 +03:00
micialware 2568b9f292 More fixes for dictionary panel 2026-09-11 13:09:20 +03:00
micialware 05c82bee02 Fix tag edit bug 2026-09-11 12:48:04 +03:00
micialware 5d305affd3 Fixing some bugs 2026-09-11 12:38:21 +03:00
micialware d59776d713 More keyboard handling for dictionary 2026-09-11 12:03:13 +03:00
micialware adb6388ee2 Prepare for extended word edit 2026-09-11 11:47:04 +03:00
micialware 8fe4f3b8f4 New selected field movement 2026-09-11 00:41:36 +03:00
micialware 47bc83d4ae New edit process 2026-09-11 00:01:19 +03:00
micialware 097c3c033c Merge pull request 'reformat custom fields table. will return to this topic later' (#18) from compressed-fields into master
Reviewed-on: #18
2026-09-09 15:16:01 +03:00
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
micialware 444352c007 Merge pull request 'Repo integration' (#15) from repo-integration into master
Reviewed-on: #15
2026-09-09 12:47:42 +03:00
micialware 2069bdf5b3 Fix already updated cache change 2026-09-09 12:47:22 +03:00
micialware 28730c0fda Delete saved card states 2026-09-09 12:35:00 +03:00
micialware 7715aaff3b Fixing tag selection 2026-09-09 12:12:44 +03:00
micialware a20c72eb9d Optimize groups query 2026-09-09 11:23:51 +03:00
micialware 58bdaac7fe Remove word display widget base 2026-09-09 11:13:50 +03:00
micialware 4a71f31e6f Fix import 2026-09-09 10:53:03 +03:00
micialware 6e3f58d21c Version update 2026-09-09 10:43:26 +03:00
micialware 3663765284 Db saving cases 2026-09-09 10:33:21 +03:00
micialware 294f0e8b98 Query time and database optimization 2026-09-09 10:19:44 +03:00
micialware 2e04450c76 Working repetitions 2026-09-09 10:08:52 +03:00
micialware 242b56fc21 Fixes for sqlite data layer 2026-09-08 00:09:13 +03:00
micialware 7f9aa11d6e More fixes for dictionary 2026-09-07 18:19:54 +03:00
micialware 92b019b072 Temporal dictionary display fix 2026-09-07 18:00:50 +03:00
micialware 9c93ca4408 Merge remote-tracking branch 'origin/word-display' into repo-integration 2026-09-07 17:17:18 +03:00
micialware 6387afba93 Incorrect data display fix 2026-09-07 16:21:40 +03:00
micialware 42c38d55e1 Fix more bugs 2026-09-07 16:10:10 +03:00
micialware 60dae212ec Fix incorrect filter light in repetition page 2026-09-07 15:49:39 +03:00
micialware b3b5785fd7 Fix some bugs in repo and repetitions page 2026-09-07 15:48:10 +03:00
micialware bdf71fa0b7 Small history table change 2026-09-07 10:59:55 +03:00
micialware f9c4bf9c03 App is building 2026-09-07 10:53:09 +03:00
micialware 7e1bb4bc22 temporal solving word 2026-09-06 23:58:41 +03:00
micialware 1afb7493f6 Sync solved 2026-09-06 23:55:50 +03:00
micialware e2f367f3aa WordDisplay base 2026-09-06 10:13:33 +03:00
micialware 22c05250aa Repetition settings remake 2026-09-04 22:34:36 +03:00
micialware a68e56e861 Solve repetition 2026-09-04 17:26:59 +03:00
micialware 08ab55f283 Fix deck data 2026-09-04 13:00:41 +03:00
micialware 9a7578727b Fix data access method and solve import page 2026-09-04 11:37:19 +03:00
micialware 532640058e Rewrite history page 2026-09-04 11:18:23 +03:00
micialware f19a6f64c3 Dictionary partial rewritten and dictionary quiz rewritten 2026-09-03 00:36:15 +03:00
micialware f2035ca749 Partial dictionary rewrite 2026-09-02 00:19:58 +03:00
micialware 1509947cb0 Temporal todo 2026-08-31 14:50:21 +03:00
micialware 55bdc5a7ad New repository methods 2026-08-31 14:35:11 +03:00
micialware 11d5144db5 Added repositories into AppState 2026-08-31 12:12:14 +03:00
micialware 6c20561ede Repository types defined 2026-08-31 11:52:54 +03:00
micialware 0c4aec3dc0 Partial AppState rewrite 2026-08-31 11:46:35 +03:00
micialware 29a90d48d9 Reformat code 2026-08-31 11:26:05 +03:00
micialware 2fc58fcc2d Custom fields table 2026-08-31 11:25:41 +03:00
micialware 6d363c8148 Custom fields table 2026-08-31 11:23:41 +03:00
micialware ba834ac9b1 Impl Id convert methods 2026-08-31 11:21:01 +03:00
micialware 97e301b0d6 Merge pull request 'Db layers' (#14) from db_layers into master
Reviewed-on: #14
2026-08-30 23:38:33 +03:00
47 changed files with 2870 additions and 3018 deletions

No files matched your search

Generated
+70 -644
View File
File diff suppressed because it is too large. Load diff
+9 -17
View File
@@ -1,23 +1,23 @@
[package]
name = "jap_learn"
version = "0.1.3"
version = "0.2.0"
edition = "2024"
[dependencies]
iced = { version = "0.14.0", features = ["tokio", "svg", "lazy"]}
iced = { version = "0.14.0", features = ["tokio", "svg", "lazy", "crisp", "tiny-skia", "wayland", "wgpu"], default-features = false}
iced_core = "0.14.0"
rand = "0.10.2"
dirs = "6.0.0"
rand = "0.10.3"
dirs = "7.0.0"
serde_json = "1.0.151"
rhai = { version = "1.25.1", features = ["sync"] }
rhai = { version = "1.26.1", features = ["sync"] }
chrono = "0.4.45"
rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] }
reqwest = { version = "0.13.4", features = ["json", "stream", "multipart"] }
reqwest = { version = "0.13.5", features = ["json", "stream", "multipart"] }
rodio = "0.22.2"
sha2 = "0.11.0"
tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros"] }
hex = "0.4.3"
zstd = "0.13.3"
zstd = "0.14.0"
zip = "8.6.0"
rfd = "0.17.2"
mimalloc = "0.1.52"
@@ -26,8 +26,8 @@ rayon = "1.12.0"
parking_lot = "0.12.5"
env_logger = "0.11.11"
log = "0.4.34"
fsrs = "6.6.1"
litemap = { version = "0.8.3", features = ["serde"] }
fsrs = "6.6.2"
litemap = { version = "0.8.3" }
[profile.super-release]
inherits = "release"
@@ -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);
+6
View File
@@ -0,0 +1,6 @@
create table custom_fields
(
id integer primary key,
name text not null,
identifier text not null
);
+5 -5
View File
@@ -1,9 +1,9 @@
create table decks
(
id INTEGER
id integer
primary key autoincrement,
name TEXT not null,
forward TEXT not null,
backward TEXT not null,
filter TEXT not null
name text not null,
forward text not null,
backward text not null,
filter text not null
);
+8 -8
View File
@@ -1,12 +1,12 @@
create table history
(
id INTEGER
id integer
primary key autoincrement,
set_id INTEGER not null references decks on delete cascade,
stat_id INTEGER not null references states on delete cascade,
timestamp INTEGER not null,
answer INTEGER not null,
before INTEGER not null,
after INTEGER not null,
fsrs INTEGER not null
deck_id integer not null references decks on delete cascade,
word_id integer not null references words on delete cascade,
timestamp integer not null,
answer integer not null,
before integer not null,
after integer not null,
fsrs integer not null
);
+8 -8
View File
@@ -1,16 +1,16 @@
create table states
(
id INTEGER
id integer
primary key autoincrement,
word_id INTEGER not null
word_id integer not null
references words
on delete cascade,
set_id INTEGER not null
deck_id integer not null
references decks
on delete cascade,
score INTEGER default 1 not null,
last_opened INTEGER not null,
fsrs_opened INTEGER not null,
fsrs_memory INTEGER not null,
fsrs_open_day INTEGER not null
score integer default 1 not null,
last_opened integer not null,
fsrs_opened integer not null,
fsrs_memory integer not null,
fsrs_open_day integer not null
);
+2 -2
View File
@@ -1,6 +1,6 @@
create table word_groups
(
id INTEGER
id integer
primary key autoincrement,
name TEXT not null
name text not null
);
+5 -5
View File
@@ -1,11 +1,11 @@
create table words
(
id INTEGER
id integer
primary key autoincrement,
key TEXT not null,
value TEXT not null,
tags TEXT not null,
more TEXT,
key text not null,
value text not null,
tags text not null,
more text,
group_id integer default 1 not null
constraint words_word_groups_id_fk
references word_groups
+41 -29
View File
@@ -1,21 +1,24 @@
use crate::data::Id;
use crate::data::entities::DeckSettings;
use crate::data::entities::{WordData, WordGroup};
use crate::data_provider::decks::load_sets;
use crate::data::repositories::Repository;
use crate::data_provider::card_states::CardStateRepository;
use crate::data_provider::custom_fields::CustomFieldRepository;
use crate::data_provider::decks::DeckRepository;
use crate::data_provider::history::HistoryRepository;
use crate::data_provider::settings::get_setting;
use crate::data_provider::sqlite::default_connection;
use crate::data_provider::words::{load_word_groups, load_words};
use crate::data_provider::sqlite::SqliteConnection;
use crate::data_provider::word_groups::WordGroupRepository;
use crate::data_provider::words::WordRepository;
use crate::navigation::RootMessage::Keyboard;
use crate::navigation::{AppSettings, RootMessage, ScreenState};
use chrono::NaiveDate;
use hashbrown::HashMap;
use iced::{Subscription, keyboard, window};
use iced_core::window::Position;
#[cfg(target_os = "linux")]
use iced_core::window::settings::PlatformSpecific;
use iced_core::{Font, Size, Theme};
use log::info;
use parking_lot::{Mutex, RwLock};
use rusqlite::Connection;
use std::sync::Arc;
pub const USER_FONT: Font = Font::with_name("Noto Sans JP");
pub fn start() -> iced::Result {
@@ -57,13 +60,16 @@ fn subscription(_state: &ScreenState) -> Subscription<RootMessage> {
keyboard::listen().map(Keyboard)
}
#[derive(Clone)]
pub struct AppState {
pub dictionary: Vec<WordData>,
pub decks: Vec<DeckSettings>,
pub word_groups: Vec<WordGroup>,
pub connection: Connection,
pub sync_data: AppSettings,
pub activity: HashMap<Id, Vec<(NaiveDate, u32)>>,
pub dictionary: WordRepository,
pub decks: DeckRepository,
pub word_groups: WordGroupRepository,
pub card_states: CardStateRepository,
pub custom_fields: CustomFieldRepository,
pub connection: Arc<Mutex<SqliteConnection>>,
pub sync_data: Arc<RwLock<AppSettings>>,
pub history: HistoryRepository,
}
impl Default for AppState {
@@ -74,30 +80,36 @@ impl Default for AppState {
impl AppState {
pub fn new() -> Self {
let connection = Arc::new(Mutex::new(SqliteConnection::default()));
Self {
dictionary: vec![],
decks: vec![],
word_groups: vec![],
connection: default_connection(),
sync_data: AppSettings {
dictionary: WordRepository::new(connection.clone()),
decks: DeckRepository::new(connection.clone()),
word_groups: WordGroupRepository::new(connection.clone()),
card_states: CardStateRepository::new(connection.clone()),
history: HistoryRepository::new(connection.clone()),
custom_fields: CustomFieldRepository::new(connection.clone()),
sync_data: Arc::new(RwLock::new(AppSettings {
key: None,
auto_web_fetch: false,
},
activity: Default::default(),
})),
connection,
}
}
}
pub fn fill_state(state: &mut AppState) {
let list = load_words(&state.connection);
let sets = load_sets(&state.connection);
let groups = load_word_groups(&state.connection);
let setting = load_settings(&state.connection);
let connection = state.connection.lock();
state.dictionary = list.unwrap_or_default();
state.decks = sets.unwrap_or_default();
state.word_groups = groups.unwrap_or_default();
state.sync_data = setting
let setting = load_settings(&connection);
drop(connection);
state.word_groups.write().start().unwrap();
state.dictionary.write().start().unwrap();
state.decks.write().start().unwrap();
state.history.write().start().unwrap();
state.card_states.write().start().unwrap();
state.custom_fields.write().start().unwrap();
state.sync_data = Arc::new(RwLock::new(setting))
}
fn load_settings(connection: &Connection) -> AppSettings {
+155 -87
View File
@@ -4,135 +4,203 @@ use crate::core::srs::{
FsrsModule, OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule,
WordOpenMode, WorstWordsSRSModule,
};
use crate::data::Id;
use crate::data::entities::*;
use crate::data_provider::card_states::{load_stats_of_deck, update_stat_score};
use crate::data_provider::history::push_note;
use crate::notification::{MaybeError, NotificationData};
use crate::data::repositories::Repository;
use crate::data_provider::card_states::CardStateRepositoryInner;
use crate::notification::MaybeError;
use chrono::Utc;
use parking_lot::Mutex;
use litemap::LiteMap;
use log::info;
use rhai::{Engine, Scope};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
#[derive(Clone)]
pub struct DeckData {
pub(crate) words: Vec<WordData>,
pub(crate) set: Vec<CardState>,
pub(crate) words: Vec<EntityHandle<WordData>>,
pub(crate) states: Vec<EntityHandle<CardState>>,
current_word_index: Option<usize>,
state: Arc<Mutex<AppState>>,
state: AppState,
order_module: OrderModule,
open_mode: OrderMode,
settings: DeckSettings,
}
struct DisplayStyle {
properties: LiteMap<String, String>,
}
impl DeckData {
pub fn new(
settings: &DeckSettings,
state: Arc<Mutex<AppState>>,
) -> Result<Self, NotificationData> {
let state_for = state.clone();
let state_locked = state.lock();
let stats = load_stats_of_deck(settings, &state_locked.connection)?;
let words = stats
.iter()
.map(|stat| {
state_locked
.dictionary
.binary_search_by_key(&stat.word_id, |w| w.id)
.expect("No word found in dictionary")
})
.map(|index| &state_locked.dictionary[index])
.cloned()
pub fn new(settings: &DeckSettings, open_mode: OrderMode, state: AppState) -> Self {
let states_repo = state.card_states.read();
let mut query = states_repo.deck_states(settings.id);
let words = query
.iter(&states_repo)
.map(|card_state| EntityHandle::from_id(card_state.word_id))
.collect();
Ok(Self {
set: stats,
let states = query.handles().collect::<Vec<EntityHandle<CardState>>>();
let fsrs = Fsrs(FsrsModule::new(
Self::set_links(states.as_slice(), &states_repo).as_slice(),
));
drop(states_repo);
Self {
states,
words,
current_word_index: None,
state: state_for,
order_module: Fsrs(FsrsModule::new()),
open_mode: settings.open_mode,
state,
order_module: fsrs,
open_mode,
settings: settings.clone(),
})
}
}
pub fn next(&mut self) -> (WordData, CardState) {
pub fn next(&mut self) -> (EntityHandle<WordData>, EntityHandle<CardState>) {
if self.fsrs_finished() {
self.switch_order_engine()
}
let repo = self.state.card_states.read();
let links = Self::set_links(self.states.as_slice(), &repo);
let index = match self.order_module {
SemiRandomSRS(ref mut module) => {
if !module.initialized {
module.init(self.set.as_slice())
}
module.next(self.set.as_slice())
}
SemiRandomSRS(ref mut module) => module.next(links.as_slice()),
RandomSRS(ref mut module) => {
if !module.initialized {
module.init(self.set.as_slice())
}
module.next(self.set.as_slice())
}
WorstWordsSRS(ref mut module) => {
if !module.initialized {
module.init(self.set.as_slice())
}
module.next(self.set.as_slice())
}
Fsrs(ref mut module) => {
if !module.initialized {
module.init(self.set.as_slice())
}
module.next(self.set.as_slice())
}
RandomSRS(ref mut module) => module.next(links.as_slice()),
WorstWordsSRS(ref mut module) => module.next(links.as_slice()),
Fsrs(ref mut module) => module.next(links.as_slice()),
};
self.current_word_index = Some(index);
(self.words[index].clone(), self.set[index].clone())
(self.words[index], self.states[index])
}
fn set_links<'a>(
set: &[EntityHandle<CardState>],
repo: &'a CardStateRepositoryInner,
) -> Vec<&'a CardState> {
set.iter()
.map(|h| repo.get_ref_static(*h).unwrap())
.collect::<Vec<_>>()
}
pub fn open(&mut self, status: WordOpenMode) -> MaybeError {
if let Some(index) = self.current_word_index {
let word: &mut CardState = &mut self.set[index];
let old_score = word.score;
word.update(status);
let new_score = word.score;
let mut repo = self.state.card_states.write();
let card_state_handle = &mut self.states[index];
let card_state = repo.get_mut(card_state_handle).unwrap();
let old_score = card_state.score;
card_state.update(status);
let new_score = card_state.score;
let mut fsrs = false;
match self.order_module {
SemiRandomSRS(ref mut module) => {
module.open(status, index, word);
module.open(status, index, card_state);
}
RandomSRS(ref mut module) => {
module.open(status, index, word);
module.open(status, index, card_state);
}
WorstWordsSRS(ref mut module) => {
module.open(status, index, word);
module.open(status, index, card_state);
}
Fsrs(ref mut module) => {
module.open(status, index, word);
module.open(status, index, card_state);
fsrs = true;
}
}
if let Fsrs(ref fsrs) = self.order_module
&& fsrs.finished()
{
self.order_module = match self.open_mode {
OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new()),
OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new()),
OrderMode::FullRandom => RandomSRS(RandomSRSModule::new()),
}
}
update_stat_score(word, &self.state.lock().connection)?;
let _ = push_note(
self.settings.id,
HistoryItem {
timestamp: Utc::now(),
word_id: word.word_id,
mode: status,
before: old_score,
after: new_score,
},
);
let word_id = card_state.word_id;
repo.save(card_state_handle)?;
let mut history = self.state.history.write();
history.create_now(HistoryNote {
id: Default::default(),
timestamp: Utc::now(),
deck_id: self.settings.id,
word_id,
answer: status,
before: old_score,
after: new_score,
fsrs,
})?;
}
Ok(())
}
fn switch_order_engine(&mut self) {
let repo = self.state.card_states.read();
let links = Self::set_links(self.states.as_slice(), &repo);
self.order_module = match self.open_mode {
OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new(links.as_slice())),
OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new(links.as_slice())),
OrderMode::FullRandom => RandomSRS(RandomSRSModule::new(links.as_slice())),
}
}
fn fsrs_finished(&self) -> bool {
if let Fsrs(ref fsrs) = self.order_module
&& fsrs.finished()
{
info!("Fsrs finished. Infinite mode enabled");
return true;
}
false
}
pub fn len(&self) -> usize {
self.set.len()
self.states.len()
}
pub fn list(deck: &DeckSettings, state: &AppState) -> Vec<EntityHandle<WordData>> {
let time = Instant::now();
let list = vec![];
let engine = Engine::new();
let ast = engine.compile(&deck.filter);
if let Ok(ast) = ast {
let groups = state.word_groups.read().iter().cloned().collect::<Vec<_>>();
let dictionary = &state.dictionary.read();
let fields: HashMap<Id, String> = HashMap::from_iter(
state
.custom_fields
.read()
.iter()
.map(|f| (f.id, f.identifier.clone())),
);
let query = dictionary.query_par(Arc::new(move |word| {
let mut more = rhai::Map::new();
for kv in &word.additional {
more.insert(fields[&kv.0].clone().into(), kv.1.clone().into());
}
let mut scope = Scope::new();
let group_name = groups[groups
.binary_search_by_key(&word.group_id, |g| g.id)
.unwrap()]
.name
.clone();
scope
.push_constant("id", word.id.0 as i64)
.push_constant("key", word.key.clone())
.push_constant("value", word.value.clone())
.push_constant("tags", word.tags.clone())
.push_constant("more", more)
.push_constant("group", group_name);
let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast);
if let Ok(result) = result {
return result;
}
false
}));
info!("Collecting available words is {:?}", time.elapsed());
let handles = query.handles().collect();
drop(query);
handles
} else {
list
}
}
}
+65 -108
View File
@@ -31,25 +31,22 @@ pub enum OrderModule {
Fsrs(FsrsModule),
}
pub trait SRSModule {
fn next(&mut self, set: &[CardState]) -> usize;
fn new(set: &[&CardState]) -> Self;
fn next(&mut self, set: &[&CardState]) -> usize;
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &mut CardState);
fn init(&mut self, set: &[CardState]);
}
#[derive(Clone)]
pub struct RandomSRSModule {
basket: Vec<usize>,
pub(crate) initialized: bool,
}
impl RandomSRSModule {
pub(crate) fn new() -> RandomSRSModule {
Self {
basket: vec![],
initialized: false,
}
}
}
impl SRSModule for RandomSRSModule {
fn next(&mut self, set: &[CardState]) -> usize {
fn new(set: &[&CardState]) -> Self {
let mut basket = (0..set.len()).collect::<Vec<usize>>();
basket.shuffle(&mut rand::rng());
Self { basket }
}
fn next(&mut self, set: &[&CardState]) -> usize {
match self.basket.pop() {
Some(index) => index,
None => {
@@ -61,32 +58,27 @@ impl SRSModule for RandomSRSModule {
}
fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {}
fn init(&mut self, set: &[CardState]) {
self.initialized = true;
self.basket = (0..set.len()).collect::<Vec<usize>>();
self.basket.shuffle(&mut rand::rng())
}
}
#[derive(Clone)]
pub struct SemiRandomSRSModule {
history: Vec<usize>,
last_weights: WeightedIndex<f32>,
generator: ThreadRng,
pub(crate) initialized: bool,
}
impl SemiRandomSRSModule {
pub(crate) fn new() -> SemiRandomSRSModule {
SemiRandomSRSModule {
history: vec![],
last_weights: WeightedIndex::new([1.0]).unwrap(),
generator: rng(),
initialized: false,
}
}
}
impl SRSModule for SemiRandomSRSModule {
fn next(&mut self, set: &[CardState]) -> usize {
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 {
generator: rng(),
last_weights: WeightedIndex::new(weights).expect("Incorrect weights update"),
history: vec![],
}
}
fn next(&mut self, set: &[&CardState]) -> usize {
let index = self.last_weights.sample(&mut self.generator);
if self.history.contains(&index) && self.history.len() != 1 {
@@ -106,23 +98,14 @@ impl SRSModule for SemiRandomSRSModule {
.update_weights(&[(index, &new_weight)])
.expect("update weights error");
}
fn init(&mut self, set: &[CardState]) {
self.initialized = true;
let weights = set
.iter()
.map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0)
.collect::<Vec<f32>>();
self.last_weights = WeightedIndex::new(weights).expect("Incorrect weights update");
}
}
impl SemiRandomSRSModule {
fn history_len(&self, set: &[CardState]) -> usize {
fn history_len(&self, set: &[&CardState]) -> usize {
((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN)
}
}
#[derive(Clone)]
pub struct WorstWordsSRSModule {
pub(crate) initialized: bool,
pool_size: usize,
rounds_remaining: u8,
pool: Vec<usize>,
@@ -130,7 +113,17 @@ pub struct WorstWordsSRSModule {
rounds_count: u8,
}
impl SRSModule for WorstWordsSRSModule {
fn next(&mut self, set: &[CardState]) -> usize {
fn new(_: &[&CardState]) -> Self {
WorstWordsSRSModule {
pool_size: PARTIAL_POOL_SIZE,
rounds_count: 2,
rounds_remaining: 0,
pool: vec![],
queue: vec![],
}
}
fn next(&mut self, set: &[&CardState]) -> usize {
if self.rounds_remaining == 0 {
self.fill_pool(set);
self.rounds_remaining = self.rounds_count;
@@ -147,23 +140,9 @@ impl SRSModule for WorstWordsSRSModule {
}
fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {}
fn init(&mut self, _: &[CardState]) {
self.initialized = true;
}
}
impl WorstWordsSRSModule {
pub(crate) fn new() -> WorstWordsSRSModule {
WorstWordsSRSModule {
initialized: false,
pool_size: PARTIAL_POOL_SIZE,
rounds_count: 2,
rounds_remaining: 0,
pool: vec![],
queue: vec![],
}
}
fn fill_pool(&mut self, set: &[CardState]) {
fn fill_pool(&mut self, set: &[&CardState]) {
let mut sorted = set.iter().enumerate().collect::<Vec<_>>();
sorted.sort_by_key(|c| c.1.score);
let mut worst = sorted[0..self.pool_size]
@@ -177,21 +156,33 @@ impl WorstWordsSRSModule {
#[derive(Clone)]
pub struct FsrsModule {
today_set: Vec<usize>,
solved: Vec<usize>,
current_group: Vec<usize>,
next_round: Vec<usize>,
queue: Vec<usize>,
rng: ThreadRng,
fsrs: FSRS,
pub(crate) initialized: bool,
}
impl SRSModule for FsrsModule {
fn next(&mut self, _: &[CardState]) -> usize {
if self.current_group.is_empty() {
self.fill_partial_pool();
}
fn new(set: &[&CardState]) -> Self {
let today = Local::now().date_naive();
let mut new = Self {
queue: set
.iter()
.enumerate()
.filter(|(_, c)| c.fsrs_open_day <= today)
.map(|(index, _)| index)
.collect(),
next_round: vec![],
rng: rng(),
fsrs: FSRS::default(),
};
new.queue.shuffle(&mut new.rng);
new
}
fn next(&mut self, _: &[&CardState]) -> usize {
if self.queue.is_empty() {
self.fill_queue();
self.queue.shuffle(&mut self.rng);
@@ -222,66 +213,32 @@ impl SRSModule for FsrsModule {
let offset = ((rng().random::<f32>() - 0.5) * 3.0).round() as i32;
next_day += TimeDelta::days(offset as i64);
}
let first_time = updated_word.fsrs_memory.is_none();
updated_word.fsrs_memory = Some(next.memory);
updated_word.fsrs_open_day = next_day;
updated_word.fsrs_opened = updated_word.last_open.date_naive();
if updated_word.fsrs_open_day != today {
self.solved.push(index);
let index = self.current_group.iter().position(|x| *x == index).unwrap();
self.current_group.remove(index);
if (updated_word.fsrs_open_day == today && !first_time)
|| (first_time && status != WordOpenMode::Easy)
{
self.next_round.push(index);
}
}
fn init(&mut self, set: &[CardState]) {
self.rng = rng();
let today = Local::now().date_naive();
self.today_set = set
.iter()
.enumerate()
.filter(|(_, c)| c.fsrs_open_day <= today)
.map(|(index, _)| index)
.collect();
self.today_set.shuffle(&mut self.rng);
self.fill_partial_pool();
self.initialized = true;
}
}
impl FsrsModule {
pub fn new() -> Self {
Self {
today_set: vec![],
current_group: vec![],
queue: vec![],
solved: vec![],
rng: Default::default(),
fsrs: FSRS::default(),
initialized: false,
}
}
fn fill_partial_pool(&mut self) {
self.current_group.extend(
self.today_set
.iter()
.filter(|x| !self.solved.contains(*x))
.take(PARTIAL_POOL_SIZE),
);
self.queue.clear();
self.queue.extend(self.current_group.clone());
}
fn fill_queue(&mut self) {
self.queue.extend(self.current_group.clone());
self.queue.append(&mut self.next_round);
}
pub fn finished(&self) -> bool {
self.today_set.len() == self.solved.len()
self.queue.is_empty() && self.next_round.is_empty()
}
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum WordOpenMode {
Easy,
Ok,
+14 -2
View File
@@ -3,7 +3,7 @@ pub mod repositories;
use rusqlite::ToSql;
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
use std::fmt::{write, Display, Formatter};
use std::fmt::{Display, Formatter};
use std::num::ParseIntError;
use std::str::FromStr;
@@ -63,8 +63,20 @@ macro_rules! id_impl {
self.0 != 0
}
}
}
};
}
id_impl!(u32, Id, INVALID_ID);
id_impl!(u16, SmallId, INVALID_SMALL_ID);
impl From<SmallId> for Id {
fn from(value: SmallId) -> Self {
Self(value.0 as u32)
}
}
impl From<Id> for SmallId {
fn from(value: Id) -> Self {
Self(value.0 as u16)
}
}
+98 -76
View File
@@ -1,15 +1,16 @@
use crate::app::AppState;
use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, OrderMode, WordOpenMode};
use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, WordOpenMode};
use crate::data::{INVALID_ID, Id};
use chrono::{DateTime, NaiveDate, Utc};
use crate::data_provider::history::HistoryRepositoryInner;
use chrono::{DateTime, Local, NaiveDate, Utc};
use fsrs::MemoryState;
use log::info;
use rayon::prelude::*;
use rhai::{Engine, Scope};
use std::cmp::Ordering;
use std::marker::PhantomData;
use std::time::Instant;
use litemap::LiteMap;
use log::info;
use rhai::Engine;
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Instant;
#[derive(Clone, Debug)]
pub struct WordData {
@@ -18,7 +19,7 @@ pub struct WordData {
pub key: String,
pub value: String,
pub tags: String,
pub additional: LiteMap<String, String>,
pub additional: LiteMap<Id, String>,
}
impl EntityData for WordData {
@@ -61,12 +62,51 @@ impl WordData {
}
#[derive(Clone)]
pub struct HistoryItem {
pub struct HistoryNote {
pub id: Id,
pub timestamp: DateTime<Utc>,
pub deck_id: Id,
pub word_id: Id,
pub mode: WordOpenMode,
pub answer: WordOpenMode,
pub before: u8,
pub after: u8,
pub fsrs: bool,
}
impl EntityData for HistoryNote {
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
}
impl HistoryRepositoryInner {
pub fn activity_of_deck(&self, id: Id) -> Vec<(NaiveDate, u32)> {
let mut query = self.query(Arc::new(move |note| note.deck_id == id));
let values = query.iter(self).collect::<Vec<_>>();
let mut counts = vec![];
if !values.is_empty() {
let mut date = values[0].timestamp.with_timezone(&Local).date_naive();
let mut count = 0;
for note in values {
let current_date = note.timestamp.with_timezone(&Local).date_naive();
if date == current_date {
count += 1;
} else {
counts.push((date, count));
date = current_date;
count = 0;
}
}
if count != 0 {
counts.push((date, count));
}
}
counts
}
}
#[derive(Clone)]
@@ -76,19 +116,26 @@ pub struct DeckSettings {
pub forward: String,
pub backward: String,
pub filter: String,
pub count: Option<usize>,
pub open_mode: OrderMode,
}
impl EntityData for DeckSettings {
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
}
impl DeckSettings {
pub(crate) fn with_name(name: String) -> DeckSettings {
DeckSettings {
id: 0.into(),
name,
forward: "".to_string(),
backward: "".to_string(),
forward: String::new(),
backward: String::new(),
filter: "true".to_string(),
count: None,
open_mode: OrderMode::Default,
}
}
pub fn check_filter(&self) -> bool {
@@ -98,57 +145,6 @@ impl DeckSettings {
info!("Compile time is {:?}", time.elapsed());
ast.is_ok()
}
pub fn get_word_list(&self, state: &AppState) -> Vec<usize> {
let time = Instant::now();
let mut list = vec![];
let engine = Engine::new();
let ast = engine.compile(&self.filter);
if let Ok(ast) = ast {
let groups = &state.word_groups;
list = state
.dictionary
.par_iter()
.enumerate()
.filter(|(_, word)| {
let mut more = rhai::Map::new();
for iced in &word.additional {
more.insert(iced.0.clone().into(), iced.1.clone().into());
}
let mut scope = Scope::new();
scope
.push_constant("id", word.id.0 as i64)
.push_constant("key", word.key.clone())
.push_constant("value", word.value.clone())
.push_constant("tags", word.tags.clone())
.push_constant("more", more)
.push_constant(
"group",
groups
.iter()
.find(|g| g.id == word.group_id)
.cloned()
.expect("No group id")
.name,
);
let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast);
if let Ok(result) = result {
return result;
}
false
})
.map(|(index, _)| index)
.collect();
info!("Collecting available words is {:?}", time.elapsed());
list
} else {
list
}
}
pub fn require_speech(&self) -> bool {
self.forward == "speech" || self.backward == "speech"
}
@@ -158,7 +154,7 @@ impl DeckSettings {
pub struct CardState {
pub id: Id,
pub word_id: Id,
pub set_id: Id,
pub deck_id: Id,
pub last_open: DateTime<Utc>,
pub score: u8,
pub fsrs_opened: NaiveDate,
@@ -203,7 +199,23 @@ impl CardState {
}
}
#[derive(Copy)]
#[derive(Clone)]
pub struct CustomField {
pub id: Id,
pub name: String,
pub identifier: String,
}
impl EntityData for CustomField {
fn id(&self) -> Id {
self.id
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
}
pub struct EntityHandle<E> {
pub(crate) id: Id,
index: u32,
@@ -212,13 +224,11 @@ pub struct EntityHandle<E> {
impl<T> Clone for EntityHandle<T> {
fn clone(&self) -> Self {
EntityHandle {
id: self.id,
index: self.index,
_phantom: Default::default(),
}
*self
}
}
impl<T> Copy for EntityHandle<T> {}
impl<T> EntityHandle<T> {
pub fn new(id: Id, index: usize, real: bool) -> Self {
Self {
@@ -227,6 +237,11 @@ impl<T> EntityHandle<T> {
_phantom: Default::default(),
}
}
pub fn from_id(id: Id) -> Self {
Self::new(id, id.0 as usize, true)
}
pub fn index(&self) -> usize {
(self.index << 1 >> 1) as usize
}
@@ -270,6 +285,13 @@ impl<T> Ord for EntityHandle<T> {
}
}
impl<T> Hash for EntityHandle<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.real().hash(state);
}
}
pub trait EntityData {
fn id(&self) -> Id;
fn set_id(&mut self, id: Id);
+254 -72
View File
@@ -1,15 +1,18 @@
use crate::data::Id;
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;
use rayon::iter::ParallelIterator;
use rusqlite::Connection;
use std::marker::PhantomData;
use std::ops::Deref;
use std::ops::{Deref, Index, IndexMut};
use std::sync::Arc;
use std::time::Instant;
pub trait Repository<T, D>
where
@@ -18,14 +21,29 @@ where
fn get(&self, handle: &mut EntityHandle<T>) -> Option<T>;
fn get_ref(&self, handle: &mut EntityHandle<T>) -> Option<&T>;
fn get_mut(&mut self, handle: &mut EntityHandle<T>) -> Option<&mut T>;
fn get_static(&self, handle: EntityHandle<T>) -> Option<T>;
fn get_ref_static(&self, handle: EntityHandle<T>) -> Option<&T>;
fn get_mut_static(&mut self, handle: EntityHandle<T>) -> Option<&mut T>;
fn set(&mut self, handle: &mut EntityHandle<T>, value: T);
fn set_static(&mut self, handle: EntityHandle<T>, value: T);
fn handle_by_id(&self, id: Id) -> EntityHandle<T>;
fn create(&mut self, new: T) -> EntityHandle<T>;
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 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;
}
struct QueryManger<T, R, D>
@@ -76,9 +94,7 @@ where
.filter(|(_, q)| {
let query = q.lock();
let rule = &query.rule;
return rule(data);
false
rule(data)
})
.map(|(i, _)| i)
.collect()
@@ -91,32 +107,58 @@ where
}
}
}
#[derive(Clone)]
pub struct Query<T, R, D>
where
R: Repository<T, D>,
D: DbLayer<T>,
{
cache: Vec<EntityHandle<T>>,
rule: Box<dyn Fn(&T) -> bool + Send + Sync>,
rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
parallel: bool,
_marker: PhantomData<(R, D)>,
shift_stamp: u16,
shift_stamp: usize,
}
impl<T: EntityData + Clone, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> {
pub fn iter<'a>(&mut self, data_repository_inner: &'a R) -> impl Iterator<Item = &'a T>
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>
where
T: 'a,
{
if self.shift_stamp != data_repository_inner.shifts() {
self.load(data_repository_inner);
}
self.cache
.iter_mut()
.filter_map(move |e| data_repository_inner.get_ref(e))
}
pub fn for_each_mut<F>(&mut self, repo: &mut R, mut f: F)
pub fn iter_with_handles<'a>(
&mut self,
data_repository_inner: &'a DataRepositoryInner<T, D>,
) -> impl Iterator<Item = (&'a T, EntityHandle<T>)>
where
T: 'a,
{
if self.shift_stamp != data_repository_inner.shifts() {
self.load(data_repository_inner);
}
self.cache
.iter_mut()
.filter_map(move |e| Some((data_repository_inner.get_ref(e)?, *e)))
}
pub fn for_each_mut<F>(&mut self, repo: &mut DataRepositoryInner<T, D>, mut f: F)
where
F: FnMut(&mut T),
{
if self.shift_stamp != repo.shifts() {
self.load(repo);
}
for handle in &mut self.cache {
if let Some(entity) = repo.get_mut(handle) {
f(entity);
@@ -128,13 +170,14 @@ impl<T: EntityData + Clone, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> {
where
T: Send + Sync,
{
let time = Instant::now();
self.cache.clear();
if self.parallel {
self.cache = data_repository
.cache
.par_iter()
.enumerate()
.filter(|(_, entity)| (self.rule)(entity))
.filter(|(_, entity)| self.rule.as_ref()(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, true))
.collect();
} else {
@@ -156,6 +199,43 @@ impl<T: EntityData + Clone, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> {
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(),
);
debug!(
"Query for {} elements (filtered {}) takes {:?}",
data_repository.len(),
self.cache.len(),
time.elapsed()
);
}
pub fn handles(&self) -> impl Iterator<Item = EntityHandle<T>> {
self.cache.iter().cloned()
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn get(&self, index: usize) -> Option<EntityHandle<T>> {
self.cache.get(index).cloned()
}
}
impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Index<usize>
for Query<T, R, D>
{
type Output = EntityHandle<T>;
fn index(&self, index: usize) -> &Self::Output {
&self.cache[index]
}
}
impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> IndexMut<usize>
for Query<T, R, D>
{
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.cache[index]
}
}
@@ -164,6 +244,10 @@ where
D: BatchedDbLayer<T>,
T: EntityData,
{
fn create_now_many(
&mut self,
entities: &mut [T],
) -> Result<Vec<EntityHandle<T>>, NotificationData>;
fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
fn delete_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
}
@@ -184,22 +268,39 @@ where
}
#[derive(Clone)]
pub struct DataRepository<T: std::clone::Clone + EntityData, D: DbLayer<T>> {
pub struct DataRepository<T: Clone + EntityData, D: DbLayer<T>> {
inner: Arc<RwLock<DataRepositoryInner<T, D>>>,
}
pub struct DataRepositoryInner<T: std::clone::Clone + EntityData, D: DbLayer<T>> {
pub struct DataRepositoryInner<T: Clone + EntityData, D: DbLayer<T>> {
cache: Vec<T>,
temporal: Vec<T>,
next_temporal: u32,
connection: Arc<RwLock<Connection>>,
connection: Arc<Mutex<SqliteConnection>>,
data_layer: D,
query_manger: QueryManger<T, Self, D>,
version: u16,
shifts: u16,
shifts: usize,
}
impl<T: std::clone::Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
impl<T: Clone + EntityData, D: DbLayer<T> + Default> DataRepository<T, D> {
pub fn new(connection: Arc<Mutex<SqliteConnection>>) -> DataRepository<T, D> {
Self {
inner: Arc::new(RwLock::new(DataRepositoryInner {
cache: vec![],
temporal: vec![],
next_temporal: 0,
connection,
data_layer: Default::default(),
query_manger: QueryManger { queries: vec![] },
version: 0,
shifts: 0,
})),
}
}
}
impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
type Target = Arc<RwLock<DataRepositoryInner<T, D>>>;
fn deref(&self) -> &Self::Target {
@@ -207,72 +308,76 @@ impl<T: std::clone::Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<
}
}
impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, D> {
impl<T: EntityData + Clone + std::marker::Send + std::marker::Sync, D: DbLayer<T>>
DataRepositoryInner<T, D>
{
pub fn query_par(
&self,
rule: Box<dyn Fn(&T) -> bool + Send + Sync>,
rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
) -> Query<T, DataRepositoryInner<T, D>, D>
where
T: Send + Sync,
{
let mut cache: Vec<EntityHandle<T>> = self
.cache
.par_iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, true))
.collect();
cache.extend(
self.temporal
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(),
);
Query {
cache,
let mut query = Query {
cache: vec![],
rule,
parallel: true,
_marker: PhantomData,
shift_stamp: self.shifts,
}
};
query.load(self);
query
}
pub fn query(
&self,
rule: Box<dyn Fn(&T) -> bool + Send + Sync>,
rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
) -> Query<T, DataRepositoryInner<T, D>, D>
where
T: Send + Sync,
{
let mut cache: Vec<EntityHandle<T>> = self
.cache
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, true))
.collect();
cache.extend(
self.temporal
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(),
);
Query {
cache,
let mut query = Query {
cache: vec![],
rule,
parallel: false,
_marker: PhantomData,
shift_stamp: self.shifts,
};
query.load(self);
query
}
pub fn empty_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query {
cache: vec![],
rule: Arc::new(|_| false),
parallel: false,
_marker: Default::default(),
shift_stamp: 0,
}
}
pub fn empty_query_par(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query {
cache: vec![],
rule: Arc::new(|_| false),
parallel: true,
_marker: Default::default(),
shift_stamp: 0,
}
}
pub fn full_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
self.query(Arc::new(|_| true))
}
pub fn len(&self) -> usize {
self.cache.len() + self.temporal.len()
}
}
impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryInner<T, D> {
@@ -306,6 +411,32 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
Some(&mut slice[index])
}
fn get_static(&self, mut handle: EntityHandle<T>) -> Option<T> {
self.get(&mut handle)
}
fn get_ref_static(&self, mut handle: EntityHandle<T>) -> Option<&T> {
self.get_ref(&mut handle)
}
fn get_mut_static(&mut self, mut handle: EntityHandle<T>) -> Option<&mut T> {
self.get_mut(&mut handle)
}
fn set(&mut self, handle: &mut EntityHandle<T>, value: T) {
let slice = if handle.real() {
self.cache.as_mut_slice()
} else {
self.temporal.as_mut_slice()
};
let index = fast_position_of(slice, handle).unwrap();
slice[index] = value;
}
fn set_static(&mut self, mut handle: EntityHandle<T>, value: T) {
self.set(&mut handle, value)
}
fn handle_by_id(&self, id: Id) -> EntityHandle<T> {
EntityHandle::new(id, id.0 as usize, true)
}
@@ -320,7 +451,8 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
}
fn create_now(&mut self, mut new: T) -> Result<EntityHandle<T>, NotificationData> {
self.data_layer.add_entity(&mut new, &self.connection.read())?;
self.data_layer
.add_entity(&mut new, &self.connection.lock())?;
let index = self.cache.len();
let handle = EntityHandle::new(new.id(), index, true);
self.cache.push(new);
@@ -328,21 +460,21 @@ 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
.write_entity(data, &self.connection.read())?;
.write_entity(data, &self.connection.lock())?;
} else {
let index = fast_position_of(self.temporal.as_slice(), entity).unwrap();
let mut data = self.temporal.remove(index);
self.data_layer
.add_entity(&mut data, &self.connection.read())?;
let data = &mut self.temporal[index];
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);
self.shifts += 1;
}
@@ -353,7 +485,7 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
fn delete(&mut self, mut handle: EntityHandle<T>) -> MaybeError {
let vec = if handle.real() {
self.data_layer
.delete_entity(handle.id, &self.connection.read())?;
.delete_entity(handle.id, &self.connection.lock())?;
&mut self.cache
} else {
&mut self.temporal
@@ -369,16 +501,32 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
Ok(())
}
fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>>
where
F: Fn(&T) -> bool,
{
if let Some(index) = self.cache.iter().position(&predicate) {
let value = &self.cache[index];
return Some(EntityHandle::new(value.id(), index, true));
}
if let Some(index) = self.temporal.iter().position(predicate) {
let value = &self.temporal[index];
return Some(EntityHandle::new(value.id(), index, false));
}
None
}
fn init(&mut self) -> MaybeError {
self.data_layer
.load_all(&mut self.cache, &self.connection.read())?;
.load_all(&mut self.cache, &self.connection.lock())?;
self.version += 1;
Ok(())
}
fn start(&mut self) -> MaybeError {
if self.version != 0 {
return Ok(())
return Ok(());
}
self.init()?;
@@ -391,10 +539,44 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
self.init()?;
Ok(())
}
fn shifts(&self) -> usize {
self.shifts
}
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T>
where
T: 'a,
{
self.cache.iter().chain(self.temporal.iter())
}
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut T>
where
T: 'a,
{
self.cache.iter_mut().chain(self.temporal.iter_mut())
}
}
impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository<T, D>
for DataRepositoryInner<T, D>
{
fn create_now_many(
&mut self,
entities: &mut [T],
) -> Result<Vec<EntityHandle<T>>, NotificationData> {
self.data_layer
.add_many_entities(entities, &mut self.connection.lock())?;
let len = self.cache.len();
let handles = entities
.iter()
.enumerate()
.map(|(index, entity)| EntityHandle::new(entity.id(), index + len, true))
.collect();
self.cache.extend(entities.to_vec());
Ok(handles)
}
fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError {
let mut temp = Vec::with_capacity(handles.len() / 2);
let mut cached = Vec::with_capacity(handles.len() / 2);
@@ -424,12 +606,12 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
});
self.data_layer
.add_many_entities(&mut temp_values, &mut self.connection.write())?;
.add_many_entities(&mut temp_values, &mut self.connection.lock())?;
let start = self.cache.len();
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);
@@ -442,7 +624,7 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
let h = handle;
let idx = fast_position_of(self.cache.as_slice(), h).unwrap();
self.data_layer
.write_entity(&self.cache[idx], &self.connection.read())?;
.write_entity(&self.cache[idx], &self.connection.lock())?;
}
self.shifts += 1;
@@ -466,7 +648,7 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
!remove
});
self.data_layer
.delete_many_entities(cached.as_slice(), &mut self.connection.write())?;
.delete_many_entities(cached.as_slice(), &mut self.connection.lock())?;
let len = cached.len();
cell = 0;
self.cache.retain(|x| {
@@ -493,7 +675,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))
+29 -111
View File
@@ -1,25 +1,28 @@
use crate::data::entities::{CardState, DeckSettings};
use crate::data_provider::TableAccessQuerySet;
use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData};
use crate::data::Id;
use crate::data::entities::CardState;
use crate::data::repositories::{DataRepository, DataRepositoryInner, Query};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use chrono::NaiveDate;
use fsrs::MemoryState;
use log::info;
use rusqlite::{Connection, Error, Params, Row, params};
use std::time::Instant;
use rusqlite::{Error, Params, Row};
use std::sync::Arc;
pub type CardStateRepository = DataRepository<CardState, CardStateDbLayer>;
pub type CardStateRepositoryInner = DataRepositoryInner<CardState, CardStateDbLayer>;
pub type CardStateDbLayer = SqliteDbLayer<CardState, CardStatesQuerySet>;
#[derive(Clone)]
pub struct CardStatesQuerySet;
impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
fn load() -> &'static str {
"SELECT id, word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states"
"SELECT id, word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states"
}
fn save() -> &'static str {
"UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3"
"UPDATE states SET score = ?1, last_opened = ?2, fsrs_opened = ?3, fsrs_memory = ?4, fsrs_open_day = ?5 WHERE id = ?6"
}
fn add() -> &'static str {
"INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)"
"INSERT INTO states (word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
}
fn name() -> &'static str {
@@ -30,7 +33,7 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
Ok(CardState {
id: row.get(0)?,
word_id: row.get(1)?,
set_id: row.get(2)?,
deck_id: row.get(2)?,
score: row.get(3)?,
fsrs_opened: NaiveDate::from_epoch_days(row.get::<usize, i32>(5)?)
.expect("Incorrect date encoding"),
@@ -44,7 +47,7 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
fn insert_params(entity: &CardState) -> impl Params {
(
entity.word_id,
entity.set_id,
entity.deck_id,
entity.score,
entity.last_open.timestamp(),
entity.fsrs_opened.to_epoch_days(),
@@ -54,38 +57,16 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
}
fn update_params(entity: &CardState) -> impl Params {
(entity.score, entity.last_open.timestamp(), entity.id)
(
entity.score,
entity.last_open.timestamp(),
entity.fsrs_opened.to_epoch_days(),
memory_to_i64(&entity.fsrs_memory),
entity.fsrs_open_day.to_epoch_days(),
entity.id,
)
}
}
pub fn load_stats_of_deck(
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)?,
set_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 {
@@ -105,74 +86,11 @@ fn memory_to_i64(value: &Option<MemoryState>) -> i64 {
value.stability.to_bits() as i64 | ((value.difficulty.to_bits() as i64) << 32)
}
pub fn add_stat_list(stats: &mut [CardState], connection: &mut Connection) -> MaybeError {
let time = Instant::now();
let tx = connection
.transaction()
.expect("Failed to create transaction");
let count = stats.len();
{
let mut stmt = tx
.prepare(
"INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)",
)
.expect("Failed to prepare state query");
for stat in stats.iter() {
stmt.execute(params![
stat.word_id,
stat.set_id,
stat.score,
stat.last_open.timestamp(),
stat.fsrs_opened.to_epoch_days(),
memory_to_i64(&stat.fsrs_memory),
stat.fsrs_open_day.to_epoch_days()
])
.map_err(|_| DbSaveError)?;
}
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))
}
let last_index: u32 = tx.last_insert_rowid() as u32;
tx.commit().expect("Failed to commit");
let start_index = last_index - (count as u32) + 1;
for (index, id) in (start_index..=last_index).enumerate() {
stats[index].id = id.into();
}
info!("Added {} cards for {:?}", stats.len(), time.elapsed());
Ok(())
}
pub fn update_stat_score(stat: &CardState, connection: &Connection) -> MaybeError {
let time = Instant::now();
connection
.execute(
"UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3",
(&stat.score, &stat.last_open.timestamp(), &stat.id),
)
.map_err(|_| DbSaveError)?;
info!("Updated stat: {}", time.elapsed().as_millis());
Ok(())
}
pub fn delete_stats(stat: &[CardState], connection: &Connection) -> MaybeError {
if stat.is_empty() {
return Ok(());
}
let ids = stat
.iter()
.map(|s| s.id.into())
.collect::<Vec<String>>()
.join(", ");
connection
.execute(
format!("DELETE FROM states WHERE id in ({ids})").as_str(),
(),
)
.map_err(|_| DbSaveError)?;
Ok(())
}
+43
View File
@@ -0,0 +1,43 @@
use crate::data::entities::CustomField;
use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::{Error, Params, Row};
pub type CustomFieldRepository = DataRepository<CustomField, CustomFieldDbLayer>;
pub type CustomFieldRepositoryInner = DataRepositoryInner<CustomField, CustomFieldDbLayer>;
pub type CustomFieldDbLayer = SqliteDbLayer<CustomField, CustomFieldsQuerySet>;
#[derive(Clone)]
pub struct CustomFieldsQuerySet;
impl TableAccessQuerySet<CustomField> for CustomFieldsQuerySet {
fn load() -> &'static str {
"SELECT id, name, identifier FROM custom_fields"
}
fn save() -> &'static str {
"UPDATE custom_fields SET name = ?1, identifier = ?2 WHERE id = ?3"
}
fn add() -> &'static str {
"INSERT INTO custom_fields (name, identifier) VALUES (?1, ?2)"
}
fn name() -> &'static str {
"custom_fields"
}
fn from_sql(row: &Row) -> Result<CustomField, Error> {
Ok(CustomField {
id: row.get(0)?,
name: row.get(1)?,
identifier: row.get(2)?,
})
}
fn insert_params(entity: &CustomField) -> impl Params {
(&entity.name, &entity.identifier)
}
fn update_params(entity: &CustomField) -> impl Params {
(&entity.name, &entity.identifier, entity.id)
}
}
+14 -81
View File
@@ -1,11 +1,11 @@
use crate::core::srs::OrderMode;
use crate::data::entities::{CardState, DeckSettings};
use crate::data_provider::TableAccessQuerySet;
use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData};
use chrono::NaiveDate;
use rusqlite::{Connection, Error, Params, Row};
use crate::data::entities::DeckSettings;
use crate::data::repositories::DataRepository;
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::{Error, Params, Row};
pub type DeckRepository = DataRepository<DeckSettings, SqliteDbLayer<DeckSettings, DeckQuerySet>>;
#[derive(Clone)]
pub struct DeckQuerySet;
impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
fn load() -> &'static str {
@@ -17,7 +17,7 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
}
fn add() -> &'static str {
"INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)"
"INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4)"
}
fn name() -> &'static str {
@@ -31,8 +31,6 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
forward: row.get(2)?,
backward: row.get(3)?,
filter: row.get(4)?,
count: None,
open_mode: OrderMode::Default,
})
}
@@ -46,77 +44,12 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
}
fn update_params(entity: &DeckSettings) -> impl Params {
(&entity.name, &entity.forward, &entity.backward, &entity.filter, entity.id)
}
}
pub fn load_sets(connection: &Connection) -> Result<Vec<DeckSettings>, NotificationData> {
let mut stmt = connection
.prepare("SELECT id, name, forward, backward, filter FROM decks")
.expect("Failed to prepare state query");
let iter = stmt
.query_map([], |row| {
Ok(DeckSettings {
id: row.get(0)?,
name: row.get(1)?,
forward: row.get(2)?,
backward: row.get(3)?,
filter: row.get(4)?,
count: None,
open_mode: OrderMode::Default,
})
})
.map_err(|_| DbLoadError)?;
let mut buffer = Vec::with_capacity(16);
iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i));
Ok(buffer)
}
pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> MaybeError {
let index : u32 = connection
.query_row(
"INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id",
(
&set.name,
&set.forward,
&set.backward,
&set.filter,
),
|row| row.get(0)
(
&entity.name,
&entity.forward,
&entity.backward,
&entity.filter,
entity.id,
)
.map_err(|_| DbSaveError)?;
set.id = index.into();
Ok(())
}
pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> MaybeError {
if !set.id.is_valid() {
add_set(set, connection)?;
} else {
connection
.execute(
"UPDATE decks SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5",
(
&set.name,
&set.forward,
&set.backward,
&set.filter,
&set.id
),
)
.map_err(|_| DbSaveError)?;
}
Ok(())
}
pub fn delete_set(set: &DeckSettings, connection: &Connection) -> MaybeError {
if !set.id.is_valid() {
return Err(DbSaveError);
}
connection
.execute("DELETE FROM decks WHERE id = ?1", (&set.id,))
.map_err(|_| DbSaveError)?;
Ok(())
}
+84 -86
View File
@@ -1,97 +1,95 @@
use crate::core::srs::WordOpenMode;
use crate::data::Id;
use crate::data::entities::HistoryItem;
use crate::fs::*;
use crate::notification::NotificationData::PermissionDenied;
use crate::notification::{MaybeError, NotificationData};
use chrono::{DateTime, NaiveDate};
use rusqlite::Connection;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use crate::data::entities::HistoryNote;
use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::{Error, Params, Row, ToSql};
pub fn get_open_count_of_set(
id: Id,
connection: &Connection,
) -> Result<Vec<(NaiveDate, u32)>, NotificationData> {
todo!()
}
pub type HistoryRepository = DataRepository<HistoryNote, HistoryDataLayer>;
pub type HistoryRepositoryInner = DataRepositoryInner<HistoryNote, HistoryDataLayer>;
pub type HistoryDataLayer = SqliteDbLayer<HistoryNote, HistoryQuerySet>;
pub fn get_history_of_set(id: Id) -> Result<Vec<HistoryItem>, NotificationData> {
let app_dir = history_dir()?;
let head = app_dir.clone().join(format!("set_{}_history.csv", id));
let mut lines = Vec::new();
append_lines_if_exists(head, &mut lines)?;
lines.reverse();
Ok(parse_history_items(lines))
}
fn append_lines_if_exists(path: PathBuf, lines: &mut Vec<String>) -> MaybeError {
if path.exists() {
let reader = BufReader::new(File::open(path).map_err(|_| PermissionDenied)?);
reader
.lines()
.map_while(Result::ok)
.for_each(|i| lines.push(i));
#[derive(Clone)]
pub struct HistoryQuerySet;
impl TableAccessQuerySet<HistoryNote> for HistoryQuerySet {
fn load() -> &'static str {
"SELECT rowid, deck_id, word_id, timestamp, answer, before, after, fsrs FROM history"
}
fn save() -> &'static str {
todo!("History is readonly")
}
fn add() -> &'static str {
"INSERT INTO history (deck_id, word_id, timestamp, answer, before, after, fsrs) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
}
fn name() -> &'static str {
"history"
}
fn id_field() -> &'static str {
"rowid"
}
fn from_sql(row: &Row) -> Result<HistoryNote, Error> {
Ok(HistoryNote {
id: row.get(0)?,
timestamp: row.get(3)?,
deck_id: row.get(1)?,
word_id: row.get(2)?,
answer: row.get(4)?,
before: row.get(5)?,
after: row.get(6)?,
fsrs: row.get(7)?,
})
}
fn insert_params(entity: &HistoryNote) -> impl Params {
(
entity.deck_id,
entity.word_id,
entity.timestamp.timestamp(),
entity.answer,
entity.before,
entity.after,
entity.fsrs,
)
}
fn update_params(entity: &HistoryNote) -> impl Params {
(
entity.deck_id,
entity.word_id,
entity.timestamp.timestamp(),
entity.answer,
entity.before,
entity.after,
entity.fsrs,
)
}
Ok(())
}
fn parse_history_items(strings: Vec<String>) -> Vec<HistoryItem> {
let mut items = Vec::with_capacity(strings.len());
for string in strings {
if let [time, word, mode, before, after] = string.split(';').collect::<Vec<&str>>()[..] {
let id = word.parse::<Id>();
if id.is_err() {
continue;
}
let id = id.unwrap_or_default();
if !id.is_valid() {
continue;
}
items.push(HistoryItem {
timestamp: DateTime::from_timestamp(time.parse().unwrap_or_default(), 0)
.unwrap_or_default(),
word_id: id,
mode: match mode.parse::<u8>().unwrap_or_default() {
2 => WordOpenMode::Hard,
3 => WordOpenMode::Ok,
4 => WordOpenMode::Easy,
_ => WordOpenMode::None,
},
before: before.parse().unwrap_or_default(),
after: after.parse().unwrap_or_default(),
});
impl FromSql for WordOpenMode {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
if let ValueRef::Integer(s) = value {
return match s {
1 => Ok(WordOpenMode::Hard),
2 => Ok(WordOpenMode::Ok),
3 => Ok(WordOpenMode::Easy),
_ => Ok(WordOpenMode::None),
};
}
Ok(WordOpenMode::None)
}
items
}
pub fn push_note(set_id: Id, item: HistoryItem) -> MaybeError {
let app_dir = history_dir()?;
let path = app_dir.clone().join(format!("set_{}_history.csv", set_id));
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|_| PermissionDenied)?;
let line_str = format!(
"{};{};{};{};{}",
item.timestamp.timestamp(),
item.word_id,
match item.mode {
WordOpenMode::Easy => 4,
WordOpenMode::Ok => 3,
WordOpenMode::Hard => 2,
WordOpenMode::None => 1,
},
item.before,
item.after
);
writeln!(&mut file, "{}", line_str).map_err(|_| PermissionDenied)?;
Ok(())
impl ToSql for WordOpenMode {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(ToSqlOutput::from(match self {
WordOpenMode::Easy => 3,
WordOpenMode::Ok => 2,
WordOpenMode::Hard => 1,
WordOpenMode::None => 0,
}))
}
}
+35 -4
View File
@@ -1,10 +1,11 @@
use crate::helpers::into_result;
use crate::notification::NotificationData;
use crate::notification::NotificationData::IncorrectAnkiFile;
use hashbrown::HashMap;
use crate::pages::import::AnkiSchema;
use log::info;
use rusqlite::Connection;
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone)]
pub struct ImportData(pub(crate) Vec<ImportGroup>);
@@ -15,7 +16,7 @@ pub struct ImportGroup {
pub name: String,
pub fields: Vec<String>,
pub length: u64,
pub mapping: HashMap<String, String>,
pub mapping: BTreeMap<String, String>,
pub imported: bool,
}
@@ -25,7 +26,17 @@ pub struct ImportNote {
pub fields: Vec<String>,
}
pub fn load_groups(connection: &Connection) -> Result<ImportData, NotificationData> {
pub fn load_groups(
connection: &Connection,
schema: AnkiSchema,
) -> Result<ImportData, NotificationData> {
match schema {
AnkiSchema::Old => load_old_schema(connection),
AnkiSchema::Beta => load_new_schema(connection),
}
}
fn load_old_schema(connection: &Connection) -> Result<ImportData, NotificationData> {
let json_models: String = connection
.query_one("select models from col", (), |row| row.get(0))
.map_err(|_| IncorrectAnkiFile)?;
@@ -45,7 +56,7 @@ pub fn load_groups(connection: &Connection) -> Result<ImportData, NotificationDa
.collect::<Vec<(i64, i64)>>();
for (_, collection) in sets {
let fields_property = collection.get("fields");
let fields_property = collection.get("flds");
if let Some(fields_property) = fields_property {
let fields = into_result(fields_property.as_array(), IncorrectAnkiFile)?
.iter()
@@ -106,3 +117,23 @@ pub fn get_words_of_group(
.filter_map(|x| x.ok())
.collect::<Vec<ImportNote>>())
}
fn load_new_schema(connection: &Connection) -> Result<ImportData, NotificationData> {
return Err(IncorrectAnkiFile);
}
pub struct DeckHeader {
pub id: u64,
pub name: String,
pub count: u64,
}
pub fn load_decks(connection: &Connection) -> Result<Vec<DeckHeader>, NotificationData> {
let mut stmt = connection
.prepare("select id, name from decks")
.unwrap();
Ok(stmt.query_map([], |row| Ok(DeckHeader{
id: row.get::<usize, i64>(0)? as u64,
name: row.get(1)?
})).unwrap().filter_map(|x| x.ok()).collect::<Vec<DeckHeader>>())
}
+53 -32
View File
@@ -1,14 +1,15 @@
use crate::data::Id;
use crate::data::entities::{CardState, EntityData};
use crate::data::entities::EntityData;
use crate::data::repositories::{BatchedDbLayer, DbLayer};
use crate::notification::MaybeError;
use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use log::info;
use crate::notification::NotificationData::DbSaveError;
use log::{debug, error};
use rusqlite::{Connection, Error, Params, Row};
use std::marker::PhantomData;
use std::time::Instant;
pub(crate) mod card_states;
pub mod custom_fields;
pub(crate) mod decks;
pub(crate) mod history;
pub(crate) mod import;
@@ -16,6 +17,7 @@ pub(crate) mod settings;
pub(crate) mod sqlite;
pub(crate) mod voice;
pub(crate) mod web_api;
pub mod word_groups;
pub(crate) mod words;
pub trait TableAccessQuerySet<T> {
@@ -23,11 +25,15 @@ pub trait TableAccessQuerySet<T> {
fn save() -> &'static str;
fn add() -> &'static str;
fn name() -> &'static str;
fn id_field() -> &'static str {
"id"
}
fn from_sql(row: &Row) -> Result<T, Error>;
fn insert_params(entity: &T) -> impl Params;
fn update_params(entity: &T) -> impl Params;
}
#[derive(Clone)]
pub struct SqliteDbLayer<T, Q>
where
Q: TableAccessQuerySet<T>,
@@ -35,18 +41,25 @@ where
_phantom: PhantomData<(T, Q)>,
}
impl<T, Q: TableAccessQuerySet<T>> Default for SqliteDbLayer<T, Q> {
fn default() -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q> {
fn write_entity(&self, entity: &T, connection: &Connection) -> MaybeError {
let time = Instant::now();
connection
.execute(Q::load(), Q::update_params(entity))
.map_err(|_| DbSaveError)?;
info!(
"Updated in {} for {:?}",
Q::name(),
time.elapsed()
);
.execute(Q::save(), Q::update_params(entity))
.map_err(|e| {
error!("{}", e);
DbSaveError
})?;
debug!("Updated in {} for {:?}", Q::name(), time.elapsed());
Ok(())
}
@@ -54,15 +67,18 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
let time = Instant::now();
let index: u32 = connection
.query_row(Q::add(), Q::insert_params(entity), |row| row.get(0))
.map_err(|_| DbSaveError)?;
.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
})?;
entity.set_id(index.into());
info!(
"Added into {} for {:?}",
Q::name(),
time.elapsed()
);
debug!("Added into {} for {:?}", Q::name(), time.elapsed());
Ok(())
}
@@ -77,12 +93,11 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
format!("DELETE FROM {} WHERE id = ?1", Q::name()).as_str(),
(&id,),
)
.map_err(|_| DbSaveError)?;
info!(
"Deleted from {} for {:?}",
Q::name(),
time.elapsed()
);
.map_err(|e| {
error!("{}", e);
DbSaveError
})?;
debug!("Deleted from {} for {:?}", Q::name(), time.elapsed());
Ok(())
}
@@ -92,13 +107,14 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
let mut stmt = connection
.prepare(Q::load())
.expect("Failed to prepare state query");
let iter = stmt
.query_map([], |row| Ok(Q::from_sql(row)?))
.map_err(|_| DbLoadError)?;
let iter = stmt.query_map([], |row| Q::from_sql(row)).map_err(|e| {
error!("{}", e);
DbSaveError
})?;
iter.filter_map(|res| res.ok())
.for_each(|i| storage.push(i));
info!(
debug!(
"Loaded {} from {} for {:?}",
storage.len(),
Q::name(),
@@ -124,9 +140,12 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> BatchedDbLayer<T> for SqliteDbLay
format!("DELETE FROM {} WHERE id in ({})", Q::name(), stringed).as_str(),
(),
)
.map_err(|_| DbSaveError)?;
.map_err(|e| {
error!("{}", e);
DbSaveError
})?;
}
info!(
debug!(
"Deleted {} from {} for {:?}",
ids.len(),
Q::name(),
@@ -146,8 +165,10 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> BatchedDbLayer<T> for SqliteDbLay
{
let mut stmt = tx.prepare(Q::add()).expect("Failed to prepare state query");
for entity in entities.iter() {
stmt.execute(Q::insert_params(entity))
.map_err(|_| DbSaveError)?;
stmt.execute(Q::insert_params(entity)).map_err(|e| {
error!("{}", e);
DbSaveError
})?;
}
}
@@ -160,7 +181,7 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> BatchedDbLayer<T> for SqliteDbLay
entities[index].set_id(id.into());
}
info!(
debug!(
"Added {} into {} for {:?}",
entities.len(),
Q::name(),
+59 -4
View File
@@ -1,5 +1,7 @@
use crate::fs::*;
use log::error;
use rusqlite::Connection;
use std::ops::{Deref, DerefMut};
pub fn create_db() {
let db_file = db_path();
@@ -21,6 +23,7 @@ fn create_tables(conn: &Connection) {
make_words(conn).expect("Failed to init table");
make_card_stats(conn).expect("Failed to init table");
make_history(conn).expect("Failed to init table");
make_custom_fields(conn).expect("Failed to init table");
}
fn make_settings(conn: &Connection) -> Result<(), rusqlite::Error> {
@@ -58,6 +61,13 @@ fn make_history(conn: &Connection) -> Result<(), rusqlite::Error> {
Ok(())
}
fn make_custom_fields(conn: &Connection) -> Result<(), rusqlite::Error> {
let query = include_str!("../../sql/custom_fields.sql");
conn.execute(query, ())?;
conn.execute("insert into custom_fields (name, identifier) values (\"Чтение\", \"reading\"), (\"Описание\", \"description\"), (\"Пример использования\", \"context\")", ())?;
Ok(())
}
pub fn default_connection() -> Connection {
let db_file = db_path();
let connection = Connection::open(db_file).expect("Failed to open database");
@@ -66,14 +76,59 @@ pub fn default_connection() -> Connection {
.expect("Failed to set PRAGMA");
connection
.query_one("PRAGMA journal_mode = WAL;", [], |row| {
println!("{:?}", row);
Ok(())
})
.unwrap_or_else(|e| {
println!("{}", e);
});
.expect("Failed to init database");
connection
.execute("PRAGMA synchronous = NORMAL;", [])
.expect("Failed to set PRAGMA");
connection
}
pub struct SqliteConnection(Option<Connection>);
impl Deref for SqliteConnection {
type Target = Connection;
fn deref(&self) -> &Self::Target {
self.0.as_ref().unwrap()
}
}
impl DerefMut for SqliteConnection {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().unwrap()
}
}
impl Default for SqliteConnection {
fn default() -> Self {
SqliteConnection(Some(default_connection()))
}
}
impl SqliteConnection {
pub fn finish(&mut self) {
let connection = self.0.take().unwrap();
connection
.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
});
connection
.execute("PRAGMA optimize", [])
.unwrap_or_else(|e| {
error!("Ошибка при оптимизации БД: {e}");
0
});
self.0 = None;
}
pub fn restart(&mut self) {
self.0 = Some(default_connection());
}
}
impl Drop for SqliteConnection {
fn drop(&mut self) {
self.finish();
}
}
+4 -2
View File
@@ -1,9 +1,9 @@
use crate::fs::*;
use crate::notification::NotificationData::{CompressError, PermissionDenied, WebApiUnavailable};
use crate::notification::NotificationData::{CompressError, OpenFileError, PermissionDenied, WebApiUnavailable};
use crate::notification::{MaybeError, NotificationData};
use log::info;
use std::io;
use tokio::fs::{File, OpenOptions};
use tokio::fs::{remove_file, File, OpenOptions};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use zstd::{DEFAULT_COMPRESSION_LEVEL, Decoder, Encoder};
@@ -74,6 +74,8 @@ pub async fn load_data(id: String, temp: bool) -> MaybeError {
.await
.map_err(|_| WebApiUnavailable)?;
set_local_version(version.parse::<u32>().unwrap_or_default()).await?;
remove_file(db_wal()).await.map_err(|_| OpenFileError)?;
remove_file(db_wal()).await.map_err(|_| OpenFileError)?;
Ok(())
}
+43
View File
@@ -0,0 +1,43 @@
use crate::data::entities::WordGroup;
use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::{Error, Params, Row};
pub type WordGroupRepository = DataRepository<WordGroup, WordGroupDataLayer>;
pub type WordGroupRepositoryInner = DataRepositoryInner<WordGroup, WordGroupDataLayer>;
pub type WordGroupDataLayer = SqliteDbLayer<WordGroup, WordGroupQuerySet>;
#[derive(Clone)]
pub struct WordGroupQuerySet;
impl TableAccessQuerySet<WordGroup> for WordGroupQuerySet {
fn load() -> &'static str {
"SELECT id, name FROM word_groups"
}
fn save() -> &'static str {
"UPDATE word_groups SET name = ?1 WHERE id = ?2"
}
fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1)"
}
fn name() -> &'static str {
"word_groups"
}
fn from_sql(row: &Row) -> Result<WordGroup, Error> {
Ok(WordGroup {
id: row.get(0)?,
name: row.get(1)?,
})
}
fn insert_params(entity: &WordGroup) -> impl Params {
(&entity.name,)
}
fn update_params(entity: &WordGroup) -> impl Params {
(&entity.name, entity.id)
}
}
+29 -205
View File
@@ -1,11 +1,14 @@
use crate::data::entities::{WordData, WordGroup};
use crate::data_provider::TableAccessQuerySet;
use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData};
use rusqlite::{Connection, Error, Params, Row, params};
use std::collections::HashMap;
use crate::data::Id;
use crate::data::entities::WordData;
use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use litemap::LiteMap;
use rusqlite::{Error, Params, Row};
pub type WordRepository = DataRepository<WordData, WordDataLayer>;
pub type WordRepositoryInner = DataRepositoryInner<WordData, WordDataLayer>;
pub type WordDataLayer = SqliteDbLayer<WordData, WordQuerySet>;
#[derive(Clone)]
pub struct WordQuerySet;
impl TableAccessQuerySet<WordData> for WordQuerySet {
fn load() -> &'static str {
@@ -31,8 +34,7 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
key: row.get(1)?,
value: row.get(2)?,
tags: row.get(3)?,
additional: serde_json::from_str(&additional)
.unwrap_or_default(),
additional: additional_from_string(additional),
group_id: row.get(5)?,
})
}
@@ -42,7 +44,7 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
&entity.key,
&entity.value,
&entity.tags,
serde_json::to_string(&entity.additional).unwrap_or_default(),
additional_to_string(&entity.additional),
entity.group_id,
)
}
@@ -52,210 +54,32 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
&entity.key,
&entity.value,
&entity.tags,
serde_json::to_string(&entity.additional).unwrap_or_default(),
additional_to_string(&entity.additional),
entity.id,
)
}
}
pub struct WordGroupQuerySet;
impl TableAccessQuerySet<WordGroup> for WordGroupQuerySet {
fn load() -> &'static str {
"SELECT id, name FROM word_groups"
}
fn save() -> &'static str {
"UPDATE word_groups SET name = ?1 WHERE id = ?2"
}
fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1)"
}
fn name() -> &'static str {
"word_groups"
}
fn from_sql(row: &Row) -> Result<WordGroup, Error> {
Ok(WordGroup {
id: row.get(0)?,
name: row.get(1)?,
})
}
fn insert_params(entity: &WordGroup) -> impl Params {
(&entity.name,)
}
fn update_params(entity: &WordGroup) -> impl Params {
(&entity.name, entity.id)
fn additional_to_string(map: &LiteMap<Id, String>) -> String {
let mut parts = Vec::with_capacity(map.len() * 2);
for (id, value) in map {
parts.push(id.to_string());
parts.push(value.clone());
}
parts.join("")
}
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) RETURNING id",
(
&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)?;
}
fn additional_from_string(data: String) -> LiteMap<Id, String> {
if data.is_empty() {
return LiteMap::new();
}
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();
let parts = data.split('').collect::<Vec<&str>>();
let chunks = parts.chunks(2).collect::<Vec<&[&str]>>();
let mut map = LiteMap::with_capacity(parts.len());
for part in &chunks {
let id: Id = part[0].parse().unwrap();
let value: String = part[1].to_string();
map.insert(id, value);
}
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) RETURNING id",
(&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(())
map
}
+8 -8
View File
@@ -16,6 +16,14 @@ pub fn app_data_dir() -> PathBuf {
pub fn db_path() -> PathBuf {
app_data_dir().join("data.db")
}
pub fn db_wal() -> PathBuf {
app_data_dir().join("data.db-wal")
}
pub fn db_shm() -> PathBuf {
app_data_dir().join("data.db-shm")
}
pub fn temp_db_path() -> PathBuf {
app_data_dir().join("data.db.tmp")
}
@@ -28,14 +36,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() {
+1
View File
@@ -10,6 +10,7 @@ pub mod notification;
mod pages;
mod styling;
pub mod tests;
pub mod widgets;
use crate::app::start;
use mimalloc::MiMalloc;
+15 -72
View File
@@ -1,15 +1,11 @@
use crate::app::{AppState, fill_state};
use crate::data::Id;
use crate::data_provider::history::get_history_of_set;
use crate::data_provider::sqlite::{create_db, default_connection};
use crate::data_provider::sqlite::create_db;
use crate::data_provider::web_api::{
get_local_version, get_web_version, load_data, set_local_version,
};
use crate::fs::*;
use crate::navigation::Page::*;
use crate::navigation::RootMessage::{
DataLoaded, Keyboard, Notification, PushNotification, UpdateData,
};
use crate::navigation::RootMessage::{Keyboard, Notification, PushNotification, UpdateData};
use crate::notification::NotificationData::PermissionDenied;
use crate::notification::{
NotificationData, NotificationManager, NotificationMessage, NotificationRequest,
@@ -25,29 +21,22 @@ use crate::pages::repetition_settings::{RepetitionSettingsMessage, RepetitionSet
use crate::pages::repetitions::{RepetitionsMessage, RepetitionsState};
use crate::pages::selector::{SelectorMessage, SelectorState};
use crate::pages::sync::{SyncMessage, SyncState};
use crate::pages::word::{WordMessage, WordState};
use crate::pages::writing::{WritingMessage, WritingState};
use crate::state_navigate_handle;
use crate::state_update;
use crate::view_navigation;
use crate::{message_navigation, push_error, push_error_task};
use chrono::NaiveDate;
use hashbrown::HashMap;
use iced::keyboard::Event;
use iced::widget::stack;
use iced::{Element, Task};
use iced_core::Length::Fill;
use log::info;
use parking_lot::Mutex;
use rusqlite::Connection;
use std::sync::Arc;
use log::{debug, info};
use std::time::Instant;
impl Default for ScreenState {
fn default() -> Self {
let mut state = AppState::new();
fill_state(&mut state);
let state = Arc::new(Mutex::new(state));
ScreenState {
stack: vec![Selector(SelectorState::new(state.clone()))],
app_state: state.clone(),
@@ -66,13 +55,11 @@ pub enum RootMessage {
Randomizer(RandomizerMessage),
Repetitions(RepetitionsMessage),
Repetition(RepetitionMessage),
Word(WordMessage),
Sync(SyncMessage),
History(HistoryMessage),
RepetitionSettings(RepetitionSettingsMessage),
Import(ImportMessage),
Keyboard(Event),
DataLoaded(HashMap<Id, Vec<(NaiveDate, u32)>>),
PushNotification(NotificationRequest),
Notification(NotificationMessage),
None,
@@ -89,7 +76,6 @@ pub enum Page {
Randomizer(RandomizerState),
Repetitions(RepetitionsState),
Repetition(RepetitionState),
Word(WordState),
Sync(SyncState),
History(HistoryState),
RepetitionSettings(RepetitionSettingsState),
@@ -99,7 +85,7 @@ pub enum Page {
pub struct ScreenState {
stack: Vec<Page>,
app_state: Arc<Mutex<AppState>>,
app_state: AppState,
notification_manager: NotificationManager,
}
@@ -109,14 +95,11 @@ impl ScreenState {
create_db();
info!("Db created");
let state = ScreenState::default();
let reading_additional_task = Task::perform(Self::load_additional_data(), |data| {
data.unwrap_or_else(|err| push_error!(err))
});
let mut final_task = reading_additional_task;
let state_guard = state.app_state.lock();
if state_guard.sync_data.auto_web_fetch
&& let Some(key) = state_guard.sync_data.key.clone()
let mut final_task = Task::none();
let state_guard = &state.app_state;
if state_guard.sync_data.read().auto_web_fetch
&& let Some(key) = state_guard.sync_data.read().key.clone()
{
final_task = Task::batch([
final_task,
@@ -133,43 +116,10 @@ impl ScreenState {
]);
}
drop(state_guard);
info!("Boot finished");
(state, final_task)
}
async fn load_additional_data() -> Result<RootMessage, NotificationData> {
let directory = history_dir()?;
let mut map = HashMap::new();
for file in directory
.read_dir()
.map_err(|_| PermissionDenied)?
.filter_map(Result::ok)
{
let mut vec = vec![];
let history_file_name = match file.file_name().into_string() {
Err(_) => continue,
Ok(v) => v,
};
if let Ok(id) = history_file_name.split('_').collect::<Vec<_>>()[1].parse::<Id>()
&& let Ok(history) = get_history_of_set(id)
{
let by_date = history.chunk_by(|x, x1| {
x.timestamp.naive_local().date() == x1.timestamp.naive_local().date()
});
for group in by_date {
vec.push((group[0].timestamp.naive_local().date(), group.len() as u32));
}
map.insert(id, vec);
} else {
continue;
}
}
Ok(DataLoaded(map))
}
async fn load_web_backup(string: String) -> Result<bool, NotificationData> {
let local = get_local_version().await?;
let web = get_web_version(string.as_str()).await?;
@@ -191,16 +141,11 @@ impl ScreenState {
return self.handle_keyboard(e);
}
if let DataLoaded(data) = message {
self.app_state.lock().activity = data;
return Task::none();
}
if let UpdateData = message {
info!("Loading data");
let mut state = self.app_state.lock();
let state = &mut self.app_state;
if cfg!(windows) {
state.connection = Connection::open_in_memory().unwrap();
state.connection.lock().finish();
}
let db_file = db_path();
let temp_db_file = temp_db_path();
@@ -208,8 +153,8 @@ impl ScreenState {
return push_error_task!(PermissionDenied);
}
state.connection = default_connection();
fill_state(&mut state);
state.connection = Default::default();
fill_state(state);
return Task::none();
}
@@ -234,14 +179,13 @@ impl ScreenState {
Randomizer,
Repetitions,
Repetition,
Word,
Sync,
History,
RepetitionSettings,
Import
);
}
info!("Update took {:?}", time.elapsed());
debug!("Update took {:?}", time.elapsed());
task.0
}
@@ -258,7 +202,6 @@ impl ScreenState {
Randomizer,
Repetitions,
Repetition,
Word,
Sync,
History,
RepetitionSettings,
@@ -279,7 +222,6 @@ impl ScreenState {
Randomizer,
Repetitions,
Repetition,
Word,
Sync,
History,
RepetitionSettings,
@@ -288,7 +230,7 @@ impl ScreenState {
let notifications = self.notification_manager.draw().map(Notification);
info!("View took {:?}", time.elapsed());
debug!("View took {:?}", time.elapsed());
stack![view, notifications].width(Fill).height(Fill).into()
}
@@ -296,6 +238,7 @@ impl ScreenState {
if let Some(page) = self.stack.last_mut() {
return match page {
Repetition(page) => page.press(&message),
Dictionary(page) => page.press(&message),
_ => Task::none(),
};
}
+7 -3
View File
@@ -138,8 +138,8 @@ impl NotificationManager {
fn draw_data<'a>(data: &NotificationData) -> Element<'a, NotificationMessage> {
match data {
NotificationData::DataLoaded => {
text!("Данные успешно загружены на сервер")
NotificationData::DataSynced => {
text!("Данные успешно синхронизированы")
}
NotificationData::ImportError => {
text!("Ошибка при импорте данных из Anki")
@@ -168,6 +168,9 @@ impl NotificationManager {
NotificationData::CompressError => {
text!("Ошибка сжатия или распаковки данных")
}
NotificationData::LocalVersionChanged => {
text!("Локальная версия будет заменена")
}
}
.width(Fill)
.into()
@@ -184,7 +187,7 @@ pub enum NotificationRequest {
#[derive(Clone, Debug)]
pub enum NotificationData {
PermissionDenied,
DataLoaded,
DataSynced,
ImportError,
IncorrectAnkiFile,
OpenFileError,
@@ -193,6 +196,7 @@ pub enum NotificationData {
DbSaveError,
DbLoadError,
CompressError,
LocalVersionChanged,
}
pub type MaybeError = Result<(), NotificationData>;
+581 -321
View File
File diff suppressed because it is too large. Load diff
+29 -21
View File
@@ -1,4 +1,6 @@
use crate::data::entities::WordData;
use crate::app::AppState;
use crate::data::entities::{EntityHandle, WordData};
use crate::data::repositories::Repository;
use crate::navigation::Page::PreviousPage;
use crate::navigation::RootMessage;
use crate::navigation::*;
@@ -6,17 +8,18 @@ use crate::pages::dictionary::split_with_coma;
use crate::pages::dictionary_quiz::DictionaryQuizMessage::*;
use crate::pages::quiz::Score;
use crate::styling::*;
use crate::{jl_button, jl_text_input};
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::widget::{Row, container, row, space, text, text_input};
use iced::{Border, Element, Fill, Task, Theme, alignment};
use rand::prelude::SliceRandom;
#[derive(Debug, Clone)]
pub struct DictionaryQuizState {
words: Vec<WordData>,
current_set: Vec<WordData>,
state: AppState,
words: Vec<EntityHandle<WordData>>,
current_set: Vec<EntityHandle<WordData>>,
answer: String,
view: String,
correct: String,
@@ -73,7 +76,7 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
]
.align_x(alignment::Horizontal::Center)
.spacing(5),
text_input("Перевод", &self.answer)
jl_text_input!("Перевод", &self.answer)
.size(28)
.width(250)
.on_input(AnswerChanged)
@@ -88,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),
@@ -104,13 +104,19 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
}
impl DictionaryQuizState {
pub fn new(words: Vec<WordData>, reverse: bool, no_typing: bool) -> DictionaryQuizState {
DictionaryQuizState {
pub fn new(
words: Vec<EntityHandle<WordData>>,
reverse: bool,
no_typing: bool,
state: AppState,
) -> DictionaryQuizState {
Self {
state,
words,
current_set: Vec::new(),
answer: "".to_string(),
answer: String::new(),
view: "---".to_string(),
correct: "".to_string(),
correct: String::new(),
score: Default::default(),
is_help: false,
reverse,
@@ -172,7 +178,7 @@ impl DictionaryQuizState {
self.is_help = false;
self.answer = String::new();
let next: WordData = match self.current_set.pop() {
let next: EntityHandle<WordData> = match self.current_set.pop() {
Some(word) => word,
None => {
self.update_set();
@@ -180,12 +186,14 @@ impl DictionaryQuizState {
}
};
let repo = self.state.dictionary.read();
let next = repo.get_static(next).unwrap();
if self.reverse {
self.view = next.value.clone();
self.correct = next.key.clone();
self.view = next.value;
self.correct = next.key;
} else {
self.view = next.key.clone();
self.correct = next.value.clone();
self.view = next.key;
self.correct = next.value;
}
}
@@ -211,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()
}
+31 -38
View File
@@ -1,7 +1,6 @@
use crate::app::AppState;
use crate::data::INVALID_ID;
use crate::data::entities::{HistoryItem, WordData};
use crate::data_provider::history::get_history_of_set;
use crate::data::entities::{HistoryNote, WordData};
use crate::data::repositories::Repository;
use crate::navigation::*;
use crate::styling::*;
use HistoryMessage::Back;
@@ -10,13 +9,10 @@ use iced::alignment::Horizontal::Center;
use iced::widget::space::horizontal;
use iced::widget::*;
use iced::{Element, Fill, FillPortion, Task};
use parking_lot::Mutex;
use std::sync::Arc;
#[derive(Clone)]
pub struct HistoryState {
list: Vec<HistoryItem>,
words: Vec<WordData>,
list: Vec<(WordData, HistoryNote)>,
}
impl NavigatedPage<HistoryMessage> for HistoryState {
@@ -48,51 +44,48 @@ impl NavigatedPage<HistoryMessage> for HistoryState {
}
impl HistoryState {
pub fn new(id: crate::data::Id, state: Arc<Mutex<AppState>>) -> Self {
let state = state.lock();
let history = get_history_of_set(id).unwrap_or_default();
pub fn new(id: crate::data::Id, state: AppState) -> Self {
let dict = &state.dictionary.read();
let history_repo = &mut state.history.write();
history_repo.init().expect("TODO: panic message");
let mut history = history_repo.query(Arc::new(move |h| h.deck_id == id));
let words = history
.iter()
.map(
|item| match state.dictionary.iter().find(|w| w.id == item.word_id) {
None => WordData {
id,
key: "Удаленное слово".to_string(),
value: "Удаленное слово".to_string(),
tags: "Удаленное слово".to_string(),
additional: Default::default(),
group_id: INVALID_ID,
},
Some(word) => word.clone(),
},
)
.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: String::new(),
tags: String::new(),
additional: Default::default(),
});
(word, item.clone())
})
.collect();
Self {
list: history,
words,
}
Self { list: words }
}
fn history_lines(&self) -> Column<'_, HistoryMessage> {
let mut column = Column::new();
for (index, item) in self.list.iter().enumerate() {
for (index, item) in &self.list {
let activity = item;
column = column.push(
row![
iced::widget::column![
text!("{}", self.words[index].key)
.width(Fill)
.align_x(Center),
text!("{}", self.words[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!("{} ➞ {}", item.before, item.after),
text!("{} ➞ {}", activity.before, activity.after),
text!(
"{}",
item.timestamp.with_timezone(&Local).format("%d.%m %H:%M")
activity
.timestamp
.with_timezone(&Local)
.format("%d.%m %H:%M")
)
]
.spacing(5)
+85 -35
View File
@@ -1,14 +1,17 @@
use crate::app::AppState;
use crate::data::entities::{WordData, WordGroup};
use crate::data::Id;
use crate::data::entities::WordGroup;
use crate::data::entities::{CustomField, WordData};
use crate::data::repositories::{BatchedRepository, Repository};
use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups};
use crate::data_provider::words::{add_group, add_words};
use crate::fs::*;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::NotificationData::OpenFileError;
use crate::notification::NotificationData::{CompressError, OpenFileError};
use crate::notification::{MaybeError, NotificationData};
use crate::pages::import::ImportMessage::*;
use crate::push_error;
use crate::styling::*;
use crate::{jl_button, push_error_task};
use crate::{jl_text_input, push_error};
use iced::widget::button::danger;
use iced::widget::container::success;
use iced::widget::{
@@ -23,18 +26,19 @@ use log::info;
use parking_lot::Mutex;
use rfd::AsyncFileDialog;
use rusqlite::Connection;
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::BufReader;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use zip::ZipArchive;
use zstd::Decoder;
const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"];
#[derive(Clone)]
pub struct ImportState {
state: Arc<Mutex<AppState>>,
state: AppState,
path: Option<PathBuf>,
import_data: Option<ImportData>,
selected_index: usize,
@@ -153,7 +157,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()
@@ -265,7 +269,7 @@ impl ImportState {
self.available_properties(),
button("Добавить напрямую").on_press(SetupDirect),
row![
text_input("Пользовательское поле", &self.custom_property_name)
jl_text_input!("Пользовательское поле", &self.custom_property_name)
.on_input(EditCustomProperty),
button("Установить привязку").on_press(SetupCustomProperty)
]
@@ -293,7 +297,7 @@ impl ImportState {
.on_toggle(SwitchSkipEmpty),
column![
text!("Разделитель при множественном объединении"),
text_input("", &self.separator).on_input(EditSeparator)
jl_text_input!("", &self.separator).on_input(EditSeparator)
]
]
.spacing(DEFAULT_SPACING)
@@ -318,14 +322,14 @@ impl ImportState {
}
impl ImportState {
pub fn new(state: Arc<Mutex<AppState>>) -> ImportState {
pub fn new(state: AppState) -> ImportState {
ImportState {
state,
path: None,
import_data: None,
selected_index: 0,
selected_property: None,
custom_property_name: "".to_string(),
custom_property_name: String::new(),
skip_empty: true,
separator: ", ".to_string(),
progress: None,
@@ -359,8 +363,8 @@ impl ImportState {
Task::perform(
async {
spawn_blocking(move || {
Self::extract_import_file(path)?;
let data = Self::read_import_file()?;
let schema = Self::extract_import_file(path)?;
let data = Self::read_import_file(schema)?;
Ok(data)
})
.await
@@ -373,7 +377,7 @@ impl ImportState {
)
}
fn extract_import_file(path: PathBuf) -> MaybeError {
fn extract_import_file(path: PathBuf) -> Result<AnkiSchema, NotificationData> {
let file = File::open(path).map_err(|_| OpenFileError)?;
let reader = BufReader::new(file);
let mut archive = ZipArchive::new(reader).map_err(|_| OpenFileError)?;
@@ -384,20 +388,24 @@ impl ImportState {
.truncate(true)
.open(&temp_file_path)
.map_err(|_| OpenFileError)?;
if let Ok(mut file) = archive.by_name("collection.anki21") {
if let Ok(mut file) = archive.by_name("collection.anki21b") {
let mut decoder = Decoder::new(&mut file).map_err(|_| CompressError)?;
std::io::copy(&mut decoder, &mut temp_file).map_err(|_| OpenFileError)?;
Ok(AnkiSchema::Beta)
} else if let Ok(mut file) = archive.by_name("collection.anki21") {
std::io::copy(&mut file, &mut temp_file).map_err(|_| OpenFileError)?;
Ok(())
Ok(AnkiSchema::Old)
} else if let Ok(mut file) = archive.by_name("collection.anki2") {
std::io::copy(&mut file, &mut temp_file).map_err(|_| OpenFileError)?;
Ok(())
Ok(AnkiSchema::Old)
} else {
Err(OpenFileError)
}
}
fn read_import_file() -> Result<ImportData, NotificationData> {
fn read_import_file(schema: AnkiSchema) -> Result<ImportData, NotificationData> {
let temp_file_path = temp_import_path();
let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?;
let data = load_groups(&connection)?;
let data = load_groups(&connection, schema)?;
Ok(data)
}
fn setup_mapping(&mut self, property_name: String) {
@@ -427,7 +435,19 @@ impl ImportState {
let separator = self.separator.clone();
let skip_empty = self.skip_empty;
let group = self.import_data.as_ref().unwrap().0[self.selected_index].clone();
let map_indices = Self::get_mapping_indices(&group);
let progress = self.progress.clone().unwrap();
if let Err(err) = Self::ensure_fields(&state, &map_indices) {
return push_error_task!(err);
}
let fields: HashMap<String, Id> = HashMap::from_iter(
state
.custom_fields
.read()
.iter()
.map(|f| (f.identifier.clone(), f.id)),
);
info!("{}", group.name);
Task::perform(
async move {
@@ -438,17 +458,17 @@ impl ImportState {
let import_list = get_words_of_group(&connection, group.id)?;
let mut words_list = Vec::with_capacity(import_list.len());
let map_indices = Self::get_mapping_indices(&group);
let mut group_entity = WordGroup {
id: 0.into(),
name: group.name.clone(),
};
let group_handle;
{
let state = state.lock();
add_group(&mut group_entity, &state.connection)?;
let group_entity = WordGroup {
id: 0.into(),
name: group.name.clone(),
};
let mut groups_repo = state.word_groups.write();
group_handle = groups_repo.create_now(group_entity)?;
}
let group_id = group_entity.id;
let group_id = group_handle.id;
for import in import_list {
let mut word = WordData::new();
@@ -472,7 +492,7 @@ impl ImportState {
word.tags = collected_string;
}
&_ => {
word.additional.insert((*dest).clone(), collected_string);
word.additional.insert(fields[dest], collected_string);
}
}
}
@@ -480,20 +500,16 @@ impl ImportState {
words_list.push(word);
}
let mut state = state.lock();
let connection = &mut state.connection;
let mut words_repo = state.dictionary.write();
let mut index = 0;
let total_len = words_list.len() as f32 / 1024.0;
for word in &mut words_list.chunks_mut(1024) {
add_words(word, connection)?;
for words in &mut words_list.chunks_mut(1024) {
words_repo.create_now_many(words)?;
index += 1;
let mut progress = progress.lock();
*progress = (index as f32 / total_len).min(1.0);
}
state.dictionary.append(&mut words_list);
state.word_groups.push(group_entity);
Ok(())
})
.await
@@ -556,4 +572,38 @@ impl ImportState {
|_| RootMessage::Import(NextProgress),
)
}
fn ensure_fields(
state: &AppState,
import_group: &Vec<(String, Vec<usize>)>,
) -> Result<(), NotificationData> {
let mut fields_repo = state.custom_fields.write();
let mut current_fields: Vec<String> = fields_repo
.iter()
.map(|field| field.identifier.clone())
.collect();
current_fields.append(&mut vec![
"key".to_string(),
"value".to_string(),
"tags".to_string(),
]);
for (field, _) in import_group {
if current_fields.contains(field) {
continue;
}
fields_repo.create_now(CustomField {
id: Default::default(),
name: field.clone(),
identifier: field.clone(),
})?;
}
Ok(())
}
}
pub enum AnkiSchema {
Old,
Beta,
}
+4 -4
View File
@@ -4,11 +4,11 @@ use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::quiz::QuizMessage::*;
use crate::styling::*;
use crate::{jl_button, jl_text_input};
use iced::widget::*;
use iced::{Element, Fill, Task, alignment};
use rand::seq::SliceRandom;
#[derive(Clone, Debug)]
pub struct QuizState {
kana: String,
current_roman: String,
@@ -77,12 +77,12 @@ impl NavigatedPage<QuizMessage> for QuizState {
]
.align_y(alignment::Vertical::Center)
.spacing(20),
text_input("Романдзи", &self.current_roman)
jl_text_input!("Романдзи", &self.current_roman)
.size(28)
.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),
@@ -113,7 +113,7 @@ impl QuizState {
kana: "ぁ".to_string(),
correct_roman: "a".to_string(),
is_help: false,
current_roman: "".to_string(),
current_roman: String::new(),
set: KanaSet::hiragana(),
queue: Vec::new(),
score: Score {
+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(),
+22 -39
View File
@@ -2,28 +2,29 @@ use crate::app::AppState;
use crate::core::deck::DeckData;
use crate::core::srs::WordOpenMode;
use crate::data::Id;
use crate::data::entities::{CardState, DeckSettings, WordData};
use crate::data::entities::{CardState, WordData};
use crate::data::repositories::Repository;
use crate::data_provider::voice::get_voice;
use crate::jl_button;
use crate::navigation::Page::PreviousPage;
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
use crate::notification::{MaybeError, NotificationData};
use crate::pages::repetitions::DeckRunData;
use crate::styling::*;
use crate::{push_error, push_error_task};
use chrono::Local;
use iced::alignment::Horizontal::Center;
use iced::keyboard::key::Physical::Code;
use iced::widget::text::Style;
use iced::widget::{Column, button, column, container, row, rule, space, text, tooltip};
use iced::{Element, Fill, Task, Theme, alignment, keyboard};
use parking_lot::Mutex;
use rodio::MixerDeviceSink;
use std::collections::HashSet;
use std::sync::Arc;
use tokio::task::spawn_blocking;
pub struct RepetitionState {
state: Arc<Mutex<AppState>>,
pub settings: DeckSettings,
state: AppState,
pub settings: DeckRunData,
pub set: DeckData,
current_word: WordData,
current_statistic: CardState,
@@ -103,12 +104,13 @@ impl NavigatedPage<RepetitionMessage> for RepetitionState {
impl RepetitionState {
pub(crate) fn new(
set: DeckSettings,
state: Arc<Mutex<AppState>>,
set: DeckRunData,
state: AppState,
) -> Result<RepetitionState, NotificationData> {
let mut card_set = DeckData::new(&set, state.clone())?;
let mut card_set = DeckData::new(&set, set.open_mode, state.clone());
let (word, stat) = card_set.next();
let word = state.dictionary.read().get_static(word).unwrap();
let stat = state.card_states.read().get_static(stat).unwrap();
let sink_handle = if set.forward.contains("speech") || set.backward.contains("speech") {
rodio::DeviceSinkBuilder::open_default_sink().ok()
} else {
@@ -150,9 +152,11 @@ impl RepetitionState {
self.open = false;
self.opened.insert(self.current_word.id);
let next = self.set.next();
self.current_word = next.0;
self.current_statistic = next.1;
let (word, state) = self.set.next();
let word = self.state.dictionary.read().get_static(word).unwrap();
let stat = self.state.card_states.read().get_static(state).unwrap();
self.current_word = word;
self.current_statistic = stat;
if self.settings.require_speech() && self.sink.is_some() {
let sink = self.sink.clone();
@@ -162,29 +166,9 @@ impl RepetitionState {
Err(err) => push_error!(err),
});
}
self.increment_counter();
Task::none()
}
fn increment_counter(&mut self) {
let mut state = self.state.lock();
let id = self.settings.id;
let activity = state.activity.get_mut(&id);
let now = Local::now().date_naive();
if let Some(activity) = activity {
let (last_day, counter) = activity.last_mut().unwrap();
if *last_day == now {
*counter += 1;
} else {
activity.push((now, 1));
}
} else {
state.activity.insert(id, vec![(now, 1)]);
}
}
fn draw_forward(&self) -> Element<'_, RepetitionMessage> {
self.draw_card_view(self.settings.forward.as_str())
}
@@ -272,28 +256,27 @@ impl RepetitionState {
}
fn draw_voice(&self) -> Element<'_, RepetitionMessage> {
button("Воспроизвести")
.style(jl_button)
jl_button!("Воспроизвести")
.on_press(RepetitionMessage::Play)
.into()
}
fn draw_reading(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("reading") {
match word.additional.get(&Id(1)) {
None => space().into(),
Some(reading) => text!("{}", reading).size(24).into(),
}
}
fn draw_context(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("context") {
match word.additional.get(&Id(3)) {
None => space().into(),
Some(context) => text!("{}", context).size(24).into(),
Some(context) => text!("{}", context).center().size(24).into(),
}
}
fn draw_description(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("description") {
match word.additional.get(&Id(2)) {
None => space().into(),
Some(context) => text!("{}", context).size(24).into(),
Some(context) => text!("{}", context).center().size(24).into(),
}
}
}
+94 -52
View File
@@ -1,48 +1,50 @@
use crate::app::AppState;
use crate::data::entities::DeckSettings;
use crate::data_provider::card_states::{delete_stats, load_stats_of_deck};
use crate::data_provider::decks::{delete_set, update_deck};
use crate::core::deck::DeckData;
use crate::data::entities::{DeckSettings, EntityHandle};
use crate::data::repositories::{BatchedRepository, Repository};
use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::MaybeError;
use crate::pages::repetition_settings::RepetitionSettingsMessage::*;
use crate::styling::*;
use crate::{jl_button, jl_text_input};
use crate::{push_error, push_error_task};
use iced::widget::button::danger;
use iced::widget::space::horizontal;
use iced::widget::text_input::Catalog;
use iced::widget::{button, column, row, scrollable, space, text, text_input};
use iced::{Element, Task};
use iced_core::Length::Fill;
use iced_core::{Padding, Theme, color};
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;
pub struct RepetitionSettingsState {
set: DeckSettings,
index: usize,
state: Arc<Mutex<AppState>>,
settings_handle: EntityHandle<DeckSettings>,
settings_data: DeckSettings,
state: AppState,
real_delete: bool,
real_delete_states: bool,
correct_filter: bool,
filter_changed: bool,
filtered_words: Option<u32>,
}
impl RepetitionSettingsState {
pub(crate) fn new(index: usize, state: Arc<Mutex<AppState>>) -> RepetitionSettingsState {
let set;
{
let local_state = state.lock();
set = local_state.decks[index].clone();
}
let filter_state = set.check_filter();
pub(crate) fn new(
settings: EntityHandle<DeckSettings>,
state: AppState,
) -> RepetitionSettingsState {
let data = state.decks.read().get_static(settings).unwrap();
RepetitionSettingsState {
index,
set,
settings_data: data,
settings_handle: settings,
state,
real_delete: false,
correct_filter: filter_state,
real_delete_states: false,
correct_filter: true,
filter_changed: false,
filtered_words: None,
}
}
}
@@ -61,35 +63,36 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
match message {
Back => {}
Save => {
let mut state = self.state.lock();
if let Err(e) = update_deck(&mut self.set, &state.connection) {
let mut decks = self.state.decks.write();
decks.set(&mut self.settings_handle, self.settings_data.clone());
if let Err(e) = decks.save(&mut self.settings_handle) {
return push_error_task!(e);
}
drop(decks);
if self.filter_changed
&& let Err(e) = update_deck_stats(&self.set, &state)
&& let Err(e) = update_deck_stats(&self.settings_data, &self.state)
{
return push_error_task!(e);
}
state.decks[self.index] = self.set.clone();
}
SetName(new) => {
self.set.name = new;
self.settings_data.name = new;
}
SetForward(new) => {
self.set.forward = new;
self.settings_data.forward = new;
}
SetBackward(backward) => {
self.set.backward = backward;
self.settings_data.backward = backward;
}
SetFilter(new) => {
self.set.filter = new;
self.correct_filter = self.set.check_filter();
self.settings_data.filter = new;
self.correct_filter = self.settings_data.check_filter();
self.filter_changed = true;
}
TryFilter => {
let state = self.state.lock();
let count = self.set.get_word_list(&state).len();
self.set.count = Some(count);
let list = DeckData::list(&self.settings_data, &self.state);
self.filtered_words = Some(list.len() as u32);
}
DeleteSet => {
if !self.real_delete {
@@ -99,15 +102,36 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
RootMessage::RepetitionSettings(RevertDeleteSet)
});
}
let mut state = self.state.lock();
if let Err(e) = delete_set(&self.set, &state.connection) {
let mut decks = self.state.decks.write();
if let Err(e) = decks.delete(self.settings_handle) {
return push_error_task!(e);
}
state.decks.remove(self.index);
return Task::done(RootMessage::RepetitionSettings(Back));
}
RevertDeleteSet => self.real_delete = false,
RevertDeleteSet => {
self.real_delete = false;
self.real_delete_states = true;
}
DeleteStates => {
if !self.real_delete_states {
self.real_delete_states = true;
return Task::future(async {
tokio::time::sleep(Duration::from_millis(3000)).await;
RootMessage::RepetitionSettings(RevertDeleteSet)
});
}
let mut states = self.state.card_states.write();
let deck_id = self.settings_handle.id;
let mut query = states
.query(Arc::new(move |state| state.deck_id == deck_id))
.handles()
.collect::<Vec<_>>();
if let Err(e) = states.delete_many(query.as_mut_slice()) {
return push_error_task!(e);
}
self.real_delete_states = false;
}
}
Task::none()
}
@@ -119,22 +143,22 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
column![
column![
text!("Название набора"),
text_input("", &self.set.name).on_input(SetName),
jl_text_input!("", &self.settings_data.name).on_input(SetName),
]
.spacing(QUARTER_SPACING),
column![
text!("Передняя сторона"),
text_input("", &self.set.forward).on_input(SetForward),
jl_text_input!("", &self.settings_data.forward).on_input(SetForward),
]
.spacing(QUARTER_SPACING),
column![
text!("Задняя сторона"),
text_input("", &self.set.backward).on_input(SetBackward),
jl_text_input!("", &self.settings_data.backward).on_input(SetBackward),
]
.spacing(QUARTER_SPACING),
column![
text!("Фильтр"),
text_input("", &self.set.filter)
jl_text_input!("", &self.settings_data.filter)
.style(|x, status| {
let mut primary = Theme::default()(x, status);
if !self.correct_filter {
@@ -143,10 +167,8 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
primary
})
.on_input(SetFilter),
button("Проверить фильтр")
.style(jl_button)
.on_press(TryFilter),
self.count_view(&self.set),
jl_button!("Проверить фильтр").on_press(TryFilter),
self.count_view(),
]
.spacing(QUARTER_SPACING),
]
@@ -160,7 +182,15 @@ 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 {
"Вы уверены?"
} else {
"Удалить прогресс?"
})
.style(danger)
.on_press(DeleteStates),
button(if self.real_delete {
"Вы уверены?"
} else {
@@ -168,7 +198,6 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
})
.style(danger)
.on_press(DeleteSet),
space().width(Fill),
]
.spacing(DEFAULT_SPACING),
]
@@ -180,22 +209,34 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
}
fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError {
let mut db_stats = load_stats_of_deck(deck, &state.connection).expect("Wrong db operation");
let filtered_words = deck.get_word_list(state);
filtered_words.iter().for_each(|word| {
let word_id = state.dictionary[*word].id;
if let Some(index) = db_stats.iter().position(|stat| stat.word_id == word_id) {
let states_repo = &mut state.card_states.write();
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)
.collect::<Vec<_>>();
let filtered_words = DeckData::list(deck, state)
.iter()
.map(|w| w.id)
.collect::<Vec<_>>();
filtered_words.iter().for_each(|word_id| {
if let Some(index) = db_stats.iter().position(|stat_id| stat_id.word_id == *word_id) {
db_stats.remove(index);
}
});
delete_stats(db_stats.as_slice(), &state.connection)?;
let mut delete_operation = db_stats
.iter()
.map(|state| EntityHandle::new(state.id, state.id.0 as usize, true))
.collect::<Vec<_>>();
states_repo.delete_many(delete_operation.as_mut_slice())?;
Ok(())
}
impl RepetitionSettingsState {
fn count_view(&self, set: &DeckSettings) -> Element<'_, RepetitionSettingsMessage> {
if let Some(count) = set.count {
fn count_view(&self) -> Element<'_, RepetitionSettingsMessage> {
if let Some(count) = self.filtered_words {
return text!("Количество слов: {}", count).into();
}
space().into()
@@ -212,5 +253,6 @@ pub enum RepetitionSettingsMessage {
SetFilter(String),
TryFilter,
DeleteSet,
DeleteStates,
RevertDeleteSet,
}
+158 -178
View File
@@ -1,12 +1,11 @@
use crate::app::AppState;
use crate::core::deck::DeckData;
use crate::core::srs::{AppendMode, OrderMode};
use crate::data::Id;
use crate::data::entities::{CardState, DeckSettings};
use crate::data_provider::card_states::{add_stat_list, load_stats_of_deck};
use crate::data_provider::decks::update_deck;
use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData};
use crate::data::repositories::{BatchedRepository, Repository};
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::{MaybeError, NotificationRequest};
use crate::notification::MaybeError;
use crate::pages::history::HistoryState;
use crate::pages::repetition::RepetitionState;
use crate::pages::repetition_settings::RepetitionSettingsState;
@@ -14,8 +13,9 @@ use crate::pages::repetitions::RepetitionsMessage::*;
use crate::push_error;
use crate::push_error_task;
use crate::styling::*;
use crate::{jl_button, jl_text_input};
use chrono::{Days, Local, Utc};
use hashbrown::{HashMap, HashSet};
use hashbrown::HashSet;
pub use iced::widget::button::{Catalog, Style};
use iced::widget::button::{Status, danger};
use iced::widget::tooltip::Position::Top;
@@ -27,16 +27,14 @@ use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Tas
use iced_core::Padding;
use iced_core::border::Radius;
use iced_core::svg::Handle;
use parking_lot::Mutex;
use std::collections::BTreeMap;
use std::ops::Deref;
use std::sync::Arc;
#[derive(Clone)]
pub struct RepetitionsState {
word_id_index_map: HashMap<Id, usize>,
selected_deck_index: Option<usize>,
decks: Vec<DeckViewData>,
state: Arc<Mutex<AppState>>,
selected_deck_handle: Option<EntityHandle<DeckSettings>>,
decks: BTreeMap<EntityHandle<DeckSettings>, DeckRunData>,
state: AppState,
}
impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
@@ -49,11 +47,10 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
self.state.clone(),
)))
} else if let GoToRepetition = message {
let clone = self.state.clone();
let mut state_clone = self.state.clone();
let selected_set = self.selected_deck_mut().unwrap();
if selected_set.append_mode == AppendMode::Full {
if let Err(_err) = Self::append_all_words(&mut clone.lock(), selected_set) {
if let Err(_err) = Self::append_all_words(&mut state_clone, selected_set) {
return None;
}
selected_set.existing_words_indices = selected_set.available_words_indices.clone();
@@ -66,7 +63,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
}
if let Ok(repetition_state) =
RepetitionState::new(selected_set.general_settings.clone(), clone)
RepetitionState::new(selected_set.clone(), self.state.clone())
{
return Some(Repetition(repetition_state));
}
@@ -74,7 +71,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
None
} else if let GoToSettings = message {
Some(RepetitionSettings(RepetitionSettingsState::new(
self.selected_deck().unwrap().index.unwrap(),
self.selected_deck_handle.unwrap(),
self.state.clone(),
)))
} else {
@@ -82,24 +79,24 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
}
}
fn navigated(&mut self) {
if let Some(index) = self.selected_deck_index {
let state = self.state.lock();
if let Some(global_deck) = state.decks.get(index) {
if global_deck.id != self.selected_deck().unwrap().id {
drop(state);
self.decks.remove(index);
self.clear_selection();
} else {
self.decks[index].general_settings = state.decks[index].clone();
drop(state);
let _ = self.fill_deck_word_indices();
if let Some(index) = self.selected_deck_handle {
let decks = self.state.decks.read();
if let Some(deck) = decks.get_static(index) {
{
let stored = self.decks.get_mut(&index).unwrap();
stored.general_settings = deck;
}
drop(decks);
self.fill_deck_word_indices();
} else {
drop(state);
self.decks.remove(index);
self.clear_selection()
self.decks.remove(&index);
drop(decks);
self.clear_selection();
}
} else {
self.fill_deck_word_indices();
}
_ = self.state.history.write().reload()
}
fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> {
match message {
@@ -111,31 +108,32 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
CreateSet => {
let index = self.decks.len() + 1;
self.decks.push(DeckViewData {
general_settings: DeckSettings::with_name(format!("Колода #{}", index)),
existing_words_indices: Default::default(),
available_words_indices: Default::default(),
indices_ready: false,
append_mode: Default::default(),
index: None,
});
let general = DeckSettings::with_name(format!("Колода #{}", index));
let handle = self.state.decks.write().create(general.clone());
self.decks.insert(
handle,
DeckRunData {
settings_handle: handle,
general_settings: general,
existing_words_indices: Default::default(),
available_words_indices: Default::default(),
indices_ready: false,
append_mode: Default::default(),
count: None,
open_mode: OrderMode::Default,
},
);
}
DeleteSet => {
let index = self.selected_deck_index.unwrap();
let index = self.selected_deck_handle.unwrap();
self.clear_selection();
let deck = self.decks.remove(index);
if deck.id.is_valid() {
let mut state = self.state.lock();
state.decks.remove(index);
self.decks = self
.decks
.clone()
.into_iter()
.map(|mut deck| {
deck.index = state.decks.iter().position(|d| d.id == deck.id);
deck
})
.collect();
self.decks.remove(&index);
if index.real() {
let mut repo = self.state.decks.write();
if let Err(err) = repo.delete(index) {
return push_error_task!(err);
}
}
}
SetName(value) => {
@@ -145,29 +143,24 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
SelectSet(index) => {
self.select_deck(index);
let deck = self.selected_deck().unwrap();
if !deck.indices_ready
&& let Err(err) = self.fill_deck_word_indices()
{
return push_error_task!(err);
if !deck.indices_ready {
self.fill_deck_word_indices()
}
}
Save => {
let mut deck = self.selected_deck().unwrap().clone();
let mut state = self.state.lock();
{
if let Err(e) = update_deck(&mut deck.general_settings, &state.connection) {
return Task::done(RootMessage::PushNotification(
NotificationRequest::Error(e),
));
}
if let Some(index) = deck.index {
state.decks[index] = deck.general_settings.clone();
} else {
state.decks.push(deck.general_settings.clone());
deck.index = Some(state.decks.len() - 1);
self.decks[self.selected_deck_index.unwrap()] = deck.clone();
}
let deck = self.selected_deck().unwrap().clone();
let handle = self.selected_deck_handle.as_mut().unwrap();
let mut repo = self.state.decks.write();
repo.set(handle, deck.general_settings.clone());
let old = *handle;
if let Err(err) = repo.save(handle) {
return push_error_task!(err);
}
let mut deck = self.decks.remove(&old).unwrap();
deck.settings_handle = *handle;
self.decks.insert(*handle, deck);
}
SetForward(value) => {
let deck = self.selected_deck_mut().unwrap();
@@ -182,23 +175,21 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
deck.general_settings.filter = value;
}
TryFilter => {
let state = self.state.lock();
let deck = self.selected_deck().unwrap();
let count = deck.get_word_list(&state).len();
drop(state);
let count = DeckData::list(deck, &self.state).len();
let deck = self.selected_deck_mut().unwrap();
deck.general_settings.count = Some(count);
deck.count = Some(count);
}
SetOpenMode(mode) => {
let deck = self.selected_deck_mut().unwrap();
deck.general_settings.open_mode = mode;
deck.open_mode = mode;
}
SetAppendMode(mode) => {
let deck = self.selected_deck_mut().unwrap();
deck.append_mode = mode;
}
AppendWords(count) => {
let adding: Vec<usize>;
let adding: Vec<EntityHandle<WordData>>;
{
{
let deck = self.selected_deck().unwrap();
@@ -220,10 +211,10 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
deck.existing_words_indices.append(&mut adding.clone());
}
let deck = self.selected_deck().unwrap();
let mut state = self.state.lock();
let deck = self.selected_deck().unwrap().clone();
let state = &mut self.state;
if let Err(err) =
Self::append_words(&mut state, &deck.general_settings, adding.into_iter())
Self::append_words(state, &deck.general_settings, adding.into_iter())
{
return push_error_task!(err);
}
@@ -237,10 +228,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)),
@@ -257,47 +245,42 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
}
impl RepetitionsState {
pub(crate) fn new(state: Arc<Mutex<AppState>>) -> RepetitionsState {
let state_ = state.lock();
let mut map = HashMap::with_capacity(state_.dictionary.len());
state_
.dictionary
.iter()
.enumerate()
.for_each(|(index, word)| {
map.insert(word.id, index);
});
let decks = Self::get_decks_from_state(&state_);
drop(state_);
pub(crate) fn new(state: AppState) -> RepetitionsState {
let decks = Self::get_decks_from_state(&state);
RepetitionsState {
word_id_index_map: map,
selected_deck_index: None,
selected_deck_handle: None,
decks,
state,
}
}
fn get_decks_from_state(state: &AppState) -> Vec<DeckViewData> {
fn get_decks_from_state(state: &AppState) -> BTreeMap<EntityHandle<DeckSettings>, DeckRunData> {
state
.decks
.read()
.iter()
.enumerate()
.map(|(index, deck)| DeckViewData {
general_settings: deck.clone(),
existing_words_indices: Default::default(),
available_words_indices: Default::default(),
indices_ready: false,
append_mode: Default::default(),
index: Some(index),
.map(|(index, deck)| {
(
EntityHandle::new(deck.id, index, true),
DeckRunData {
settings_handle: EntityHandle::new(deck.id, index, true),
general_settings: deck.clone(),
existing_words_indices: Default::default(),
available_words_indices: Default::default(),
indices_ready: false,
append_mode: Default::default(),
count: None,
open_mode: OrderMode::Default,
},
)
})
.collect()
.collect::<BTreeMap<_, _>>()
}
}
impl RepetitionsState {
fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> {
fn launch_delete_button(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
if deck.id.is_valid() {
if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.is_empty() {
return text!("Добавьте карточки или измените фильтр").into();
@@ -320,7 +303,7 @@ impl RepetitionsState {
column![
text!("Название набора"),
row![
text_input("", &deck.name).on_input(SetName),
jl_text_input!("", &deck.name).on_input(SetName),
button(svg(Handle::from_memory(SETTINGS_ICON)))
.width(43)
.on_press(GoToSettings),
@@ -329,24 +312,22 @@ impl RepetitionsState {
]
.spacing(QUARTER_SPACING),
{
if !deck.id.is_valid() {
if !deck.settings_handle.real() {
column![
column![
text!("Передняя сторона"),
text_input("", &deck.forward).on_input(SetForward),
jl_text_input!("", &deck.forward).on_input(SetForward),
]
.spacing(QUARTER_SPACING),
column![
text!("Задняя сторона"),
text_input("", &deck.backward).on_input(SetBackward),
jl_text_input!("", &deck.backward).on_input(SetBackward),
]
.spacing(QUARTER_SPACING),
column![
text!("Фильтр"),
text_input("", &deck.filter).on_input(SetFilter),
button("Проверить фильтр")
.style(jl_button)
.on_press(TryFilter),
jl_text_input!("", &deck.filter).on_input(SetFilter),
jl_button!("Проверить фильтр").on_press(TryFilter),
self.count_view(deck),
]
.spacing(QUARTER_SPACING),
@@ -374,7 +355,7 @@ impl RepetitionsState {
Some(deck.open_mode),
SetOpenMode
),
button("История").style(jl_button).on_press(GoToHistory),
jl_button!("История").on_press(GoToHistory),
]
.spacing(HALF_SPACING)
}
@@ -391,7 +372,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)
@@ -412,22 +393,20 @@ impl RepetitionsState {
fn activity_bar(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> {
const MAX_DAY_COUNT: f32 = 128.0;
let state = self.state.lock();
let history = state.activity.get(&deck.id);
let history = self.state.history.read();
let history = history.activity_of_deck(deck.id);
let mut counts: Vec<u32> = vec![0; 30 * 7];
let now = Local::now().date_naive();
if let Some(history) = history {
for (date, activity) in history {
let distance = now - *date;
let index = counts.len() as i64 - distance.num_days() - 1;
for (date, activity) in history {
let distance = now - date;
let index = counts.len() as i64 - distance.num_days() - 1;
if index < 0 {
continue;
}
counts[index as usize] = *activity;
if index < 0 {
continue;
}
counts[index as usize] = activity;
}
lazy((counts, now), |(counts, now)| {
@@ -483,7 +462,7 @@ impl RepetitionsState {
})
.into()
}
fn word_append_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> {
fn word_append_panel(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
column![
text!("Режим добавления карточек"),
row![
@@ -506,7 +485,7 @@ impl RepetitionsState {
.spacing(HALF_SPACING)
.into()
}
fn adder_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> {
fn adder_panel(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
match deck.append_mode {
AppendMode::Full => space().into(),
AppendMode::Manual => row![
@@ -526,7 +505,7 @@ impl RepetitionsState {
let available = selected_deck.available_words_indices.len();
text!("{} слова добавлено из {}", now, available).into()
}
fn count_view(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> {
fn count_view(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
if let Some(count) = deck.count {
return text!("Количество слов: {}", count).into();
}
@@ -535,10 +514,10 @@ impl RepetitionsState {
fn decks_list(&self) -> Column<'_, RepetitionsMessage> {
let mut column = Column::new();
for (i, 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(i))
.on_press_with(move || SelectSet(*handle))
.style(move |_x: &Theme, status: Status| Style {
background: if status == Status::Hovered {
Some(Background::Color(Color::WHITE.scale_alpha(0.2)))
@@ -562,7 +541,7 @@ impl RepetitionsState {
}
impl RepetitionsState {
fn append_all_words(state: &mut AppState, deck: &DeckViewData) -> MaybeError {
fn append_all_words(state: &mut AppState, deck: &DeckRunData) -> MaybeError {
let existing = &deck.existing_words_indices;
let mut created_set = HashSet::with_capacity(existing.len());
existing.iter().for_each(|c| {
@@ -579,60 +558,59 @@ impl RepetitionsState {
fn append_words(
state: &mut AppState,
deck: &DeckSettings,
indices: impl Iterator<Item = usize>,
indices: impl Iterator<Item = EntityHandle<WordData>>,
) -> MaybeError {
let words = &state.dictionary;
let stats = &mut indices
.map(|i| {
let word = &words[i];
CardState {
id: 0.into(),
word_id: word.id,
last_open: Utc::now(),
score: 1,
fsrs_opened: Utc::now().date_naive(),
fsrs_memory: None,
set_id: deck.id,
fsrs_open_day: Utc::now().date_naive(),
}
.map(|i| CardState {
id: 0.into(),
word_id: i.id,
last_open: Utc::now(),
score: 1,
fsrs_opened: Utc::now().date_naive(),
fsrs_memory: None,
deck_id: deck.id,
fsrs_open_day: Utc::now().date_naive(),
})
.collect::<Vec<_>>();
if !stats.is_empty() {
add_stat_list(stats, &mut state.connection)?;
let mut repo = state.card_states.write();
repo.create_now_many(stats.as_mut_slice())?;
}
Ok(())
}
fn select_deck(&mut self, index: usize) {
if self.decks.get(index).is_some() {
self.selected_deck_index = Some(index);
fn select_deck(&mut self, index: EntityHandle<DeckSettings>) {
if self.decks.contains_key(&index) {
self.selected_deck_handle = Some(index);
}
}
fn selected_deck(&self) -> Option<&DeckViewData> {
self.decks.get(self.selected_deck_index?)
fn selected_deck(&self) -> Option<&DeckRunData> {
self.decks.get(&self.selected_deck_handle?)
}
fn selected_deck_mut(&mut self) -> Option<&mut DeckViewData> {
self.decks.get_mut(self.selected_deck_index?)
fn selected_deck_mut(&mut self) -> Option<&mut DeckRunData> {
self.decks.get_mut(&self.selected_deck_handle?)
}
fn clear_selection(&mut self) {
self.selected_deck_index = None;
self.selected_deck_handle = None;
}
fn fill_deck_word_indices(&mut self) -> MaybeError {
let state = &self.state.lock();
fn fill_deck_word_indices(&mut self) {
let card_states = self.state.card_states.read();
let deck_id = self.selected_deck().unwrap().id;
let added = card_states
.query_par(Arc::new(move |state| state.deck_id == deck_id))
.iter(&card_states)
.map(|state| EntityHandle::from_id(state.word_id))
.collect::<Vec<_>>();
let mut deck = self.selected_deck().unwrap().clone();
let added: Vec<_> = load_stats_of_deck(&deck, &state.connection)?
.iter()
.map(|c| self.word_id_index_map[&c.word_id])
.collect();
let total = deck.get_word_list(state);
let total = DeckData::list(&deck, &self.state);
deck.existing_words_indices = added;
deck.available_words_indices = total;
self.decks[self.selected_deck_index.unwrap()] = deck;
Ok(())
deck.indices_ready = true;
self.decks.insert(self.selected_deck_handle.unwrap(), deck);
}
}
#[derive(Clone)]
@@ -643,7 +621,7 @@ pub enum RepetitionsMessage {
CreateSet,
DeleteSet,
SetName(String),
SelectSet(usize),
SelectSet(EntityHandle<DeckSettings>),
Save,
SetForward(String),
SetBackward(String),
@@ -657,16 +635,18 @@ pub enum RepetitionsMessage {
}
#[derive(Clone)]
struct DeckViewData {
general_settings: DeckSettings,
existing_words_indices: Vec<usize>,
available_words_indices: Vec<usize>,
pub struct DeckRunData {
pub settings_handle: EntityHandle<DeckSettings>,
pub general_settings: DeckSettings,
existing_words_indices: Vec<EntityHandle<WordData>>,
available_words_indices: Vec<EntityHandle<WordData>>,
indices_ready: bool,
append_mode: AppendMode,
index: Option<usize>,
pub append_mode: AppendMode,
count: Option<usize>,
pub open_mode: OrderMode,
}
impl Deref for DeckViewData {
impl Deref for DeckRunData {
type Target = DeckSettings;
fn deref(&self) -> &Self::Target {
+4 -7
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;
@@ -16,13 +17,11 @@ use iced::{Element, Task, alignment};
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,
is_writing: bool,
state: Arc<Mutex<AppState>>,
state: AppState,
}
#[derive(Debug, Clone)]
@@ -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),
@@ -114,7 +111,7 @@ impl NavigatedPage<SelectorMessage> for SelectorState {
}
impl SelectorState {
pub fn new(state: Arc<Mutex<AppState>>) -> Self {
pub fn new(state: AppState) -> Self {
Self {
set: Default::default(),
is_writing: false,
+84 -47
View File
@@ -1,6 +1,8 @@
use crate::app::{AppState, fill_state};
use crate::app::AppState;
use crate::data::repositories::Repository;
use crate::data_provider::settings::{delete_settings, set_setting};
use crate::data_provider::web_api::*;
use crate::jl_button;
use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::{NotificationData, NotificationRequest};
@@ -12,9 +14,8 @@ use iced::widget::container::rounded_box;
use iced::widget::{button, column, container, progress_bar, row, space, text, toggler};
use iced::{Center, Element, Fill, Font, Length, Task};
use log::error;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;
use crate::notification::NotificationData::LocalVersionChanged;
#[derive(Clone)]
pub enum SyncMessage {
@@ -31,12 +32,13 @@ pub enum SyncMessage {
Disable,
DisableSync,
SwitchAutoSync,
ResetRealLoad,
}
#[derive(Clone)]
pub struct SyncState {
state: Arc<Mutex<AppState>>,
state: AppState,
auto_fetch: bool,
frozen: bool,
real_load: bool,
progress: f32,
}
@@ -56,8 +58,7 @@ impl NavigatedPage<SyncMessage> for SyncState {
match message {
Back => {}
CopyKey => {
let state = self.state.lock();
let key = state.sync_data.key.clone().unwrap();
let key = self.state.sync_data.read().key.clone().unwrap();
return iced::clipboard::write(key)
.map(|_val: String| RootMessage::Sync(KeyCopied));
}
@@ -76,16 +77,20 @@ impl NavigatedPage<SyncMessage> for SyncState {
if !validate_id(&new_id) {
return Task::none();
}
let mut state = self.state.lock();
state.sync_data.key = Some(new_id.clone());
if let Err(err) = set_setting("SYNC_KEY".to_string(), new_id, &state.connection) {
let mut settings = self.state.sync_data.write();
settings.key = Some(new_id.clone());
if let Err(err) = set_setting(
"SYNC_KEY".to_string(),
new_id,
&self.state.connection.lock(),
) {
return push_error_task!(err);
}
}
Send => {
self.prepare_to_db_interaction();
let id = self.state.lock().sync_data.key.clone().unwrap();
let id = self.state.sync_data.read().key.clone().unwrap();
let tasks = Task::batch([
Task::perform(
async { tokio::time::sleep(Duration::from_millis(200)).await },
@@ -97,7 +102,19 @@ impl NavigatedPage<SyncMessage> for SyncState {
return tasks;
}
GetLast => {
let id = self.state.lock().sync_data.key.clone().unwrap();
if !self.real_load {
self.real_load = true;
return Task::batch([
Task::future(async {
tokio::time::sleep(Duration::from_millis(3000)).await;
return RootMessage::Sync(ResetRealLoad);
}),
Task::done(RootMessage::PushNotification(NotificationRequest::Warning(LocalVersionChanged)))
]);
}
self.real_load = false;
let id = self.state.sync_data.read().key.clone().unwrap();
let tasks = Task::batch([
Task::perform(
async { tokio::time::sleep(Duration::from_millis(200)).await },
@@ -122,26 +139,27 @@ impl NavigatedPage<SyncMessage> for SyncState {
}
NetworkFinished => {
self.frozen = false;
let mut updated_state = AppState::new();
fill_state(&mut updated_state);
{
let mut connection = self.state.connection.lock();
connection.restart();
}
if let Err(err) = self.reload_data() {
return push_error_task!(err);
}
let mut state = self.state.lock();
state.sync_data = updated_state.sync_data;
state.connection = updated_state.connection;
state.decks = updated_state.decks;
state.dictionary = updated_state.dictionary;
state.word_groups = updated_state.word_groups;
return Task::done(RootMessage::PushNotification(NotificationRequest::Info(
NotificationData::DataLoaded,
NotificationData::DataSynced,
)));
}
Disable => {}
DisableSync => {
let mut state = self.state.lock();
state.sync_data.key = None;
let mut sync_data = self.state.sync_data.write();
let connection = self.state.connection.lock();
sync_data.key = None;
let save_result = || {
delete_settings("SYNC_KEY".to_string(), &state.connection)?;
delete_settings("AUTO_WEB_FETCH".to_string(), &state.connection)?;
delete_settings("SYNC_KEY".to_string(), &connection)?;
delete_settings("AUTO_WEB_FETCH".to_string(), &connection)?;
Ok::<(), NotificationData>(())
};
@@ -151,16 +169,21 @@ impl NavigatedPage<SyncMessage> for SyncState {
}
SwitchAutoSync => {
self.auto_fetch = !self.auto_fetch;
let mut state = self.state.lock();
state.sync_data.auto_web_fetch = self.auto_fetch;
let mut sync_data = self.state.sync_data.write();
let connection = self.state.connection.lock();
sync_data.auto_web_fetch = self.auto_fetch;
if let Err(err) = set_setting(
"AUTO_WEB_FETCH".to_string(),
self.auto_fetch.to_string(),
&state.connection,
&connection,
) {
return push_error_task!(err);
}
}
ResetRealLoad => {
self.real_load = false;
}
}
Task::none()
}
@@ -177,19 +200,27 @@ impl NavigatedPage<SyncMessage> for SyncState {
}
impl SyncState {
pub fn new(state: Arc<Mutex<AppState>>) -> SyncState {
let auto_fetch;
{
auto_fetch = state.lock().sync_data.auto_web_fetch;
}
pub fn new(state: AppState) -> SyncState {
let auto_fetch = state.sync_data.read().auto_web_fetch;
Self {
auto_fetch,
state,
frozen: false,
real_load: false,
progress: 0.0,
}
}
fn reload_data(&mut self) -> Result<(), NotificationData> {
self.state.card_states.write().reload()?;
self.state.word_groups.write().reload()?;
self.state.decks.write().reload()?;
self.state.dictionary.write().reload()?;
self.state.history.write().reload()?;
Ok(())
}
fn sync_column(&self) -> Element<'_, SyncMessage> {
let network_view: Element<'_, SyncMessage> = if self.frozen {
progress_bar(0.0..=5.0, self.progress).into()
@@ -197,7 +228,7 @@ impl SyncState {
space().into()
};
{
if let Some(key) = self.state.lock().sync_data.key.clone() {
if let Some(key) = self.state.sync_data.read().key.as_ref() {
column![
text!("Ваш ключ синхронизации"),
container(
@@ -209,14 +240,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)
@@ -229,12 +259,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),
]
}
}
@@ -254,13 +280,24 @@ impl SyncState {
}
self.frozen = true;
self.progress = 0.0;
let state = self.state.lock();
state
.connection
let mut connection = self.state.connection.lock();
connection
.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
});
connection.execute("VACUUM;", []).unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
0
});
connection
.execute("PRAGMA optimize;", [])
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
0
});
connection.finish();
}
}
+97 -123
View File
@@ -1,180 +1,156 @@
use crate::app::AppState;
use crate::data::entities::WordData;
use crate::data_provider::words::{delete_word, update_word};
use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::data::Id;
use crate::data::entities::{CustomField, EntityHandle, WordData};
use crate::data::repositories::Repository;
use crate::jl_text_input;
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, text, text_input};
use iced::{Element, Fill, Task};
use parking_lot::Mutex;
use std::sync::Arc;
use iced::widget::space::horizontal;
use iced::widget::{button, column, row, rule, scrollable, text};
use iced::{Element, Fill};
use iced_core::Padding;
#[derive(Clone)]
pub struct WordState {
state: Arc<Mutex<AppState>>,
index: usize,
word: WordData,
pub struct WordEditor<Message : Clone> {
state: AppState,
pub word_handle: EntityHandle<WordData>,
on_delete: Option<Message>,
on_edit: Option<Message>,
fields: Vec<CustomField>,
}
impl NavigatedPage<WordMessage> for WordState {
fn navigate(&mut self, message: &WordMessage) -> Option<Page> {
if let Back = message {
Some(PreviousPage)
} else {
None
}
}
fn navigated(&mut self) {}
fn update(&mut self, message: WordMessage) -> Task<RootMessage> {
impl<Message: std::clone::Clone> WordEditor<Message> {
pub(crate) fn update(&mut self, message: WordMessage) -> Option<Message> {
let dictionary = &mut self.state.dictionary.write();
let word = dictionary.get_mut(&mut self.word_handle).unwrap();
match message {
Back => {}
Save => {
let mut state = self.state.lock();
state.dictionary[self.index] = self.word.clone();
if let Err(err) = update_word(&mut self.word, &state.connection) {
return push_error_task!(err);
}
return Task::done(RootMessage::Word(Back));
}
Delete => {
let mut state = self.state.lock();
state.dictionary.remove(self.index);
if let Err(err) = delete_word(&self.word, &state.connection) {
return push_error_task!(err);
}
return Task::done(RootMessage::Word(Back));
return self.on_delete.clone();
}
SetTags(n) => self.word.tags = n,
SetTags(n) => word.tags = n,
SetKey(n) => {
self.word.key = n;
word.key = n;
}
SetValue(n) => {
self.word.value = n;
word.value = n;
}
SetAdditional(key, value) => {
self.word.additional.insert(key, value.clone());
word.additional.insert(key, value.replace("", ""));
}
AddAdditional(key) => {
self.word.additional.insert(key, "".to_string());
word.additional.insert(key, String::new());
}
RemoveAdditional(key) => {
self.word.additional.remove(key.as_str());
word.additional.remove(&key);
}
}
Task::none()
self.on_edit.clone()
}
fn view(&self) -> Element<'_, WordMessage> {
pub(crate) fn view(&self) -> Element<'_, WordMessage> {
let mut fast_add = row![];
if !self.word.additional.contains_key("reading") {
fast_add = fast_add.push(
button("Чтение")
.style(button::text)
.on_press(AddAdditional("reading".to_string())),
);
}
if !self.word.additional.contains_key("description") {
fast_add = fast_add.push(
button("Описание")
.style(button::text)
.on_press(AddAdditional("description".to_string())),
);
}
if !self.word.additional.contains_key("context") {
fast_add = fast_add.push(
button("В контексте")
.style(button::text)
.on_press(AddAdditional("context".to_string())),
);
let word = self
.state
.dictionary
.read()
.get_static(self.word_handle)
.unwrap();
for field in &self.fields {
if !word.additional.contains_key(&field.id) {
fast_add = fast_add.push(
button(field.name.as_str())
.style(button::text)
.on_press(AddAdditional(field.id)),
);
}
}
let mut col = iced::widget::column![
column![
text!("Ключ"),
text_input("key", &self.word.key).on_input(SetKey),
jl_text_input!("key", &word.key).on_input(SetKey),
]
.spacing(QUARTER_SPACING),
column![
text!("Значение"),
text_input("value", &self.word.value).on_input(SetValue),
jl_text_input!("value", &word.value).on_input(SetValue),
]
.spacing(QUARTER_SPACING),
column![
text!("Теги"),
text_input("tags", &self.word.tags).on_input(SetTags),
jl_text_input!("tags", &word.tags).on_input(SetTags),
]
.spacing(QUARTER_SPACING),
rule::horizontal(2),
scrollable(fast_add),
scrollable(fast_add.padding(Padding {
top: 0.0,
right: 0.0,
bottom: 4.0,
left: 0.0,
}))
.horizontal(),
];
for more in &self.word.additional {
for more in &word.additional {
col = col.push(self.get_view_for_more(more));
}
back_overlay(
column![
col.spacing(DEFAULT_SPACING / 2.0).width(Fill).height(Fill),
row![
button("Сохранить").style(jl_button).on_press(Save),
button("Удалить").style(danger).on_press(Delete),
]
.spacing(DEFAULT_SPACING)
column![
col.spacing(DEFAULT_SPACING / 2.0).width(Fill).height(Fill),
row![
horizontal().width(Fill),
button("Удалить").style(danger).on_press(Delete),
]
.spacing(DEFAULT_SPACING)
.into(),
Back,
)
]
.spacing(DEFAULT_SPACING)
.into()
}
}
impl WordState {
pub(crate) fn new(word: WordData, index: usize, state: Arc<Mutex<AppState>>) -> WordState {
WordState { state, index, word }
}
}
impl WordState {
fn get_view_for_more(&self, value: (&String, &String)) -> Element<'_, WordMessage> {
match value.0.as_str() {
"reading" => self.reading_field(value.1),
"description" => self.description_field(value.1),
"context" => self.context_field(value.1),
_ => self.additional_field(value.1, value.0.clone(), value.0.clone()),
impl<Message: std::clone::Clone> WordEditor<Message> {
pub fn new(handle: EntityHandle<WordData>, state: AppState) -> Self {
let fields = state
.custom_fields
.read()
.iter()
.cloned()
.collect::<Vec<_>>();
Self {
state,
word_handle: handle,
on_delete: None,
on_edit: None,
fields,
}
}
fn reading_field(&self, value: &str) -> Element<'_, WordMessage> {
self.additional_field(value, "Чтение слова".to_string(), "reading".to_string())
pub fn on_delete(mut self, message: Message) -> Self {
self.on_delete = Some(message);
self
}
pub fn on_edit(mut self, message: Message) -> Self {
self.on_edit = Some(message);
self
}
}
impl<Message: std::clone::Clone> WordEditor<Message> {
fn get_view_for_more(&self, value: (&Id, &String)) -> Element<'_, WordMessage> {
let field = self.fields.iter().find(|f| f.id == *value.0).unwrap();
match value.0 {
_ => self.additional_field(value.1, field.name.as_str(), value.0.clone()),
}
}
fn description_field(&self, value: &str) -> Element<'_, WordMessage> {
self.additional_field(value, "Описание".to_string(), "description".to_string())
}
fn context_field(&self, value: &str) -> Element<'_, WordMessage> {
self.additional_field(value, "В контексте".to_string(), "context".to_string())
}
fn additional_field(&self, value: &str, name: String, id: String) -> Element<'_, WordMessage> {
fn additional_field(&self, value: &str, name: &str, id: Id) -> Element<'_, WordMessage> {
column![
text!("{}", name),
row![
text_input(id.clone().as_str(), value)
.on_input({
let value = id.clone();
move |string| SetAdditional(value.clone(), string)
})
jl_text_input!(name, value)
.on_input(move |string| SetAdditional(id, string))
.width(Fill),
button("-")
.on_press(RemoveAdditional(id.clone()))
.style(danger),
button("-").on_press(RemoveAdditional(id)).style(danger),
]
.spacing(QUARTER_SPACING),
]
@@ -185,13 +161,11 @@ impl WordState {
#[derive(Debug, Clone)]
pub enum WordMessage {
Save,
Back,
Delete,
SetTags(String),
SetKey(String),
SetValue(String),
AddAdditional(String),
RemoveAdditional(String),
SetAdditional(String, String),
AddAdditional(Id),
RemoveAdditional(Id),
SetAdditional(Id, String),
}
+9 -13
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)
]
@@ -69,11 +65,11 @@ impl WritingState {
let mut list = set.list();
list.shuffle(&mut rand::rng());
WritingState {
kana: "".to_string(),
kana: String::new(),
set: list.clone(),
original_set: list,
kana_total: "".to_string(),
roman_total: "".to_string(),
kana_total: String::new(),
roman_total: String::new(),
next_text: "Дальше".to_string(),
show_all: false,
}
@@ -83,9 +79,9 @@ impl WritingState {
impl WritingState {
fn next(&mut self) {
if self.set.is_empty() {
self.kana = "".to_string();
self.roman_total = "".to_string();
self.kana_total = "".to_string();
self.kana = String::new();
self.roman_total = String::new();
self.kana_total = String::new();
self.set = self.original_set.clone();
self.set.shuffle(&mut rand::rng());
+29 -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(),
@@ -23,6 +23,15 @@ pub fn jl_button(theme: &Theme, status: Status) -> Style {
};
style
}
pub fn jl_text_input_style(
theme: &Theme,
status: iced::widget::text_input::Status,
) -> iced::widget::text_input::Style {
let mut style = iced::widget::text_input::default(theme, status);
style.border.radius = Radius::new(BUTTON_RADIUS);
style
}
pub fn cta_button(theme: &Theme, status: Status) -> Style {
let mut style = secondary(theme, status);
style.border.width = 1.0;
@@ -30,15 +39,26 @@ 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)
};
}
#[macro_export]
macro_rules! jl_text_input {
($placeholder:expr, $content:expr) => {
iced::widget::text_input($placeholder, $content).style($crate::styling::jl_text_input_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)
+21 -5
View File
@@ -1,5 +1,5 @@
#[cfg(test)]
mod mod_tests {
mod data_access {
use crate::data::Id;
use crate::data::entities::{EntityData, EntityHandle};
use crate::data::repositories::fast_position_of;
@@ -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]
@@ -121,3 +121,19 @@ mod mod_tests {
assert_eq!(entity.index(), 0)
}
}
#[cfg(test)]
mod anki_import{
use std::path::{Path, PathBuf};
use rusqlite::Connection;
use crate::data_provider::import::load_decks;
use crate::notification::NotificationData::IncorrectAnkiFile;
#[test]
pub fn decks(){
let file_name = Path::new("import");
let connection = Connection::open(file_name).unwrap();
let decks = load_decks(&connection).unwrap();
assert_eq!(decks.len(), 2);
}
}
+453
View File
@@ -0,0 +1,453 @@
use iced::widget::MouseArea;
use iced_core::widget::{Operation, Tree, tree};
use iced_core::{
Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget,
layout, mouse, overlay, renderer, touch,
};
pub fn hard_mouse_area<'a, Message, Theme, Renderer>(
widget: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> HardMouseArea<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
HardMouseArea::new(widget)
}
pub struct HardMouseArea<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> {
content: Element<'a, Message, Theme, Renderer>,
on_press: Option<Message>,
on_release: Option<Message>,
on_double_click: Option<Message>,
on_right_press: Option<Message>,
on_right_release: Option<Message>,
on_middle_press: Option<Message>,
on_middle_release: Option<Message>,
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
on_enter: Option<Message>,
on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
on_exit: Option<Message>,
interaction: Option<mouse::Interaction>,
}
impl<'a, Message, Theme, Renderer> HardMouseArea<'a, Message, Theme, Renderer> {
/// The message to emit on a left button press.
#[must_use]
pub fn on_press(mut self, message: Message) -> Self {
self.on_press = Some(message);
self
}
/// The message to emit on a left button release.
#[must_use]
pub fn on_release(mut self, message: Message) -> Self {
self.on_release = Some(message);
self
}
/// The message to emit on a double click.
///
/// If you use this with [`on_press`]/[`on_release`], those
/// event will be emit as normal.
///
/// The events stream will be: on_press -> on_release -> on_press
/// -> on_double_click -> on_release -> on_press ...
///
/// [`on_press`]: Self::on_press
/// [`on_release`]: Self::on_release
#[must_use]
pub fn on_double_click(mut self, message: Message) -> Self {
self.on_double_click = Some(message);
self
}
/// The message to emit on a right button press.
#[must_use]
pub fn on_right_press(mut self, message: Message) -> Self {
self.on_right_press = Some(message);
self
}
/// The message to emit on a right button release.
#[must_use]
pub fn on_right_release(mut self, message: Message) -> Self {
self.on_right_release = Some(message);
self
}
/// The message to emit on a middle button press.
#[must_use]
pub fn on_middle_press(mut self, message: Message) -> Self {
self.on_middle_press = Some(message);
self
}
/// The message to emit on a middle button release.
#[must_use]
pub fn on_middle_release(mut self, message: Message) -> Self {
self.on_middle_release = Some(message);
self
}
/// The message to emit when scroll wheel is used
#[must_use]
pub fn on_scroll(mut self, on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a) -> Self {
self.on_scroll = Some(Box::new(on_scroll));
self
}
/// The message to emit when the mouse enters the area.
#[must_use]
pub fn on_enter(mut self, message: Message) -> Self {
self.on_enter = Some(message);
self
}
/// The message to emit when the mouse moves in the area.
#[must_use]
pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
self.on_move = Some(Box::new(on_move));
self
}
/// The message to emit when the mouse exits the area.
#[must_use]
pub fn on_exit(mut self, message: Message) -> Self {
self.on_exit = Some(message);
self
}
/// The [`mouse::Interaction`] to use when hovering the area.
#[must_use]
pub fn interaction(mut self, interaction: mouse::Interaction) -> Self {
self.interaction = Some(interaction);
self
}
}
/// Local state of the [`HardMouseArea`].
#[derive(Default)]
struct State {
is_hovered: bool,
bounds: Rectangle,
cursor_position: Option<Point>,
previous_click: Option<mouse::Click>,
}
impl<'a, Message, Theme, Renderer> HardMouseArea<'a, Message, Theme, Renderer> {
/// Creates a [`HardMouseArea`] with the given content.
pub fn new(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
HardMouseArea {
content: content.into(),
on_press: None,
on_release: None,
on_double_click: None,
on_right_press: None,
on_right_release: None,
on_middle_press: None,
on_middle_release: None,
on_scroll: None,
on_enter: None,
on_move: None,
on_exit: None,
interaction: None,
}
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for HardMouseArea<'_, Message, Theme, Renderer>
where
Renderer: renderer::Renderer,
Message: Clone,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if self.on_move.is_none()
&& let Event::Mouse(mouse::Event::CursorMoved { .. }) = event
{
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
if self.on_enter.is_none()
&& let Event::Mouse(mouse::Event::CursorEntered { .. }) = event
{
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
if self.on_exit.is_none()
&& let Event::Mouse(mouse::Event::CursorLeft { .. }) = event
{
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
if shell.is_event_captured() {
return;
}
update(self, tree, event, layout, cursor, shell);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let content_interaction = self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
);
match (self.interaction, content_interaction) {
(Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => {
interaction
}
_ => content_interaction,
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
renderer_style,
layout,
cursor,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<HardMouseArea<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
Theme: 'a,
Renderer: 'a + renderer::Renderer,
{
fn from(
area: HardMouseArea<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(area)
}
}
/// Processes the given [`Event`] and updates the [`State`] of an [`HardMouseArea`]
/// accordingly.
fn update<Message: Clone, Theme, Renderer>(
widget: &mut HardMouseArea<'_, Message, Theme, Renderer>,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) {
let state: &mut State = tree.state.downcast_mut();
let cursor_position = cursor.position();
let bounds = layout.bounds();
if state.cursor_position != cursor_position || state.bounds != bounds {
let was_hovered = state.is_hovered;
state.is_hovered = cursor.is_over(layout.bounds());
state.cursor_position = cursor_position;
state.bounds = bounds;
match (
widget.on_enter.as_ref(),
widget.on_move.as_ref(),
widget.on_exit.as_ref(),
) {
(Some(on_enter), _, _) if state.is_hovered && !was_hovered => {
shell.publish(on_enter.clone());
}
(_, Some(on_move), _) if state.is_hovered => {
if let Some(position) = cursor.position_in(layout.bounds()) {
shell.publish(on_move(position));
}
}
(_, _, Some(on_exit)) if !state.is_hovered && was_hovered => {
shell.publish(on_exit.clone());
}
_ => {}
}
}
if !cursor.is_over(layout.bounds()) {
return;
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(message) = widget.on_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
if let Some(position) = cursor_position
&& let Some(message) = widget.on_double_click.as_ref()
{
let new_click =
mouse::Click::new(position, mouse::Button::Left, state.previous_click);
if new_click.kind() == mouse::click::Kind::Double {
shell.publish(message.clone());
}
state.previous_click = Some(new_click);
// Even if this is not a double click, but the press is nevertheless
// processed by us and should not be popup to parent widgets.
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(message) = widget.on_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
if let Some(message) = widget.on_right_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => {
if let Some(message) = widget.on_right_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
if let Some(message) = widget.on_middle_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => {
if let Some(message) = widget.on_middle_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
if let Some(on_scroll) = widget.on_scroll.as_ref() {
shell.publish(on_scroll(*delta));
shell.capture_event();
}
}
_ => {}
}
}
+1
View File
@@ -0,0 +1 @@
pub mod hard_mouse_area;