Author SHA1 Message Date
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: https://gitea.micialware.ru/micialware/jap_learn/pulls/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: https://gitea.micialware.ru/micialware/jap_learn/pulls/15
2026-09-09 12:47:42 +03:00
micialware 2069bdf5b3 Fix already updated cache change 2026-09-09 12:47:22 +03:00
micialware 28730c0fda Delete saved card states 2026-09-09 12:35:00 +03:00
micialware 7715aaff3b Fixing tag selection 2026-09-09 12:12:44 +03:00
micialware a20c72eb9d Optimize groups query 2026-09-09 11:23:51 +03:00
micialware 58bdaac7fe Remove word display widget base 2026-09-09 11:13:50 +03:00
micialware 4a71f31e6f Fix import 2026-09-09 10:53:03 +03:00
micialware 6e3f58d21c Version update 2026-09-09 10:43:26 +03:00
micialware 3663765284 Db saving cases 2026-09-09 10:33:21 +03:00
micialware 294f0e8b98 Query time and database optimization 2026-09-09 10:19:44 +03:00
micialware 2e04450c76 Working repetitions 2026-09-09 10:08:52 +03:00
micialware 242b56fc21 Fixes for sqlite data layer 2026-09-08 00:09:13 +03:00
micialware 7f9aa11d6e More fixes for dictionary 2026-09-07 18:19:54 +03:00
micialware 92b019b072 Temporal dictionary display fix 2026-09-07 18:00:50 +03:00
micialware 9c93ca4408 Merge remote-tracking branch 'origin/word-display' into repo-integration 2026-09-07 17:17:18 +03:00
micialware e2f367f3aa WordDisplay base 2026-09-06 10:13:33 +03:00
44 changed files with 1719 additions and 2204 deletions

No files matched your search

Generated
+60 -631
View File
File diff suppressed because it is too large. Load diff
+4 -12
View File
@@ -4,20 +4,20 @@ version = "0.1.3"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
iced = { version = "0.14.0", features = ["tokio", "svg", "lazy"]} iced = { version = "0.14.0", features = ["tokio", "svg", "lazy", "crisp", "tiny-skia", "wayland", "wgpu"], default-features = false}
iced_core = "0.14.0" iced_core = "0.14.0"
rand = "0.10.2" rand = "0.10.2"
dirs = "6.0.0" dirs = "7.0.0"
serde_json = "1.0.151" serde_json = "1.0.151"
rhai = { version = "1.26.0", features = ["sync"] } rhai = { version = "1.26.0", features = ["sync"] }
chrono = "0.4.45" chrono = "0.4.45"
rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] } rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] }
reqwest = { version = "0.13.4", features = ["json", "stream", "multipart"] } reqwest = { version = "0.13.5", features = ["json", "stream", "multipart"] }
rodio = "0.22.2" rodio = "0.22.2"
sha2 = "0.11.0" sha2 = "0.11.0"
tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros"] }
hex = "0.4.3" hex = "0.4.3"
zstd = "0.13.3" zstd = "0.14.0"
zip = "8.6.0" zip = "8.6.0"
rfd = "0.17.2" rfd = "0.17.2"
mimalloc = "0.1.52" mimalloc = "0.1.52"
@@ -40,11 +40,3 @@ panic = "abort"
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] } criterion = { version = "0.8.2", features = ["html_reports"] }
rustc-hash = "2.1.3" rustc-hash = "2.1.3"
[[bench]]
name = "map_bench"
harness = false
[[bench]]
name = "repo_cache_bench"
harness = false
-126
View File
@@ -1,126 +0,0 @@
use criterion::{Criterion, criterion_group, criterion_main};
use std::collections::HashMap as StdHashMap;
use std::hint::black_box;
use hashbrown::HashMap as BrownHashMap;
const SIZES: [usize; 3] = [5, 10, 50];
const QUERY_COUNT: usize = 4096;
/// Детерминированный ключ ~17 символов, например "key_9e3779b9_0042".
fn make_key(i: u64) -> String {
let h = i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
format!("key_{:08x}_{:04}", h & 0xFFFF_FFFF, i)
}
// Для длинных ключей (пути/URL) замени на:
// fn make_key(i: u64) -> String {
// format!(
// "/api/v2/users/{i}/settings/visibility_{:08x}",
// i.wrapping_mul(0x9E37_79B9)
// )
// }
fn make_vec(n: usize) -> Vec<(String, u64)> {
(0..n as u64).map(|i| (make_key(i), i)).collect()
}
fn make_std_map(data: &[(String, u64)]) -> StdHashMap<String, u64> {
data.iter().map(|(k, v)| (k.clone(), *v)).collect()
}
fn make_brown_map(data: &[(String, u64)]) -> BrownHashMap<String, u64> {
data.iter().map(|(k, v)| (k.clone(), *v)).collect()
}
fn make_queries(n: usize, with_misses: bool) -> Vec<String> {
let keys: Vec<String> = (0..n as u64).map(make_key).collect();
let missing = "__missing_key__".to_string();
(0..QUERY_COUNT)
.map(|i| {
if with_misses && i % 16 == 15 {
missing.clone()
} else {
let idx = i.wrapping_mul(2_654_435_761) % n;
keys[idx].clone()
}
})
.collect()
}
fn lookup_std(map: &StdHashMap<String, u64>, queries: &[String]) -> u64 {
let mut sum = 0u64;
for q in queries {
match map.get(black_box(q.as_str())) {
Some(v) => sum = sum.wrapping_add(*v),
None => sum = sum.wrapping_add(1),
}
}
black_box(sum)
}
fn lookup_brown(map: &BrownHashMap<String, u64>, queries: &[String]) -> u64 {
let mut sum = 0u64;
for q in queries {
match map.get(black_box(q.as_str())) {
Some(v) => sum = sum.wrapping_add(*v),
None => sum = sum.wrapping_add(1),
}
}
black_box(sum)
}
fn lookup_vec(data: &[(String, u64)], queries: &[String]) -> u64 {
let mut sum = 0u64;
for q in queries {
let q = black_box(q);
match data.iter().find(|(k, _)| k == q) {
Some((_, v)) => sum = sum.wrapping_add(*v),
None => sum = sum.wrapping_add(1),
}
}
black_box(sum)
}
fn bench(c: &mut Criterion) {
let mut group = c.benchmark_group("map_vs_vec_str");
for n in SIZES {
let vec_data = make_vec(n);
let std_map = make_std_map(&vec_data);
let brown_map = make_brown_map(&vec_data);
let queries_hit = make_queries(n, false);
let queries_mixed = make_queries(n, true);
// std::collections::HashMap (SipHash)
group.bench_function(format!("std_hashmap_hit/{n}"), |b| {
b.iter(|| lookup_std(black_box(&std_map), black_box(&queries_hit)))
});
group.bench_function(format!("std_hashmap_mixed/{n}"), |b| {
b.iter(|| lookup_std(black_box(&std_map), black_box(&queries_mixed)))
});
// hashbrown (SwissTable + foldhash)
group.bench_function(format!("hashbrown_hit/{n}"), |b| {
b.iter(|| lookup_brown(black_box(&brown_map), black_box(&queries_hit)))
});
group.bench_function(format!("hashbrown_mixed/{n}"), |b| {
b.iter(|| lookup_brown(black_box(&brown_map), black_box(&queries_mixed)))
});
// Vec<(String, u64)>, линейный поиск
group.bench_function(format!("vec_linear_hit/{n}"), |b| {
b.iter(|| lookup_vec(black_box(&vec_data), black_box(&queries_hit)))
});
group.bench_function(format!("vec_linear_mixed/{n}"), |b| {
b.iter(|| lookup_vec(black_box(&vec_data), black_box(&queries_mixed)))
});
}
group.finish();
}
criterion_group!(benches, bench);
criterion_main!(benches);
-325
View File
@@ -1,325 +0,0 @@
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use hashbrown::HashMap as HashbrownMap;
use rustc_hash::FxHashMap;
use std::collections::{BTreeMap, HashMap};
use std::hint::black_box;
// Тестовая структура (аналог вашей сущности)
#[derive(Debug, Clone)]
struct Entity {
id: i64,
data: [u8; 64], // 64 байта данных для реалистичности
}
impl Entity {
fn new(id: i64) -> Self {
Self {
id,
data: [id as u8; 64],
}
}
}
// ========== ТЕСТОВЫЕ ФУНКЦИИ ==========
fn bench_vec_insert_sorted(vec: &mut Vec<Entity>, entities: &[Entity]) {
vec.clear();
for entity in entities {
vec.push(entity.clone());
}
// Vec уже отсортирован, так как ID монотонно возрастают
}
fn bench_vec_search_separated<'a>(ids: &[i64], vec: &'a [Entity], id: i64) -> Option<&'a Entity> {
match ids.binary_search(&id) {
Ok(index) => Some(&vec[index]),
Err(_) => None,
}
}
fn bench_vec_search(vec: &Vec<Entity>, id: i64) -> Option<&Entity> {
match vec.binary_search_by(|probe| probe.id.cmp(&id)) {
Ok(index) => Some(&vec[index]),
Err(_) => None,
}
}
fn bench_btreemap_insert(map: &mut BTreeMap<i64, Entity>, entities: &[Entity]) {
map.clear();
for entity in entities {
map.insert(entity.id, entity.clone());
}
}
fn bench_btreemap_search(map: &BTreeMap<i64, Entity>, id: i64) -> Option<&Entity> {
map.get(&id)
}
fn bench_hashmap_insert(map: &mut HashMap<i64, Entity>, entities: &[Entity]) {
map.clear();
for entity in entities {
map.insert(entity.id, entity.clone());
}
}
fn bench_hashmap_search(map: &HashMap<i64, Entity>, id: i64) -> Option<&Entity> {
map.get(&id)
}
fn bench_hashbrown_insert(map: &mut HashbrownMap<i64, Entity>, entities: &[Entity]) {
map.clear();
for entity in entities {
map.insert(entity.id, entity.clone());
}
}
fn bench_hashbrown_search(map: &HashbrownMap<i64, Entity>, id: i64) -> Option<&Entity> {
map.get(&id)
}
fn bench_fxhashmap_insert(map: &mut FxHashMap<i64, Entity>, entities: &[Entity]) {
map.clear();
for entity in entities {
map.insert(entity.id, entity.clone());
}
}
fn bench_fxhashmap_search(map: &FxHashMap<i64, Entity>, id: i64) -> Option<&Entity> {
map.get(&id)
}
// ========== ГЕНЕРАЦИЯ ДАННЫХ ==========
fn generate_entities(count: usize) -> Vec<Entity> {
(0..count).map(|i| Entity::new(i as i64)).collect()
}
fn generate_search_ids(count: usize, max_id: i64) -> Vec<i64> {
// 70% попаданий, 30% промахов (реалистичный сценарий)
let mut ids = Vec::with_capacity(count);
for i in 0..count {
if i % 10 < 7 {
// Попадание
ids.push((i as i64 * 13) % max_id);
} else {
// Промах (несуществующий ID)
ids.push(max_id + i as i64);
}
}
ids
}
// ========== БЕНЧМАРКИ ==========
fn bench_insert(c: &mut Criterion) {
let mut group = c.benchmark_group("insert");
for size in [10_000, 100_000, 500_000] {
let entities = generate_entities(size);
group.bench_with_input(
BenchmarkId::new("Vec (sorted)", size),
&entities,
|b, entities| {
let mut vec = Vec::with_capacity(size);
b.iter(|| {
bench_vec_insert_sorted(&mut vec, black_box(entities));
});
},
);
group.bench_with_input(
BenchmarkId::new("BTreeMap", size),
&entities,
|b, entities| {
let mut map = BTreeMap::new();
b.iter(|| {
bench_btreemap_insert(&mut map, black_box(entities));
});
},
);
group.bench_with_input(
BenchmarkId::new("HashMap (std)", size),
&entities,
|b, entities| {
let mut map = HashMap::new();
b.iter(|| {
bench_hashmap_insert(&mut map, black_box(entities));
});
},
);
group.bench_with_input(
BenchmarkId::new("HashMap (hashbrown)", size),
&entities,
|b, entities| {
let mut map = HashbrownMap::new();
b.iter(|| {
bench_hashbrown_insert(&mut map, black_box(entities));
});
},
);
group.bench_with_input(
BenchmarkId::new("FxHashMap", size),
&entities,
|b, entities| {
let mut map = FxHashMap::default();
b.iter(|| {
bench_fxhashmap_insert(&mut map, black_box(entities));
});
},
);
}
group.finish();
}
fn bench_search(c: &mut Criterion) {
let mut group = c.benchmark_group("search");
for size in [10_000, 100_000, 500_000] {
let entities = generate_entities(size);
let entity_ids = entities.iter().map(|entity| entity.id).collect::<Vec<_>>();
let search_ids = generate_search_ids(1000, size as i64);
// Подготовка коллекций
let mut vec = Vec::with_capacity(size);
bench_vec_insert_sorted(&mut vec, &entities);
let mut btreemap = BTreeMap::new();
bench_btreemap_insert(&mut btreemap, &entities);
let mut hashmap = HashMap::new();
bench_hashmap_insert(&mut hashmap, &entities);
let mut hashbrown = HashbrownMap::new();
bench_hashbrown_insert(&mut hashbrown, &entities);
let mut fxhashmap = FxHashMap::default();
bench_fxhashmap_insert(&mut fxhashmap, &entities);
group.bench_with_input(
BenchmarkId::new("Vec (binary search)", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_vec_search_separated(&entity_ids, &vec, black_box(id)));
}
});
},
);
group.bench_with_input(
BenchmarkId::new("Vec (binary search) separated", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_vec_search(&vec, black_box(id)));
}
});
},
);
group.bench_with_input(BenchmarkId::new("BTreeMap", size), &search_ids, |b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_btreemap_search(&btreemap, black_box(id)));
}
});
});
group.bench_with_input(
BenchmarkId::new("HashMap (std)", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_hashmap_search(&hashmap, black_box(id)));
}
});
},
);
group.bench_with_input(
BenchmarkId::new("HashMap (hashbrown)", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_hashbrown_search(&hashbrown, black_box(id)));
}
});
},
);
group.bench_with_input(
BenchmarkId::new("FxHashMap", size),
&search_ids,
|b, ids| {
b.iter(|| {
for &id in ids {
black_box(bench_fxhashmap_search(&fxhashmap, black_box(id)));
}
});
},
);
}
group.finish();
}
fn bench_memory_efficiency(c: &mut Criterion) {
let mut group = c.benchmark_group("memory_efficiency");
let size = 100_000;
let entities = generate_entities(size);
group.bench_function("Vec memory", |b| {
b.iter(|| {
let mut vec = Vec::with_capacity(size);
bench_vec_insert_sorted(&mut vec, black_box(&entities));
black_box(vec);
});
});
group.bench_function("BTreeMap memory", |b| {
b.iter(|| {
let mut map = BTreeMap::new();
bench_btreemap_insert(&mut map, black_box(&entities));
black_box(map);
});
});
group.bench_function("HashMap (std) memory", |b| {
b.iter(|| {
let mut map = HashMap::new();
bench_hashmap_insert(&mut map, black_box(&entities));
black_box(map);
});
});
group.bench_function("HashMap (hashbrown) memory", |b| {
b.iter(|| {
let mut map = HashbrownMap::new();
bench_hashbrown_insert(&mut map, black_box(&entities));
black_box(map);
});
});
group.bench_function("FxHashMap memory", |b| {
b.iter(|| {
let mut map = FxHashMap::default();
bench_fxhashmap_insert(&mut map, black_box(&entities));
black_box(map);
});
});
group.finish();
}
criterion_group!(benches, bench_insert, bench_search, bench_memory_efficiency);
criterion_main!(benches);
+5 -3
View File
@@ -1,4 +1,6 @@
create table custom_fields( create table custom_fields
id INTEGER PRIMARY KEY, (
name TEXT not null id integer primary key,
name text not null,
identifier text not null
); );
+5 -5
View File
@@ -1,9 +1,9 @@
create table decks create table decks
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
name TEXT not null, name text not null,
forward TEXT not null, forward text not null,
backward TEXT not null, backward text not null,
filter TEXT not null filter text not null
); );
+8 -8
View File
@@ -1,12 +1,12 @@
create table history create table history
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
deck_id INTEGER not null references decks on delete cascade, deck_id integer not null references decks on delete cascade,
word_id INTEGER not null references words on delete cascade, word_id integer not null references words on delete cascade,
timestamp INTEGER not null, timestamp integer not null,
answer INTEGER not null, answer integer not null,
before INTEGER not null, before integer not null,
after INTEGER not null, after integer not null,
fsrs INTEGER not null fsrs integer not null
); );
+8 -8
View File
@@ -1,16 +1,16 @@
create table states create table states
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
word_id INTEGER not null word_id integer not null
references words references words
on delete cascade, on delete cascade,
deck_id INTEGER not null deck_id integer not null
references decks references decks
on delete cascade, on delete cascade,
score INTEGER default 1 not null, score integer default 1 not null,
last_opened INTEGER not null, last_opened integer not null,
fsrs_opened INTEGER not null, fsrs_opened integer not null,
fsrs_memory INTEGER not null, fsrs_memory integer not null,
fsrs_open_day INTEGER not null fsrs_open_day integer not null
); );
+2 -2
View File
@@ -1,6 +1,6 @@
create table word_groups create table word_groups
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
name TEXT not null name text not null
); );
+5 -5
View File
@@ -1,11 +1,11 @@
create table words create table words
( (
id INTEGER id integer
primary key autoincrement, primary key autoincrement,
key TEXT not null, key text not null,
value TEXT not null, value text not null,
tags TEXT not null, tags text not null,
more TEXT, more text,
group_id integer default 1 not null group_id integer default 1 not null
constraint words_word_groups_id_fk constraint words_word_groups_id_fk
references word_groups references word_groups
+5
View File
@@ -17,6 +17,7 @@ use log::info;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use rusqlite::Connection; use rusqlite::Connection;
use std::sync::Arc; use std::sync::Arc;
use crate::data_provider::custom_fields::CustomFieldRepository;
pub const USER_FONT: Font = Font::with_name("Noto Sans JP"); pub const USER_FONT: Font = Font::with_name("Noto Sans JP");
@@ -65,6 +66,7 @@ pub struct AppState {
pub decks: DeckRepository, pub decks: DeckRepository,
pub word_groups: WordGroupRepository, pub word_groups: WordGroupRepository,
pub card_states: CardStateRepository, pub card_states: CardStateRepository,
pub custom_fields: CustomFieldRepository,
pub connection: Arc<Mutex<SqliteConnection>>, pub connection: Arc<Mutex<SqliteConnection>>,
pub sync_data: Arc<RwLock<AppSettings>>, pub sync_data: Arc<RwLock<AppSettings>>,
pub history: HistoryRepository, pub history: HistoryRepository,
@@ -85,6 +87,7 @@ impl AppState {
word_groups: WordGroupRepository::new(connection.clone()), word_groups: WordGroupRepository::new(connection.clone()),
card_states: CardStateRepository::new(connection.clone()), card_states: CardStateRepository::new(connection.clone()),
history: HistoryRepository::new(connection.clone()), history: HistoryRepository::new(connection.clone()),
custom_fields: CustomFieldRepository::new(connection.clone()),
sync_data: Arc::new(RwLock::new(AppSettings { sync_data: Arc::new(RwLock::new(AppSettings {
key: None, key: None,
auto_web_fetch: false, auto_web_fetch: false,
@@ -103,6 +106,8 @@ pub fn fill_state(state: &mut AppState) {
state.dictionary.write().start().unwrap(); state.dictionary.write().start().unwrap();
state.decks.write().start().unwrap(); state.decks.write().start().unwrap();
state.history.write().start().unwrap(); state.history.write().start().unwrap();
state.card_states.write().start().unwrap();
state.custom_fields.write().start().unwrap();
state.sync_data = Arc::new(RwLock::new(setting)) state.sync_data = Arc::new(RwLock::new(setting))
} }
+82 -97
View File
@@ -1,23 +1,25 @@
use std::sync::Arc; use std::collections::HashMap;
use std::time::Instant;
use chrono::Utc;
use crate::app::AppState; use crate::app::AppState;
use crate::core::srs::OrderModule::{Fsrs, RandomSRS, SemiRandomSRS, WorstWordsSRS}; use crate::core::srs::OrderModule::{Fsrs, RandomSRS, SemiRandomSRS, WorstWordsSRS};
use crate::core::srs::{ use crate::core::srs::{
OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, FsrsModule, OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule,
WordOpenMode, WorstWordsSRSModule, WordOpenMode, WorstWordsSRSModule,
}; };
use crate::data::entities::*; use crate::data::entities::*;
use crate::data_provider::card_states::{CardStateRepositoryInner}; use crate::data::repositories::Repository;
use crate::notification::{MaybeError, NotificationData}; use crate::data_provider::card_states::CardStateRepositoryInner;
use crate::notification::MaybeError;
use chrono::Utc;
use log::info; use log::info;
use rhai::{Engine, Scope}; use rhai::{Engine, Scope};
use crate::data::repositories::Repository; use std::sync::Arc;
use std::time::Instant;
use crate::data::Id;
#[derive(Clone)] #[derive(Clone)]
pub struct DeckData { pub struct DeckData {
pub(crate) words: Vec<EntityHandle<WordData>>, pub(crate) words: Vec<EntityHandle<WordData>>,
pub(crate) set: Vec<EntityHandle<CardState>>, pub(crate) states: Vec<EntityHandle<CardState>>,
current_word_index: Option<usize>, current_word_index: Option<usize>,
state: AppState, state: AppState,
order_module: OrderModule, order_module: OrderModule,
@@ -25,80 +27,60 @@ pub struct DeckData {
settings: DeckSettings, settings: DeckSettings,
} }
impl DeckData { impl DeckData {
pub fn new( pub fn new(settings: &DeckSettings, open_mode: OrderMode, state: AppState) -> Self {
settings: &DeckSettings, let states_repo = state.card_states.read();
state: AppState, let mut query = states_repo.deck_states(settings.id);
) -> Result<Self, NotificationData> { let words = query
todo!() .iter(&states_repo)
// let state_for = state.clone(); .map(|card_state| EntityHandle::from_id(card_state.word_id))
// .collect();
// let stats = state (settings, &state.connection.read())?; let states = query.handles().collect::<Vec<EntityHandle<CardState>>>();
// let words = stats let fsrs = Fsrs(FsrsModule::new(
// .iter() Self::set_links(states.as_slice(), &states_repo).as_slice(),
// .map(|stat| { ));
// state drop(states_repo);
// .dictionary Self {
// .binary_search_by_key(&stat.word_id, |w| w.id) states,
// .expect("No word found in dictionary") words,
// }) current_word_index: None,
// .map(|index| &state.dictionary[index]) state,
// .cloned() order_module: fsrs,
// .collect(); open_mode,
// settings: settings.clone(),
// Ok(Self { }
// set: stats,
// words,
// current_word_index: None,
// state: state_for,
// order_module: Fsrs(FsrsModule::new()),
// open_mode: settings.open_mode,
// settings: settings.clone(),
// })
} }
pub fn next(&mut self) -> (EntityHandle<WordData>, EntityHandle<CardState>) { pub fn next(&mut self) -> (EntityHandle<WordData>, EntityHandle<CardState>) {
if self.fsrs_finished() {
self.switch_order_engine()
}
let repo = self.state.card_states.read(); let repo = self.state.card_states.read();
let links = Self::set_links(self.set.as_slice(), &repo); let links = Self::set_links(self.states.as_slice(), &repo);
let index = match self.order_module { let index = match self.order_module {
SemiRandomSRS(ref mut module) => { SemiRandomSRS(ref mut module) => module.next(links.as_slice()),
if !module.initialized {
module.init(links.as_slice());
}
module.next(links.as_slice())
}
RandomSRS(ref mut module) => { RandomSRS(ref mut module) => module.next(links.as_slice()),
if !module.initialized { WorstWordsSRS(ref mut module) => module.next(links.as_slice()),
module.init(links.as_slice()) Fsrs(ref mut module) => module.next(links.as_slice()),
}
module.next(links.as_slice())
}
WorstWordsSRS(ref mut module) => {
if !module.initialized {
module.init(links.as_slice())
}
module.next(links.as_slice())
}
Fsrs(ref mut module) => {
if !module.initialized {
module.init(links.as_slice())
}
module.next(links.as_slice())
}
}; };
self.current_word_index = Some(index); self.current_word_index = Some(index);
(self.words[index].clone(), self.set[index].clone()) (self.words[index], self.states[index])
} }
fn set_links<'a>(set: &[EntityHandle<CardState>], repo: &'a CardStateRepositoryInner) -> Vec<&'a CardState> { fn set_links<'a>(
set.iter().map(|h| repo.get_ref_static(*h).unwrap()).collect::<Vec<_>>() set: &[EntityHandle<CardState>],
repo: &'a CardStateRepositoryInner,
) -> Vec<&'a CardState> {
set.iter()
.map(|h| repo.get_ref_static(*h).unwrap())
.collect::<Vec<_>>()
} }
pub fn open(&mut self, status: WordOpenMode) -> MaybeError { pub fn open(&mut self, status: WordOpenMode) -> MaybeError {
if let Some(index) = self.current_word_index { if let Some(index) = self.current_word_index {
let mut repo = self.state.card_states.write(); let mut repo = self.state.card_states.write();
let card_state_handle = &mut self.set[index]; let card_state_handle = &mut self.states[index];
let card_state = repo.get_mut(card_state_handle).unwrap(); let card_state = repo.get_mut(card_state_handle).unwrap();
let old_score = card_state.score; let old_score = card_state.score;
card_state.update(status); card_state.update(status);
@@ -121,16 +103,6 @@ impl DeckData {
} }
} }
if let Fsrs(ref fsrs) = self.order_module
&& fsrs.finished()
{
self.order_module = match self.open_mode {
OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new()),
OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new()),
OrderMode::FullRandom => RandomSRS(RandomSRSModule::new()),
}
}
let word_id = card_state.word_id; let word_id = card_state.word_id;
repo.save(card_state_handle)?; repo.save(card_state_handle)?;
@@ -140,28 +112,39 @@ impl DeckData {
timestamp: Utc::now(), timestamp: Utc::now(),
deck_id: self.settings.id, deck_id: self.settings.id,
word_id, word_id,
answer: status.clone(), answer: status,
before: old_score, before: old_score,
after: new_score, after: new_score,
fsrs, fsrs,
})?; })?;
todo!("Implement history pushing")
// let _ = push_note(
// self.settings.id,
// HistoryNote {
// timestamp: Utc::now(),
// word_id: word.word_id,
// answer: status,
// before: old_score,
// after: new_score,
// },
// );
} }
Ok(()) Ok(())
} }
fn switch_order_engine(&mut self) {
let repo = self.state.card_states.read();
let links = Self::set_links(self.states.as_slice(), &repo);
self.order_module = match self.open_mode {
OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new(links.as_slice())),
OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new(links.as_slice())),
OrderMode::FullRandom => RandomSRS(RandomSRSModule::new(links.as_slice())),
}
}
fn fsrs_finished(&self) -> bool {
if let Fsrs(ref fsrs) = self.order_module
&& fsrs.finished()
{
info!("Fsrs finished. Infinite mode enabled");
return true;
}
false
}
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.set.len() self.states.len()
} }
pub fn list(deck: &DeckSettings, state: &AppState) -> Vec<EntityHandle<WordData>> { pub fn list(deck: &DeckSettings, state: &AppState) -> Vec<EntityHandle<WordData>> {
@@ -173,23 +156,25 @@ impl DeckData {
if let Ok(ast) = ast { if let Ok(ast) = ast {
let groups = state.word_groups.read().iter().cloned().collect::<Vec<_>>(); let groups = state.word_groups.read().iter().cloned().collect::<Vec<_>>();
let dictionary = &state.dictionary.read(); 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 query = dictionary.query_par(Arc::new(move |word| {
let mut more = rhai::Map::new(); let mut more = rhai::Map::new();
for iced in &word.additional { for kv in &word.additional {
more.insert(iced.0.clone().into(), iced.1.clone().into()); more.insert(fields[&kv.0].clone().into(), kv.1.clone().into());
} }
let mut scope = Scope::new(); let mut scope = Scope::new();
let group_name = groups[groups.binary_search_by_key(&word.group_id, |g| g.id).unwrap()].name.clone(); let group_name = groups[groups
.binary_search_by_key(&word.group_id, |g| g.id)
.unwrap()]
.name
.clone();
scope scope
.push_constant("id", word.id.0 as i64) .push_constant("id", word.id.0 as i64)
.push_constant("key", word.key.clone()) .push_constant("key", word.key.clone())
.push_constant("value", word.value.clone()) .push_constant("value", word.value.clone())
.push_constant("tags", word.tags.clone()) .push_constant("tags", word.tags.clone())
.push_constant("more", more) .push_constant("more", more)
.push_constant( .push_constant("group", group_name);
"group",
group_name,
);
let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast); let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast);
if let Ok(result) = result { if let Ok(result) = result {
@@ -199,7 +184,7 @@ impl DeckData {
})); }));
info!("Collecting available words is {:?}", time.elapsed()); info!("Collecting available words is {:?}", time.elapsed());
let handles =query.handles().collect(); let handles = query.handles().collect();
drop(query); drop(query);
handles handles
} else { } else {
+49 -66
View File
@@ -1,6 +1,7 @@
use crate::data::entities::CardState; use crate::data::entities::CardState;
use chrono::{Local, TimeDelta}; use chrono::{Local, TimeDelta};
use fsrs::FSRS; use fsrs::FSRS;
use log::{debug, info};
use rand::distr::weighted::WeightedIndex; use rand::distr::weighted::WeightedIndex;
use rand::prelude::ThreadRng; use rand::prelude::ThreadRng;
use rand::prelude::{Distribution, SliceRandom}; use rand::prelude::{Distribution, SliceRandom};
@@ -31,24 +32,21 @@ pub enum OrderModule {
Fsrs(FsrsModule), Fsrs(FsrsModule),
} }
pub trait SRSModule { pub trait SRSModule {
fn new(set: &[&CardState]) -> Self;
fn next(&mut self, set: &[&CardState]) -> usize; fn next(&mut self, set: &[&CardState]) -> usize;
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &mut CardState); fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &mut CardState);
fn init(&mut self, set: &[&CardState]);
} }
#[derive(Clone)] #[derive(Clone)]
pub struct RandomSRSModule { pub struct RandomSRSModule {
basket: Vec<usize>, basket: Vec<usize>,
pub(crate) initialized: bool,
}
impl RandomSRSModule {
pub(crate) fn new() -> RandomSRSModule {
Self {
basket: vec![],
initialized: false,
}
}
} }
impl SRSModule for RandomSRSModule { impl SRSModule for RandomSRSModule {
fn new(set: &[&CardState]) -> Self {
let mut basket = (0..set.len()).collect::<Vec<usize>>();
basket.shuffle(&mut rand::rng());
Self { basket }
}
fn next(&mut self, set: &[&CardState]) -> usize { fn next(&mut self, set: &[&CardState]) -> usize {
match self.basket.pop() { match self.basket.pop() {
Some(index) => index, Some(index) => index,
@@ -61,29 +59,12 @@ impl SRSModule for RandomSRSModule {
} }
fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {} fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {}
fn init(&mut self, set: &[&CardState]) {
self.initialized = true;
self.basket = (0..set.len()).collect::<Vec<usize>>();
self.basket.shuffle(&mut rand::rng())
}
} }
#[derive(Clone)] #[derive(Clone)]
pub struct SemiRandomSRSModule { pub struct SemiRandomSRSModule {
history: Vec<usize>, history: Vec<usize>,
last_weights: WeightedIndex<f32>, last_weights: WeightedIndex<f32>,
generator: ThreadRng, generator: ThreadRng,
pub(crate) initialized: bool,
}
impl SemiRandomSRSModule {
pub(crate) fn new() -> SemiRandomSRSModule {
SemiRandomSRSModule {
history: vec![],
last_weights: WeightedIndex::new([1.0]).unwrap(),
generator: rng(),
initialized: false,
}
}
} }
impl SRSModule for SemiRandomSRSModule { impl SRSModule for SemiRandomSRSModule {
fn next(&mut self, set: &[&CardState]) -> usize { fn next(&mut self, set: &[&CardState]) -> usize {
@@ -106,13 +87,17 @@ impl SRSModule for SemiRandomSRSModule {
.update_weights(&[(index, &new_weight)]) .update_weights(&[(index, &new_weight)])
.expect("update weights error"); .expect("update weights error");
} }
fn init(&mut self, set: &[&CardState]) { fn new(set: &[&CardState]) -> Self {
self.initialized = true;
let weights = set let weights = set
.iter() .iter()
.map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0) .map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0)
.collect::<Vec<f32>>(); .collect::<Vec<f32>>();
self.last_weights = WeightedIndex::new(weights).expect("Incorrect weights update");
Self {
generator: rng(),
last_weights: WeightedIndex::new(weights).expect("Incorrect weights update"),
history: vec![],
}
} }
} }
impl SemiRandomSRSModule { impl SemiRandomSRSModule {
@@ -122,7 +107,6 @@ impl SemiRandomSRSModule {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct WorstWordsSRSModule { pub struct WorstWordsSRSModule {
pub(crate) initialized: bool,
pool_size: usize, pool_size: usize,
rounds_remaining: u8, rounds_remaining: u8,
pool: Vec<usize>, pool: Vec<usize>,
@@ -148,14 +132,8 @@ impl SRSModule for WorstWordsSRSModule {
fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {} fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {}
fn init(&mut self, _: &[&CardState]) { fn new(_: &[&CardState]) -> Self {
self.initialized = true;
}
}
impl WorstWordsSRSModule {
pub(crate) fn new() -> WorstWordsSRSModule {
WorstWordsSRSModule { WorstWordsSRSModule {
initialized: false,
pool_size: PARTIAL_POOL_SIZE, pool_size: PARTIAL_POOL_SIZE,
rounds_count: 2, rounds_count: 2,
rounds_remaining: 0, rounds_remaining: 0,
@@ -163,6 +141,8 @@ impl WorstWordsSRSModule {
queue: vec![], queue: vec![],
} }
} }
}
impl WorstWordsSRSModule {
fn fill_pool(&mut self, set: &[&CardState]) { fn fill_pool(&mut self, set: &[&CardState]) {
let mut sorted = set.iter().enumerate().collect::<Vec<_>>(); let mut sorted = set.iter().enumerate().collect::<Vec<_>>();
sorted.sort_by_key(|c| c.1.score); sorted.sort_by_key(|c| c.1.score);
@@ -183,10 +163,31 @@ pub struct FsrsModule {
queue: Vec<usize>, queue: Vec<usize>,
rng: ThreadRng, rng: ThreadRng,
fsrs: FSRS, fsrs: FSRS,
pub(crate) initialized: bool,
} }
impl SRSModule for FsrsModule { impl SRSModule for FsrsModule {
fn new(set: &[&CardState]) -> Self {
let today = Local::now().date_naive();
let mut new = Self {
today_set: set
.iter()
.enumerate()
.filter(|(_, c)| c.fsrs_open_day <= today)
.map(|(index, _)| index)
.collect(),
current_group: vec![],
queue: vec![],
solved: vec![],
rng: rng(),
fsrs: FSRS::default(),
};
new.today_set.shuffle(&mut new.rng);
new.fill_partial_pool();
new
}
fn next(&mut self, _: &[&CardState]) -> usize { fn next(&mut self, _: &[&CardState]) -> usize {
if self.current_group.is_empty() { if self.current_group.is_empty() {
self.fill_partial_pool(); self.fill_partial_pool();
@@ -222,45 +223,27 @@ impl SRSModule for FsrsModule {
let offset = ((rng().random::<f32>() - 0.5) * 3.0).round() as i32; let offset = ((rng().random::<f32>() - 0.5) * 3.0).round() as i32;
next_day += TimeDelta::days(offset as i64); next_day += TimeDelta::days(offset as i64);
} }
let first_time = updated_word.fsrs_memory.is_none();
updated_word.fsrs_memory = Some(next.memory); updated_word.fsrs_memory = Some(next.memory);
updated_word.fsrs_open_day = next_day; updated_word.fsrs_open_day = next_day;
updated_word.fsrs_opened = updated_word.last_open.date_naive(); updated_word.fsrs_opened = updated_word.last_open.date_naive();
if updated_word.fsrs_open_day != today { if (updated_word.fsrs_open_day != today && !first_time)
|| (first_time && status == WordOpenMode::Easy)
{
info!("For today word on index {} solved", index);
self.solved.push(index); self.solved.push(index);
let index = self.current_group.iter().position(|x| *x == index).unwrap(); let index = self.current_group.iter().position(|x| *x == index).unwrap();
self.current_group.remove(index); self.current_group.remove(index);
} }
} }
fn init(&mut self, set: &[&CardState]) {
self.rng = rng();
let today = Local::now().date_naive();
self.today_set = set
.iter()
.enumerate()
.filter(|(_, c)| c.fsrs_open_day <= today)
.map(|(index, _)| index)
.collect();
self.today_set.shuffle(&mut self.rng);
self.fill_partial_pool();
self.initialized = true;
}
} }
impl FsrsModule { impl FsrsModule {
pub fn new() -> Self {
Self {
today_set: vec![],
current_group: vec![],
queue: vec![],
solved: vec![],
rng: Default::default(),
fsrs: FSRS::default(),
initialized: false,
}
}
fn fill_partial_pool(&mut self) { fn fill_partial_pool(&mut self) {
debug!("Filling partial pool");
self.current_group.extend( self.current_group.extend(
self.today_set self.today_set
.iter() .iter()
@@ -281,7 +264,7 @@ impl FsrsModule {
} }
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum WordOpenMode { pub enum WordOpenMode {
Easy, Easy,
Ok, Ok,
+21 -13
View File
@@ -1,5 +1,6 @@
use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, WordOpenMode}; use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, WordOpenMode};
use crate::data::{INVALID_ID, Id}; use crate::data::{Id, SmallId, INVALID_ID};
use crate::data_provider::history::HistoryRepositoryInner;
use chrono::{DateTime, Local, NaiveDate, Utc}; use chrono::{DateTime, Local, NaiveDate, Utc};
use fsrs::MemoryState; use fsrs::MemoryState;
use litemap::LiteMap; use litemap::LiteMap;
@@ -10,7 +11,6 @@ use std::hash::{Hash, Hasher};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use crate::data_provider::history::{HistoryRepository, HistoryRepositoryInner};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct WordData { pub struct WordData {
@@ -19,7 +19,7 @@ pub struct WordData {
pub key: String, pub key: String,
pub value: String, pub value: String,
pub tags: String, pub tags: String,
pub additional: LiteMap<String, String>, pub additional: LiteMap<Id, String>,
} }
impl EntityData for WordData { impl EntityData for WordData {
@@ -70,7 +70,7 @@ pub struct HistoryNote {
pub answer: WordOpenMode, pub answer: WordOpenMode,
pub before: u8, pub before: u8,
pub after: u8, pub after: u8,
pub fsrs: bool pub fsrs: bool,
} }
impl EntityData for HistoryNote { impl EntityData for HistoryNote {
@@ -86,17 +86,16 @@ impl EntityData for HistoryNote {
impl HistoryRepositoryInner { impl HistoryRepositoryInner {
pub fn activity_of_deck(&self, id: Id) -> Vec<(NaiveDate, u32)> { pub fn activity_of_deck(&self, id: Id) -> Vec<(NaiveDate, u32)> {
let mut query = self.query(Arc::new(move |note| note.deck_id == id)); let mut query = self.query(Arc::new(move |note| note.deck_id == id));
let values = query.iter(&self).collect::<Vec<_>>(); let values = query.iter(self).collect::<Vec<_>>();
let mut counts = vec![]; let mut counts = vec![];
if !values.is_empty() { if !values.is_empty() {
let mut date = values[0].timestamp.with_timezone(&Local).date_naive(); let mut date = values[0].timestamp.with_timezone(&Local).date_naive();
let mut count = 0; let mut count = 0;
for note in values { for note in values {
let current_date = note.timestamp.with_timezone(&Local).date_naive(); let current_date = note.timestamp.with_timezone(&Local).date_naive();
if date == current_date{ if date == current_date {
count += 1; count += 1;
} } else {
else {
counts.push((date, count)); counts.push((date, count));
date = current_date; date = current_date;
count = 0; count = 0;
@@ -197,9 +196,21 @@ impl CardState {
} }
} }
#[derive(Clone)]
pub struct CustomField { pub struct CustomField {
pub id: Id, pub id: Id,
pub name: String, 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 struct EntityHandle<E> {
@@ -210,13 +221,10 @@ pub struct EntityHandle<E> {
impl<T> Clone for EntityHandle<T> { impl<T> Clone for EntityHandle<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
EntityHandle { *self
id: self.id,
index: self.index,
_phantom: Default::default(),
}
} }
} }
impl<T> Copy for EntityHandle<T> {} impl<T> Copy for EntityHandle<T> {}
impl<T> EntityHandle<T> { impl<T> EntityHandle<T> {
pub fn new(id: Id, index: usize, real: bool) -> Self { pub fn new(id: Id, index: usize, real: bool) -> Self {
+99 -82
View File
@@ -1,8 +1,9 @@
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{CardState, EntityData, EntityHandle}; use crate::data::entities::{EntityData, EntityHandle};
use crate::data_provider::sqlite::SqliteConnection; use crate::data_provider::sqlite::SqliteConnection;
use crate::helpers::into_result; use crate::helpers::into_result;
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
use log::debug;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use rayon::iter::IndexedParallelIterator; use rayon::iter::IndexedParallelIterator;
use rayon::iter::IntoParallelRefIterator; use rayon::iter::IntoParallelRefIterator;
@@ -11,6 +12,7 @@ use rusqlite::Connection;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{Deref, Index, IndexMut}; use std::ops::{Deref, Index, IndexMut};
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant;
pub trait Repository<T, D> pub trait Repository<T, D>
where where
@@ -29,13 +31,19 @@ where
fn create_now(&mut self, new: T) -> Result<EntityHandle<T>, NotificationData>; fn create_now(&mut self, new: T) -> Result<EntityHandle<T>, NotificationData>;
fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError; fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError;
fn delete(&mut self, handle: EntityHandle<T>) -> MaybeError; fn delete(&mut self, handle: EntityHandle<T>) -> MaybeError;
fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>> where F : Fn(&T) -> bool; fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>>
where
F: Fn(&T) -> bool;
fn init(&mut self) -> MaybeError; fn init(&mut self) -> MaybeError;
fn start(&mut self) -> MaybeError; fn start(&mut self) -> MaybeError;
fn reload(&mut self) -> MaybeError; fn reload(&mut self) -> MaybeError;
fn shifts(&self) -> usize; fn shifts(&self) -> usize;
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T> where T: 'a; fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T>
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut T> where T: 'a; where
T: 'a;
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut T>
where
T: 'a;
} }
struct QueryManger<T, R, D> struct QueryManger<T, R, D>
@@ -86,9 +94,7 @@ where
.filter(|(_, q)| { .filter(|(_, q)| {
let query = q.lock(); let query = q.lock();
let rule = &query.rule; let rule = &query.rule;
return rule(data); rule(data)
false
}) })
.map(|(i, _)| i) .map(|(i, _)| i)
.collect() .collect()
@@ -116,7 +122,10 @@ where
} }
impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> { impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Query<T, R, D> {
pub fn iter<'a>(&mut self, data_repository_inner: &'a DataRepositoryInner<T, D>) -> impl Iterator<Item = &'a T> pub fn iter<'a>(
&mut self,
data_repository_inner: &'a DataRepositoryInner<T, D>,
) -> impl Iterator<Item = &'a T>
where where
T: 'a, T: 'a,
{ {
@@ -128,7 +137,10 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
.filter_map(move |e| data_repository_inner.get_ref(e)) .filter_map(move |e| data_repository_inner.get_ref(e))
} }
pub fn iter_with_handles<'a>(&mut self, data_repository_inner: &'a DataRepositoryInner<T, D>) -> impl Iterator<Item = (&'a T, EntityHandle<T>)> pub fn iter_with_handles<'a>(
&mut self,
data_repository_inner: &'a DataRepositoryInner<T, D>,
) -> impl Iterator<Item = (&'a T, EntityHandle<T>)>
where where
T: 'a, T: 'a,
{ {
@@ -137,9 +149,9 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
} }
self.cache self.cache
.iter_mut() .iter_mut()
.filter_map(move |e| Some((data_repository_inner.get_ref(e)?, e.clone()))) .filter_map(move |e| Some((data_repository_inner.get_ref(e)?, *e)))
} }
pub fn for_each_mut<F>(&mut self, repo: &mut DataRepositoryInner<T, D>, mut f: F) pub fn for_each_mut<F>(&mut self, repo: &mut DataRepositoryInner<T, D>, mut f: F)
where where
F: FnMut(&mut T), F: FnMut(&mut T),
@@ -158,6 +170,7 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
where where
T: Send + Sync, T: Send + Sync,
{ {
let time = Instant::now();
self.cache.clear(); self.cache.clear();
if self.parallel { if self.parallel {
self.cache = data_repository self.cache = data_repository
@@ -186,18 +199,31 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false)) .map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
debug!(
"Query for {} elements (filtered {}) takes {:?}",
data_repository.len(),
self.cache.len(),
time.elapsed()
);
} }
pub fn handles(&self) -> impl Iterator<Item = EntityHandle<T>> { pub fn handles(&self) -> impl Iterator<Item = EntityHandle<T>> {
self.cache.iter().cloned() self.cache.iter().cloned()
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.cache.len() 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> { impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Index<usize>
for Query<T, R, D>
{
type Output = EntityHandle<T>; type Output = EntityHandle<T>;
fn index(&self, index: usize) -> &Self::Output { fn index(&self, index: usize) -> &Self::Output {
@@ -205,7 +231,9 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> In
} }
} }
impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> IndexMut<usize> for Query<T, R, D> { impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> IndexMut<usize>
for Query<T, R, D>
{
fn index_mut(&mut self, index: usize) -> &mut Self::Output { fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.cache[index] &mut self.cache[index]
} }
@@ -216,7 +244,10 @@ where
D: BatchedDbLayer<T>, D: BatchedDbLayer<T>,
T: EntityData, T: EntityData,
{ {
fn create_now_many(&mut self, entities: &mut [T]) -> Result<Vec<EntityHandle<T>>, NotificationData>; fn create_now_many(
&mut self,
entities: &mut [T],
) -> Result<Vec<EntityHandle<T>>, NotificationData>;
fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError; fn save_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
fn delete_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError; fn delete_many(&mut self, handles: &mut [EntityHandle<T>]) -> MaybeError;
} }
@@ -252,10 +283,10 @@ pub struct DataRepositoryInner<T: Clone + EntityData, D: DbLayer<T>> {
shifts: usize, shifts: usize,
} }
impl<T: Clone + EntityData, D: DbLayer<T> + Default> DataRepository<T, D> { impl<T: Clone + EntityData, D: DbLayer<T> + Default> DataRepository<T, D> {
pub fn new(connection: Arc<Mutex<SqliteConnection>>) -> DataRepository<T, D> { pub fn new(connection: Arc<Mutex<SqliteConnection>>) -> DataRepository<T, D> {
Self{ Self {
inner: Arc::new(RwLock::new(DataRepositoryInner{ inner: Arc::new(RwLock::new(DataRepositoryInner {
cache: vec![], cache: vec![],
temporal: vec![], temporal: vec![],
next_temporal: 0, next_temporal: 0,
@@ -269,7 +300,6 @@ impl<T: Clone + EntityData, D: DbLayer<T> + Default> DataRepository<T, D> {
} }
} }
impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> { impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
type Target = Arc<RwLock<DataRepositoryInner<T, D>>>; type Target = Arc<RwLock<DataRepositoryInner<T, D>>>;
@@ -278,7 +308,9 @@ impl<T: Clone + EntityData, D: DbLayer<T>> Deref for DataRepository<T, D> {
} }
} }
impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, D> { impl<T: EntityData + Clone + std::marker::Send + std::marker::Sync, D: DbLayer<T>>
DataRepositoryInner<T, D>
{
pub fn query_par( pub fn query_par(
&self, &self,
rule: Arc<dyn Fn(&T) -> bool + Send + Sync>, rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
@@ -286,67 +318,41 @@ impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, D> {
where where
T: Send + Sync, T: Send + Sync,
{ {
let mut cache: Vec<EntityHandle<T>> = self let mut query = Query {
.cache cache: vec![],
.par_iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, true))
.collect();
cache.extend(
self.temporal
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(),
);
Query {
cache,
rule, rule,
parallel: true, parallel: true,
_marker: PhantomData, _marker: PhantomData,
shift_stamp: self.shifts, shift_stamp: self.shifts,
} };
query.load(self);
query
} }
pub fn query( pub fn query(
&self, &self,
rule: Arc<dyn Fn(& T) -> bool + Send + Sync>, rule: Arc<dyn Fn(&T) -> bool + Send + Sync>,
) -> Query<T, DataRepositoryInner<T, D>, D> ) -> Query<T, DataRepositoryInner<T, D>, D>
where where
T: Send + Sync, T: Send + Sync,
{ {
let mut cache: Vec<EntityHandle<T>> = self let mut query = Query {
.cache cache: vec![],
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, true))
.collect();
cache.extend(
self.temporal
.iter()
.enumerate()
.filter(|(_, entity)| rule(entity))
.map(|(i, entity)| EntityHandle::new(entity.id(), i, false))
.collect::<Vec<_>>(),
);
Query {
cache,
rule, rule,
parallel: false, parallel: false,
_marker: PhantomData, _marker: PhantomData,
shift_stamp: self.shifts, shift_stamp: self.shifts,
} };
query.load(self);
query
} }
pub fn empty_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> { pub fn empty_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query{ Query {
cache: vec![], cache: vec![],
rule: Arc::new(|_| false), rule: Arc::new(|_| false),
parallel: false, parallel: false,
@@ -356,7 +362,7 @@ impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, D> {
} }
pub fn empty_query_par(&self) -> Query<T, DataRepositoryInner<T, D>, D> { pub fn empty_query_par(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
Query{ Query {
cache: vec![], cache: vec![],
rule: Arc::new(|_| false), rule: Arc::new(|_| false),
parallel: true, parallel: true,
@@ -365,6 +371,10 @@ impl<T: EntityData + Clone, D: DbLayer<T>> DataRepositoryInner<T, D> {
} }
} }
pub fn full_query(&self) -> Query<T, DataRepositoryInner<T, D>, D> {
self.query(Arc::new(|_| true))
}
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.cache.len() + self.temporal.len() self.cache.len() + self.temporal.len()
} }
@@ -450,7 +460,7 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
} }
fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError { fn save(&mut self, entity: &mut EntityHandle<T>) -> MaybeError {
let original_state = entity.clone(); let original_state = *entity;
if entity.real() { if entity.real() {
let data = self.get_ref(entity).unwrap(); let data = self.get_ref(entity).unwrap();
self.data_layer self.data_layer
@@ -458,12 +468,11 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
} else { } else {
let index = fast_position_of(self.temporal.as_slice(), entity).unwrap(); let index = fast_position_of(self.temporal.as_slice(), entity).unwrap();
let data = &mut self.temporal[index]; let data = &mut self.temporal[index];
self.data_layer self.data_layer.add_entity(data, &self.connection.lock())?;
.add_entity(data, &self.connection.lock())?;
entity.set_real(true); entity.set_real(true);
entity.id = data.id(); entity.id = data.id();
entity.set_index(self.cache.len()); entity.set_index(self.cache.len());
self.query_manger.handle_save(original_state, entity, &data); self.query_manger.handle_save(original_state, entity, data);
let data = self.temporal.remove(index); let data = self.temporal.remove(index);
self.cache.push(data); self.cache.push(data);
@@ -494,16 +503,16 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>> fn find<F>(&self, predicate: F) -> Option<EntityHandle<T>>
where where
F: Fn(&T) -> bool F: Fn(&T) -> bool,
{ {
if let Some(index) = self.cache.iter().position(|entity| predicate(entity)){ if let Some(index) = self.cache.iter().position(&predicate) {
let value = &self.cache[index]; let value = &self.cache[index];
return Some(EntityHandle::new(value.id(), index, true)) return Some(EntityHandle::new(value.id(), index, true));
} }
if let Some(index) = self.temporal.iter().position(|entity| predicate(entity)){ if let Some(index) = self.temporal.iter().position(predicate) {
let value = &self.temporal[index]; let value = &self.temporal[index];
return Some(EntityHandle::new(value.id(), index, false)) return Some(EntityHandle::new(value.id(), index, false));
} }
None None
} }
@@ -535,16 +544,16 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
self.shifts self.shifts
} }
fn iter<'a>(&'a self) -> impl Iterator<Item=&'a T> fn iter<'a>(&'a self) -> impl Iterator<Item = &'a T>
where where
T: 'a T: 'a,
{ {
self.cache.iter().chain(self.temporal.iter()) self.cache.iter().chain(self.temporal.iter())
} }
fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item=&'a mut T> fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut T>
where where
T: 'a T: 'a,
{ {
self.cache.iter_mut().chain(self.temporal.iter_mut()) self.cache.iter_mut().chain(self.temporal.iter_mut())
} }
@@ -552,10 +561,18 @@ impl<T: EntityData + Clone, D: DbLayer<T>> Repository<T, D> for DataRepositoryIn
impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository<T, D> impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository<T, D>
for DataRepositoryInner<T, D> for DataRepositoryInner<T, D>
{ {
fn create_now_many(&mut self, entities: &mut [T]) -> Result<Vec<EntityHandle<T>>, NotificationData> { fn create_now_many(
self.data_layer.add_many_entities(entities, &mut self.connection.lock())?; &mut self,
entities: &mut [T],
) -> Result<Vec<EntityHandle<T>>, NotificationData> {
self.data_layer
.add_many_entities(entities, &mut self.connection.lock())?;
let len = self.cache.len(); let len = self.cache.len();
let handles = entities.iter().enumerate().map(|(index, entity)| {EntityHandle::new(entity.id(), index + len, true)}).collect(); let handles = entities
.iter()
.enumerate()
.map(|(index, entity)| EntityHandle::new(entity.id(), index + len, true))
.collect();
self.cache.extend(entities.to_vec()); self.cache.extend(entities.to_vec());
Ok(handles) Ok(handles)
} }
@@ -594,7 +611,7 @@ impl<T: EntityData + Clone, D: BatchedDbLayer<T> + DbLayer<T>> BatchedRepository
let start = self.cache.len(); let start = self.cache.len();
for (i, (handle, data)) in temp.into_iter().zip(&mut temp_values).enumerate() { for (i, (handle, data)) in temp.into_iter().zip(&mut temp_values).enumerate() {
let old = handle.clone(); let old = *handle;
handle.id = data.id(); handle.id = data.id();
handle.set_index(start + i); handle.set_index(start + i);
handle.set_real(true); handle.set_real(true);
@@ -658,7 +675,7 @@ where
{ {
let mut temp = Vec::with_capacity(handles.len() / 2); let mut temp = Vec::with_capacity(handles.len() / 2);
let mut cached = Vec::with_capacity(handles.len() / 2); let mut cached = Vec::with_capacity(handles.len() / 2);
handles.sort_by(|a, b| a.id.cmp(&b.id)); handles.sort_by_key(|a| a.id);
handles.iter().for_each(|handle| { handles.iter().for_each(|handle| {
if handle.real() { if handle.real() {
cached.push(getter(handle)) cached.push(getter(handle))
+24 -110
View File
@@ -1,16 +1,15 @@
use crate::data::entities::{CardState, DeckSettings}; use crate::data::Id;
use crate::data::entities::CardState;
use crate::data::repositories::{DataRepository, DataRepositoryInner, Query};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError};
use crate::notification::{MaybeError, NotificationData};
use chrono::NaiveDate; use chrono::NaiveDate;
use fsrs::MemoryState; use fsrs::MemoryState;
use log::info; use rusqlite::{Error, Params, Row};
use rusqlite::{Connection, Error, Params, Row, params}; use std::sync::Arc;
use std::time::Instant;
use crate::data::repositories::{DataRepository, DataRepositoryInner};
pub type CardStateRepository = DataRepository<CardState, SqliteDbLayer<CardState, CardStatesQuerySet>>; pub type CardStateRepository = DataRepository<CardState, CardStateDbLayer>;
pub type CardStateRepositoryInner = DataRepositoryInner<CardState, SqliteDbLayer<CardState, CardStatesQuerySet>>; pub type CardStateRepositoryInner = DataRepositoryInner<CardState, CardStateDbLayer>;
pub type CardStateDbLayer = SqliteDbLayer<CardState, CardStatesQuerySet>;
#[derive(Clone)] #[derive(Clone)]
pub struct CardStatesQuerySet; pub struct CardStatesQuerySet;
impl TableAccessQuerySet<CardState> for CardStatesQuerySet { impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
@@ -19,11 +18,11 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
} }
fn save() -> &'static str { fn save() -> &'static str {
"UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3" "UPDATE states SET score = ?1, last_opened = ?2, fsrs_opened = ?3, fsrs_memory = ?4, fsrs_open_day = ?5 WHERE id = ?6"
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO states (word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4) RETURNING id" "INSERT INTO states (word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
} }
fn name() -> &'static str { fn name() -> &'static str {
@@ -58,38 +57,16 @@ impl TableAccessQuerySet<CardState> for CardStatesQuerySet {
} }
fn update_params(entity: &CardState) -> impl Params { fn update_params(entity: &CardState) -> impl Params {
(entity.score, entity.last_open.timestamp(), entity.id) (
entity.score,
entity.last_open.timestamp(),
entity.fsrs_opened.to_epoch_days(),
memory_to_i64(&entity.fsrs_memory),
entity.fsrs_open_day.to_epoch_days(),
entity.id,
)
} }
} }
pub fn load_stats_of_deck(
set: &DeckSettings,
connection: &Connection,
) -> Result<Vec<CardState>, NotificationData> {
let mut stmt = connection
.prepare("SELECT id, word_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states WHERE set_id = ?1")
.expect("Failed to prepare state query");
let iter = stmt
.query_map((set.id,), |row| {
Ok(CardState {
id: row.get(0)?,
word_id: row.get(1)?,
deck_id: set.id,
score: row.get(2)?,
fsrs_opened: NaiveDate::from_epoch_days(row.get::<usize, i32>(4)?)
.expect("Incorrect date encoding"),
fsrs_memory: get_memory(row.get(5)?),
last_open: row.get(3)?,
fsrs_open_day: NaiveDate::from_epoch_days(row.get::<usize, i32>(6)?)
.expect("Incorrect date encoding"),
})
})
.map_err(|_| DbLoadError)?;
let mut buffer = Vec::with_capacity(512);
iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i));
Ok(buffer)
}
fn get_memory(value: i64) -> Option<MemoryState> { fn get_memory(value: i64) -> Option<MemoryState> {
if value == 0 { if value == 0 {
@@ -109,74 +86,11 @@ fn memory_to_i64(value: &Option<MemoryState>) -> i64 {
value.stability.to_bits() as i64 | ((value.difficulty.to_bits() as i64) << 32) value.stability.to_bits() as i64 | ((value.difficulty.to_bits() as i64) << 32)
} }
pub fn add_stat_list(stats: &mut [CardState], connection: &mut Connection) -> MaybeError { impl CardStateRepositoryInner {
let time = Instant::now(); pub fn deck_states(
let tx = connection &self,
.transaction() deck_id: Id,
.expect("Failed to create transaction"); ) -> Query<CardState, CardStateRepositoryInner, CardStateDbLayer> {
let count = stats.len(); self.query(Arc::new(move |state| state.deck_id == deck_id))
{
let mut stmt = tx
.prepare(
"INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)",
)
.expect("Failed to prepare state query");
for stat in stats.iter() {
stmt.execute(params![
stat.word_id,
stat.deck_id,
stat.score,
stat.last_open.timestamp(),
stat.fsrs_opened.to_epoch_days(),
memory_to_i64(&stat.fsrs_memory),
stat.fsrs_open_day.to_epoch_days()
])
.map_err(|_| DbSaveError)?;
}
} }
let last_index: u32 = tx.last_insert_rowid() as u32;
tx.commit().expect("Failed to commit");
let start_index = last_index - (count as u32) + 1;
for (index, id) in (start_index..=last_index).enumerate() {
stats[index].id = id.into();
}
info!("Added {} cards for {:?}", stats.len(), time.elapsed());
Ok(())
}
pub fn update_stat_score(stat: &CardState, connection: &Connection) -> MaybeError {
let time = Instant::now();
connection
.execute(
"UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3",
(&stat.score, &stat.last_open.timestamp(), &stat.id),
)
.map_err(|_| DbSaveError)?;
info!("Updated stat: {}", time.elapsed().as_millis());
Ok(())
}
pub fn delete_stats(stat: &[CardState], connection: &Connection) -> MaybeError {
if stat.is_empty() {
return Ok(());
}
let ids = stat
.iter()
.map(|s| s.id.into())
.collect::<Vec<String>>()
.join(", ");
connection
.execute(
format!("DELETE FROM states WHERE id in ({ids})").as_str(),
(),
)
.map_err(|_| DbSaveError)?;
Ok(())
} }
+17 -14
View File
@@ -1,40 +1,43 @@
use crate::data::entities::{CustomField, WordGroup}; use crate::data::entities::CustomField;
use crate::data::repositories::DataRepository; use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use rusqlite::{Error, Params, Row}; use rusqlite::{Error, Params, Row};
pub type CustomFieldRepository = DataRepository<CustomField, SqliteDbLayer<CustomField, CustomFieldsQuerySet>>; pub type CustomFieldRepository = DataRepository<CustomField, CustomFieldDbLayer>;
pub type CustomFieldRepositoryInner = DataRepositoryInner<CustomField, CustomFieldDbLayer>;
pub type CustomFieldDbLayer = SqliteDbLayer<CustomField, CustomFieldsQuerySet>;
#[derive(Clone)]
pub struct CustomFieldsQuerySet; pub struct CustomFieldsQuerySet;
impl TableAccessQuerySet<WordGroup> for CustomFieldsQuerySet { impl TableAccessQuerySet<CustomField> for CustomFieldsQuerySet {
fn load() -> &'static str { fn load() -> &'static str {
"SELECT id, name FROM custom_fields" "SELECT id, name, identifier FROM custom_fields"
} }
fn save() -> &'static str { fn save() -> &'static str {
"UPDATE custom_fields SET name = ?1 WHERE id = ?2" "UPDATE custom_fields SET name = ?1, identifier = ?2 WHERE id = ?3"
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1) RETURNING id" "INSERT INTO custom_fields (name, identifier) VALUES (?1, ?2)"
} }
fn name() -> &'static str { fn name() -> &'static str {
"custom_fields" "custom_fields"
} }
fn from_sql(row: &Row) -> Result<WordGroup, Error> { fn from_sql(row: &Row) -> Result<CustomField, Error> {
Ok(WordGroup { Ok(CustomField {
id: row.get(0)?, id: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
identifier: row.get(2)?,
}) })
} }
fn insert_params(entity: &WordGroup) -> impl Params { fn insert_params(entity: &CustomField) -> impl Params {
(&entity.name,) (&entity.name, &entity.identifier)
} }
fn update_params(entity: &WordGroup) -> impl Params { fn update_params(entity: &CustomField) -> impl Params {
(&entity.name, entity.id) (&entity.name, &entity.identifier, entity.id)
} }
} }
+2 -2
View File
@@ -17,7 +17,7 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id" "INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4)"
} }
fn name() -> &'static str { fn name() -> &'static str {
@@ -52,4 +52,4 @@ impl TableAccessQuerySet<DeckSettings> for DeckQuerySet {
entity.id, entity.id,
) )
} }
} }
+9 -6
View File
@@ -21,13 +21,16 @@ impl TableAccessQuerySet<HistoryNote> for HistoryQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO history (deck_id, state_id, timestamp, answer, before, after, fsrs) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) RETURNING rowid" "INSERT INTO history (deck_id, word_id, timestamp, answer, before, after, fsrs) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
} }
fn name() -> &'static str { fn name() -> &'static str {
"history" "history"
} }
fn id_field() -> &'static str {
"rowid"
}
fn from_sql(row: &Row) -> Result<HistoryNote, Error> { fn from_sql(row: &Row) -> Result<HistoryNote, Error> {
Ok(HistoryNote { Ok(HistoryNote {
id: row.get(0)?, id: row.get(0)?,
@@ -45,7 +48,7 @@ impl TableAccessQuerySet<HistoryNote> for HistoryQuerySet {
( (
entity.deck_id, entity.deck_id,
entity.word_id, entity.word_id,
entity.timestamp, entity.timestamp.timestamp(),
entity.answer, entity.answer,
entity.before, entity.before,
entity.after, entity.after,
@@ -57,7 +60,7 @@ impl TableAccessQuerySet<HistoryNote> for HistoryQuerySet {
( (
entity.deck_id, entity.deck_id,
entity.word_id, entity.word_id,
entity.timestamp, entity.timestamp.timestamp(),
entity.answer, entity.answer,
entity.before, entity.before,
entity.after, entity.after,
@@ -74,7 +77,7 @@ impl FromSql for WordOpenMode {
2 => Ok(WordOpenMode::Ok), 2 => Ok(WordOpenMode::Ok),
3 => Ok(WordOpenMode::Easy), 3 => Ok(WordOpenMode::Easy),
_ => Ok(WordOpenMode::None), _ => Ok(WordOpenMode::None),
} };
} }
Ok(WordOpenMode::None) Ok(WordOpenMode::None)
} }
@@ -86,7 +89,7 @@ impl ToSql for WordOpenMode {
WordOpenMode::Easy => 3, WordOpenMode::Easy => 3,
WordOpenMode::Ok => 2, WordOpenMode::Ok => 2,
WordOpenMode::Hard => 1, WordOpenMode::Hard => 1,
WordOpenMode::None => 0 WordOpenMode::None => 0,
})) }))
} }
} }
+19 -5
View File
@@ -1,10 +1,11 @@
use crate::helpers::into_result; use crate::helpers::into_result;
use crate::notification::NotificationData; use crate::notification::NotificationData;
use crate::notification::NotificationData::IncorrectAnkiFile; use crate::notification::NotificationData::IncorrectAnkiFile;
use hashbrown::HashMap;
use log::info; use log::info;
use rusqlite::Connection; use rusqlite::Connection;
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap;
use crate::pages::import::AnkiSchema;
#[derive(Clone)] #[derive(Clone)]
pub struct ImportData(pub(crate) Vec<ImportGroup>); pub struct ImportData(pub(crate) Vec<ImportGroup>);
@@ -15,7 +16,7 @@ pub struct ImportGroup {
pub name: String, pub name: String,
pub fields: Vec<String>, pub fields: Vec<String>,
pub length: u64, pub length: u64,
pub mapping: HashMap<String, String>, pub mapping: BTreeMap<String, String>,
pub imported: bool, pub imported: bool,
} }
@@ -25,7 +26,18 @@ pub struct ImportNote {
pub fields: Vec<String>, 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 let json_models: String = connection
.query_one("select models from col", (), |row| row.get(0)) .query_one("select models from col", (), |row| row.get(0))
.map_err(|_| IncorrectAnkiFile)?; .map_err(|_| IncorrectAnkiFile)?;
@@ -45,7 +57,7 @@ pub fn load_groups(connection: &Connection) -> Result<ImportData, NotificationDa
.collect::<Vec<(i64, i64)>>(); .collect::<Vec<(i64, i64)>>();
for (_, collection) in sets { for (_, collection) in sets {
let fields_property = collection.get("fields"); let fields_property = collection.get("flds");
if let Some(fields_property) = fields_property { if let Some(fields_property) = fields_property {
let fields = into_result(fields_property.as_array(), IncorrectAnkiFile)? let fields = into_result(fields_property.as_array(), IncorrectAnkiFile)?
.iter() .iter()
@@ -82,7 +94,9 @@ pub fn load_groups(connection: &Connection) -> Result<ImportData, NotificationDa
} }
Err(IncorrectAnkiFile) Err(IncorrectAnkiFile)
} }
fn load_new_schema(connection: &Connection) -> Result<ImportData, NotificationData> {
return Err(IncorrectAnkiFile);
}
pub fn get_words_of_group( pub fn get_words_of_group(
connection: &Connection, connection: &Connection,
group_id: u64, group_id: u64,
+36 -14
View File
@@ -1,8 +1,8 @@
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{EntityData}; use crate::data::entities::EntityData;
use crate::data::repositories::{BatchedDbLayer, DbLayer}; use crate::data::repositories::{BatchedDbLayer, DbLayer};
use crate::notification::MaybeError; use crate::notification::MaybeError;
use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::NotificationData::DbSaveError;
use log::{debug, error}; use log::{debug, error};
use rusqlite::{Connection, Error, Params, Row}; use rusqlite::{Connection, Error, Params, Row};
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -25,6 +25,9 @@ pub trait TableAccessQuerySet<T> {
fn save() -> &'static str; fn save() -> &'static str;
fn add() -> &'static str; fn add() -> &'static str;
fn name() -> &'static str; fn name() -> &'static str;
fn id_field() -> &'static str {
"id"
}
fn from_sql(row: &Row) -> Result<T, Error>; fn from_sql(row: &Row) -> Result<T, Error>;
fn insert_params(entity: &T) -> impl Params; fn insert_params(entity: &T) -> impl Params;
fn update_params(entity: &T) -> impl Params; fn update_params(entity: &T) -> impl Params;
@@ -38,9 +41,9 @@ where
_phantom: PhantomData<(T, Q)>, _phantom: PhantomData<(T, Q)>,
} }
impl<T, Q: TableAccessQuerySet<T>> Default for SqliteDbLayer<T, Q>{ impl<T, Q: TableAccessQuerySet<T>> Default for SqliteDbLayer<T, Q> {
fn default() -> Self { fn default() -> Self {
Self{ Self {
_phantom: PhantomData, _phantom: PhantomData,
} }
} }
@@ -52,7 +55,10 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
connection connection
.execute(Q::save(), Q::update_params(entity)) .execute(Q::save(), Q::update_params(entity))
.map_err(|e| {error!("{}", e); DbSaveError})?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
debug!("Updated in {} for {:?}", Q::name(), time.elapsed()); debug!("Updated in {} for {:?}", Q::name(), time.elapsed());
Ok(()) Ok(())
} }
@@ -61,8 +67,15 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
let time = Instant::now(); let time = Instant::now();
let index: u32 = connection let index: u32 = connection
.query_row(Q::add(), Q::insert_params(entity), |row| row.get(0)) .query_row(
.map_err(|e| {error!("{}", e); DbSaveError})?; 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()); entity.set_id(index.into());
debug!("Added into {} for {:?}", Q::name(), time.elapsed()); debug!("Added into {} for {:?}", Q::name(), time.elapsed());
@@ -80,7 +93,10 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
format!("DELETE FROM {} WHERE id = ?1", Q::name()).as_str(), format!("DELETE FROM {} WHERE id = ?1", Q::name()).as_str(),
(&id,), (&id,),
) )
.map_err(|e| {error!("{}", e); DbSaveError})?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
debug!("Deleted from {} for {:?}", Q::name(), time.elapsed()); debug!("Deleted from {} for {:?}", Q::name(), time.elapsed());
Ok(()) Ok(())
} }
@@ -91,9 +107,10 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> DbLayer<T> for SqliteDbLayer<T, Q
let mut stmt = connection let mut stmt = connection
.prepare(Q::load()) .prepare(Q::load())
.expect("Failed to prepare state query"); .expect("Failed to prepare state query");
let iter = stmt let iter = stmt.query_map([], |row| Q::from_sql(row)).map_err(|e| {
.query_map([], |row| Ok(Q::from_sql(row)?)) error!("{}", e);
.map_err(|e| {error!("{}", e); DbSaveError})?; DbSaveError
})?;
iter.filter_map(|res| res.ok()) iter.filter_map(|res| res.ok())
.for_each(|i| storage.push(i)); .for_each(|i| storage.push(i));
@@ -123,7 +140,10 @@ impl<T: EntityData, Q: TableAccessQuerySet<T>> BatchedDbLayer<T> for SqliteDbLay
format!("DELETE FROM {} WHERE id in ({})", Q::name(), stringed).as_str(), format!("DELETE FROM {} WHERE id in ({})", Q::name(), stringed).as_str(),
(), (),
) )
.map_err(|_| DbSaveError)?; .map_err(|e| {
error!("{}", e);
DbSaveError
})?;
} }
debug!( debug!(
"Deleted {} from {} for {:?}", "Deleted {} from {} for {:?}",
@@ -145,8 +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"); let mut stmt = tx.prepare(Q::add()).expect("Failed to prepare state query");
for entity in entities.iter() { for entity in entities.iter() {
stmt.execute(Q::insert_params(entity)) stmt.execute(Q::insert_params(entity)).map_err(|e| {
.map_err(|_| DbSaveError)?; error!("{}", e);
DbSaveError
})?;
} }
} }
+21
View File
@@ -1,4 +1,5 @@
use crate::fs::*; use crate::fs::*;
use log::error;
use rusqlite::Connection; use rusqlite::Connection;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
@@ -63,6 +64,7 @@ fn make_history(conn: &Connection) -> Result<(), rusqlite::Error> {
fn make_custom_fields(conn: &Connection) -> Result<(), rusqlite::Error> { fn make_custom_fields(conn: &Connection) -> Result<(), rusqlite::Error> {
let query = include_str!("../../sql/custom_fields.sql"); let query = include_str!("../../sql/custom_fields.sql");
conn.execute(query, ())?; conn.execute(query, ())?;
conn.execute("insert into custom_fields (name, identifier) values (\"Чтение\", \"reading\"), (\"Описание\", \"description\"), (\"Пример использования\", \"context\")", ())?;
Ok(()) Ok(())
} }
@@ -108,9 +110,28 @@ impl Default for SqliteConnection {
impl SqliteConnection { impl SqliteConnection {
pub fn finish(&mut self) { pub fn finish(&mut self) {
let connection = self.0.take().unwrap();
connection
.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
});
connection
.execute("PRAGMA optimize", [])
.unwrap_or_else(|e| {
error!("Ошибка при оптимизации БД: {e}");
0
});
self.0 = None; self.0 = None;
} }
pub fn restart(&mut self) { pub fn restart(&mut self) {
self.0 = Some(default_connection()); self.0 = Some(default_connection());
} }
} }
impl Drop for SqliteConnection {
fn drop(&mut self) {
self.finish();
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ impl TableAccessQuerySet<WordGroup> for WordGroupQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO word_groups (name) VALUES (?1) RETURNING id" "INSERT INTO word_groups (name) VALUES (?1)"
} }
fn name() -> &'static str { fn name() -> &'static str {
+34 -167
View File
@@ -1,9 +1,9 @@
use crate::data::entities::{WordData, WordGroup}; use crate::data::entities::WordData;
use crate::data::repositories::{DataRepository, DataRepositoryInner}; use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data::Id;
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use litemap::LiteMap;
use crate::notification::{MaybeError, NotificationData}; use rusqlite::{Error, Params, Row};
use rusqlite::{Connection, Error, Params, Row, params};
pub type WordRepository = DataRepository<WordData, WordDataLayer>; pub type WordRepository = DataRepository<WordData, WordDataLayer>;
pub type WordRepositoryInner = DataRepositoryInner<WordData, WordDataLayer>; pub type WordRepositoryInner = DataRepositoryInner<WordData, WordDataLayer>;
@@ -20,7 +20,7 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
} }
fn add() -> &'static str { fn add() -> &'static str {
"INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id" "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5)"
} }
fn name() -> &'static str { fn name() -> &'static str {
@@ -34,7 +34,7 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
key: row.get(1)?, key: row.get(1)?,
value: row.get(2)?, value: row.get(2)?,
tags: row.get(3)?, tags: row.get(3)?,
additional: serde_json::from_str(&additional).unwrap_or_default(), additional: additional_from_string(additional),
group_id: row.get(5)?, group_id: row.get(5)?,
}) })
} }
@@ -44,7 +44,7 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
&entity.key, &entity.key,
&entity.value, &entity.value,
&entity.tags, &entity.tags,
serde_json::to_string(&entity.additional).unwrap_or_default(), additional_to_string(&entity.additional),
entity.group_id, entity.group_id,
) )
} }
@@ -54,175 +54,42 @@ impl TableAccessQuerySet<WordData> for WordQuerySet {
&entity.key, &entity.key,
&entity.value, &entity.value,
&entity.tags, &entity.tags,
serde_json::to_string(&entity.additional).unwrap_or_default(), additional_to_string(&entity.additional),
entity.id, entity.id,
) )
} }
} }
pub fn add_word(word: &mut WordData, connection: &Connection) -> MaybeError { fn additional_to_string(map: &LiteMap<Id, String>) -> String {
let index : u32 = connection let mut parts = Vec::with_capacity(map.len() * 2);
.query_row( for (id, value) in map {
"INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id", parts.push(id.to_string());
( parts.push(value.clone());
&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)?;
}
} }
parts.join("")
}
let last_index: u32 = tx.last_insert_rowid() as u32; fn additional_from_string_temp(data: String) -> LiteMap<Id, String> {
tx.commit().map_err(|_| DbSaveError)?;
let start_index = last_index - (count as u32) + 1; let pre_map : LiteMap<String, String> = serde_json::from_str(data.as_str()).unwrap();
LiteMap::from(pre_map.iter().map(|(k, v)| (match k.as_str() {
"reading" => Id(1),
"description" => Id(2),
"context" => Id(3),
&_ => Id(0) }, v.to_string())).collect())
}
for (index, id) in (start_index..=last_index).enumerate() { fn additional_from_string(data: String) -> LiteMap<Id, String> {
words[index].id = id.into(); if data.is_empty() {
return LiteMap::new()
} }
let parts = data.split('').collect::<Vec<&str>>();
Ok(()) let chunks = parts.chunks(2).collect::<Vec<&[&str]>>();
} let mut map = LiteMap::with_capacity(parts.len());
for part in &chunks {
pub fn update_word(word: &mut WordData, connection: &Connection) -> MaybeError { let id : Id = part[0].parse().unwrap();
if !word.id.is_valid() { let value: String = part[1].to_string();
Ok(add_word(word, connection)?) map.insert(id, value);
} 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(())
} }
} map
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(())
} }
-8
View File
@@ -28,14 +28,6 @@ pub fn db_meta_path() -> PathBuf {
app_data_dir().join("data.meta") app_data_dir().join("data.meta")
} }
pub fn history_dir() -> Result<PathBuf, NotificationData> {
let directory = app_data_dir().join("history");
if !directory.exists() {
fs::create_dir_all(directory.clone()).map_err(|_| PermissionDenied)?;
}
Ok(directory)
}
pub fn voice_dir() -> Result<PathBuf, NotificationData> { pub fn voice_dir() -> Result<PathBuf, NotificationData> {
let directory = app_data_dir().join("voice"); let directory = app_data_dir().join("voice");
if !directory.exists() { if !directory.exists() {
+1
View File
@@ -10,6 +10,7 @@ pub mod notification;
mod pages; mod pages;
mod styling; mod styling;
pub mod tests; pub mod tests;
pub mod widgets;
use crate::app::start; use crate::app::start;
use mimalloc::MiMalloc; use mimalloc::MiMalloc;
+3 -8
View File
@@ -1,5 +1,5 @@
use crate::app::{fill_state, AppState}; use crate::app::{AppState, fill_state};
use crate::data_provider::sqlite::{create_db}; use crate::data_provider::sqlite::create_db;
use crate::data_provider::web_api::{ use crate::data_provider::web_api::{
get_local_version, get_web_version, load_data, set_local_version, get_local_version, get_web_version, load_data, set_local_version,
}; };
@@ -21,7 +21,6 @@ use crate::pages::repetition_settings::{RepetitionSettingsMessage, RepetitionSet
use crate::pages::repetitions::{RepetitionsMessage, RepetitionsState}; use crate::pages::repetitions::{RepetitionsMessage, RepetitionsState};
use crate::pages::selector::{SelectorMessage, SelectorState}; use crate::pages::selector::{SelectorMessage, SelectorState};
use crate::pages::sync::{SyncMessage, SyncState}; use crate::pages::sync::{SyncMessage, SyncState};
use crate::pages::word::{WordMessage, WordState};
use crate::pages::writing::{WritingMessage, WritingState}; use crate::pages::writing::{WritingMessage, WritingState};
use crate::state_navigate_handle; use crate::state_navigate_handle;
use crate::state_update; use crate::state_update;
@@ -56,7 +55,6 @@ pub enum RootMessage {
Randomizer(RandomizerMessage), Randomizer(RandomizerMessage),
Repetitions(RepetitionsMessage), Repetitions(RepetitionsMessage),
Repetition(RepetitionMessage), Repetition(RepetitionMessage),
Word(WordMessage),
Sync(SyncMessage), Sync(SyncMessage),
History(HistoryMessage), History(HistoryMessage),
RepetitionSettings(RepetitionSettingsMessage), RepetitionSettings(RepetitionSettingsMessage),
@@ -78,7 +76,6 @@ pub enum Page {
Randomizer(RandomizerState), Randomizer(RandomizerState),
Repetitions(RepetitionsState), Repetitions(RepetitionsState),
Repetition(RepetitionState), Repetition(RepetitionState),
Word(WordState),
Sync(SyncState), Sync(SyncState),
History(HistoryState), History(HistoryState),
RepetitionSettings(RepetitionSettingsState), RepetitionSettings(RepetitionSettingsState),
@@ -182,7 +179,6 @@ impl ScreenState {
Randomizer, Randomizer,
Repetitions, Repetitions,
Repetition, Repetition,
Word,
Sync, Sync,
History, History,
RepetitionSettings, RepetitionSettings,
@@ -206,7 +202,6 @@ impl ScreenState {
Randomizer, Randomizer,
Repetitions, Repetitions,
Repetition, Repetition,
Word,
Sync, Sync,
History, History,
RepetitionSettings, RepetitionSettings,
@@ -227,7 +222,6 @@ impl ScreenState {
Randomizer, Randomizer,
Repetitions, Repetitions,
Repetition, Repetition,
Word,
Sync, Sync,
History, History,
RepetitionSettings, RepetitionSettings,
@@ -244,6 +238,7 @@ impl ScreenState {
if let Some(page) = self.stack.last_mut() { if let Some(page) = self.stack.last_mut() {
return match page { return match page {
Repetition(page) => page.press(&message), Repetition(page) => page.press(&message),
Dictionary(page) => page.press(&message),
_ => Task::none(), _ => Task::none(),
}; };
} }
+406 -173
View File
@@ -1,34 +1,43 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::Id;
use crate::data::entities::{EntityHandle, WordData, WordGroup}; use crate::data::entities::{EntityHandle, WordData, WordGroup};
use crate::data::repositories::{Query, Repository}; use crate::data::repositories::{Query, Repository};
use crate::data::Id;
use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner};
use crate::data_provider::words::WordDataLayer; use crate::data_provider::words::WordDataLayer;
use crate::data_provider::words::WordRepositoryInner; use crate::data_provider::words::WordRepositoryInner;
use crate::navigation::Page::{Import, Word}; use crate::navigation::Page::Import;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
use crate::pages::dictionary::BasicWordFields::*;
use crate::pages::dictionary::DictionaryMessage::*; use crate::pages::dictionary::DictionaryMessage::*;
use crate::pages::dictionary::SelectionChangeDirection::*;
use crate::pages::dictionary_quiz::DictionaryQuizState; use crate::pages::dictionary_quiz::DictionaryQuizState;
use crate::pages::import::ImportState; use crate::pages::import::ImportState;
use crate::pages::word::WordState; use crate::pages::word::{WordEditor, WordMessage};
use crate::push_error;
use crate::push_error_task; use crate::push_error_task;
use crate::styling::*; use crate::styling::*;
use crate::widgets::hard_mouse_area::hard_mouse_area;
use crate::{jl_button, push_error};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use iced::alignment::Vertical::Center; use iced::alignment::Vertical::Center;
use iced::widget::button::Style; use iced::widget::button::Style;
use iced::widget::button::{danger, text}; use iced::widget::button::{danger, text};
use iced::widget::operation::focus;
use iced::widget::space::horizontal; use iced::widget::space::horizontal;
use iced::widget::text_input::default; use iced::widget::text_input::default;
use iced::widget::text_input::Status::Active;
use iced::widget::*; use iced::widget::*;
use iced::{Border, Color, Shadow, Task}; use iced::{Border, Color, Shadow, Task};
use iced_core::Length::Fill; use iced_core::keyboard;
use iced_core::keyboard::key::Physical::Code;
use iced_core::keyboard::{Event, Modifiers};
use iced_core::keyboard::Key::Named;
use iced_core::Length::{Fill, FillPortion};
use log::info; use log::info;
use rand::random_range; use rand::random_range;
use std::ops::Add; use std::ops::Add;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner};
pub struct DictionaryState { pub struct DictionaryState {
state: AppState, state: AppState,
@@ -43,15 +52,15 @@ pub struct DictionaryState {
auto_save_queue: HashMap<EntityHandle<WordData>, DateTime<Utc>>, auto_save_queue: HashMap<EntityHandle<WordData>, DateTime<Utc>>,
displayed_words_query: Query<WordData, WordRepositoryInner, WordDataLayer>, displayed_words_query: Query<WordData, WordRepositoryInner, WordDataLayer>,
groups_query: Query<WordGroup, WordGroupRepositoryInner, WordGroupDataLayer>, groups_query: Query<WordGroup, WordGroupRepositoryInner, WordGroupDataLayer>,
selected_cell: Option<(EntityHandle<WordData>, BasicWordFields, usize)>,
word_editor: Option<WordEditor<DictionaryMessage>>,
} }
#[derive(Clone)] #[derive(Clone)]
pub enum DictionaryMessage { pub enum DictionaryMessage {
Back, Back,
SetTags(EntityHandle<WordData>, String), ExtendedEdit(EntityHandle<WordData>),
SetKey(EntityHandle<WordData>, String), DeleteWord(EntityHandle<WordData>),
SetValue(EntityHandle<WordData>, String),
WordAction(EntityHandle<WordData>),
NewWord, NewWord,
Include(EntityHandle<WordData>, bool), Include(EntityHandle<WordData>, bool),
IncludeTag(String, bool), IncludeTag(String, bool),
@@ -68,8 +77,30 @@ pub enum DictionaryMessage {
ChangeDirection, ChangeDirection,
TrySave(EntityHandle<WordData>), TrySave(EntityHandle<WordData>),
GotoImport, GotoImport,
SelectField(EntityHandle<WordData>, BasicWordFields, usize),
EditField(String),
ChangeSelectedField(SelectionChangeDirection),
EditorAction(WordMessage),
ExtendedWordEdited
} }
#[derive(Clone, PartialEq, Eq, Hash, Copy)]
pub enum SelectionChangeDirection {
Next,
Previous,
Forward,
Backward,
}
#[derive(Clone, PartialEq, Eq, Hash, Copy)]
pub enum BasicWordFields {
Key,
Value,
Tags,
}
const FIELDS: &[BasicWordFields] = &[Key, Value, Tags];
impl NavigatedPage<DictionaryMessage> for DictionaryState { impl NavigatedPage<DictionaryMessage> for DictionaryState {
fn navigate(&mut self, message: &DictionaryMessage) -> Option<Page> { fn navigate(&mut self, message: &DictionaryMessage) -> Option<Page> {
if let Back = message { if let Back = message {
@@ -81,7 +112,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let mut words = vec![]; let mut words = vec![];
let dict = &self.state.dictionary.read(); let dict = &self.state.dictionary.read();
let mut query = dict.query(Arc::new(|_| true)); let query = dict.query(Arc::new(|_| true));
let handles = query.handles(); let handles = query.handles();
words = handles.filter(|h| self.included.contains(h)).collect(); words = handles.filter(|h| self.included.contains(h)).collect();
@@ -89,14 +120,10 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
words, words,
self.reverse, self.reverse,
self.no_typing, self.no_typing,
self.state.clone() self.state.clone(),
))); )));
} }
if let WordAction(handle) = message
&& handle.real()
{
return Some(Word(WordState::new(*handle, self.state.clone())));
}
if let GotoImport = message { if let GotoImport = message {
return Some(Import(ImportState::new(self.state.clone()))); return Some(Import(ImportState::new(self.state.clone())));
} }
@@ -106,6 +133,9 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
fn navigated(&mut self) { fn navigated(&mut self) {
self.included = Default::default(); self.included = Default::default();
self.tag_map = Default::default(); self.tag_map = Default::default();
let groups = self.state.word_groups.read();
self.groups_query.load(&groups);
drop(groups);
self.update_tags(); self.update_tags();
} }
@@ -116,47 +146,12 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let mut word = WordData::new(); let mut word = WordData::new();
word.group_id = self.selected_group.id; word.group_id = self.selected_group.id;
dictionary.create(word); dictionary.create(word);
self.displayed_words_query.load(&dictionary);
} }
ExtendedEdit(i) => {
SetKey(handle, v) => { self.word_editor = Some(self.editor_for_word(i));
let mut dictionary = self.state.dictionary.write(); if let Some((handle, cell, index)) = self.selected_cell.clone() {
dictionary.get_mut_static(handle).unwrap().key = v; self.selected_cell = Some((i, cell, index))
drop(dictionary);
return self.launch_auto_save_offset(handle);
}
SetValue(handle, v) => {
let mut dictionary = self.state.dictionary.write();
dictionary.get_mut_static(handle).unwrap().value = v;
drop(dictionary);
return self.launch_auto_save_offset(handle);
}
SetTags(i, mut v) => {
{
let mut dictionary = self.state.dictionary.write();
let word = dictionary.get_mut_static(i).unwrap();
let current_tags_value = word.tags.clone();
if v.ends_with(", ") && v.len() < current_tags_value.len() {
v = v[..v.len() - 2].to_string()
}
while let Some(index) = v.find(",,") {
v.remove(index);
}
word.tags = v;
}
self.update_tags();
return self.launch_auto_save_offset(i);
}
WordAction(i) => {
let mut dictionary = self.state.dictionary.write();
if let Err(err) = dictionary.delete(i) {
return push_error_task!(err);
} }
} }
Include(i, b) => { Include(i, b) => {
@@ -188,6 +183,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
id: 0.into(), id: 0.into(),
name: format!("Группа слов {}", random_range(100..1000)), name: format!("Группа слов {}", random_range(100..1000)),
}); });
self.groups_query.load(&groups);
} }
EditGroup(new) => { EditGroup(new) => {
let mut groups = self.state.word_groups.write(); let mut groups = self.state.word_groups.write();
@@ -198,14 +194,21 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
} }
} }
SaveGroup => { SaveGroup => {
let mut groups = self.state.word_groups.write(); {
let mut groups = self.state.word_groups.write();
if let Err(err) = groups.save(&mut self.selected_group) { if let Err(err) = groups.save(&mut self.selected_group) {
return push_error_task!(err); return push_error_task!(err);
}
self.groups_query.load(&groups);
} }
self.setup_words_query();
} }
SelectGroup(i) => { SelectGroup(i) => {
self.selected_group = i; self.selected_group = i;
self.setup_words_query();
self.update_words_include(i.id) self.update_words_include(i.id)
} }
DeleteGroup => { DeleteGroup => {
@@ -213,9 +216,10 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
return Task::none(); return Task::none();
} }
{ {
let mut groups = self.state.word_groups.write();
let delete_group = self.selected_group; let delete_group = self.selected_group;
self.selected_group = groups.handle_by_id(Id(1)); self.selected_group = EntityHandle::from_id(1.into());
self.setup_words_query();
let mut groups = self.state.word_groups.write();
if let Err(err) = groups.delete(delete_group) { if let Err(err) = groups.delete(delete_group) {
return push_error_task!(err); return push_error_task!(err);
@@ -224,28 +228,147 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
if let Err(err) = self.state.dictionary.write().reload() { if let Err(err) = self.state.dictionary.write().reload() {
return push_error_task!(err); return push_error_task!(err);
} }
self.groups_query.load(&groups);
} }
self.selected_cell = None;
self.word_editor = None;
self.update_tags() self.update_tags()
} }
ChangeDirection => { ChangeDirection => {
self.reverse_list = !self.reverse_list; self.reverse_list = !self.reverse_list;
} }
TrySave(mut handle) => { TrySave(mut handle) => {
let original = handle;
let now = Utc::now(); let now = Utc::now();
if !self.auto_save_queue.contains_key(&handle) { if !self.auto_save_queue.contains_key(&handle) {
return Task::none(); return Task::none();
} }
if self.auto_save_queue[&handle] <= now { if self.auto_save_queue[&handle] <= now {
self.update_tags();
self.auto_save_queue.remove(&handle); self.auto_save_queue.remove(&handle);
let mut dictionary = self.state.dictionary.write(); let mut dictionary = self.state.dictionary.write();
let _ = dictionary.save(&mut handle); let reload_query = !handle.real();
let _ = dictionary.save(&mut handle);
if reload_query {
self.displayed_words_query.load(&dictionary);
if let Some(selected) = self.selected_cell && selected.0 == original {
self.selected_cell = Some((handle, selected.1, selected.2));
if self.word_editor.is_some() {
self.word_editor = Some(self.editor_for_word(handle));
}
}
}
info!("Saved word");
return Task::none(); return Task::none();
} }
} }
GotoImport => {} GotoImport => {}
SelectField(e, f, index) => {
info!("Editing word {}", e.id);
if self.word_editor.is_some() && let Some(selected) = &self.selected_cell && selected.0 != e {
self.word_editor = Some(self.editor_for_word(e));
}
self.selected_cell = Some((e, f, index));
return focus(iced::widget::Id::new("editor"));
}
EditField(value) => {
if let Some(selected) = self.selected_cell.as_ref() {
{
let mut dictionary = self.state.dictionary.write();
let word = dictionary.get_mut_static(selected.0).unwrap();
match selected.1 {
Key => {
word.key = value;
}
Value => {
word.value = value;
}
Tags => {
word.tags = value;
}
}
}
return self.launch_auto_save_offset(selected.0);
}
}
ChangeSelectedField(change) => {
return if let Some(selected) = self.selected_cell {
let update_vertical = move |mut dir: bool| {
if self.reverse_list {
dir = !dir;
}
let new_index = if dir {
selected.2.saturating_add(1)
} else {
selected.2.saturating_sub(1)
};
if new_index < self.displayed_words_query.len() {
return SelectField(
self.displayed_words_query.get(new_index).unwrap(),
selected.1,
new_index,
);
}
SelectField(selected.0, selected.1, selected.2)
};
let update_horizontal = move |dir: bool| {
let current = selected.1;
let delta = if dir { 1 } else { -1 };
let mut index =
FIELDS.iter().position(|&f| f == current).unwrap() as i8 + delta;
if index < 0 {
index = (FIELDS.len() - 1) as i8;
}
if index >= FIELDS.len() as i8 {
index = 0;
}
SelectField(selected.0, FIELDS[index as usize], selected.2)
};
Task::done(RootMessage::Dictionary(match change {
Next => update_vertical(true),
Previous => update_vertical(false),
Forward => update_horizontal(true),
Backward => update_horizontal(false),
}))
} else {
Task::none()
};
}
DeleteWord(i) => {
let mut dictionary = self.state.dictionary.write();
if let Err(err) = dictionary.delete(i) {
return push_error_task!(err);
}
self.displayed_words_query.load(&dictionary);
self.selected_cell = None;
self.word_editor = None;
}
EditorAction(action) => {
let result = self.word_editor.as_mut().unwrap().update(action);
if let Some(result) = result {
match result {
DeleteWord(_) |
ExtendedWordEdited => {
return Task::done(RootMessage::Dictionary(result));
},
_ => {}
}
}
}
ExtendedWordEdited => {
return self.launch_auto_save_offset(self.selected_cell.unwrap().0);
}
} }
Task::none() Task::none()
@@ -257,14 +380,15 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
iced::widget::column![ iced::widget::column![
self.groups_panel(), self.groups_panel(),
row![horizontal().width(8), self.words_list(),], row![horizontal().width(8), self.words_list(),],
self.selection_editor(),
row![ row![
self.add_word_button(), self.add_word_button(),
horizontal().width(Fill), horizontal().width(Fill),
button("Импорт").style(text).on_press(GotoImport), button("Импорт").style(text).on_press(GotoImport),
] ]
] ]
.spacing(5), .spacing(5).width(FillPortion(4)),
self.filters(), self.right_panel(),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.into(), .into(),
@@ -290,6 +414,8 @@ impl DictionaryState {
auto_save_queue: HashMap::new(), auto_save_queue: HashMap::new(),
displayed_words_query: words_query, displayed_words_query: words_query,
groups_query, groups_query,
selected_cell: None,
word_editor: None,
}; };
result.init(); result.init();
@@ -298,117 +424,143 @@ impl DictionaryState {
result result
} }
fn init(&mut self) { fn editor_for_word(&self, handle: EntityHandle<WordData>) -> WordEditor<DictionaryMessage> {
self.displayed_words_query = self.state.dictionary.read().query_par(Arc::new(|word| word.group_id == Id(1))); WordEditor::new(handle, self.state.clone()).on_delete(DeleteWord(handle)).on_edit(ExtendedWordEdited)
} }
fn setup_words_query(&mut self, id: Id) { fn init(&mut self) {
// self.displayed_words_query = self.state.dictionary.read().query_par(Arc::new(|word| word.group_id == Self::get_id(self))); self.displayed_words_query = self
.state
.dictionary
.read()
.query(Arc::new(|word| word.group_id == Id(1)));
}
fn setup_words_query(&mut self) {
let id = self.selected_group.id;
self.displayed_words_query = self
.state
.dictionary
.read()
.query(Arc::new(move |word| word.group_id == id));
} }
fn launch_auto_save_offset(&mut self, handle: EntityHandle<WordData>) -> Task<RootMessage> { fn launch_auto_save_offset(&mut self, handle: EntityHandle<WordData>) -> Task<RootMessage> {
let save_time = Utc::now().add(TimeDelta::milliseconds(900)); let save_time = Utc::now().add(TimeDelta::milliseconds(500));
self.auto_save_queue.insert(handle, save_time); self.auto_save_queue.insert(handle, save_time);
let message = RootMessage::Dictionary(TrySave(handle)); let message = RootMessage::Dictionary(TrySave(handle));
Task::perform( Task::perform(
async { tokio::time::sleep(Duration::from_secs(1)).await }, async { tokio::time::sleep(Duration::from_millis(520)).await },
|_| message, |_| message,
) )
} }
fn words_list(&self) -> iced::Element<'_, DictionaryMessage> { fn words_list(&self) -> iced::Element<'_, DictionaryMessage> {
todo!("Make new display system") let time = Instant::now();
// let time = Instant::now(); let mut col = Column::new().width(Fill);
// let mut col = Column::new().width(Fill);
// let query = &self.displayed_words_query;
// let query = &self.displayed_words_query; let dictionary = self.state.dictionary.read();
// let dictionary = self.state.dictionary.read();
// for access_index in 0..query.len() {
// let mut i = access_index;
// for access_index in 0..query.len() { if self.reverse_list {
// let mut i = access_index; i = query.len() - access_index - 1;
// if self.reverse_list { }
// i = query.len() - access_index - 1; let word_handle = &query[i];
// } let word = dictionary.get_ref_static(*word_handle).unwrap();
// let word_handle = &query[i];
// let word = dictionary.get_ref_static(*word_handle).unwrap(); if !self.search.is_empty()
// && !word.key.contains(&self.search)
// if !self.search.is_empty() && !word.value.contains(&self.search)
// && !word.key.contains(&self.search) && !word.tags.contains(&self.search)
// && !word.value.contains(&self.search) {
// && !word.tags.contains(&self.search) continue;
// { }
// continue;
// } let mut selection = None;
//
// let word_line_data = WordLineState { if let Some(cell) = self.selected_cell.as_ref()
// is_included: self.included.contains(word_handle), && cell.0 == *word_handle
// key: word.key.clone().into(), {
// value: word.value.clone().into(), selection = Some(cell.1);
// tags: word.tags.clone().into(), }
// handle: word_handle.clone(),
// }; let word_line_data = WordLineState {
// is_included: self.included.contains(word_handle),
// key: word.key.clone().into(),
// let lazy_line = lazy(word_line_data, |data: &WordLineState| Self::word_line(data)); value: word.value.clone().into(),
// tags: word.tags.clone().into(),
// col = col.push(lazy_line); handle: *word_handle,
// } selection,
// number: i,
// info!("Words rendering time: {:?}", time.elapsed()); };
// scrollable(col).height(Fill).into()
let lazy_line = lazy(word_line_data, Self::word_line);
col = col.push(lazy_line);
}
info!("Words rendering time: {:?}", time.elapsed());
scrollable(col).height(Fill).into()
} }
pub fn word_line(data: &WordLineState) -> Row<'_, DictionaryMessage> fn word_line<'a>(data: &WordLineState) -> Row<'a, DictionaryMessage> {
{
let mut line = Row::new().width(Fill).align_y(Center); let mut line = Row::new().width(Fill).align_y(Center);
let handle = data.handle;
line = line.push( line = line.push(
checkbox(data.is_included) checkbox(data.is_included)
.label("") .label("")
.spacing(15) .spacing(15)
.on_toggle(move |b| Include(data.handle, b)), .on_toggle(move |b| Include(handle, b)),
);
let field = |placeholder: &str, data: &str, selected: bool| {
text_input(placeholder, data)
.size(ACCENT_FONT_SIZE)
.width(Fill)
.style(move |x, _| {
let mut default_style = default(x, Active);
default_style.border.radius = 0.0.into();
if selected {
default_style.border.color = x.extended_palette().secondary.base.color;
default_style.border.width = 3.0_f32;
}
default_style
})
};
line = line.push(
hard_mouse_area(field(
"Слово",
&data.key,
data.selection.is_some() && data.selection.unwrap() == Key,
))
.on_press(SelectField(data.handle, Key, data.number)),
);
line = line.push(
hard_mouse_area(field(
"Перевод",
&data.value,
data.selection.is_some() && data.selection.unwrap() == Value,
))
.on_press(SelectField(data.handle, Value, data.number)),
); );
line = line.push( line = line.push(
text_input("Слово", &data.key) hard_mouse_area(field(
.size(ACCENT_FONT_SIZE) "Теги",
.width(Fill) &data.tags,
.on_input(move |string| SetKey(data.handle, string)) data.selection.is_some() && data.selection.unwrap() == Tags,
.style(|x, status| { ))
let mut default_style = default(x, status); .on_press(SelectField(data.handle, Tags, data.number)),
default_style.border.radius = 0.0.into();
default_style
}),
);
line = line.push(
text_input("Перевод", &data.value)
.size(ACCENT_FONT_SIZE)
.width(Fill)
.on_input(move |string| SetValue(data.handle, string))
.style(|x, status| {
let mut default_style = default(x, status);
default_style.border.radius = 0.0.into();
default_style
}),
);
line = line.push(
text_input("Теги", data.tags.as_ref())
.size(ACCENT_FONT_SIZE)
.width(Fill)
.on_input(move |string| SetTags(data.handle, string))
.style(|x, status| {
let mut default_style = default(x, status);
default_style.border.radius = 0.0.into();
default_style
}),
); );
let line_button = || { let line_button = || {
if !data.handle.real() { if !data.handle.real() {
return button("-") return button("-")
.on_press(WordAction(data.handle)) .on_press(DeleteWord(data.handle))
.style(|_x: &iced::Theme, _status: iced::widget::button::Status| Style { .style(|_x: &Theme, _status: button::Status| Style {
background: None, background: None,
text_color: Color::BLACK, text_color: Color::BLACK,
border: Border::default(), border: Border::default(),
@@ -419,13 +571,21 @@ impl DictionaryState {
} }
button("") button("")
.on_press(WordAction(data.handle)) .on_press(ExtendedEdit(data.handle))
.style(button::primary) .style(button::primary)
.width(15) .width(15)
}; };
line.push(line_button()).push(space().width(10)) line.push(line_button()).push(space().width(10))
} }
fn right_panel(&self) -> iced::Element<'_, DictionaryMessage> {
if let Some(editor) = &self.word_editor {
container(editor.view().map(EditorAction)).width(FillPortion(3)).into()
} else {
self.filters()
}
}
fn filters(&self) -> iced::Element<'_, DictionaryMessage> { fn filters(&self) -> iced::Element<'_, DictionaryMessage> {
let dict = &self.state.dictionary.read(); let dict = &self.state.dictionary.read();
@@ -434,10 +594,7 @@ impl DictionaryState {
.on_input(Search) .on_input(Search)
.width(Fill), .width(Fill),
text!("Всего слов: {}", dict.len()), text!("Всего слов: {}", dict.len()),
text!( text!("Выбрано слов: {}", self.included.iter().count()),
"Выбрано слов: {}",
self.included.iter().count()
),
self.tags_selector(), self.tags_selector(),
toggler(self.no_typing) toggler(self.no_typing)
.label("Без набора") .label("Без набора")
@@ -450,7 +607,7 @@ impl DictionaryState {
.on_press(GotoTest) .on_press(GotoTest)
.width(Fill), .width(Fill),
] ]
.width(250) .width(FillPortion(1))
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.into() .into()
} }
@@ -482,16 +639,21 @@ impl DictionaryState {
} }
fn update_tags(&mut self) { fn update_tags(&mut self) {
self.words_tags.clear();
let mut tags_list: HashSet<String> = HashSet::new(); let mut tags_list: HashSet<String> = HashSet::new();
let dict = &self.state.dictionary.write(); let dict = &self.state.dictionary.write();
let mut words_query = dict.full_query();
dict.iter().for_each(|element| { let handles = words_query.handles().collect::<Vec<_>>();
split_with_coma(element.tags.as_str()) words_query
.iter() .iter(dict)
.for_each(|tag| { .zip(handles)
.for_each(|(element, handle)| {
let tags = split_with_coma(element.tags.as_str());
tags.iter().for_each(|tag| {
tags_list.insert(tag.clone()); tags_list.insert(tag.clone());
}); });
}); self.words_tags.push((handle, tags))
});
let current_tags = self let current_tags = self
.tag_map .tag_map
@@ -529,17 +691,15 @@ impl DictionaryState {
self.included.clear(); self.included.clear();
for (handle, tags) in &mut self.words_tags { for (handle, tags) in &mut self.words_tags {
if let Some(word) = dict.get_ref(handle) { if let Some(word) = dict.get_ref(handle) {
if word.group_id != group_id { if word.group_id != group_id {
continue; continue;
} }
if !tags.is_empty() if !tags.is_empty() && tags.iter().all(|t| include_tags.contains(t)) {
&& tags.iter().all(|t| include_tags.contains(t)) {
self.included.insert(*handle); self.included.insert(*handle);
} }
} } else {
else {
continue; continue;
} }
} }
@@ -547,6 +707,31 @@ impl DictionaryState {
info!("Time {}", time.elapsed().as_micros()); info!("Time {}", time.elapsed().as_micros());
} }
fn selection_editor(&self) -> iced::Element<'_, DictionaryMessage> {
let mut display = "".to_string();
if let Some(selected) = self.selected_cell.as_ref() {
let mut dictionary = self.state.dictionary.write();
let word = dictionary.get_mut_static(selected.0).unwrap();
match selected.1 {
Key => {
display = word.key.clone();
}
Value => {
display = word.value.clone();
}
Tags => {
display = word.tags.clone();
}
}
}
text_input("", display.as_str())
.width(Fill)
.id(iced::widget::Id::new("editor"))
.on_input(EditField)
.into()
}
fn groups_panel(&self) -> iced::Element<'_, DictionaryMessage> { fn groups_panel(&self) -> iced::Element<'_, DictionaryMessage> {
let mut row = Row::new(); let mut row = Row::new();
row = row.push(button("+").style(text).on_press(CreateGroup)); row = row.push(button("+").style(text).on_press(CreateGroup));
@@ -563,12 +748,12 @@ impl DictionaryState {
} }
let groups = self.state.word_groups.read(); let groups = self.state.word_groups.read();
let group = groups.get_static(self.selected_group.clone()).unwrap(); let group = groups.get_static(self.selected_group).unwrap();
iced::widget::column![ iced::widget::column![
scrollable(row).width(Fill).horizontal(), scrollable(row).width(Fill).horizontal(),
row![ row![
button("⇳").on_press(ChangeDirection).style(jl_button), jl_button!("⇳").on_press(ChangeDirection),
text_input("Название группы слов", &group.name) text_input("Название группы слов", &group.name)
.on_input(EditGroup) .on_input(EditGroup)
.width(250) .width(250)
@@ -590,7 +775,7 @@ impl DictionaryState {
} }
fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> {
let button = button("Добавить слово").style(jl_button); let button = jl_button!("Добавить слово");
if self.selected_group.real() { if self.selected_group.real() {
return button.on_press(NewWord).into(); return button.on_press(NewWord).into();
@@ -600,6 +785,52 @@ impl DictionaryState {
} }
} }
impl KeyPressedPage for DictionaryState {
fn press(&mut self, message: &Event) -> Task<RootMessage> {
if let Event::KeyPressed {
key: _,
modified_key: modifier,
physical_key: pk,
location: _,
modifiers: mods,
text: _,
repeat: _,
} = message
&& let Code(code) = pk
{
return match code {
keyboard::key::Code::ArrowDown => {
Task::done(RootMessage::Dictionary(ChangeSelectedField(Next)))
}
keyboard::key::Code::ArrowUp => {
Task::done(RootMessage::Dictionary(ChangeSelectedField(Previous)))
}
keyboard::key::Code::Tab => {
return if *mods == Modifiers::CTRL && let Some(selected) = self.selected_cell {
Task::done(RootMessage::Dictionary(ExtendedEdit(selected.0)))
} else if let Named(keyboard::key::Named::Alt) = modifier {
Task::done(RootMessage::Dictionary(ChangeSelectedField(Backward)))
} else {
Task::done(RootMessage::Dictionary(ChangeSelectedField(Forward)))
}
}
keyboard::key::Code::Escape => {
if self.word_editor.is_some() {
self.word_editor = None;
}else if self.selected_cell.is_some() {
self.selected_cell = None;
}
Task::none()
}
_ => Task::none(),
};
}
Task::none()
}
}
pub fn split_with_coma(ts: &str) -> Vec<String> { pub fn split_with_coma(ts: &str) -> Vec<String> {
ts.split(',') ts.split(',')
.map(|ts| ts.to_lowercase().trim().to_string()) .map(|ts| ts.to_lowercase().trim().to_string())
@@ -610,8 +841,10 @@ pub fn split_with_coma(ts: &str) -> Vec<String> {
#[derive(Clone, Hash, PartialEq, Eq)] #[derive(Clone, Hash, PartialEq, Eq)]
struct WordLineState { struct WordLineState {
is_included: bool, is_included: bool,
key: Box<str>, key: String,
value: Box<str>, value: String,
tags: Box<str>, tags: String,
handle: EntityHandle<WordData>, handle: EntityHandle<WordData>,
selection: Option<BasicWordFields>,
number: usize,
} }
+15 -14
View File
@@ -1,4 +1,7 @@
use crate::app::AppState;
use crate::data::entities::{EntityHandle, WordData}; use crate::data::entities::{EntityHandle, WordData};
use crate::data::repositories::Repository;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::RootMessage; use crate::navigation::RootMessage;
use crate::navigation::*; use crate::navigation::*;
@@ -9,11 +12,9 @@ use crate::styling::*;
use iced::Background::Color; use iced::Background::Color;
use iced::border::Radius; use iced::border::Radius;
use iced::widget::container::Style; use iced::widget::container::Style;
use iced::widget::{Row, button, container, row, space, text, text_input}; use iced::widget::{Row, container, row, space, text, text_input};
use iced::{Border, Element, Fill, Task, Theme, alignment}; use iced::{Border, Element, Fill, Task, Theme, alignment};
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use crate::app::AppState;
use crate::data::repositories::Repository;
pub struct DictionaryQuizState { pub struct DictionaryQuizState {
state: AppState, state: AppState,
@@ -90,11 +91,8 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
.size(25), .size(25),
] ]
.spacing(DEFAULT_SPACING), .spacing(DEFAULT_SPACING),
row![ row![jl_button!("Закончить").on_press(Back), self.appeal_button()]
button("Закончить").style(jl_button).on_press(Back), .spacing(DEFAULT_SPACING),
self.appeal_button()
]
.spacing(DEFAULT_SPACING),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.align_x(alignment::Horizontal::Center), .align_x(alignment::Horizontal::Center),
@@ -106,8 +104,13 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
} }
impl DictionaryQuizState { impl DictionaryQuizState {
pub fn new(words: Vec<EntityHandle<WordData>>, reverse: bool, no_typing: bool, state: AppState) -> DictionaryQuizState { pub fn new(
let state = DictionaryQuizState { words: Vec<EntityHandle<WordData>>,
reverse: bool,
no_typing: bool,
state: AppState,
) -> DictionaryQuizState {
Self {
state, state,
words, words,
current_set: Vec::new(), current_set: Vec::new(),
@@ -119,9 +122,7 @@ impl DictionaryQuizState {
reverse, reverse,
laps: 0, laps: 0,
no_typing, no_typing,
}; }
state
} }
fn submit(&mut self) { fn submit(&mut self) {
@@ -218,7 +219,7 @@ impl DictionaryQuizState {
fn appeal_button(&self) -> Element<'_, DictionaryQuizMessage> { fn appeal_button(&self) -> Element<'_, DictionaryQuizMessage> {
if self.is_help && !self.no_typing { if self.is_help && !self.no_typing {
return button("Апелляция").style(jl_button).on_press(Appeal).into(); return jl_button!("Апелляция").on_press(Appeal).into();
} }
space().into() space().into()
} }
+24 -33
View File
@@ -3,18 +3,16 @@ use crate::data::entities::{HistoryNote, WordData};
use crate::data::repositories::Repository; use crate::data::repositories::Repository;
use crate::navigation::*; use crate::navigation::*;
use crate::styling::*; use crate::styling::*;
use HistoryMessage::Back;
use chrono::Local; use chrono::Local;
use iced::alignment::Horizontal::Center; use iced::alignment::Horizontal::Center;
use iced::widget::space::horizontal; use iced::widget::space::horizontal;
use iced::widget::*; use iced::widget::*;
use iced::{Element, Fill, FillPortion, Task}; use iced::{Element, Fill, FillPortion, Task};
use std::sync::Arc; use std::sync::Arc;
use HistoryMessage::Back;
#[derive(Clone)]
pub struct HistoryState { pub struct HistoryState {
list: Vec<(WordData, HistoryNote)>, list: Vec<(WordData, HistoryNote)>,
pub state: AppState,
} }
impl NavigatedPage<HistoryMessage> for HistoryState { impl NavigatedPage<HistoryMessage> for HistoryState {
@@ -37,9 +35,9 @@ impl NavigatedPage<HistoryMessage> for HistoryState {
.width(FillPortion(5)), .width(FillPortion(5)),
horizontal().width(FillPortion(1)) horizontal().width(FillPortion(1))
] ]
.height(Fill) .height(Fill)
.width(Fill) .width(Fill)
.into(), .into(),
Back, Back,
) )
} }
@@ -50,57 +48,50 @@ impl HistoryState {
let dict = &state.dictionary.read(); let dict = &state.dictionary.read();
let history_repo = &mut state.history.write(); let history_repo = &mut state.history.write();
history_repo.init().expect("TODO: panic message"); history_repo.init().expect("TODO: panic message");
let id = id.clone();
let mut history = history_repo.query(Arc::new(move |h| h.deck_id == id)); let mut history = history_repo.query(Arc::new(move |h| h.deck_id == id));
let words = history let words = history
.iter(history_repo) .iter(history_repo)
.map( .map(|item| {
|item| {let handle = dict.handle_by_id(item.word_id); let handle = dict.handle_by_id(item.word_id);
let word = dict.get_static(handle).unwrap_or(WordData { let word = dict.get_static(handle).unwrap_or(WordData {
id: Default::default(), id: Default::default(),
group_id: Default::default(), group_id: Default::default(),
key: "Удаленное слово".to_string(), key: "Удаленное слово".to_string(),
value: "".to_string(), value: "".to_string(),
tags: "".to_string(), tags: "".to_string(),
additional: Default::default(), additional: Default::default(),
}); });
(word, item.clone()) (word, item.clone())
} })
)
.collect(); .collect();
Self { Self { list: words }
list: words,
state: state.clone(),
}
} }
fn history_lines(&self) -> Column<'_, HistoryMessage> { fn history_lines(&self) -> Column<'_, HistoryMessage> {
let mut column = Column::new(); let mut column = Column::new();
for (index, item) in &self.list { for (index, item) in &self.list {
let activity = item; let activity = item;
column = column.push( column = column.push(
row![ row![
iced::widget::column![ iced::widget::column![
text!("{}", index.key) text!("{}", index.key).width(Fill).align_x(Center),
.width(Fill) text!("{}", index.value).width(Fill).align_x(Center),
.align_x(Center),
text!("{}", index.value)
.width(Fill)
.align_x(Center),
] ]
.align_x(Center), .align_x(Center),
iced::widget::column![ iced::widget::column![
text!("{} ➞ {}", activity.before, activity.after), text!("{} ➞ {}", activity.before, activity.after),
text!( text!(
"{}", "{}",
activity.timestamp.with_timezone(&Local).format("%d.%m %H:%M") activity
.timestamp
.with_timezone(&Local)
.format("%d.%m %H:%M")
) )
] ]
.spacing(5) .spacing(5)
.align_x(Center) .align_x(Center)
] ]
.spacing(5), .spacing(5),
); );
column = column.push(rule::horizontal(1)); column = column.push(rule::horizontal(1));
} }
+64 -17
View File
@@ -1,15 +1,17 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::EntityData; use crate::data::Id;
use crate::data::entities::WordData;
use crate::data::entities::WordGroup; 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::import::{ImportData, ImportGroup, get_words_of_group, load_groups};
use crate::fs::*; use crate::fs::*;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::NotificationData::OpenFileError; use crate::notification::NotificationData::{CompressError, OpenFileError};
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
use crate::pages::import::ImportMessage::*; use crate::pages::import::ImportMessage::*;
use crate::push_error; use crate::push_error;
use crate::styling::*; use crate::styling::*;
use crate::{jl_button, push_error_task};
use iced::widget::button::danger; use iced::widget::button::danger;
use iced::widget::container::success; use iced::widget::container::success;
use iced::widget::{ use iced::widget::{
@@ -24,17 +26,17 @@ use log::info;
use parking_lot::Mutex; use parking_lot::Mutex;
use rfd::AsyncFileDialog; use rfd::AsyncFileDialog;
use rusqlite::Connection; use rusqlite::Connection;
use std::collections::HashMap;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::BufReader; use std::io::BufReader;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use zip::ZipArchive; use zip::ZipArchive;
use crate::data::repositories::{BatchedRepository, Repository}; use zstd::Decoder;
const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"];
#[derive(Clone)]
pub struct ImportState { pub struct ImportState {
state: AppState, state: AppState,
path: Option<PathBuf>, path: Option<PathBuf>,
@@ -155,7 +157,7 @@ impl NavigatedPage<ImportMessage> for ImportState {
scrollable( scrollable(
column![ column![
row![ row![
button("Выбрать файл").style(jl_button).on_press(SelectFile), jl_button!("Выбрать файл").on_press(SelectFile),
text!("{}", { text!("{}", {
if let Some(path) = &self.path { if let Some(path) = &self.path {
path.to_string_lossy().to_string() path.to_string_lossy().to_string()
@@ -361,8 +363,8 @@ impl ImportState {
Task::perform( Task::perform(
async { async {
spawn_blocking(move || { spawn_blocking(move || {
Self::extract_import_file(path)?; let schema = Self::extract_import_file(path)?;
let data = Self::read_import_file()?; let data = Self::read_import_file(schema)?;
Ok(data) Ok(data)
}) })
.await .await
@@ -375,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 file = File::open(path).map_err(|_| OpenFileError)?;
let reader = BufReader::new(file); let reader = BufReader::new(file);
let mut archive = ZipArchive::new(reader).map_err(|_| OpenFileError)?; let mut archive = ZipArchive::new(reader).map_err(|_| OpenFileError)?;
@@ -386,20 +388,24 @@ impl ImportState {
.truncate(true) .truncate(true)
.open(&temp_file_path) .open(&temp_file_path)
.map_err(|_| OpenFileError)?; .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)?; 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") { } else if let Ok(mut file) = archive.by_name("collection.anki2") {
std::io::copy(&mut file, &mut temp_file).map_err(|_| OpenFileError)?; std::io::copy(&mut file, &mut temp_file).map_err(|_| OpenFileError)?;
Ok(()) Ok(AnkiSchema::Old)
} else { } else {
Err(OpenFileError) 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 temp_file_path = temp_import_path();
let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?; let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?;
let data = load_groups(&connection)?; let data = load_groups(&connection, schema)?;
Ok(data) Ok(data)
} }
fn setup_mapping(&mut self, property_name: String) { fn setup_mapping(&mut self, property_name: String) {
@@ -429,17 +435,29 @@ impl ImportState {
let separator = self.separator.clone(); let separator = self.separator.clone();
let skip_empty = self.skip_empty; let skip_empty = self.skip_empty;
let group = self.import_data.as_ref().unwrap().0[self.selected_index].clone(); 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(); 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); info!("{}", group.name);
Task::perform( Task::perform(
async move { async move {
spawn_blocking(move || { spawn_blocking(move || {
let temp_file_path = temp_import_path(); let temp_file_path = temp_import_path();
let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?; let connection =
Connection::open(&temp_file_path).map_err(|_| OpenFileError)?;
let import_list = get_words_of_group(&connection, group.id)?; let import_list = get_words_of_group(&connection, group.id)?;
let mut words_list = Vec::with_capacity(import_list.len()); let mut words_list = Vec::with_capacity(import_list.len());
let map_indices = Self::get_mapping_indices(&group);
let group_handle; let group_handle;
{ {
let group_entity = WordGroup { let group_entity = WordGroup {
@@ -474,7 +492,7 @@ impl ImportState {
word.tags = collected_string; word.tags = collected_string;
} }
&_ => { &_ => {
word.additional.insert((*dest).clone(), collected_string); word.additional.insert(fields[dest], collected_string);
} }
} }
} }
@@ -554,4 +572,33 @@ impl ImportState {
|_| RootMessage::Import(NextProgress), |_| 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
}
+2 -2
View File
@@ -1,5 +1,6 @@
use crate::app::USER_FONT; use crate::app::USER_FONT;
use crate::core::kana_set::KanaSet; use crate::core::kana_set::KanaSet;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::quiz::QuizMessage::*; use crate::pages::quiz::QuizMessage::*;
@@ -8,7 +9,6 @@ use iced::widget::*;
use iced::{Element, Fill, Task, alignment}; use iced::{Element, Fill, Task, alignment};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
#[derive(Clone, Debug)]
pub struct QuizState { pub struct QuizState {
kana: String, kana: String,
current_roman: String, current_roman: String,
@@ -82,7 +82,7 @@ impl NavigatedPage<QuizMessage> for QuizState {
.width(150) .width(150)
.on_input(ContentChanged), .on_input(ContentChanged),
score_display(&self.score), score_display(&self.score),
button("Закончить").style(jl_button).on_press(Back), jl_button!("Закончить").on_press(Back),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.align_x(alignment::Horizontal::Center), .align_x(alignment::Horizontal::Center),
+4 -4
View File
@@ -1,12 +1,12 @@
use crate::jl_button;
use crate::navigation::RootMessage; use crate::navigation::RootMessage;
use crate::navigation::{NavigatedPage, Page}; use crate::navigation::{NavigatedPage, Page};
use crate::pages::randomizer::RandomizerMessage::{Back, Edit, Start}; use crate::pages::randomizer::RandomizerMessage::{Back, Edit, Start};
use crate::styling::{HALF_SPACING, back_overlay, jl_button}; use crate::styling::{HALF_SPACING, back_overlay};
use iced::Task; use iced::Task;
use iced::widget::{button, text_editor}; use iced::widget::text_editor;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
#[derive(Clone, Debug)]
pub struct RandomizerState { pub struct RandomizerState {
text: text_editor::Content, text: text_editor::Content,
list: Vec<String>, list: Vec<String>,
@@ -56,7 +56,7 @@ impl NavigatedPage<RandomizerMessage> for RandomizerState {
.width(400) .width(400)
.height(400) .height(400)
.placeholder("Каждый элемент с новой строки"), .placeholder("Каждый элемент с новой строки"),
button("Перемешать").style(jl_button).on_press(Start), jl_button!("Перемешать").on_press(Start),
] ]
.spacing(HALF_SPACING) .spacing(HALF_SPACING)
.into(), .into(),
+11 -10
View File
@@ -2,11 +2,14 @@ use crate::app::AppState;
use crate::core::deck::DeckData; use crate::core::deck::DeckData;
use crate::core::srs::WordOpenMode; use crate::core::srs::WordOpenMode;
use crate::data::Id; use crate::data::Id;
use crate::data::entities::{CardState, DeckSettings, WordData}; use crate::data::entities::{CardState, WordData};
use crate::data::repositories::Repository;
use crate::data_provider::voice::get_voice; use crate::data_provider::voice::get_voice;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage}; use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
use crate::notification::{MaybeError, NotificationData}; use crate::notification::{MaybeError, NotificationData};
use crate::pages::repetitions::DeckRunData;
use crate::styling::*; use crate::styling::*;
use crate::{push_error, push_error_task}; use crate::{push_error, push_error_task};
use iced::alignment::Horizontal::Center; use iced::alignment::Horizontal::Center;
@@ -18,11 +21,10 @@ use rodio::MixerDeviceSink;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use crate::data::repositories::Repository;
pub struct RepetitionState { pub struct RepetitionState {
state: AppState, state: AppState,
pub settings: DeckSettings, pub settings: DeckRunData,
pub set: DeckData, pub set: DeckData,
current_word: WordData, current_word: WordData,
current_statistic: CardState, current_statistic: CardState,
@@ -102,10 +104,10 @@ impl NavigatedPage<RepetitionMessage> for RepetitionState {
impl RepetitionState { impl RepetitionState {
pub(crate) fn new( pub(crate) fn new(
set: DeckSettings, set: DeckRunData,
state: AppState, state: AppState,
) -> Result<RepetitionState, NotificationData> { ) -> Result<RepetitionState, NotificationData> {
let mut card_set = DeckData::new(&set, state.clone())?; let mut card_set = DeckData::new(&set, set.open_mode, state.clone());
let (word, stat) = card_set.next(); let (word, stat) = card_set.next();
let word = state.dictionary.read().get_static(word).unwrap(); let word = state.dictionary.read().get_static(word).unwrap();
let stat = state.card_states.read().get_static(stat).unwrap(); let stat = state.card_states.read().get_static(stat).unwrap();
@@ -254,26 +256,25 @@ impl RepetitionState {
} }
fn draw_voice(&self) -> Element<'_, RepetitionMessage> { fn draw_voice(&self) -> Element<'_, RepetitionMessage> {
button("Воспроизвести") jl_button!("Воспроизвести")
.style(jl_button)
.on_press(RepetitionMessage::Play) .on_press(RepetitionMessage::Play)
.into() .into()
} }
fn draw_reading(&self, word: &WordData) -> Element<'_, RepetitionMessage> { fn draw_reading(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("reading") { match word.additional.get(&Id(1)) {
None => space().into(), None => space().into(),
Some(reading) => text!("{}", reading).size(24).into(), Some(reading) => text!("{}", reading).size(24).into(),
} }
} }
fn draw_context(&self, word: &WordData) -> Element<'_, RepetitionMessage> { fn draw_context(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("context") { match word.additional.get(&Id(3)) {
None => space().into(), None => space().into(),
Some(context) => text!("{}", context).size(24).into(), Some(context) => text!("{}", context).size(24).into(),
} }
} }
fn draw_description(&self, word: &WordData) -> Element<'_, RepetitionMessage> { fn draw_description(&self, word: &WordData) -> Element<'_, RepetitionMessage> {
match word.additional.get("description") { match word.additional.get(&Id(2)) {
None => space().into(), None => space().into(),
Some(context) => text!("{}", context).size(24).into(), Some(context) => text!("{}", context).size(24).into(),
} }
+39 -7
View File
@@ -2,6 +2,7 @@ use crate::app::AppState;
use crate::core::deck::DeckData; use crate::core::deck::DeckData;
use crate::data::entities::{DeckSettings, EntityHandle}; use crate::data::entities::{DeckSettings, EntityHandle};
use crate::data::repositories::{BatchedRepository, Repository}; use crate::data::repositories::{BatchedRepository, Repository};
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::MaybeError; use crate::notification::MaybeError;
@@ -9,6 +10,7 @@ use crate::pages::repetition_settings::RepetitionSettingsMessage::*;
use crate::styling::*; use crate::styling::*;
use crate::{push_error, push_error_task}; use crate::{push_error, push_error_task};
use iced::widget::button::danger; use iced::widget::button::danger;
use iced::widget::space::horizontal;
use iced::widget::text_input::Catalog; use iced::widget::text_input::Catalog;
use iced::widget::{button, column, row, scrollable, space, text, text_input}; use iced::widget::{button, column, row, scrollable, space, text, text_input};
use iced::{Element, Task}; use iced::{Element, Task};
@@ -22,6 +24,7 @@ pub struct RepetitionSettingsState {
settings_data: DeckSettings, settings_data: DeckSettings,
state: AppState, state: AppState,
real_delete: bool, real_delete: bool,
real_delete_states: bool,
correct_filter: bool, correct_filter: bool,
filter_changed: bool, filter_changed: bool,
filtered_words: Option<u32>, filtered_words: Option<u32>,
@@ -38,6 +41,7 @@ impl RepetitionSettingsState {
settings_handle: settings, settings_handle: settings,
state, state,
real_delete: false, real_delete: false,
real_delete_states: false,
correct_filter: true, correct_filter: true,
filter_changed: false, filter_changed: false,
filtered_words: None, filtered_words: None,
@@ -105,7 +109,29 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
return Task::done(RootMessage::RepetitionSettings(Back)); return Task::done(RootMessage::RepetitionSettings(Back));
} }
RevertDeleteSet => self.real_delete = false, RevertDeleteSet => {
self.real_delete = false;
self.real_delete_states = true;
}
DeleteStates => {
if !self.real_delete_states {
self.real_delete_states = true;
return Task::future(async {
tokio::time::sleep(Duration::from_millis(3000)).await;
RootMessage::RepetitionSettings(RevertDeleteSet)
});
}
let mut states = self.state.card_states.write();
let deck_id = self.settings_handle.id;
let mut query = states
.query(Arc::new(move |state| state.deck_id == deck_id))
.handles()
.collect::<Vec<_>>();
if let Err(e) = states.delete_many(query.as_mut_slice()) {
return push_error_task!(e);
}
self.real_delete_states = false;
}
} }
Task::none() Task::none()
} }
@@ -141,9 +167,7 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
primary primary
}) })
.on_input(SetFilter), .on_input(SetFilter),
button("Проверить фильтр") jl_button!("Проверить фильтр").on_press(TryFilter),
.style(jl_button)
.on_press(TryFilter),
self.count_view(), self.count_view(),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
@@ -158,7 +182,15 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
) )
.height(Fill), .height(Fill),
row![ 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 { button(if self.real_delete {
"Вы уверены?" "Вы уверены?"
} else { } else {
@@ -166,7 +198,6 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
}) })
.style(danger) .style(danger)
.on_press(DeleteSet), .on_press(DeleteSet),
space().width(Fill),
] ]
.spacing(DEFAULT_SPACING), .spacing(DEFAULT_SPACING),
] ]
@@ -182,7 +213,7 @@ fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError {
let deck_id = deck.id; let deck_id = deck.id;
let mut db_stats = states_repo let mut db_stats = states_repo
.query_par(Arc::new(move |s| s.deck_id == deck_id)) .query_par(Arc::new(move |s| s.deck_id == deck_id))
.iter(&states_repo) .iter(states_repo)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let filtered_words = DeckData::list(deck, state) let filtered_words = DeckData::list(deck, state)
.iter() .iter()
@@ -222,5 +253,6 @@ pub enum RepetitionSettingsMessage {
SetFilter(String), SetFilter(String),
TryFilter, TryFilter,
DeleteSet, DeleteSet,
DeleteStates,
RevertDeleteSet, RevertDeleteSet,
} }
+29 -36
View File
@@ -3,6 +3,7 @@ use crate::core::deck::DeckData;
use crate::core::srs::{AppendMode, OrderMode}; use crate::core::srs::{AppendMode, OrderMode};
use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData}; use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData};
use crate::data::repositories::{BatchedRepository, Repository}; use crate::data::repositories::{BatchedRepository, Repository};
use crate::jl_button;
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::MaybeError; use crate::notification::MaybeError;
@@ -30,10 +31,9 @@ use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone)]
pub struct RepetitionsState { pub struct RepetitionsState {
selected_deck_handle: Option<EntityHandle<DeckSettings>>, selected_deck_handle: Option<EntityHandle<DeckSettings>>,
decks: BTreeMap<EntityHandle<DeckSettings>, DeckViewData>, decks: BTreeMap<EntityHandle<DeckSettings>, DeckRunData>,
state: AppState, state: AppState,
} }
@@ -63,7 +63,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
} }
if let Ok(repetition_state) = if let Ok(repetition_state) =
RepetitionState::new(selected_set.general_settings.clone(), self.state.clone()) RepetitionState::new(selected_set.clone(), self.state.clone())
{ {
return Some(Repetition(repetition_state)); return Some(Repetition(repetition_state));
} }
@@ -94,7 +94,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
self.clear_selection(); self.clear_selection();
} }
} else { } else {
let _ = self.fill_deck_word_indices(); self.fill_deck_word_indices();
} }
} }
fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> { fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> {
@@ -111,7 +111,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
let handle = self.state.decks.write().create(general.clone()); let handle = self.state.decks.write().create(general.clone());
self.decks.insert( self.decks.insert(
handle, handle,
DeckViewData { DeckRunData {
settings_handle: handle, settings_handle: handle,
general_settings: general, general_settings: general,
existing_words_indices: Default::default(), existing_words_indices: Default::default(),
@@ -126,7 +126,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
DeleteSet => { DeleteSet => {
let index = self.selected_deck_handle.unwrap(); let index = self.selected_deck_handle.unwrap();
self.clear_selection(); self.clear_selection();
let deck = self.decks.remove(&index); self.decks.remove(&index);
if index.real() { if index.real() {
let mut repo = self.state.decks.write(); let mut repo = self.state.decks.write();
@@ -152,7 +152,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
let mut repo = self.state.decks.write(); let mut repo = self.state.decks.write();
repo.set(handle, deck.general_settings.clone()); repo.set(handle, deck.general_settings.clone());
let old = handle.clone(); let old = *handle;
if let Err(err) = repo.save(handle) { if let Err(err) = repo.save(handle) {
return push_error_task!(err); return push_error_task!(err);
} }
@@ -175,7 +175,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
} }
TryFilter => { TryFilter => {
let deck = self.selected_deck().unwrap(); let deck = self.selected_deck().unwrap();
let count = DeckData::list(&deck, &self.state).len(); let count = DeckData::list(deck, &self.state).len();
let deck = self.selected_deck_mut().unwrap(); let deck = self.selected_deck_mut().unwrap();
deck.count = Some(count); deck.count = Some(count);
} }
@@ -227,10 +227,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
row![ row![
column![ column![
scrollable(self.decks_list()).height(Fill), scrollable(self.decks_list()).height(Fill),
button("Добавить") jl_button!("Добавить").width(Fill).on_press(CreateSet),
.style(jl_button)
.width(Fill)
.on_press(CreateSet),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.width(Length::FillPortion(1)), .width(Length::FillPortion(1)),
@@ -256,9 +253,7 @@ impl RepetitionsState {
} }
} }
fn get_decks_from_state( fn get_decks_from_state(state: &AppState) -> BTreeMap<EntityHandle<DeckSettings>, DeckRunData> {
state: &AppState,
) -> BTreeMap<EntityHandle<DeckSettings>, DeckViewData> {
state state
.decks .decks
.read() .read()
@@ -267,7 +262,7 @@ impl RepetitionsState {
.map(|(index, deck)| { .map(|(index, deck)| {
( (
EntityHandle::new(deck.id, index, true), EntityHandle::new(deck.id, index, true),
DeckViewData { DeckRunData {
settings_handle: EntityHandle::new(deck.id, index, true), settings_handle: EntityHandle::new(deck.id, index, true),
general_settings: deck.clone(), general_settings: deck.clone(),
existing_words_indices: Default::default(), existing_words_indices: Default::default(),
@@ -284,7 +279,7 @@ impl RepetitionsState {
} }
impl RepetitionsState { impl RepetitionsState {
fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { fn launch_delete_button(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
if deck.id.is_valid() { if deck.id.is_valid() {
if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.is_empty() { if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.is_empty() {
return text!("Добавьте карточки или измените фильтр").into(); return text!("Добавьте карточки или измените фильтр").into();
@@ -331,9 +326,7 @@ impl RepetitionsState {
column![ column![
text!("Фильтр"), text!("Фильтр"),
text_input("", &deck.filter).on_input(SetFilter), text_input("", &deck.filter).on_input(SetFilter),
button("Проверить фильтр") jl_button!("Проверить фильтр").on_press(TryFilter),
.style(jl_button)
.on_press(TryFilter),
self.count_view(deck), self.count_view(deck),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
@@ -361,7 +354,7 @@ impl RepetitionsState {
Some(deck.open_mode), Some(deck.open_mode),
SetOpenMode SetOpenMode
), ),
button("История").style(jl_button).on_press(GoToHistory), jl_button!("История").on_press(GoToHistory),
] ]
.spacing(HALF_SPACING) .spacing(HALF_SPACING)
} }
@@ -378,7 +371,7 @@ impl RepetitionsState {
) )
.height(Fill), .height(Fill),
row![ row![
button("Сохранить").style(jl_button).on_press(Save), jl_button!("Сохранить").on_press(Save),
self.launch_delete_button(deck) self.launch_delete_button(deck)
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
@@ -468,7 +461,7 @@ impl RepetitionsState {
}) })
.into() .into()
} }
fn word_append_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { fn word_append_panel(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
column![ column![
text!("Режим добавления карточек"), text!("Режим добавления карточек"),
row![ row![
@@ -491,7 +484,7 @@ impl RepetitionsState {
.spacing(HALF_SPACING) .spacing(HALF_SPACING)
.into() .into()
} }
fn adder_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { fn adder_panel(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
match deck.append_mode { match deck.append_mode {
AppendMode::Full => space().into(), AppendMode::Full => space().into(),
AppendMode::Manual => row![ AppendMode::Manual => row![
@@ -511,7 +504,7 @@ impl RepetitionsState {
let available = selected_deck.available_words_indices.len(); let available = selected_deck.available_words_indices.len();
text!("{} слова добавлено из {}", now, available).into() text!("{} слова добавлено из {}", now, available).into()
} }
fn count_view(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { fn count_view(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> {
if let Some(count) = deck.count { if let Some(count) = deck.count {
return text!("Количество слов: {}", count).into(); return text!("Количество слов: {}", count).into();
} }
@@ -520,7 +513,7 @@ impl RepetitionsState {
fn decks_list(&self) -> Column<'_, RepetitionsMessage> { fn decks_list(&self) -> Column<'_, RepetitionsMessage> {
let mut column = Column::new(); let mut column = Column::new();
for (i, (handle, deck)) in self.decks.iter().enumerate() { for (handle, deck) in self.decks.iter() {
column = column.push( column = column.push(
button(text!("{}", &deck.name)) button(text!("{}", &deck.name))
.on_press_with(move || SelectSet(*handle)) .on_press_with(move || SelectSet(*handle))
@@ -547,7 +540,7 @@ impl RepetitionsState {
} }
impl RepetitionsState { impl RepetitionsState {
fn append_all_words(state: &mut AppState, deck: &DeckViewData) -> MaybeError { fn append_all_words(state: &mut AppState, deck: &DeckRunData) -> MaybeError {
let existing = &deck.existing_words_indices; let existing = &deck.existing_words_indices;
let mut created_set = HashSet::with_capacity(existing.len()); let mut created_set = HashSet::with_capacity(existing.len());
existing.iter().for_each(|c| { existing.iter().for_each(|c| {
@@ -566,7 +559,6 @@ impl RepetitionsState {
deck: &DeckSettings, deck: &DeckSettings,
indices: impl Iterator<Item = EntityHandle<WordData>>, indices: impl Iterator<Item = EntityHandle<WordData>>,
) -> MaybeError { ) -> MaybeError {
let words = &state.dictionary;
let stats = &mut indices let stats = &mut indices
.map(|i| CardState { .map(|i| CardState {
id: 0.into(), id: 0.into(),
@@ -593,10 +585,10 @@ impl RepetitionsState {
} }
} }
fn selected_deck(&self) -> Option<&DeckViewData> { fn selected_deck(&self) -> Option<&DeckRunData> {
self.decks.get(&self.selected_deck_handle?) self.decks.get(&self.selected_deck_handle?)
} }
fn selected_deck_mut(&mut self) -> Option<&mut DeckViewData> { fn selected_deck_mut(&mut self) -> Option<&mut DeckRunData> {
self.decks.get_mut(&self.selected_deck_handle?) self.decks.get_mut(&self.selected_deck_handle?)
} }
@@ -616,6 +608,7 @@ impl RepetitionsState {
let total = DeckData::list(&deck, &self.state); let total = DeckData::list(&deck, &self.state);
deck.existing_words_indices = added; deck.existing_words_indices = added;
deck.available_words_indices = total; deck.available_words_indices = total;
deck.indices_ready = true;
self.decks.insert(self.selected_deck_handle.unwrap(), deck); self.decks.insert(self.selected_deck_handle.unwrap(), deck);
} }
} }
@@ -641,18 +634,18 @@ pub enum RepetitionsMessage {
} }
#[derive(Clone)] #[derive(Clone)]
struct DeckViewData { pub struct DeckRunData {
settings_handle: EntityHandle<DeckSettings>, pub settings_handle: EntityHandle<DeckSettings>,
general_settings: DeckSettings, pub general_settings: DeckSettings,
existing_words_indices: Vec<EntityHandle<WordData>>, existing_words_indices: Vec<EntityHandle<WordData>>,
available_words_indices: Vec<EntityHandle<WordData>>, available_words_indices: Vec<EntityHandle<WordData>>,
indices_ready: bool, indices_ready: bool,
append_mode: AppendMode, pub append_mode: AppendMode,
count: Option<usize>, count: Option<usize>,
open_mode: OrderMode, pub open_mode: OrderMode,
} }
impl Deref for DeckViewData { impl Deref for DeckRunData {
type Target = DeckSettings; type Target = DeckSettings;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
+2 -5
View File
@@ -1,5 +1,6 @@
use crate::app::AppState; use crate::app::AppState;
use crate::core::kana_set::{KanaSet, KanaType}; use crate::core::kana_set::{KanaSet, KanaType};
use crate::jl_button;
use crate::navigation::Page::*; use crate::navigation::Page::*;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::dictionary::DictionaryState; use crate::pages::dictionary::DictionaryState;
@@ -16,8 +17,6 @@ use iced::{Element, Task, alignment};
use iced_core::Alignment::Center; use iced_core::Alignment::Center;
use iced_core::Background; use iced_core::Background;
use iced_core::Color; use iced_core::Color;
use parking_lot::Mutex;
use std::sync::Arc;
pub struct SelectorState { pub struct SelectorState {
pub set: KanaSet, pub set: KanaSet,
@@ -81,9 +80,7 @@ impl NavigatedPage<SelectorMessage> for SelectorState {
container( container(
iced::widget::column![ iced::widget::column![
row![ row![
button("あ ↔ ア") jl_button!("あ ↔ ア").on_press(SelectorMessage::Change),
.on_press(SelectorMessage::Change)
.style(jl_button),
button("Словарь") button("Словарь")
.on_press(SelectorMessage::ToDictionary) .on_press(SelectorMessage::ToDictionary)
.style(Self::nav_style), .style(Self::nav_style),
+16 -12
View File
@@ -2,6 +2,7 @@ use crate::app::AppState;
use crate::data::repositories::Repository; use crate::data::repositories::Repository;
use crate::data_provider::settings::{delete_settings, set_setting}; use crate::data_provider::settings::{delete_settings, set_setting};
use crate::data_provider::web_api::*; use crate::data_provider::web_api::*;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::{NotificationData, NotificationRequest}; use crate::notification::{NotificationData, NotificationRequest};
@@ -31,7 +32,6 @@ pub enum SyncMessage {
DisableSync, DisableSync,
SwitchAutoSync, SwitchAutoSync,
} }
#[derive(Clone)]
pub struct SyncState { pub struct SyncState {
state: AppState, state: AppState,
auto_fetch: bool, auto_fetch: bool,
@@ -221,14 +221,13 @@ impl SyncState {
.padding(3) .padding(3)
) )
.style(rounded_box), .style(rounded_box),
button("Скопировать в буфер обмена") jl_button!("Скопировать в буфер обмена")
.style(jl_button)
.on_press(CopyKey) .on_press(CopyKey)
.width(Fill), .width(Fill),
row![ row![
button("↑ Отправить").style(jl_button).on_press(Send), jl_button!("↑ Отправить").on_press(Send),
space().width(Fill), space().width(Fill),
button("↓ Скачать").style(jl_button).on_press(GetLast) jl_button!("↓ Скачать").on_press(GetLast)
] ]
.spacing(DEFAULT_SPACING), .spacing(DEFAULT_SPACING),
toggler(self.auto_fetch) toggler(self.auto_fetch)
@@ -241,12 +240,8 @@ impl SyncState {
] ]
} else { } else {
column![ column![
button("Создать сохранение") jl_button!("Создать сохранение").on_press(InitSync),
.style(jl_button) jl_button!("Вставить ключ из буфера").on_press(GetKey),
.on_press(InitSync),
button("Вставить ключ из буфера")
.style(jl_button)
.on_press(GetKey),
] ]
} }
} }
@@ -273,7 +268,16 @@ impl SyncState {
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}"); error!("Ошибка при checkpoint WAL: {e}");
}); });
connection.execute("VACUUM;", []).unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
0
});
connection
.execute("PRAGMA optimize;", [])
.unwrap_or_else(|e| {
error!("Ошибка при checkpoint WAL: {e}");
0
});
connection.finish(); connection.finish();
} }
} }
+119 -153
View File
@@ -1,182 +1,150 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::{EntityHandle, WordData}; use crate::data::Id;
use crate::data_provider::words::{delete_word, update_word}; use crate::data::entities::{CustomField, EntityHandle, WordData};
use crate::navigation::Page::PreviousPage; use crate::data::repositories::Repository;
use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::pages::word::WordMessage::*; use crate::pages::word::WordMessage::*;
use crate::push_error;
use crate::push_error_task;
use crate::styling::*; use crate::styling::*;
use iced::widget::button::danger; use iced::widget::button::danger;
use iced::widget::{button, column, row, rule, scrollable, space, text, text_input}; use iced::widget::space::horizontal;
use iced::{Element, Fill, Task}; use iced::widget::{button, column, row, rule, scrollable, text, text_input};
use parking_lot::Mutex; use iced::{Element, Fill};
use std::sync::Arc; use iced_core::Padding;
#[derive(Clone)] pub struct WordEditor<Message> {
pub struct WordState {
state: AppState, state: AppState,
handle: EntityHandle<WordData>, handle: EntityHandle<WordData>,
on_delete: Option<Message>,
on_edit: Option<Message>,
fields: Vec<CustomField>,
} }
impl NavigatedPage<WordMessage> for WordState { impl<Message> WordEditor<Message> {
fn navigate(&mut self, message: &WordMessage) -> Option<Page> { pub(crate) fn update(&mut self, message: WordMessage) -> Option<Message> {
if let Back = message { let dictionary = &mut self.state.dictionary.write();
Some(PreviousPage) let word = dictionary.get_mut(&mut self.handle).unwrap();
} else { match message {
None Delete => {
return self.on_delete.take();
}
SetTags(n) => word.tags = n,
SetKey(n) => {
word.key = n;
}
SetValue(n) => {
word.value = n;
}
SetAdditional(key, value) => {
word.additional.insert(key, value.clone());
}
AddAdditional(key) => {
word.additional.insert(key, "".to_string());
}
RemoveAdditional(key) => {
word.additional.remove(&key);
}
} }
self.on_edit.take()
} }
fn navigated(&mut self) {} pub(crate) fn view(&self) -> Element<'_, WordMessage> {
fn update(&mut self, message: WordMessage) -> Task<RootMessage> { let mut fast_add = row![];
// match message { let word = self
// Back => {} .state
// Save => { .dictionary
// let mut state = self.state.lock(); .read()
// state.dictionary[self.index] = self.word.clone(); .get_static(self.handle)
// .unwrap();
// if let Err(err) = update_word(&mut self.word, &state.connection) { for field in &self.fields {
// return push_error_task!(err); if !word.additional.contains_key(&field.id) {
// } fast_add = fast_add.push(
// return Task::done(RootMessage::Word(Back)); button(field.name.as_str())
// } .style(button::text)
// Delete => { .on_press(AddAdditional(field.id)),
// 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));
// }
// SetTags(n) => self.word.tags = n,
// SetKey(n) => {
// self.word.key = n;
// }
// SetValue(n) => {
// self.word.value = n;
// }
// SetAdditional(key, value) => {
// self.word.additional.insert(key, value.clone());
// }
// AddAdditional(key) => {
// self.word.additional.insert(key, "".to_string());
// }
// RemoveAdditional(key) => {
// self.word.additional.remove(key.as_str());
// }
// }
Task::none()
}
fn view(&self) -> Element<'_, WordMessage> { let mut col = iced::widget::column![
// let mut fast_add = row![]; column![text!("Ключ"), text_input("key", &word.key).on_input(SetKey),]
// if !self.word.additional.contains_key("reading") { .spacing(QUARTER_SPACING),
// fast_add = fast_add.push( column![
// button("Чтение") text!("Значение"),
// .style(button::text) text_input("value", &word.value).on_input(SetValue),
// .on_press(AddAdditional("reading".to_string())), ]
// ); .spacing(QUARTER_SPACING),
// } column![
// if !self.word.additional.contains_key("description") { text!("Теги"),
// fast_add = fast_add.push( text_input("tags", &word.tags).on_input(SetTags),
// button("Описание") ]
// .style(button::text) .spacing(QUARTER_SPACING),
// .on_press(AddAdditional("description".to_string())), rule::horizontal(2),
// ); scrollable(fast_add.padding(Padding{
// }
//
// if !self.word.additional.contains_key("context") { top: 0.0 , right: 0.0 , bottom: 4.0 , left: 0.0 , })).horizontal(),
// fast_add = fast_add.push( ];
// button("В контексте")
// .style(button::text) for more in &word.additional {
// .on_press(AddAdditional("context".to_string())), col = col.push(self.get_view_for_more(more));
// ); }
// }
// column![
// let mut col = iced::widget::column![ col.spacing(DEFAULT_SPACING / 2.0).width(Fill).height(Fill),
// column![ row![
// text!("Ключ"), horizontal().width(Fill),
// text_input("key", &self.word.key).on_input(SetKey), button("Удалить").style(danger).on_press(Delete),
// ] ]
// .spacing(QUARTER_SPACING), .spacing(DEFAULT_SPACING)
// column![ ]
// text!("Значение"), .spacing(DEFAULT_SPACING)
// text_input("value", &self.word.value).on_input(SetValue), .into()
// ]
// .spacing(QUARTER_SPACING),
// column![
// text!("Теги"),
// text_input("tags", &self.word.tags).on_input(SetTags),
// ]
// .spacing(QUARTER_SPACING),
// rule::horizontal(2),
// scrollable(fast_add),
// ];
//
// for more in &self.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)
// ]
// .spacing(DEFAULT_SPACING)
// .into(),
// Back,
// )
space().into()
} }
} }
impl WordState { impl<Message> WordEditor<Message> {
pub fn new(handle: EntityHandle<WordData>, state: AppState) -> Self { pub fn new(handle: EntityHandle<WordData>, state: AppState) -> Self {
Self{ let fields = state
.custom_fields
.read()
.iter()
.cloned()
.collect::<Vec<_>>();
Self {
state, state,
handle, handle,
on_delete: None,
on_edit: None,
fields,
} }
} }
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 WordState { impl<Message> WordEditor<Message> {
fn get_view_for_more(&self, value: (&String, &String)) -> Element<'_, WordMessage> { fn get_view_for_more(&self, value: (&Id, &String)) -> Element<'_, WordMessage> {
match value.0.as_str() { let field = self.fields.iter().find(|f| f.id == *value.0).unwrap();
"reading" => self.reading_field(value.1), match value.0 {
"description" => self.description_field(value.1), _ => self.additional_field(value.1, field.name.as_str(), value.0.clone()),
"context" => self.context_field(value.1),
_ => self.additional_field(value.1, value.0.clone(), value.0.clone()),
} }
} }
fn reading_field(&self, value: &str) -> Element<'_, WordMessage> { fn additional_field(&self, value: &str, name: &str, id: Id) -> Element<'_, WordMessage> {
self.additional_field(value, "Чтение слова".to_string(), "reading".to_string())
}
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> {
column![ column![
text!("{}", name), text!("{}", name),
row![ row![
text_input(id.clone().as_str(), value) text_input(name, value)
.on_input({ .on_input(move |string| SetAdditional(id, string))
let value = id.clone();
move |string| SetAdditional(value.clone(), string)
})
.width(Fill), .width(Fill),
button("-") button("-")
.on_press(RemoveAdditional(id.clone())) .on_press(RemoveAdditional(id))
.style(danger), .style(danger),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
@@ -188,13 +156,11 @@ impl WordState {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum WordMessage { pub enum WordMessage {
Save,
Back,
Delete, Delete,
SetTags(String), SetTags(String),
SetKey(String), SetKey(String),
SetValue(String), SetValue(String),
AddAdditional(String), AddAdditional(Id),
RemoveAdditional(String), RemoveAdditional(Id),
SetAdditional(String, String), SetAdditional(Id, String),
} }
+3 -7
View File
@@ -1,4 +1,5 @@
use crate::core::kana_set::KanaSet; use crate::core::kana_set::KanaSet;
use crate::jl_button;
use crate::navigation::Page::PreviousPage; use crate::navigation::Page::PreviousPage;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::styling::*; use crate::styling::*;
@@ -6,7 +7,6 @@ use iced::widget::*;
use iced::{Element, Fill, Task, alignment}; use iced::{Element, Fill, Task, alignment};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
#[derive(Clone, Debug)]
pub struct WritingState { pub struct WritingState {
kana: String, kana: String,
set: Vec<(String, String)>, set: Vec<(String, String)>,
@@ -45,12 +45,8 @@ impl NavigatedPage<WritingMessage> for WritingState {
text!("{}", self.kana).size(48), text!("{}", self.kana).size(48),
self.answers(), self.answers(),
row![ row![
button(text!("{}", self.next_text)) jl_button!(text!("{}", self.next_text)).on_press(WritingMessage::Next),
.style(jl_button) jl_button!("Закончить").on_press(WritingMessage::Back),
.on_press(WritingMessage::Next),
button("Закончить")
.style(jl_button)
.on_press(WritingMessage::Back),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
] ]
+13 -9
View File
@@ -1,7 +1,7 @@
use iced::Element; use iced::Element;
use iced::border::Radius; use iced::border::Radius;
use iced::widget::button::*; use iced::widget::button::*;
use iced::widget::{button, container}; use iced::widget::container;
use iced_core::Length::Fill; use iced_core::Length::Fill;
use iced_core::alignment::Horizontal::Left; use iced_core::alignment::Horizontal::Left;
use iced_core::{Border, Theme}; use iced_core::{Border, Theme};
@@ -14,7 +14,7 @@ pub const BUTTON_RADIUS: f32 = 8.0;
pub const SETTINGS_ICON: &[u8] = include_bytes!("../settings_icon.svg"); pub const SETTINGS_ICON: &[u8] = include_bytes!("../settings_icon.svg");
pub fn jl_button(theme: &Theme, status: Status) -> Style { pub fn jl_button_style(theme: &Theme, status: Status) -> Style {
let mut style = primary(theme, status); let mut style = primary(theme, status);
style.border = Border { style.border = Border {
color: Default::default(), color: Default::default(),
@@ -30,15 +30,19 @@ pub fn cta_button(theme: &Theme, status: Status) -> Style {
style style
} }
#[macro_export]
macro_rules! jl_button {
($content:expr) => {
iced::widget::button($content).style($crate::styling::jl_button_style)
};
}
pub fn back_overlay<'a, T: Clone + 'a>(content: Element<'a, T>, back_message: T) -> Element<'a, T> { pub fn back_overlay<'a, T: Clone + 'a>(content: Element<'a, T>, back_message: T) -> Element<'a, T> {
container( container(
iced::widget::column![ iced::widget::column![jl_button!("Назад").on_press(back_message), content]
button("Назад").style(jl_button).on_press(back_message), .spacing(DEFAULT_SPACING)
content .align_x(Left)
] .width(Fill),
.spacing(DEFAULT_SPACING)
.align_x(Left)
.width(Fill),
) )
.center_x(Fill) .center_x(Fill)
.padding(10) .padding(10)
+4 -4
View File
@@ -73,13 +73,13 @@ mod mod_tests {
#[test] #[test]
fn id_0() { fn id_0() {
let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true); let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true);
assert_eq!(entity.real(), true) assert!(entity.real())
} }
#[test] #[test]
fn id_1() { fn id_1() {
let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false); let entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false);
assert_eq!(entity.real(), false) assert!(!entity.real())
} }
#[test] #[test]
@@ -97,14 +97,14 @@ mod mod_tests {
fn id_5() { fn id_5() {
let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true); let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, true);
entity.set_real(true); entity.set_real(true);
assert_eq!(entity.real(), true) assert!(entity.real())
} }
#[test] #[test]
fn id_6() { fn id_6() {
let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false); let mut entity: EntityHandle<ProtoEntity> = EntityHandle::new(8.into(), 25, false);
entity.set_real(true); entity.set_real(true);
assert_eq!(entity.real(), true) assert!(entity.real())
} }
#[test] #[test]
+447
View File
@@ -0,0 +1,447 @@
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;