Files
jap_learn/src/pages/dictionary.rs
T

871 lines
29 KiB
Rust
Raw Normal View History

2026-08-24 18:46:54 +03:00
use crate::app::AppState;
2026-09-15 22:09:06 +03:00
use crate::data::Id;
2026-09-02 00:19:58 +03:00
use crate::data::entities::{EntityHandle, WordData, WordGroup};
use crate::data::repositories::{Query, Repository};
2026-09-07 18:00:50 +03:00
use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner};
2026-09-04 17:26:59 +03:00
use crate::data_provider::words::WordDataLayer;
use crate::data_provider::words::WordRepositoryInner;
2026-09-11 11:47:04 +03:00
use crate::navigation::Page::Import;
2026-09-11 00:41:36 +03:00
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
2026-09-11 11:47:04 +03:00
use crate::pages::dictionary::BasicWordFields::*;
2026-08-24 01:22:00 +03:00
use crate::pages::dictionary::DictionaryMessage::*;
2026-09-11 11:47:04 +03:00
use crate::pages::dictionary::SelectionChangeDirection::*;
2026-08-24 01:22:00 +03:00
use crate::pages::dictionary_quiz::DictionaryQuizState;
use crate::pages::import::ImportState;
2026-09-11 11:47:04 +03:00
use crate::pages::word::{WordEditor, WordMessage};
2026-08-24 18:46:54 +03:00
use crate::styling::*;
2026-09-11 00:01:19 +03:00
use crate::widgets::hard_mouse_area::hard_mouse_area;
2026-09-09 15:00:52 +03:00
use crate::{jl_button, push_error};
2026-09-15 22:09:06 +03:00
use crate::{jl_text_input, push_error_task};
2026-05-10 22:15:03 +03:00
use chrono::{DateTime, TimeDelta, Utc};
2026-08-18 17:31:19 +03:00
use hashbrown::{HashMap, HashSet};
2026-02-22 11:07:57 +03:00
use iced::alignment::Vertical::Center;
use iced::widget::button::Style;
2026-05-05 14:26:34 +03:00
use iced::widget::button::{danger, text};
2026-09-11 00:01:19 +03:00
use iced::widget::operation::focus;
2026-05-05 14:26:34 +03:00
use iced::widget::space::horizontal;
2026-09-11 11:47:04 +03:00
use iced::widget::text_input::Status::Active;
2026-09-15 22:09:06 +03:00
use iced::widget::text_input::default;
2026-02-22 11:07:57 +03:00
use iced::widget::*;
use iced::{Border, Color, Shadow, Task};
2026-09-15 22:09:06 +03:00
use iced_core::Length::{Fill, FillPortion};
use iced_core::border::Radius;
2026-09-11 00:41:36 +03:00
use iced_core::keyboard;
2026-09-15 22:09:06 +03:00
use iced_core::keyboard::Key::Named;
2026-09-11 00:41:36 +03:00
use iced_core::keyboard::key::Physical::Code;
2026-09-11 11:47:04 +03:00
use iced_core::keyboard::{Event, Modifiers};
2026-08-24 18:46:54 +03:00
use log::info;
2026-05-05 14:26:34 +03:00
use rand::random_range;
2026-05-10 22:15:03 +03:00
use std::ops::Add;
2026-08-20 00:07:09 +03:00
use std::sync::Arc;
2026-05-12 17:12:03 +03:00
use std::time::{Duration, Instant};
2026-02-22 11:07:57 +03:00
pub struct DictionaryState {
2026-08-31 11:46:35 +03:00
state: AppState,
2026-09-02 00:19:58 +03:00
included: HashSet<EntityHandle<WordData>>,
2026-02-22 11:07:57 +03:00
tag_map: HashMap<String, bool>,
2026-09-02 00:19:58 +03:00
words_tags: Vec<(EntityHandle<WordData>, Vec<String>)>,
reverse: bool,
search: String,
no_typing: bool,
2026-09-02 00:19:58 +03:00
selected_group: EntityHandle<WordGroup>,
2026-05-05 14:26:34 +03:00
reverse_list: bool,
2026-09-02 00:19:58 +03:00
auto_save_queue: HashMap<EntityHandle<WordData>, DateTime<Utc>>,
displayed_words_query: Query<WordData, WordRepositoryInner, WordDataLayer>,
groups_query: Query<WordGroup, WordGroupRepositoryInner, WordGroupDataLayer>,
2026-09-11 00:41:36 +03:00
selected_cell: Option<(EntityHandle<WordData>, BasicWordFields, usize)>,
2026-09-11 12:38:21 +03:00
word_editor: Option<WordEditor<DictionaryMessage>>,
2026-02-22 11:07:57 +03:00
}
2026-09-02 00:19:58 +03:00
#[derive(Clone)]
2026-02-22 11:07:57 +03:00
pub enum DictionaryMessage {
Back,
2026-09-11 11:47:04 +03:00
ExtendedEdit(EntityHandle<WordData>),
DeleteWord(EntityHandle<WordData>),
2026-02-22 11:07:57 +03:00
NewWord,
2026-09-02 00:19:58 +03:00
Include(EntityHandle<WordData>, bool),
2026-02-22 11:07:57 +03:00
IncludeTag(String, bool),
2026-09-02 00:19:58 +03:00
GotoTest,
2026-02-22 11:07:57 +03:00
ResetTags,
SetReverse(bool),
Search(String),
SetTyping(bool),
2026-05-05 14:26:34 +03:00
CreateGroup,
EditGroup(String),
SaveGroup,
2026-09-02 00:19:58 +03:00
SelectGroup(EntityHandle<WordGroup>),
2026-05-05 14:26:34 +03:00
DeleteGroup,
ChangeDirection,
2026-09-02 00:19:58 +03:00
TrySave(EntityHandle<WordData>),
GotoImport,
2026-09-11 00:41:36 +03:00
SelectField(EntityHandle<WordData>, BasicWordFields, usize),
2026-09-11 00:01:19 +03:00
EditField(String),
2026-09-11 00:41:36 +03:00
ChangeSelectedField(SelectionChangeDirection),
2026-09-11 11:47:04 +03:00
EditorAction(WordMessage),
2026-09-15 22:09:06 +03:00
ExtendedWordEdited,
2026-09-11 00:41:36 +03:00
}
#[derive(Clone, PartialEq, Eq, Hash, Copy)]
pub enum SelectionChangeDirection {
Next,
Previous,
Forward,
Backward,
2026-09-11 00:01:19 +03:00
}
#[derive(Clone, PartialEq, Eq, Hash, Copy)]
pub enum BasicWordFields {
Key,
Value,
Tags,
2026-02-22 11:07:57 +03:00
}
2026-09-11 00:41:36 +03:00
const FIELDS: &[BasicWordFields] = &[Key, Value, Tags];
2026-02-22 11:07:57 +03:00
impl NavigatedPage<DictionaryMessage> for DictionaryState {
2026-08-18 12:14:35 +03:00
fn navigate(&mut self, message: &DictionaryMessage) -> Option<Page> {
2026-02-22 11:07:57 +03:00
if let Back = message {
return Some(Page::PreviousPage);
}
2026-09-02 00:19:58 +03:00
if let GotoTest = message
&& !self.included.is_empty()
2026-08-15 13:22:59 +03:00
{
let mut words = vec![];
2026-09-02 00:19:58 +03:00
let dict = &self.state.dictionary.read();
2026-08-15 13:22:59 +03:00
2026-09-09 14:26:55 +03:00
let query = dict.query(Arc::new(|_| true));
2026-09-04 11:18:23 +03:00
let handles = query.handles();
2026-09-02 00:19:58 +03:00
words = handles.filter(|h| self.included.contains(h)).collect();
2026-08-15 13:22:59 +03:00
return Some(Page::DictionaryQuiz(DictionaryQuizState::new(
words,
self.reverse,
self.no_typing,
2026-09-07 18:00:50 +03:00
self.state.clone(),
2026-08-15 13:22:59 +03:00
)));
2026-02-22 11:07:57 +03:00
}
2026-09-11 11:47:04 +03:00
2026-09-02 00:19:58 +03:00
if let GotoImport = message {
return Some(Import(ImportState::new(self.state.clone())));
2026-08-13 22:38:48 +03:00
}
2026-02-22 11:07:57 +03:00
None
}
2026-08-06 23:20:14 +03:00
fn navigated(&mut self) {
2026-09-02 00:19:58 +03:00
self.included = Default::default();
self.tag_map = Default::default();
2026-09-09 10:33:21 +03:00
let groups = self.state.word_groups.read();
self.groups_query.load(&groups);
drop(groups);
self.update_tags();
}
2026-08-13 22:03:47 +03:00
fn update(&mut self, message: DictionaryMessage) -> Task<RootMessage> {
2026-02-22 11:07:57 +03:00
match message {
2026-08-01 19:36:43 +03:00
NewWord => {
2026-09-02 00:19:58 +03:00
let mut dictionary = self.state.dictionary.write();
2026-05-05 14:26:34 +03:00
let mut word = WordData::new();
2026-09-02 00:19:58 +03:00
word.group_id = self.selected_group.id;
dictionary.create(word);
2026-09-07 18:19:54 +03:00
self.displayed_words_query.load(&dictionary);
2026-02-22 11:07:57 +03:00
}
2026-09-11 11:47:04 +03:00
ExtendedEdit(i) => {
2026-09-11 12:38:21 +03:00
self.word_editor = Some(self.editor_for_word(i));
2026-09-11 13:09:20 +03:00
if let Some((handle, cell, index)) = self.selected_cell.clone() {
self.selected_cell = Some((i, cell, index))
}
2026-02-22 11:07:57 +03:00
}
2026-09-02 00:19:58 +03:00
Include(i, b) => {
if b {
self.included.insert(i);
} else {
self.included.remove(&i);
2026-05-10 22:15:03 +03:00
}
2026-09-02 00:19:58 +03:00
}
IncludeTag(t, v) => {
2026-02-22 11:07:57 +03:00
self.tag_map.insert(t, v);
2026-09-02 00:19:58 +03:00
self.update_words_include(self.selected_group.id)
2026-02-22 11:07:57 +03:00
}
2026-04-13 22:25:58 +03:00
2026-08-01 19:36:43 +03:00
ResetTags => {
2026-02-22 11:07:57 +03:00
self.tag_map.iter_mut().for_each(|(_, v)| *v = false);
2026-09-02 00:19:58 +03:00
self.included.clear();
2026-02-22 11:07:57 +03:00
}
2026-08-01 19:36:43 +03:00
SetReverse(v) => self.reverse = v,
Search(s) => {
self.search = s;
}
2026-08-01 19:36:43 +03:00
SetTyping(b) => self.no_typing = b,
2026-05-05 14:26:34 +03:00
Back => {}
2026-09-02 00:19:58 +03:00
GotoTest => {}
2026-08-01 19:36:43 +03:00
CreateGroup => {
2026-09-02 00:19:58 +03:00
let mut groups = self.state.word_groups.write();
groups.create(WordGroup {
2026-08-18 12:47:06 +03:00
id: 0.into(),
2026-05-05 14:26:34 +03:00
name: format!("Группа слов {}", random_range(100..1000)),
});
2026-09-07 18:19:54 +03:00
self.groups_query.load(&groups);
2026-05-05 14:26:34 +03:00
}
EditGroup(new) => {
2026-09-02 00:19:58 +03:00
let mut groups = self.state.word_groups.write();
let group = groups.get_mut(&mut self.selected_group);
2026-05-05 14:26:34 +03:00
if let Some(group) = group {
2026-09-02 00:19:58 +03:00
group.name = new;
2026-05-05 14:26:34 +03:00
}
}
SaveGroup => {
2026-09-07 18:19:54 +03:00
{
let mut groups = self.state.word_groups.write();
2026-05-05 14:26:34 +03:00
2026-09-07 18:19:54 +03:00
if let Err(err) = groups.save(&mut self.selected_group) {
return push_error_task!(err);
}
self.groups_query.load(&groups);
2026-08-24 18:46:54 +03:00
}
2026-09-07 18:19:54 +03:00
self.setup_words_query();
2026-05-05 14:26:34 +03:00
}
2026-08-01 19:36:43 +03:00
SelectGroup(i) => {
2026-09-02 00:19:58 +03:00
self.selected_group = i;
2026-09-07 18:19:54 +03:00
self.setup_words_query();
2026-09-02 00:19:58 +03:00
self.update_words_include(i.id)
2026-05-05 14:26:34 +03:00
}
DeleteGroup => {
2026-09-02 00:19:58 +03:00
if self.selected_group.id.0 == 1 {
2026-05-05 14:26:34 +03:00
return Task::none();
}
{
2026-09-02 00:19:58 +03:00
let delete_group = self.selected_group;
2026-09-07 18:19:54 +03:00
self.selected_group = EntityHandle::from_id(1.into());
self.setup_words_query();
let mut groups = self.state.word_groups.write();
2026-09-02 00:19:58 +03:00
if let Err(err) = groups.delete(delete_group) {
return push_error_task!(err);
}
2026-08-24 18:46:54 +03:00
2026-09-02 00:19:58 +03:00
if let Err(err) = self.state.dictionary.write().reload() {
return push_error_task!(err);
}
2026-09-07 18:19:54 +03:00
self.groups_query.load(&groups);
2026-05-05 14:26:34 +03:00
}
2026-09-11 13:12:45 +03:00
self.selected_cell = None;
self.word_editor = None;
self.update_tags()
2026-05-05 14:26:34 +03:00
}
ChangeDirection => {
self.reverse_list = !self.reverse_list;
2026-05-12 16:57:05 +03:00
}
2026-09-02 00:19:58 +03:00
TrySave(mut handle) => {
2026-09-11 12:38:21 +03:00
let original = handle;
2026-05-10 22:15:03 +03:00
let now = Utc::now();
2026-09-02 00:19:58 +03:00
if !self.auto_save_queue.contains_key(&handle) {
2026-05-12 16:57:05 +03:00
return Task::none();
}
2026-09-02 00:19:58 +03:00
if self.auto_save_queue[&handle] <= now {
2026-09-11 13:09:20 +03:00
self.update_tags();
2026-09-02 00:19:58 +03:00
self.auto_save_queue.remove(&handle);
let mut dictionary = self.state.dictionary.write();
2026-08-24 18:46:54 +03:00
2026-09-09 14:26:55 +03:00
let reload_query = !handle.real();
2026-09-07 18:00:50 +03:00
let _ = dictionary.save(&mut handle);
2026-09-07 18:19:54 +03:00
if reload_query {
self.displayed_words_query.load(&dictionary);
2026-09-15 22:09:06 +03:00
if let Some(selected) = self.selected_cell
&& selected.0 == original
{
2026-09-11 12:38:21 +03:00
self.selected_cell = Some((handle, selected.1, selected.2));
2026-09-11 13:09:20 +03:00
if self.word_editor.is_some() {
self.word_editor = Some(self.editor_for_word(handle));
}
2026-09-11 12:38:21 +03:00
}
2026-09-07 18:19:54 +03:00
}
2026-09-11 00:01:19 +03:00
info!("Saved word");
2026-09-02 00:19:58 +03:00
return Task::none();
2026-05-10 22:15:03 +03:00
}
2026-05-05 14:26:34 +03:00
}
2026-09-02 00:19:58 +03:00
GotoImport => {}
2026-09-11 00:41:36 +03:00
SelectField(e, f, index) => {
2026-09-11 00:01:19 +03:00
info!("Editing word {}", e.id);
2026-09-15 22:09:06 +03:00
if self.word_editor.is_some()
&& let Some(selected) = &self.selected_cell
&& selected.0 != e
{
2026-09-11 12:38:21 +03:00
self.word_editor = Some(self.editor_for_word(e));
2026-09-11 12:03:13 +03:00
}
2026-09-11 00:41:36 +03:00
self.selected_cell = Some((e, f, index));
2026-09-11 12:03:13 +03:00
2026-09-11 00:01:19 +03:00
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 {
2026-09-11 11:47:04 +03:00
Key => {
2026-09-11 00:01:19 +03:00
word.key = value;
}
2026-09-11 11:47:04 +03:00
Value => {
2026-09-11 00:01:19 +03:00
word.value = value;
}
2026-09-11 11:47:04 +03:00
Tags => {
2026-09-11 12:48:04 +03:00
word.tags = value;
2026-09-11 00:01:19 +03:00
}
}
}
return self.launch_auto_save_offset(selected.0);
}
}
2026-09-11 00:41:36 +03:00
ChangeSelectedField(change) => {
2026-09-11 11:47:04 +03:00
return if let Some(selected) = self.selected_cell {
2026-09-11 00:41:36 +03:00
let update_vertical = move |mut dir: bool| {
if self.reverse_list {
dir = !dir;
}
2026-09-11 11:47:04 +03:00
let new_index = if dir {
selected.2.saturating_add(1)
} else {
selected.2.saturating_sub(1)
};
2026-09-11 00:41:36 +03:00
if new_index < self.displayed_words_query.len() {
2026-09-11 11:47:04 +03:00
return SelectField(
self.displayed_words_query.get(new_index).unwrap(),
selected.1,
new_index,
);
2026-09-11 00:41:36 +03:00
}
SelectField(selected.0, selected.1, selected.2)
};
let update_horizontal = move |dir: bool| {
let current = selected.1;
let delta = if dir { 1 } else { -1 };
2026-09-11 11:47:04 +03:00
let mut index =
FIELDS.iter().position(|&f| f == current).unwrap() as i8 + delta;
2026-09-11 00:41:36 +03:00
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 {
2026-09-11 11:47:04 +03:00
Next => update_vertical(true),
Previous => update_vertical(false),
Forward => update_horizontal(true),
Backward => update_horizontal(false),
2026-09-11 00:41:36 +03:00
}))
} else {
Task::none()
2026-09-11 11:47:04 +03:00
};
}
DeleteWord(i) => {
let mut dictionary = self.state.dictionary.write();
if let Err(err) = dictionary.delete(i) {
return push_error_task!(err);
2026-09-11 00:41:36 +03:00
}
2026-09-11 11:47:04 +03:00
self.displayed_words_query.load(&dictionary);
2026-09-15 22:09:06 +03:00
self.selected_cell = None;
self.word_editor = None;
2026-09-11 11:47:04 +03:00
}
EditorAction(action) => {
2026-09-11 12:38:21 +03:00
let result = self.word_editor.as_mut().unwrap().update(action);
if let Some(result) = result {
match result {
2026-09-15 22:09:06 +03:00
DeleteWord(_) | ExtendedWordEdited => {
2026-09-11 12:38:21 +03:00
return Task::done(RootMessage::Dictionary(result));
2026-09-15 22:09:06 +03:00
}
2026-09-11 12:38:21 +03:00
_ => {}
}
}
}
ExtendedWordEdited => {
return self.launch_auto_save_offset(self.selected_cell.unwrap().0);
2026-09-11 00:41:36 +03:00
}
2026-02-22 11:07:57 +03:00
}
Task::none()
}
2026-08-13 22:38:48 +03:00
2026-08-13 22:03:47 +03:00
fn view(&self) -> iced::Element<'_, DictionaryMessage> {
back_overlay(
row![
iced::widget::column![
self.groups_panel(),
row![horizontal().width(8), self.words_list(),],
2026-09-11 11:47:04 +03:00
self.selection_editor(),
row![
2026-08-19 23:37:59 +03:00
self.add_word_button(),
horizontal().width(Fill),
2026-09-02 00:19:58 +03:00
button("Импорт").style(text).on_press(GotoImport),
2026-08-13 22:38:48 +03:00
]
2026-08-13 22:03:47 +03:00
]
2026-09-15 22:09:06 +03:00
.spacing(5)
.width(FillPortion(4)),
2026-09-11 11:47:04 +03:00
self.right_panel(),
2026-08-13 22:03:47 +03:00
]
2026-08-13 22:38:48 +03:00
.spacing(DEFAULT_SPACING)
.into(),
2026-08-13 22:03:47 +03:00
Back,
)
}
}
impl DictionaryState {
2026-08-31 11:46:35 +03:00
pub fn new(state: AppState) -> Self {
2026-09-02 00:19:58 +03:00
let words_query = state.dictionary.read().empty_query_par();
let groups_query = state.word_groups.read().query(Arc::new(|_| true));
2026-08-13 22:03:47 +03:00
let mut result = DictionaryState {
2026-09-02 00:19:58 +03:00
included: Default::default(),
selected_group: EntityHandle::new(Id(1), 0, true),
2026-08-13 22:03:47 +03:00
state,
tag_map: HashMap::new(),
2026-09-02 00:19:58 +03:00
words_tags: vec![],
2026-08-13 22:03:47 +03:00
reverse: false,
search: "".to_string(),
no_typing: true,
reverse_list: true,
auto_save_queue: HashMap::new(),
2026-09-02 00:19:58 +03:00
displayed_words_query: words_query,
groups_query,
2026-09-11 00:01:19 +03:00
selected_cell: None,
2026-09-11 11:47:04 +03:00
word_editor: None,
2026-08-13 22:03:47 +03:00
};
2026-09-02 00:19:58 +03:00
result.init();
2026-08-13 22:03:47 +03:00
result.update_tags();
result
}
2026-02-22 11:07:57 +03:00
2026-09-11 12:38:21 +03:00
fn editor_for_word(&self, handle: EntityHandle<WordData>) -> WordEditor<DictionaryMessage> {
2026-09-15 22:09:06 +03:00
WordEditor::new(handle, self.state.clone())
.on_delete(DeleteWord(handle))
.on_edit(ExtendedWordEdited)
2026-09-11 12:38:21 +03:00
}
2026-09-02 00:19:58 +03:00
fn init(&mut self) {
2026-09-07 18:00:50 +03:00
self.displayed_words_query = self
.state
.dictionary
.read()
2026-09-09 11:23:51 +03:00
.query(Arc::new(|word| word.group_id == Id(1)));
2026-09-02 00:19:58 +03:00
}
2026-08-24 18:46:54 +03:00
2026-09-07 18:19:54 +03:00
fn setup_words_query(&mut self) {
let id = self.selected_group.id;
self.displayed_words_query = self
.state
.dictionary
.read()
2026-09-09 11:23:51 +03:00
.query(Arc::new(move |word| word.group_id == id));
2026-05-10 22:15:03 +03:00
}
2026-09-02 00:19:58 +03:00
fn launch_auto_save_offset(&mut self, handle: EntityHandle<WordData>) -> Task<RootMessage> {
2026-09-11 00:01:19 +03:00
let save_time = Utc::now().add(TimeDelta::milliseconds(500));
2026-09-02 00:19:58 +03:00
self.auto_save_queue.insert(handle, save_time);
let message = RootMessage::Dictionary(TrySave(handle));
2026-05-12 16:57:05 +03:00
Task::perform(
2026-09-11 00:01:19 +03:00
async { tokio::time::sleep(Duration::from_millis(520)).await },
2026-05-12 16:57:05 +03:00
|_| message,
)
2026-05-10 22:15:03 +03:00
}
2026-02-22 11:07:57 +03:00
fn words_list(&self) -> iced::Element<'_, DictionaryMessage> {
2026-09-07 18:00:50 +03:00
let time = Instant::now();
let mut col = Column::new().width(Fill);
let query = &self.displayed_words_query;
let dictionary = self.state.dictionary.read();
for access_index in 0..query.len() {
let mut i = access_index;
if self.reverse_list {
i = query.len() - access_index - 1;
}
let word_handle = &query[i];
let word = dictionary.get_ref_static(*word_handle).unwrap();
if !self.search.is_empty()
&& !word.key.contains(&self.search)
&& !word.value.contains(&self.search)
&& !word.tags.contains(&self.search)
{
continue;
}
2026-09-11 00:01:19 +03:00
let mut selection = None;
2026-09-11 11:47:04 +03:00
if let Some(cell) = self.selected_cell.as_ref()
&& cell.0 == *word_handle
{
selection = Some(cell.1);
2026-09-11 00:01:19 +03:00
}
2026-09-07 18:00:50 +03:00
let word_line_data = WordLineState {
is_included: self.included.contains(word_handle),
key: word.key.clone().into(),
value: word.value.clone().into(),
tags: word.tags.clone().into(),
2026-09-09 14:26:55 +03:00
handle: *word_handle,
2026-09-11 00:01:19 +03:00
selection,
2026-09-11 00:41:36 +03:00
number: i,
2026-09-07 18:00:50 +03:00
};
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()
}
2026-09-08 00:09:13 +03:00
fn word_line<'a>(data: &WordLineState) -> Row<'a, DictionaryMessage> {
let mut line = Row::new().width(Fill).align_y(Center);
2026-09-09 14:26:55 +03:00
let handle = data.handle;
line = line.push(
checkbox(data.is_included)
.label("")
.spacing(15)
2026-09-07 18:00:50 +03:00
.on_toggle(move |b| Include(handle, b)),
);
2026-09-08 00:09:13 +03:00
2026-09-11 00:01:19 +03:00
let field = |placeholder: &str, data: &str, selected: bool| {
text_input(placeholder, data)
.size(ACCENT_FONT_SIZE)
.width(Fill)
2026-09-11 00:01:19 +03:00
.style(move |x, _| {
let mut default_style = default(x, Active);
default_style.border.radius = 0.0.into();
2026-09-11 00:01:19 +03:00
if selected {
default_style.border.color = x.extended_palette().secondary.base.color;
2026-09-11 11:47:04 +03:00
default_style.border.width = 3.0_f32;
2026-09-11 00:01:19 +03:00
}
default_style
2026-09-11 00:01:19 +03:00
})
};
line = line.push(
hard_mouse_area(field(
"Слово",
&data.key,
2026-09-11 11:47:04 +03:00
data.selection.is_some() && data.selection.unwrap() == Key,
2026-09-11 00:01:19 +03:00
))
2026-09-11 11:47:04 +03:00
.on_press(SelectField(data.handle, Key, data.number)),
);
line = line.push(
2026-09-11 00:01:19 +03:00
hard_mouse_area(field(
"Перевод",
&data.value,
2026-09-11 11:47:04 +03:00
data.selection.is_some() && data.selection.unwrap() == Value,
2026-09-11 00:01:19 +03:00
))
2026-09-11 11:47:04 +03:00
.on_press(SelectField(data.handle, Value, data.number)),
);
2026-08-04 14:09:40 +03:00
line = line.push(
2026-09-11 00:01:19 +03:00
hard_mouse_area(field(
"Теги",
&data.tags,
2026-09-11 11:47:04 +03:00
data.selection.is_some() && data.selection.unwrap() == Tags,
2026-09-11 00:01:19 +03:00
))
2026-09-11 11:47:04 +03:00
.on_press(SelectField(data.handle, Tags, data.number)),
);
2026-02-22 11:07:57 +03:00
let line_button = || {
if !data.handle.real() {
return button("-")
2026-09-11 12:38:21 +03:00
.on_press(DeleteWord(data.handle))
2026-09-09 15:00:52 +03:00
.style(|_x: &Theme, _status: button::Status| Style {
background: None,
text_color: Color::BLACK,
border: Border::default(),
shadow: Shadow::default(),
snap: false,
})
.width(15);
}
2026-02-22 11:07:57 +03:00
button("")
2026-09-11 11:47:04 +03:00
.on_press(ExtendedEdit(data.handle))
.style(button::primary)
.width(15)
};
line.push(line_button()).push(space().width(10))
2026-02-22 11:07:57 +03:00
}
2026-09-11 11:47:04 +03:00
fn right_panel(&self) -> iced::Element<'_, DictionaryMessage> {
if let Some(editor) = &self.word_editor {
2026-09-15 22:09:06 +03:00
container(editor.view().map(EditorAction))
.width(FillPortion(3))
.into()
2026-09-11 11:47:04 +03:00
} else {
self.filters()
}
}
2026-02-22 11:07:57 +03:00
fn filters(&self) -> iced::Element<'_, DictionaryMessage> {
2026-09-02 00:19:58 +03:00
let dict = &self.state.dictionary.read();
2026-04-09 22:45:31 +03:00
2026-02-22 11:07:57 +03:00
iced::widget::column![
2026-09-15 22:09:06 +03:00
jl_text_input!("Поиск", &self.search)
2026-08-01 19:36:43 +03:00
.on_input(Search)
.width(Fill),
2026-04-09 22:45:31 +03:00
text!("Всего слов: {}", dict.len()),
2026-09-07 18:00:50 +03:00
text!("Выбрано слов: {}", self.included.iter().count()),
2026-02-22 11:07:57 +03:00
self.tags_selector(),
toggler(self.no_typing)
.label("Без набора")
2026-08-01 19:36:43 +03:00
.on_toggle(SetTyping),
toggler(self.reverse)
.label("Обратный тест")
2026-08-01 19:36:43 +03:00
.on_toggle(SetReverse),
button(text!("Тест").center().width(Fill))
2026-08-04 14:09:40 +03:00
.style(cta_button)
2026-09-02 00:19:58 +03:00
.on_press(GotoTest)
.width(Fill),
2026-02-22 11:07:57 +03:00
]
2026-09-11 11:47:04 +03:00
.width(FillPortion(1))
2026-08-01 17:23:43 +03:00
.spacing(DEFAULT_SPACING)
.into()
2026-02-22 11:07:57 +03:00
}
fn tags_selector(&self) -> iced::Element<'_, DictionaryMessage> {
let mut col = Column::new().width(Fill);
2026-02-22 11:07:57 +03:00
col = col.push(
button("Сбросить")
2026-08-01 19:36:43 +03:00
.on_press(ResetTags)
2026-02-22 11:07:57 +03:00
.style(|x: &Theme, _status| Style {
background: None,
text_color: x.palette().primary,
border: Border::default(),
shadow: Shadow::default(),
snap: false,
}),
);
let mut sorted_tags = self.tag_map.iter().collect::<Vec<_>>();
sorted_tags.sort();
for tag in sorted_tags {
2026-02-22 11:07:57 +03:00
col = col.push(
checkbox(*tag.1)
.label(tag.0)
2026-08-01 19:36:43 +03:00
.on_toggle(|x1| IncludeTag(tag.0.clone(), x1)),
2026-02-22 11:07:57 +03:00
)
}
container(scrollable(col)).height(Fill).into()
2026-02-22 11:07:57 +03:00
}
fn update_tags(&mut self) {
2026-09-09 12:12:44 +03:00
self.words_tags.clear();
2026-05-12 16:57:05 +03:00
let mut tags_list: HashSet<String> = HashSet::new();
2026-09-02 00:19:58 +03:00
let dict = &self.state.dictionary.write();
2026-09-09 12:12:44 +03:00
let mut words_query = dict.full_query();
let handles = words_query.handles().collect::<Vec<_>>();
2026-09-09 15:00:52 +03:00
words_query
.iter(dict)
.zip(handles)
.for_each(|(element, handle)| {
let tags = split_with_coma(element.tags.as_str());
tags.iter().for_each(|tag| {
2026-05-12 16:57:05 +03:00
tags_list.insert(tag.clone());
});
2026-09-09 15:00:52 +03:00
self.words_tags.push((handle, tags))
});
2026-02-22 11:07:57 +03:00
let current_tags = self
.tag_map
.keys()
.map(|k| k.clone().to_string())
.collect::<Vec<String>>();
2026-05-12 16:57:05 +03:00
current_tags
.iter()
.filter(|tag| !tags_list.contains(*tag))
.for_each(|tag| {
self.tag_map.remove(tag);
});
tags_list.iter().for_each(|tag| {
self.tag_map.insert(tag.clone(), false);
});
2026-02-22 11:07:57 +03:00
}
2026-08-24 11:35:37 +03:00
fn update_words_include(&mut self, group_id: Id) {
2026-02-22 11:07:57 +03:00
let include_tags = self
.tag_map
.iter()
2026-05-12 16:57:05 +03:00
.filter(|(_, value)| **value)
2026-02-22 11:07:57 +03:00
.map(|(t, _)| t.clone())
.collect::<Vec<String>>();
if include_tags.is_empty() {
2026-09-02 00:19:58 +03:00
self.included.clear();
2026-02-22 11:07:57 +03:00
return;
}
2026-09-02 00:19:58 +03:00
let dict = &self.state.dictionary.read();
2026-05-12 17:12:03 +03:00
let time = Instant::now();
2026-04-09 22:45:31 +03:00
2026-09-02 00:19:58 +03:00
self.included.clear();
for (handle, tags) in &mut self.words_tags {
2026-09-07 18:00:50 +03:00
if let Some(word) = dict.get_ref(handle) {
2026-09-02 00:19:58 +03:00
if word.group_id != group_id {
continue;
}
2026-09-07 18:00:50 +03:00
if !tags.is_empty() && tags.iter().all(|t| include_tags.contains(t)) {
2026-09-02 00:19:58 +03:00
self.included.insert(*handle);
}
2026-09-07 18:00:50 +03:00
} else {
2026-09-02 00:19:58 +03:00
continue;
}
}
2026-05-12 17:12:03 +03:00
2026-08-23 22:48:00 +03:00
info!("Time {}", time.elapsed().as_micros());
2026-02-22 11:07:57 +03:00
}
2026-05-05 14:26:34 +03:00
2026-09-11 11:47:04 +03:00
fn selection_editor(&self) -> iced::Element<'_, DictionaryMessage> {
let mut display = "".to_string();
2026-09-15 22:09:06 +03:00
let mut editable = false;
2026-09-11 11:47:04 +03:00
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();
}
}
2026-09-15 22:09:06 +03:00
editable = true;
2026-09-11 11:47:04 +03:00
}
2026-09-15 22:09:06 +03:00
let mut field = jl_text_input!("", display.as_str())
2026-09-11 11:47:04 +03:00
.width(Fill)
.id(iced::widget::Id::new("editor"))
2026-09-15 22:09:06 +03:00
.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()
2026-09-11 11:47:04 +03:00
}
2026-05-05 14:26:34 +03:00
fn groups_panel(&self) -> iced::Element<'_, DictionaryMessage> {
let mut row = Row::new();
2026-08-01 19:43:07 +03:00
row = row.push(button("+").style(text).on_press(CreateGroup));
2026-05-05 14:26:34 +03:00
2026-09-02 00:19:58 +03:00
let groups = &self.state.word_groups.read();
2026-05-05 14:26:34 +03:00
2026-09-04 11:18:23 +03:00
for mut group in self.groups_query.handles() {
2026-09-02 00:19:58 +03:00
let group_name = groups.get(&mut group).unwrap().name.clone();
2026-05-05 14:26:34 +03:00
row = row.push(
2026-09-02 00:19:58 +03:00
button(text!("{}", group_name))
2026-05-05 14:26:34 +03:00
.style(text)
2026-09-02 00:19:58 +03:00
.on_press(SelectGroup(group)),
2026-05-05 14:26:34 +03:00
);
}
2026-09-02 00:19:58 +03:00
let groups = self.state.word_groups.read();
2026-09-09 14:26:55 +03:00
let group = groups.get_static(self.selected_group).unwrap();
2026-05-05 14:26:34 +03:00
iced::widget::column![
scrollable(row).width(Fill).horizontal(),
2026-05-05 14:26:34 +03:00
row![
2026-09-09 14:26:55 +03:00
jl_button!("⇳").on_press(ChangeDirection),
2026-09-15 22:09:06 +03:00
jl_text_input!("Название группы слов", &group.name)
2026-05-05 14:26:34 +03:00
.on_input(EditGroup)
.width(250)
.on_submit(SaveGroup),
horizontal(),
self.group_delete_button(),
2026-05-05 14:26:34 +03:00
]
2026-08-01 23:40:56 +03:00
.spacing(DEFAULT_SPACING / 2.0)
2026-05-05 14:26:34 +03:00
]
2026-08-01 17:23:43 +03:00
.into()
2026-05-05 14:26:34 +03:00
}
fn group_delete_button(&self) -> iced::Element<'_, DictionaryMessage> {
2026-09-02 00:19:58 +03:00
if self.selected_group.real() && self.selected_group.id != 1.into() {
button("Удалить").style(danger).on_press(DeleteGroup).into()
} else {
space().into()
}
}
2026-08-19 23:37:59 +03:00
fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> {
2026-09-09 14:26:55 +03:00
let button = jl_button!("Добавить слово");
2026-08-19 23:37:59 +03:00
2026-09-02 00:19:58 +03:00
if self.selected_group.real() {
2026-08-19 23:51:41 +03:00
return button.on_press(NewWord).into();
2026-08-19 23:37:59 +03:00
}
button.into()
}
2026-02-22 11:07:57 +03:00
}
2026-09-11 00:41:36 +03:00
impl KeyPressedPage for DictionaryState {
fn press(&mut self, message: &Event) -> Task<RootMessage> {
if let Event::KeyPressed {
key: _,
modified_key: modifier,
physical_key: pk,
location: _,
2026-09-11 11:47:04 +03:00
modifiers: mods,
2026-09-11 00:41:36 +03:00
text: _,
repeat: _,
} = message
&& let Code(code) = pk
{
return match code {
2026-09-11 11:47:04 +03:00
keyboard::key::Code::ArrowDown => {
Task::done(RootMessage::Dictionary(ChangeSelectedField(Next)))
}
keyboard::key::Code::ArrowUp => {
Task::done(RootMessage::Dictionary(ChangeSelectedField(Previous)))
}
2026-09-11 00:41:36 +03:00
keyboard::key::Code::Tab => {
2026-09-15 22:09:06 +03:00
return if *mods == Modifiers::CTRL
&& let Some(selected) = self.selected_cell
{
2026-09-11 11:47:04 +03:00
Task::done(RootMessage::Dictionary(ExtendedEdit(selected.0)))
} else if let Named(keyboard::key::Named::Alt) = modifier {
Task::done(RootMessage::Dictionary(ChangeSelectedField(Backward)))
2026-09-11 00:41:36 +03:00
} else {
2026-09-11 11:47:04 +03:00
Task::done(RootMessage::Dictionary(ChangeSelectedField(Forward)))
2026-09-15 22:09:06 +03:00
};
2026-09-11 11:47:04 +03:00
}
keyboard::key::Code::Escape => {
if self.word_editor.is_some() {
self.word_editor = None;
2026-09-15 22:09:06 +03:00
} else if self.selected_cell.is_some() {
2026-09-11 11:47:04 +03:00
self.selected_cell = None;
2026-09-11 00:41:36 +03:00
}
2026-09-11 11:47:04 +03:00
Task::none()
2026-09-11 00:41:36 +03:00
}
_ => Task::none(),
};
}
Task::none()
}
}
2026-05-12 16:57:05 +03:00
pub fn split_with_coma(ts: &str) -> Vec<String> {
2026-02-22 11:07:57 +03:00
ts.split(',')
.map(|ts| ts.to_lowercase().trim().to_string())
.filter(|t| !t.is_empty())
.collect::<Vec<String>>()
}
#[derive(Clone, Hash, PartialEq, Eq)]
2026-08-13 22:38:48 +03:00
struct WordLineState {
2026-08-09 14:24:59 +03:00
is_included: bool,
2026-09-11 12:48:04 +03:00
key: String,
value: String,
tags: String,
2026-09-02 00:19:58 +03:00
handle: EntityHandle<WordData>,
2026-09-11 00:01:19 +03:00
selection: Option<BasicWordFields>,
2026-09-11 00:41:36 +03:00
number: usize,
2026-08-13 22:38:48 +03:00
}