24 Commits
Author SHA1 Message Date
micialware 144e258292 Add performance measuring 2026-09-22 00:05:25 +03:00
micialware 48cd68d1c7 Fix incorrect states clearing 2026-09-21 11:53:07 +03:00
micialware a5526dc5e9 Migrate to String::new() 2026-09-20 22:24:42 +03:00
micialware 1d4b37e381 Update version, remove unused features 2026-09-20 22:18:59 +03:00
micialware 96a58c61bc Rollback default control radius 2026-09-15 22:11:38 +03:00
micialware 1847d4f44f apply new style 2026-09-15 22:09:06 +03:00
micialware b1902b35cb style and macros for text input 2026-09-15 21:54:24 +03:00
micialware 5d12f3b02a Fixing incorrect history reading bug 2026-09-14 19:41:02 +03:00
micialware 0e867a33ce Merge pull request 'Updated word display' (#19) from updated-word-display into master
Reviewed-on: https://gitea.micialware.ru/micialware/jap_learn/pulls/19
2026-09-13 17:42:58 +03:00
micialware ccfb302770 Prepare for new anki format 2026-09-13 12:44:51 +03:00
micialware 0de809fcce Compressing word fields 2026-09-11 22:08:50 +03:00
micialware e5622c70de Fix deck filter 2026-09-11 14:02:57 +03:00
micialware cbf49412ec additional fields encoding and decoding 2026-09-11 13:56:26 +03:00
micialware f61f93717c Custom fields repo into AppState 2026-09-11 13:28:23 +03:00
micialware be64b1da2d Fix group delete 2026-09-11 13:12:45 +03:00
micialware 2568b9f292 More fixes for dictionary panel 2026-09-11 13:09:20 +03:00
micialware 05c82bee02 Fix tag edit bug 2026-09-11 12:48:04 +03:00
micialware 5d305affd3 Fixing some bugs 2026-09-11 12:38:21 +03:00
micialware d59776d713 More keyboard handling for dictionary 2026-09-11 12:03:13 +03:00
micialware adb6388ee2 Prepare for extended word edit 2026-09-11 11:47:04 +03:00
micialware 8fe4f3b8f4 New selected field movement 2026-09-11 00:41:36 +03:00
micialware 47bc83d4ae New edit process 2026-09-11 00:01:19 +03:00
micialware 097c3c033c Merge pull request 'reformat custom fields table. will return to this topic later' (#18) from compressed-fields into master
Reviewed-on: 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
37 changed files with 1997 additions and 413 deletions

No files matched your search

Generated
+782 -35
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -1,15 +1,15 @@
[package] [package]
name = "jap_learn" name = "jap_learn"
version = "0.1.3" version = "0.2.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
iced = { version = "0.14.0", features = ["tokio", "svg", "lazy", "crisp", "tiny-skia", "wayland", "wgpu"], default-features = false} iced = { version = "0.14.0", features = ["tokio", "svg", "lazy", "crisp", "tiny-skia", "wayland", "wgpu", "tester"], default-features = false}
iced_core = "0.14.0" iced_core = "0.14.0"
rand = "0.10.2" rand = "0.10.3"
dirs = "7.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.1", 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.5", features = ["json", "stream", "multipart"] } reqwest = { version = "0.13.5", features = ["json", "stream", "multipart"] }
@@ -27,7 +27,8 @@ parking_lot = "0.12.5"
env_logger = "0.11.11" env_logger = "0.11.11"
log = "0.4.34" log = "0.4.34"
fsrs = "6.6.2" fsrs = "6.6.2"
litemap = { version = "0.8.3", features = ["serde"] } litemap = { version = "0.8.3" }
remote_marker = {git = "https://gitea.micialware.ru/micialware/remote_marker"}
[profile.super-release] [profile.super-release]
inherits = "release" inherits = "release"
+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
+4
View File
@@ -1,5 +1,6 @@
use crate::data::repositories::Repository; use crate::data::repositories::Repository;
use crate::data_provider::card_states::CardStateRepository; use crate::data_provider::card_states::CardStateRepository;
use crate::data_provider::custom_fields::CustomFieldRepository;
use crate::data_provider::decks::DeckRepository; use crate::data_provider::decks::DeckRepository;
use crate::data_provider::history::HistoryRepository; use crate::data_provider::history::HistoryRepository;
use crate::data_provider::settings::get_setting; use crate::data_provider::settings::get_setting;
@@ -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,
@@ -104,6 +107,7 @@ pub fn fill_state(state: &mut AppState) {
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.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))
} }
+17 -2
View File
@@ -4,13 +4,16 @@ use crate::core::srs::{
FsrsModule, OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, FsrsModule, OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule,
WordOpenMode, WorstWordsSRSModule, WordOpenMode, WorstWordsSRSModule,
}; };
use crate::data::Id;
use crate::data::entities::*; use crate::data::entities::*;
use crate::data::repositories::Repository; use crate::data::repositories::Repository;
use crate::data_provider::card_states::CardStateRepositoryInner; use crate::data_provider::card_states::CardStateRepositoryInner;
use crate::notification::MaybeError; use crate::notification::MaybeError;
use chrono::Utc; use chrono::Utc;
use litemap::LiteMap;
use log::info; use log::info;
use rhai::{Engine, Scope}; use rhai::{Engine, Scope};
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
@@ -24,6 +27,11 @@ pub struct DeckData {
open_mode: OrderMode, open_mode: OrderMode,
settings: DeckSettings, settings: DeckSettings,
} }
struct DisplayStyle {
properties: LiteMap<String, String>,
}
impl DeckData { impl DeckData {
pub fn new(settings: &DeckSettings, open_mode: OrderMode, state: AppState) -> Self { pub fn new(settings: &DeckSettings, open_mode: OrderMode, state: AppState) -> Self {
let states_repo = state.card_states.read(); let states_repo = state.card_states.read();
@@ -154,10 +162,17 @@ 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 let group_name = groups[groups
+21 -4
View File
@@ -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 {
@@ -101,6 +101,9 @@ impl HistoryRepositoryInner {
count = 0; count = 0;
} }
} }
if count != 0 {
counts.push((date, count));
}
} }
counts counts
} }
@@ -130,8 +133,8 @@ impl DeckSettings {
DeckSettings { DeckSettings {
id: 0.into(), id: 0.into(),
name, name,
forward: "".to_string(), forward: String::new(),
backward: "".to_string(), backward: String::new(),
filter: "true".to_string(), filter: "true".to_string(),
} }
} }
@@ -196,9 +199,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> {
@@ -208,7 +223,9 @@ pub struct EntityHandle<E> {
} }
impl<T> Clone for EntityHandle<T> { impl<T> Clone for EntityHandle<T> {
fn clone(&self) -> Self { *self } fn clone(&self) -> Self {
*self
}
} }
impl<T> Copy for EntityHandle<T> {} impl<T> Copy for EntityHandle<T> {}
+4
View File
@@ -215,6 +215,10 @@ impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Qu
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> impl<T: EntityData + Clone + Send + Sync, R: Repository<T, D>, D: DbLayer<T>> Index<usize>
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::data::Id;
use crate::data::entities::CardState; use crate::data::entities::CardState;
use crate::data::repositories::{DataRepository, DataRepositoryInner, Query}; use crate::data::repositories::{DataRepository, DataRepositoryInner, Query};
use crate::data::Id;
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use chrono::NaiveDate; use chrono::NaiveDate;
use fsrs::MemoryState; use fsrs::MemoryState;
+17 -15
View File
@@ -1,41 +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 = pub type CustomFieldRepository = DataRepository<CustomField, CustomFieldDbLayer>;
DataRepository<CustomField, SqliteDbLayer<CustomField, CustomFieldsQuerySet>>; 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)" "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
@@ -48,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,
@@ -60,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,
+15 -2
View File
@@ -1,6 +1,7 @@
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 crate::pages::import::AnkiSchema;
use log::info; use log::info;
use rusqlite::Connection; use rusqlite::Connection;
use serde_json::Value; use serde_json::Value;
@@ -25,7 +26,17 @@ 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)?;
@@ -82,7 +93,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,
+1 -3
View File
@@ -107,9 +107,7 @@ 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| Q::from_sql(row))
.map_err(|e| {
error!("{}", e); error!("{}", e);
DbSaveError DbSaveError
})?; })?;
+1
View File
@@ -64,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(())
} }
+29 -3
View File
@@ -1,6 +1,8 @@
use crate::data::Id;
use crate::data::entities::WordData; use crate::data::entities::WordData;
use crate::data::repositories::{DataRepository, DataRepositoryInner}; use crate::data::repositories::{DataRepository, DataRepositoryInner};
use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet};
use litemap::LiteMap;
use rusqlite::{Error, Params, Row}; use rusqlite::{Error, Params, Row};
pub type WordRepository = DataRepository<WordData, WordDataLayer>; pub type WordRepository = DataRepository<WordData, WordDataLayer>;
@@ -32,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)?,
}) })
} }
@@ -42,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,
) )
} }
@@ -52,8 +54,32 @@ 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,
) )
} }
} }
fn additional_to_string(map: &LiteMap<Id, String>) -> String {
let mut parts = Vec::with_capacity(map.len() * 2);
for (id, value) in map {
parts.push(id.to_string());
parts.push(value.clone());
}
parts.join("")
}
fn additional_from_string(data: String) -> LiteMap<Id, String> {
if data.is_empty() {
return LiteMap::new();
}
let parts = data.split('').collect::<Vec<&str>>();
let chunks = parts.chunks(2).collect::<Vec<&[&str]>>();
let mut map = LiteMap::with_capacity(parts.len());
for part in &chunks {
let id: Id = part[0].parse().unwrap();
let value: String = part[1].to_string();
map.insert(id, value);
}
map
}
+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;
+8 -7
View File
@@ -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;
@@ -33,6 +32,8 @@ use iced::{Element, Task};
use iced_core::Length::Fill; use iced_core::Length::Fill;
use log::{debug, info}; use log::{debug, info};
use std::time::Instant; use std::time::Instant;
use chrono::Local;
use remote_marker::{alias, push};
impl Default for ScreenState { impl Default for ScreenState {
fn default() -> Self { fn default() -> Self {
@@ -56,7 +57,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 +78,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),
@@ -94,6 +93,9 @@ pub struct ScreenState {
impl ScreenState { impl ScreenState {
pub fn boot() -> (ScreenState, Task<RootMessage>) { pub fn boot() -> (ScreenState, Task<RootMessage>) {
let time = Local::now().timestamp_millis();
alias("frame", format!("frame_{}", time).as_str());
alias("update", format!("update_{}", time).as_str());
info!("Booting"); info!("Booting");
create_db(); create_db();
info!("Db created"); info!("Db created");
@@ -182,7 +184,6 @@ impl ScreenState {
Randomizer, Randomizer,
Repetitions, Repetitions,
Repetition, Repetition,
Word,
Sync, Sync,
History, History,
RepetitionSettings, RepetitionSettings,
@@ -190,7 +191,7 @@ impl ScreenState {
); );
} }
debug!("Update took {:?}", time.elapsed()); debug!("Update took {:?}", time.elapsed());
push("update", time.elapsed().as_nanos());
task.0 task.0
} }
@@ -206,7 +207,6 @@ impl ScreenState {
Randomizer, Randomizer,
Repetitions, Repetitions,
Repetition, Repetition,
Word,
Sync, Sync,
History, History,
RepetitionSettings, RepetitionSettings,
@@ -227,7 +227,6 @@ impl ScreenState {
Randomizer, Randomizer,
Repetitions, Repetitions,
Repetition, Repetition,
Word,
Sync, Sync,
History, History,
RepetitionSettings, RepetitionSettings,
@@ -237,6 +236,7 @@ impl ScreenState {
let notifications = self.notification_manager.draw().map(Notification); let notifications = self.notification_manager.draw().map(Notification);
debug!("View took {:?}", time.elapsed()); debug!("View took {:?}", time.elapsed());
push("frame", time.elapsed().as_nanos());
stack![view, notifications].width(Fill).height(Fill).into() stack![view, notifications].width(Fill).height(Fill).into()
} }
@@ -244,6 +244,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(),
}; };
} }
+7 -3
View File
@@ -138,8 +138,8 @@ impl NotificationManager {
fn draw_data<'a>(data: &NotificationData) -> Element<'a, NotificationMessage> { fn draw_data<'a>(data: &NotificationData) -> Element<'a, NotificationMessage> {
match data { match data {
NotificationData::DataLoaded => { NotificationData::DataSynced => {
text!("Данные успешно загружены на сервер") text!("Данные успешно синхронизированы")
} }
NotificationData::ImportError => { NotificationData::ImportError => {
text!("Ошибка при импорте данных из Anki") text!("Ошибка при импорте данных из Anki")
@@ -168,6 +168,9 @@ impl NotificationManager {
NotificationData::CompressError => { NotificationData::CompressError => {
text!("Ошибка сжатия или распаковки данных") text!("Ошибка сжатия или распаковки данных")
} }
NotificationData::LocalVersionChanged => {
text!("Локальная версия будет заменена")
}
} }
.width(Fill) .width(Fill)
.into() .into()
@@ -184,7 +187,7 @@ pub enum NotificationRequest {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum NotificationData { pub enum NotificationData {
PermissionDenied, PermissionDenied,
DataLoaded, DataSynced,
ImportError, ImportError,
IncorrectAnkiFile, IncorrectAnkiFile,
OpenFileError, OpenFileError,
@@ -193,6 +196,7 @@ pub enum NotificationData {
DbSaveError, DbSaveError,
DbLoadError, DbLoadError,
CompressError, CompressError,
LocalVersionChanged,
} }
pub type MaybeError = Result<(), NotificationData>; pub type MaybeError = Result<(), NotificationData>;
+322 -94
View File
@@ -5,25 +5,35 @@ use crate::data::repositories::{Query, Repository};
use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner}; 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_task;
use crate::styling::*; use crate::styling::*;
use crate::widgets::hard_mouse_area::hard_mouse_area;
use crate::{jl_button, push_error}; use crate::{jl_button, push_error};
use crate::{jl_text_input, push_error_task};
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::Status::Active;
use iced::widget::text_input::default; use iced::widget::text_input::default;
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::Length::{Fill, FillPortion};
use iced_core::border::Radius;
use iced_core::keyboard;
use iced_core::keyboard::Key::Named;
use iced_core::keyboard::key::Physical::Code;
use iced_core::keyboard::{Event, Modifiers};
use log::info; use log::info;
use rand::random_range; use rand::random_range;
use std::ops::Add; use std::ops::Add;
@@ -43,15 +53,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 +78,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 {
@@ -92,11 +124,7 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
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())));
} }
@@ -121,49 +149,11 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
dictionary.create(word); dictionary.create(word);
self.displayed_words_query.load(&dictionary); 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);
}
self.displayed_words_query.load(&dictionary);
} }
Include(i, b) => { Include(i, b) => {
if b { if b {
@@ -241,18 +231,21 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
} }
self.groups_query.load(&groups); 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();
@@ -261,11 +254,125 @@ impl NavigatedPage<DictionaryMessage> for DictionaryState {
let _ = dictionary.save(&mut handle); let _ = dictionary.save(&mut handle);
if reload_query { if reload_query {
self.displayed_words_query.load(&dictionary); self.displayed_words_query.load(&dictionary);
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()
@@ -277,14 +384,16 @@ 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)
self.filters(), .width(FillPortion(4)),
self.right_panel(),
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
.into(), .into(),
@@ -304,12 +413,14 @@ impl DictionaryState {
tag_map: HashMap::new(), tag_map: HashMap::new(),
words_tags: vec![], words_tags: vec![],
reverse: false, reverse: false,
search: "".to_string(), search: String::new(),
no_typing: true, no_typing: true,
reverse_list: true, reverse_list: true,
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();
@@ -318,6 +429,12 @@ impl DictionaryState {
result result
} }
fn editor_for_word(&self, handle: EntityHandle<WordData>) -> WordEditor<DictionaryMessage> {
WordEditor::new(handle, self.state.clone())
.on_delete(DeleteWord(handle))
.on_edit(ExtendedWordEdited)
}
fn init(&mut self) { fn init(&mut self) {
self.displayed_words_query = self self.displayed_words_query = self
.state .state
@@ -336,11 +453,11 @@ impl DictionaryState {
} }
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,
) )
} }
@@ -368,12 +485,22 @@ impl DictionaryState {
continue; continue;
} }
let mut selection = None;
if let Some(cell) = self.selected_cell.as_ref()
&& cell.0 == *word_handle
{
selection = Some(cell.1);
}
let word_line_data = WordLineState { let word_line_data = WordLineState {
is_included: self.included.contains(word_handle), is_included: self.included.contains(word_handle),
key: word.key.clone().into(), key: word.key.clone().into(),
value: word.value.clone().into(), value: word.value.clone().into(),
tags: word.tags.clone().into(), tags: word.tags.clone().into(),
handle: *word_handle, handle: *word_handle,
selection,
number: i,
}; };
let lazy_line = lazy(word_line_data, Self::word_line); let lazy_line = lazy(word_line_data, Self::word_line);
@@ -395,45 +522,51 @@ impl DictionaryState {
.on_toggle(move |b| Include(handle, b)), .on_toggle(move |b| Include(handle, b)),
); );
line = line.push( let field = |placeholder: &str, data: &str, selected: bool| {
text_input("Слово", &data.key) text_input(placeholder, data)
.size(ACCENT_FONT_SIZE) .size(ACCENT_FONT_SIZE)
.width(Fill) .width(Fill)
.on_input(move |string| SetKey(handle, string)) .style(move |x, _| {
.style(|x, status| { let mut default_style = default(x, Active);
let mut default_style = default(x, status);
default_style.border.radius = 0.0.into(); 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 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( line = line.push(
text_input("Перевод", &data.value) hard_mouse_area(field(
.size(ACCENT_FONT_SIZE) "Перевод",
.width(Fill) &data.value,
.on_input(move |string| SetValue(handle, string)) data.selection.is_some() && data.selection.unwrap() == Value,
.style(|x, status| { ))
let mut default_style = default(x, status); .on_press(SelectField(data.handle, Value, data.number)),
default_style.border.radius = 0.0.into();
default_style
}),
); );
line = line.push( line = line.push(
text_input("Теги", data.tags.as_ref()) hard_mouse_area(field(
.size(ACCENT_FONT_SIZE) "Теги",
.width(Fill) &data.tags,
.on_input(move |string| SetTags(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
}),
); );
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: &Theme, _status: button::Status| Style { .style(|_x: &Theme, _status: button::Status| Style {
background: None, background: None,
text_color: Color::BLACK, text_color: Color::BLACK,
@@ -445,18 +578,28 @@ 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();
iced::widget::column![ iced::widget::column![
text_input("Поиск", &self.search) jl_text_input!("Поиск", &self.search)
.on_input(Search) .on_input(Search)
.width(Fill), .width(Fill),
text!("Всего слов: {}", dict.len()), text!("Всего слов: {}", dict.len()),
@@ -473,7 +616,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()
} }
@@ -573,6 +716,41 @@ impl DictionaryState {
info!("Time {}", time.elapsed().as_micros()); info!("Time {}", time.elapsed().as_micros());
} }
fn selection_editor(&self) -> iced::Element<'_, DictionaryMessage> {
let mut display = String::new();
let mut editable = false;
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();
}
}
editable = true;
}
let mut field = jl_text_input!("", display.as_str())
.width(Fill)
.id(iced::widget::Id::new("editor"))
.style(|theme, status| {
let mut default = default(theme, status);
default.border.radius = Radius::new(0);
default
});
if editable {
field = field.on_input(EditField)
}
field.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));
@@ -595,7 +773,7 @@ impl DictionaryState {
scrollable(row).width(Fill).horizontal(), scrollable(row).width(Fill).horizontal(),
row![ row![
jl_button!("⇳").on_press(ChangeDirection), jl_button!("⇳").on_press(ChangeDirection),
text_input("Название группы слов", &group.name) jl_text_input!("Название группы слов", &group.name)
.on_input(EditGroup) .on_input(EditGroup)
.width(250) .width(250)
.on_submit(SaveGroup), .on_submit(SaveGroup),
@@ -626,6 +804,54 @@ 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())
@@ -636,8 +862,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,
} }
+7 -7
View File
@@ -1,7 +1,6 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::{EntityHandle, WordData}; use crate::data::entities::{EntityHandle, WordData};
use crate::data::repositories::Repository; 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 +8,12 @@ use crate::pages::dictionary::split_with_coma;
use crate::pages::dictionary_quiz::DictionaryQuizMessage::*; use crate::pages::dictionary_quiz::DictionaryQuizMessage::*;
use crate::pages::quiz::Score; use crate::pages::quiz::Score;
use crate::styling::*; use crate::styling::*;
use crate::{jl_button, jl_text_input};
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::{container, row, space, text, text_input, Row}; use iced::widget::{Row, container, row, space, text, text_input};
use iced::Background::Color; use iced::{Border, Element, Fill, Task, Theme, alignment};
use iced::{alignment, Border, Element, Fill, Task, Theme};
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
pub struct DictionaryQuizState { pub struct DictionaryQuizState {
@@ -76,7 +76,7 @@ impl NavigatedPage<DictionaryQuizMessage> for DictionaryQuizState {
] ]
.align_x(alignment::Horizontal::Center) .align_x(alignment::Horizontal::Center)
.spacing(5), .spacing(5),
text_input("Перевод", &self.answer) jl_text_input!("Перевод", &self.answer)
.size(28) .size(28)
.width(250) .width(250)
.on_input(AnswerChanged) .on_input(AnswerChanged)
@@ -114,9 +114,9 @@ impl DictionaryQuizState {
state, state,
words, words,
current_set: Vec::new(), current_set: Vec::new(),
answer: "".to_string(), answer: String::new(),
view: "---".to_string(), view: "---".to_string(),
correct: "".to_string(), correct: String::new(),
score: Default::default(), score: Default::default(),
is_help: false, is_help: false,
reverse, reverse,
+3 -5
View File
@@ -57,16 +57,14 @@ impl HistoryState {
id: Default::default(), id: Default::default(),
group_id: Default::default(), group_id: Default::default(),
key: "Удаленное слово".to_string(), key: "Удаленное слово".to_string(),
value: "".to_string(), value: String::new(),
tags: "".to_string(), tags: String::new(),
additional: Default::default(), additional: Default::default(),
}); });
(word, item.clone()) (word, item.clone())
}) })
.collect(); .collect();
Self { Self { list: words }
list: words,
}
} }
fn history_lines(&self) -> Column<'_, HistoryMessage> { fn history_lines(&self) -> Column<'_, HistoryMessage> {
+69 -17
View File
@@ -1,16 +1,17 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::WordData; use crate::data::Id;
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::repositories::{BatchedRepository, Repository};
use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups}; use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups};
use crate::fs::*; use crate::fs::*;
use crate::jl_button;
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::navigation::{NavigatedPage, Page, RootMessage};
use crate::notification::NotificationData::OpenFileError; use crate::notification::NotificationData::{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::styling::*; use crate::styling::*;
use crate::{jl_button, push_error_task};
use crate::{jl_text_input, push_error};
use iced::widget::button::danger; use iced::widget::button::danger;
use iced::widget::container::success; use iced::widget::container::success;
use iced::widget::{ use iced::widget::{
@@ -25,12 +26,14 @@ 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 zstd::Decoder;
const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"];
@@ -266,7 +269,7 @@ impl ImportState {
self.available_properties(), self.available_properties(),
button("Добавить напрямую").on_press(SetupDirect), button("Добавить напрямую").on_press(SetupDirect),
row![ row![
text_input("Пользовательское поле", &self.custom_property_name) jl_text_input!("Пользовательское поле", &self.custom_property_name)
.on_input(EditCustomProperty), .on_input(EditCustomProperty),
button("Установить привязку").on_press(SetupCustomProperty) button("Установить привязку").on_press(SetupCustomProperty)
] ]
@@ -294,7 +297,7 @@ impl ImportState {
.on_toggle(SwitchSkipEmpty), .on_toggle(SwitchSkipEmpty),
column![ column![
text!("Разделитель при множественном объединении"), text!("Разделитель при множественном объединении"),
text_input("", &self.separator).on_input(EditSeparator) jl_text_input!("", &self.separator).on_input(EditSeparator)
] ]
] ]
.spacing(DEFAULT_SPACING) .spacing(DEFAULT_SPACING)
@@ -326,7 +329,7 @@ impl ImportState {
import_data: None, import_data: None,
selected_index: 0, selected_index: 0,
selected_property: None, selected_property: None,
custom_property_name: "".to_string(), custom_property_name: String::new(),
skip_empty: true, skip_empty: true,
separator: ", ".to_string(), separator: ", ".to_string(),
progress: None, progress: None,
@@ -360,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
@@ -374,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)?;
@@ -385,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) {
@@ -428,7 +435,19 @@ 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 {
@@ -439,7 +458,6 @@ impl ImportState {
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,38 @@ 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,
} }
+3 -3
View File
@@ -1,10 +1,10 @@
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::*;
use crate::styling::*; use crate::styling::*;
use crate::{jl_button, jl_text_input};
use iced::widget::*; use iced::widget::*;
use iced::{Element, Fill, Task, alignment}; use iced::{Element, Fill, Task, alignment};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
@@ -77,7 +77,7 @@ impl NavigatedPage<QuizMessage> for QuizState {
] ]
.align_y(alignment::Vertical::Center) .align_y(alignment::Vertical::Center)
.spacing(20), .spacing(20),
text_input("Романдзи", &self.current_roman) jl_text_input!("Романдзи", &self.current_roman)
.size(28) .size(28)
.width(150) .width(150)
.on_input(ContentChanged), .on_input(ContentChanged),
@@ -113,7 +113,7 @@ impl QuizState {
kana: "ぁ".to_string(), kana: "ぁ".to_string(),
correct_roman: "a".to_string(), correct_roman: "a".to_string(),
is_help: false, is_help: false,
current_roman: "".to_string(), current_roman: String::new(),
set: KanaSet::hiragana(), set: KanaSet::hiragana(),
queue: Vec::new(), queue: Vec::new(),
score: Score { score: Score {
+5 -5
View File
@@ -262,21 +262,21 @@ impl RepetitionState {
} }
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).center().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).center().size(24).into(),
} }
} }
} }
+6 -6
View File
@@ -2,12 +2,12 @@ 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;
use crate::pages::repetition_settings::RepetitionSettingsMessage::*; use crate::pages::repetition_settings::RepetitionSettingsMessage::*;
use crate::styling::*; use crate::styling::*;
use crate::{jl_button, jl_text_input};
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::space::horizontal;
@@ -143,22 +143,22 @@ impl NavigatedPage<RepetitionSettingsMessage> for RepetitionSettingsState {
column![ column![
column![ column![
text!("Название набора"), text!("Название набора"),
text_input("", &self.settings_data.name).on_input(SetName), jl_text_input!("", &self.settings_data.name).on_input(SetName),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
column![ column![
text!("Передняя сторона"), text!("Передняя сторона"),
text_input("", &self.settings_data.forward).on_input(SetForward), jl_text_input!("", &self.settings_data.forward).on_input(SetForward),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
column![ column![
text!("Задняя сторона"), text!("Задняя сторона"),
text_input("", &self.settings_data.backward).on_input(SetBackward), jl_text_input!("", &self.settings_data.backward).on_input(SetBackward),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
column![ column![
text!("Фильтр"), text!("Фильтр"),
text_input("", &self.settings_data.filter) jl_text_input!("", &self.settings_data.filter)
.style(|x, status| { .style(|x, status| {
let mut primary = Theme::default()(x, status); let mut primary = Theme::default()(x, status);
if !self.correct_filter { if !self.correct_filter {
@@ -220,7 +220,7 @@ fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError {
.map(|w| w.id) .map(|w| w.id)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
filtered_words.iter().for_each(|word_id| { filtered_words.iter().for_each(|word_id| {
if let Some(index) = db_stats.iter().position(|stat_id| stat_id.id == *word_id) { if let Some(index) = db_stats.iter().position(|stat_id| stat_id.word_id == *word_id) {
db_stats.remove(index); db_stats.remove(index);
} }
}); });
+10 -9
View File
@@ -3,7 +3,6 @@ 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;
@@ -14,19 +13,20 @@ use crate::pages::repetitions::RepetitionsMessage::*;
use crate::push_error; use crate::push_error;
use crate::push_error_task; use crate::push_error_task;
use crate::styling::*; use crate::styling::*;
use crate::{jl_button, jl_text_input};
use chrono::{Days, Local, Utc}; use chrono::{Days, Local, Utc};
use hashbrown::HashSet; use hashbrown::HashSet;
use iced::widget::button::{danger, Status};
pub use iced::widget::button::{Catalog, Style}; pub use iced::widget::button::{Catalog, Style};
use iced::widget::button::{Status, danger};
use iced::widget::tooltip::Position::Top; use iced::widget::tooltip::Position::Top;
use iced::widget::{ use iced::widget::{
button, column, container, lazy, radio, row, scrollable, space, svg, text, text_input, tooltip, Column, Row, button, column, container, lazy, radio, row, scrollable, space, svg, text,
Column, Row, text_input, tooltip,
}; };
use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme}; use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme};
use iced_core::Padding;
use iced_core::border::Radius; use iced_core::border::Radius;
use iced_core::svg::Handle; use iced_core::svg::Handle;
use iced_core::Padding;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
@@ -96,6 +96,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
} else { } else {
self.fill_deck_word_indices(); self.fill_deck_word_indices();
} }
_ = self.state.history.write().reload()
} }
fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> { fn update(&mut self, message: RepetitionsMessage) -> Task<RootMessage> {
match message { match message {
@@ -302,7 +303,7 @@ impl RepetitionsState {
column![ column![
text!("Название набора"), text!("Название набора"),
row![ row![
text_input("", &deck.name).on_input(SetName), jl_text_input!("", &deck.name).on_input(SetName),
button(svg(Handle::from_memory(SETTINGS_ICON))) button(svg(Handle::from_memory(SETTINGS_ICON)))
.width(43) .width(43)
.on_press(GoToSettings), .on_press(GoToSettings),
@@ -315,17 +316,17 @@ impl RepetitionsState {
column![ column![
column![ column![
text!("Передняя сторона"), text!("Передняя сторона"),
text_input("", &deck.forward).on_input(SetForward), jl_text_input!("", &deck.forward).on_input(SetForward),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
column![ column![
text!("Задняя сторона"), text!("Задняя сторона"),
text_input("", &deck.backward).on_input(SetBackward), jl_text_input!("", &deck.backward).on_input(SetBackward),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
column![ column![
text!("Фильтр"), text!("Фильтр"),
text_input("", &deck.filter).on_input(SetFilter), jl_text_input!("", &deck.filter).on_input(SetFilter),
jl_button!("Проверить фильтр").on_press(TryFilter), jl_button!("Проверить фильтр").on_press(TryFilter),
self.count_view(deck), self.count_view(deck),
] ]
+1 -1
View File
@@ -13,7 +13,7 @@ use crate::pages::writing::WritingState;
use crate::styling::*; use crate::styling::*;
use iced::widget::button::{Status, Style}; use iced::widget::button::{Status, Style};
use iced::widget::*; use iced::widget::*;
use iced::{alignment, Element, Task}; 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;
+20 -1
View File
@@ -15,6 +15,7 @@ use iced::widget::{button, column, container, progress_bar, row, space, text, to
use iced::{Center, Element, Fill, Font, Length, Task}; use iced::{Center, Element, Fill, Font, Length, Task};
use log::error; use log::error;
use std::time::Duration; use std::time::Duration;
use crate::notification::NotificationData::LocalVersionChanged;
#[derive(Clone)] #[derive(Clone)]
pub enum SyncMessage { pub enum SyncMessage {
@@ -31,11 +32,13 @@ pub enum SyncMessage {
Disable, Disable,
DisableSync, DisableSync,
SwitchAutoSync, SwitchAutoSync,
ResetRealLoad,
} }
pub struct SyncState { pub struct SyncState {
state: AppState, state: AppState,
auto_fetch: bool, auto_fetch: bool,
frozen: bool, frozen: bool,
real_load: bool,
progress: f32, progress: f32,
} }
@@ -99,6 +102,18 @@ impl NavigatedPage<SyncMessage> for SyncState {
return tasks; return tasks;
} }
GetLast => { GetLast => {
if self.real_load == false {
self.real_load = true;
return Task::batch([
Task::future(async {
tokio::time::sleep(Duration::from_millis(3000)).await;
return RootMessage::Sync(ResetRealLoad);
}),
Task::done(RootMessage::PushNotification(NotificationRequest::Warning(LocalVersionChanged)))
]);
}
self.real_load = false;
let id = self.state.sync_data.read().key.clone().unwrap(); let id = self.state.sync_data.read().key.clone().unwrap();
let tasks = Task::batch([ let tasks = Task::batch([
Task::perform( Task::perform(
@@ -134,7 +149,7 @@ impl NavigatedPage<SyncMessage> for SyncState {
} }
return Task::done(RootMessage::PushNotification(NotificationRequest::Info( return Task::done(RootMessage::PushNotification(NotificationRequest::Info(
NotificationData::DataLoaded, NotificationData::DataSynced,
))); )));
} }
Disable => {} Disable => {}
@@ -166,6 +181,9 @@ impl NavigatedPage<SyncMessage> for SyncState {
return push_error_task!(err); return push_error_task!(err);
} }
} }
ResetRealLoad => {
self.real_load = false;
}
} }
Task::none() Task::none()
} }
@@ -189,6 +207,7 @@ impl SyncState {
auto_fetch, auto_fetch,
state, state,
frozen: false, frozen: false,
real_load: false,
progress: 0.0, progress: 0.0,
} }
} }
+126 -146
View File
@@ -1,174 +1,156 @@
use crate::app::AppState; use crate::app::AppState;
use crate::data::entities::{EntityHandle, WordData}; use crate::data::Id;
use crate::navigation::Page::PreviousPage; use crate::data::entities::{CustomField, EntityHandle, WordData};
use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::data::repositories::Repository;
use crate::jl_text_input;
use crate::pages::word::WordMessage::*; use crate::pages::word::WordMessage::*;
use crate::styling::*; use crate::styling::*;
use iced::widget::button::danger; use iced::widget::button::danger;
use iced::widget::{button, column, row, space, text, text_input}; use iced::widget::space::horizontal;
use iced::{Element, Fill, Task}; use iced::widget::{button, column, row, rule, scrollable, text};
use iced::{Element, Fill};
use iced_core::Padding;
pub struct WordState { pub struct WordEditor<Message> {
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.replace("", ""));
}
AddAdditional(key) => {
word.additional.insert(key, String::new());
}
RemoveAdditional(key) => {
word.additional.remove(&key);
}
}
self.on_edit.take()
}
pub(crate) fn view(&self) -> Element<'_, WordMessage> {
let mut fast_add = row![];
let word = self
.state
.dictionary
.read()
.get_static(self.handle)
.unwrap();
for field in &self.fields {
if !word.additional.contains_key(&field.id) {
fast_add = fast_add.push(
button(field.name.as_str())
.style(button::text)
.on_press(AddAdditional(field.id)),
);
} }
} }
fn navigated(&mut self) {} let mut col = iced::widget::column![
fn update(&mut self, message: WordMessage) -> Task<RootMessage> { column![
// match message { text!("Ключ"),
// Back => {} jl_text_input!("key", &word.key).on_input(SetKey),
// Save => { ]
// let mut state = self.state.lock(); .spacing(QUARTER_SPACING),
// state.dictionary[self.index] = self.word.clone(); column![
// text!("Значение"),
// if let Err(err) = update_word(&mut self.word, &state.connection) { jl_text_input!("value", &word.value).on_input(SetValue),
// return push_error_task!(err); ]
// } .spacing(QUARTER_SPACING),
// return Task::done(RootMessage::Word(Back)); column![
// } text!("Теги"),
// Delete => { jl_text_input!("tags", &word.tags).on_input(SetTags),
// let mut state = self.state.lock(); ]
// state.dictionary.remove(self.index); .spacing(QUARTER_SPACING),
// if let Err(err) = delete_word(&self.word, &state.connection) { rule::horizontal(2),
// return push_error_task!(err); scrollable(fast_add.padding(Padding {
// } top: 0.0,
// return Task::done(RootMessage::Word(Back)); right: 0.0,
// } bottom: 4.0,
// SetTags(n) => self.word.tags = n, left: 0.0,
// SetKey(n) => { }))
// self.word.key = n; .horizontal(),
// } ];
// SetValue(n) => {
// self.word.value = n; for more in &word.additional {
// } col = col.push(self.get_view_for_more(more));
// 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> { column![
// let mut fast_add = row![]; col.spacing(DEFAULT_SPACING / 2.0).width(Fill).height(Fill),
// if !self.word.additional.contains_key("reading") { row![
// fast_add = fast_add.push( horizontal().width(Fill),
// button("Чтение") button("Удалить").style(danger).on_press(Delete),
// .style(button::text) ]
// .on_press(AddAdditional("reading".to_string())), .spacing(DEFAULT_SPACING)
// ); ]
// } .spacing(DEFAULT_SPACING)
// if !self.word.additional.contains_key("description") { .into()
// fast_add = fast_add.push(
// button("Описание")
// .style(button::text)
// .on_press(AddAdditional("description".to_string())),
// );
// }
//
// if !self.word.additional.contains_key("context") {
// fast_add = fast_add.push(
// button("В контексте")
// .style(button::text)
// .on_press(AddAdditional("context".to_string())),
// );
// }
//
// let mut col = iced::widget::column![
// column![
// text!("Ключ"),
// text_input("key", &self.word.key).on_input(SetKey),
// ]
// .spacing(QUARTER_SPACING),
// column![
// text!("Значение"),
// text_input("value", &self.word.value).on_input(SetValue),
// ]
// .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 { state, handle } let fields = state
.custom_fields
.read()
.iter()
.cloned()
.collect::<Vec<_>>();
Self {
state,
handle,
on_delete: None,
on_edit: None,
fields,
} }
} }
impl WordState { pub fn on_delete(mut self, message: Message) -> Self {
fn get_view_for_more(&self, value: (&String, &String)) -> Element<'_, WordMessage> { self.on_delete = Some(message);
match value.0.as_str() { self
"reading" => self.reading_field(value.1), }
"description" => self.description_field(value.1), pub fn on_edit(mut self, message: Message) -> Self {
"context" => self.context_field(value.1), self.on_edit = Some(message);
_ => self.additional_field(value.1, value.0.clone(), value.0.clone()), self
} }
} }
fn reading_field(&self, value: &str) -> Element<'_, WordMessage> { impl<Message> WordEditor<Message> {
self.additional_field(value, "Чтение слова".to_string(), "reading".to_string()) fn get_view_for_more(&self, value: (&Id, &String)) -> Element<'_, WordMessage> {
let field = self.fields.iter().find(|f| f.id == *value.0).unwrap();
match value.0 {
_ => self.additional_field(value.1, field.name.as_str(), value.0.clone()),
}
} }
fn description_field(&self, value: &str) -> Element<'_, WordMessage> { fn additional_field(&self, value: &str, name: &str, id: Id) -> 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) jl_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)).style(danger),
.on_press(RemoveAdditional(id.clone()))
.style(danger),
] ]
.spacing(QUARTER_SPACING), .spacing(QUARTER_SPACING),
] ]
@@ -179,13 +161,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),
} }
+6 -6
View File
@@ -65,11 +65,11 @@ impl WritingState {
let mut list = set.list(); let mut list = set.list();
list.shuffle(&mut rand::rng()); list.shuffle(&mut rand::rng());
WritingState { WritingState {
kana: "".to_string(), kana: String::new(),
set: list.clone(), set: list.clone(),
original_set: list, original_set: list,
kana_total: "".to_string(), kana_total: String::new(),
roman_total: "".to_string(), roman_total: String::new(),
next_text: "Дальше".to_string(), next_text: "Дальше".to_string(),
show_all: false, show_all: false,
} }
@@ -79,9 +79,9 @@ impl WritingState {
impl WritingState { impl WritingState {
fn next(&mut self) { fn next(&mut self) {
if self.set.is_empty() { if self.set.is_empty() {
self.kana = "".to_string(); self.kana = String::new();
self.roman_total = "".to_string(); self.roman_total = String::new();
self.kana_total = "".to_string(); self.kana_total = String::new();
self.set = self.original_set.clone(); self.set = self.original_set.clone();
self.set.shuffle(&mut rand::rng()); self.set.shuffle(&mut rand::rng());
+16
View File
@@ -23,6 +23,15 @@ pub fn jl_button_style(theme: &Theme, status: Status) -> Style {
}; };
style style
} }
pub fn jl_text_input_style(
theme: &Theme,
status: iced::widget::text_input::Status,
) -> iced::widget::text_input::Style {
let mut style = iced::widget::text_input::default(theme, status);
style.border.radius = Radius::new(BUTTON_RADIUS);
style
}
pub fn cta_button(theme: &Theme, status: Status) -> Style { pub fn cta_button(theme: &Theme, status: Status) -> Style {
let mut style = secondary(theme, status); let mut style = secondary(theme, status);
style.border.width = 1.0; style.border.width = 1.0;
@@ -37,6 +46,13 @@ macro_rules! jl_button {
}; };
} }
#[macro_export]
macro_rules! jl_text_input {
($placeholder:expr, $content:expr) => {
iced::widget::text_input($placeholder, $content).style($crate::styling::jl_text_input_style)
};
}
pub fn back_overlay<'a, T: Clone + 'a>(content: Element<'a, T>, back_message: T) -> Element<'a, T> { pub fn back_overlay<'a, T: Clone + 'a>(content: Element<'a, T>, back_message: T) -> Element<'a, T> {
container( container(
iced::widget::column![jl_button!("Назад").on_press(back_message), content] iced::widget::column![jl_button!("Назад").on_press(back_message), content]
+453
View File
@@ -0,0 +1,453 @@
use iced::widget::MouseArea;
use iced_core::widget::{Operation, Tree, tree};
use iced_core::{
Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget,
layout, mouse, overlay, renderer, touch,
};
pub fn hard_mouse_area<'a, Message, Theme, Renderer>(
widget: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> HardMouseArea<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
HardMouseArea::new(widget)
}
pub struct HardMouseArea<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> {
content: Element<'a, Message, Theme, Renderer>,
on_press: Option<Message>,
on_release: Option<Message>,
on_double_click: Option<Message>,
on_right_press: Option<Message>,
on_right_release: Option<Message>,
on_middle_press: Option<Message>,
on_middle_release: Option<Message>,
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
on_enter: Option<Message>,
on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
on_exit: Option<Message>,
interaction: Option<mouse::Interaction>,
}
impl<'a, Message, Theme, Renderer> HardMouseArea<'a, Message, Theme, Renderer> {
/// The message to emit on a left button press.
#[must_use]
pub fn on_press(mut self, message: Message) -> Self {
self.on_press = Some(message);
self
}
/// The message to emit on a left button release.
#[must_use]
pub fn on_release(mut self, message: Message) -> Self {
self.on_release = Some(message);
self
}
/// The message to emit on a double click.
///
/// If you use this with [`on_press`]/[`on_release`], those
/// event will be emit as normal.
///
/// The events stream will be: on_press -> on_release -> on_press
/// -> on_double_click -> on_release -> on_press ...
///
/// [`on_press`]: Self::on_press
/// [`on_release`]: Self::on_release
#[must_use]
pub fn on_double_click(mut self, message: Message) -> Self {
self.on_double_click = Some(message);
self
}
/// The message to emit on a right button press.
#[must_use]
pub fn on_right_press(mut self, message: Message) -> Self {
self.on_right_press = Some(message);
self
}
/// The message to emit on a right button release.
#[must_use]
pub fn on_right_release(mut self, message: Message) -> Self {
self.on_right_release = Some(message);
self
}
/// The message to emit on a middle button press.
#[must_use]
pub fn on_middle_press(mut self, message: Message) -> Self {
self.on_middle_press = Some(message);
self
}
/// The message to emit on a middle button release.
#[must_use]
pub fn on_middle_release(mut self, message: Message) -> Self {
self.on_middle_release = Some(message);
self
}
/// The message to emit when scroll wheel is used
#[must_use]
pub fn on_scroll(mut self, on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a) -> Self {
self.on_scroll = Some(Box::new(on_scroll));
self
}
/// The message to emit when the mouse enters the area.
#[must_use]
pub fn on_enter(mut self, message: Message) -> Self {
self.on_enter = Some(message);
self
}
/// The message to emit when the mouse moves in the area.
#[must_use]
pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
self.on_move = Some(Box::new(on_move));
self
}
/// The message to emit when the mouse exits the area.
#[must_use]
pub fn on_exit(mut self, message: Message) -> Self {
self.on_exit = Some(message);
self
}
/// The [`mouse::Interaction`] to use when hovering the area.
#[must_use]
pub fn interaction(mut self, interaction: mouse::Interaction) -> Self {
self.interaction = Some(interaction);
self
}
}
/// Local state of the [`HardMouseArea`].
#[derive(Default)]
struct State {
is_hovered: bool,
bounds: Rectangle,
cursor_position: Option<Point>,
previous_click: Option<mouse::Click>,
}
impl<'a, Message, Theme, Renderer> HardMouseArea<'a, Message, Theme, Renderer> {
/// Creates a [`HardMouseArea`] with the given content.
pub fn new(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
HardMouseArea {
content: content.into(),
on_press: None,
on_release: None,
on_double_click: None,
on_right_press: None,
on_right_release: None,
on_middle_press: None,
on_middle_release: None,
on_scroll: None,
on_enter: None,
on_move: None,
on_exit: None,
interaction: None,
}
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for HardMouseArea<'_, Message, Theme, Renderer>
where
Renderer: renderer::Renderer,
Message: Clone,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if self.on_move.is_none()
&& let Event::Mouse(mouse::Event::CursorMoved { .. }) = event
{
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
if self.on_enter.is_none()
&& let Event::Mouse(mouse::Event::CursorEntered { .. }) = event
{
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
if self.on_exit.is_none()
&& let Event::Mouse(mouse::Event::CursorLeft { .. }) = event
{
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
if shell.is_event_captured() {
return;
}
update(self, tree, event, layout, cursor, shell);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let content_interaction = self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
);
match (self.interaction, content_interaction) {
(Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => {
interaction
}
_ => content_interaction,
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
renderer_style,
layout,
cursor,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<HardMouseArea<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
Theme: 'a,
Renderer: 'a + renderer::Renderer,
{
fn from(
area: HardMouseArea<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(area)
}
}
/// Processes the given [`Event`] and updates the [`State`] of an [`HardMouseArea`]
/// accordingly.
fn update<Message: Clone, Theme, Renderer>(
widget: &mut HardMouseArea<'_, Message, Theme, Renderer>,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) {
let state: &mut State = tree.state.downcast_mut();
let cursor_position = cursor.position();
let bounds = layout.bounds();
if state.cursor_position != cursor_position || state.bounds != bounds {
let was_hovered = state.is_hovered;
state.is_hovered = cursor.is_over(layout.bounds());
state.cursor_position = cursor_position;
state.bounds = bounds;
match (
widget.on_enter.as_ref(),
widget.on_move.as_ref(),
widget.on_exit.as_ref(),
) {
(Some(on_enter), _, _) if state.is_hovered && !was_hovered => {
shell.publish(on_enter.clone());
}
(_, Some(on_move), _) if state.is_hovered => {
if let Some(position) = cursor.position_in(layout.bounds()) {
shell.publish(on_move(position));
}
}
(_, _, Some(on_exit)) if !state.is_hovered && was_hovered => {
shell.publish(on_exit.clone());
}
_ => {}
}
}
if !cursor.is_over(layout.bounds()) {
return;
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(message) = widget.on_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
if let Some(position) = cursor_position
&& let Some(message) = widget.on_double_click.as_ref()
{
let new_click =
mouse::Click::new(position, mouse::Button::Left, state.previous_click);
if new_click.kind() == mouse::click::Kind::Double {
shell.publish(message.clone());
}
state.previous_click = Some(new_click);
// Even if this is not a double click, but the press is nevertheless
// processed by us and should not be popup to parent widgets.
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(message) = widget.on_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
if let Some(message) = widget.on_right_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => {
if let Some(message) = widget.on_right_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
if let Some(message) = widget.on_middle_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => {
if let Some(message) = widget.on_middle_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
if let Some(on_scroll) = widget.on_scroll.as_ref() {
shell.publish(on_scroll(*delta));
shell.capture_event();
}
}
_ => {}
}
}
+1
View File
@@ -0,0 +1 @@
pub mod hard_mouse_area;