Files
jap_learn/src/pages/dictionary.rs
T

830 lines
28 KiB
Rust
Raw Normal View History

2026-08-24 18:46:54 +03:00
use crate::app::AppState;
2026-08-24 11:35:37 +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-08-24 18:46:54 +03:00
use crate::navigation::Page::{Import, Word};
2026-09-11 00:41:36 +03:00
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
2026-08-24 01:22:00 +03:00
use crate::pages::dictionary::DictionaryMessage::*;
use crate::pages::dictionary_quiz::DictionaryQuizState;
use crate::pages::import::ImportState;
use crate::pages::word::WordState;
2026-08-24 18:46:54 +03:00
use crate::push_error_task;
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-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 00:01:19 +03:00
use iced::widget::text_input::Status::Active;
2026-08-01 19:43:07 +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};
use iced_core::Length::Fill;
2026-09-11 00:41:36 +03:00
use iced_core::keyboard;
use iced_core::keyboard::Event;
use iced_core::keyboard::Key::Named;
use iced_core::keyboard::key::Physical::Code;
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-09-11 00:41:36 +03:00
use crate::pages::dictionary::BasicWordFields::*;
use crate::pages::dictionary::SelectionChangeDirection::*;
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-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-02 00:19:58 +03:00
SetTags(EntityHandle<WordData>, String),
SetKey(EntityHandle<WordData>, String),
SetValue(EntityHandle<WordData>, String),
WordAction(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),
}
#[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-02 00:19:58 +03:00
if let WordAction(handle) = message
&& handle.real()
{
return Some(Word(WordState::new(*handle, self.state.clone())));
2026-04-17 12:38:05 +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-04-13 22:25:58 +03:00
2026-09-02 00:19:58 +03:00
SetKey(handle, v) => {
let mut dictionary = self.state.dictionary.write();
dictionary.get_mut_static(handle).unwrap().key = v;
drop(dictionary);
return self.launch_auto_save_offset(handle);
2026-04-09 22:45:31 +03:00
}
2026-09-02 00:19:58 +03:00
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);
2026-04-13 22:25:58 +03:00
}
2026-08-01 19:36:43 +03:00
SetTags(i, mut v) => {
2026-04-09 22:45:31 +03:00
{
2026-09-02 00:19:58 +03:00
let mut dictionary = self.state.dictionary.write();
2026-05-25 12:23:32 +03:00
2026-09-02 00:19:58 +03:00
let word = dictionary.get_mut_static(i).unwrap();
let current_tags_value = word.tags.clone();
2026-05-25 12:23:32 +03:00
if v.ends_with(", ") && v.len() < current_tags_value.len() {
v = v[..v.len() - 2].to_string()
}
2026-08-19 18:00:03 +03:00
while let Some(index) = v.find(",,") {
2026-05-25 12:23:32 +03:00
v.remove(index);
}
2026-09-02 00:19:58 +03:00
word.tags = v;
2026-04-09 22:45:31 +03:00
}
2026-02-22 11:07:57 +03:00
self.update_tags();
2026-05-12 16:57:05 +03:00
return self.launch_auto_save_offset(i);
2026-02-22 11:07:57 +03:00
}
2026-04-13 22:25:58 +03:00
2026-08-01 19:36:43 +03:00
WordAction(i) => {
2026-09-02 00:19:58 +03:00
let mut dictionary = self.state.dictionary.write();
if let Err(err) = dictionary.delete(i) {
2026-08-24 18:46:54 +03:00
return push_error_task!(err);
}
2026-09-07 18:19:54 +03:00
self.displayed_words_query.load(&dictionary);
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
}
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-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 {
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-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-11 00:41:36 +03:00
self.selected_cell = Some((e, f, index));
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 {
BasicWordFields::Key => {
word.key = value;
}
BasicWordFields::Value => {
word.value = value;
}
BasicWordFields::Tags => {
word.key = value;
}
}
}
return self.launch_auto_save_offset(selected.0);
}
}
2026-09-11 00:41:36 +03:00
ChangeSelectedField(change) => {
return if let Some(selected) = self.selected_cell.clone() {
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()
}
}
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 00:01:19 +03:00
{
let mut display = "".to_string();
if let Some(selected) = self.selected_cell.as_ref() {
let mut dictionary = self.state.dictionary.write();
let word = dictionary.get_mut_static(selected.0).unwrap();
match selected.1 {
BasicWordFields::Key => {
display = word.key.clone();
}
BasicWordFields::Value => {
display = word.value.clone();
}
BasicWordFields::Tags => {
display = word.tags.clone();
}
}
}
text_input("", display.as_str())
.width(Fill)
.id(iced::widget::Id::new("editor"))
.on_input(EditField)
},
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
]
.spacing(5),
self.filters(),
]
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-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-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;
if let Some(cell) = self.selected_cell.as_ref() {
if cell.0 == *word_handle {
selection = Some(cell.1);
}
}
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;
default_style.border.width = 3.0_f32.into();
}
default_style
2026-09-11 00:01:19 +03:00
})
};
line = line.push(
hard_mouse_area(field(
"Слово",
&data.key,
data.selection.is_some() && data.selection.unwrap() == BasicWordFields::Key,
))
2026-09-11 00:41:36 +03:00
.on_press(SelectField(data.handle, BasicWordFields::Key, data.number)),
);
line = line.push(
2026-09-11 00:01:19 +03:00
hard_mouse_area(field(
"Перевод",
&data.value,
data.selection.is_some() && data.selection.unwrap() == BasicWordFields::Value,
))
2026-09-11 00:41:36 +03:00
.on_press(SelectField(
data.handle,
BasicWordFields::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,
data.selection.is_some() && data.selection.unwrap() == BasicWordFields::Tags,
))
2026-09-11 00:41:36 +03:00
.on_press(SelectField(data.handle, BasicWordFields::Tags, data.number)),
);
2026-02-22 11:07:57 +03:00
let line_button = || {
if !data.handle.real() {
return button("-")
.on_press(WordAction(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("")
.on_press(WordAction(data.handle))
.style(button::primary)
.width(15)
};
line.push(line_button()).push(space().width(10))
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-08-01 01:22:26 +03:00
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-08-01 17:23:43 +03:00
.width(250)
.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
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-08-01 01:22:26 +03:00
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: _,
modifiers: _,
text: _,
repeat: _,
} = message
&& let Code(code) = pk
{
return match code {
keyboard::key::Code::ArrowDown => Task::done(RootMessage::Dictionary(
ChangeSelectedField(SelectionChangeDirection::Next),
)),
keyboard::key::Code::ArrowUp => Task::done(RootMessage::Dictionary(
ChangeSelectedField(SelectionChangeDirection::Previous),
)),
keyboard::key::Code::Tab => {
if let Named(keyboard::key::Named::Alt) = modifier {
Task::done(RootMessage::Dictionary(ChangeSelectedField(
SelectionChangeDirection::Backward,
)))
} else {
Task::done(RootMessage::Dictionary(ChangeSelectedField(
SelectionChangeDirection::Forward,
)))
}
}
_ => 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-02 00:19:58 +03:00
key: Box<str>,
value: Box<str>,
tags: Box<str>,
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
}