use crate::data_provider::card_sets::{delete_set, update_card_set}; use crate::data_provider::card_stats::load_stats_of_set; use crate::history::HistoryState; use crate::lang::{SetOrderMode, WordData}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page}; use crate::repetition::RepetitionState; use crate::repetition_settings::RepetitionSettingsState; use crate::repetitions::RepetitionsMessage::*; use crate::styling::*; use crate::{AppState, RootMessage}; use chrono::{Days, Local}; use iced::widget::button::{danger, Status}; pub use iced::widget::button::{Catalog, Style}; use iced::widget::container::bordered_box; use iced::widget::tooltip::Position::Top; use iced::widget::{ button, column, container, radio, row, scrollable, space, svg, text, text_input, tooltip, Column, Row, }; use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme}; use iced_core::border::Radius; use iced_core::svg::Handle; use iced_core::Padding; use rhai::{Engine, Scope}; use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct RepetitionsState { selected_set: Option, correct_filters: Vec, pub state: Arc>, } impl NavigatedPage for RepetitionsState { fn navigate(&self, message: &RepetitionsMessage) -> Option { if let Back = message { Some(PreviousPage) } else if let GoToHistory = message { let card_set; { card_set = self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].id; } Some(History(HistoryState::new(card_set, self.state.clone()))) } else if let GoToRepetition = message { let clone = self.state.clone(); let card_set; { card_set = self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].clone(); } Some(Repetition(RepetitionState::new(card_set, clone))) } else if let GoToSettings = message { Some(RepetitionSettings(RepetitionSettingsState::new( self.selected_set.unwrap(), self.state.clone(), ))) } else { None } } } impl RepetitionsState { pub(crate) fn new(state: Arc>) -> RepetitionsState { let count = state.lock().unwrap().card_sets.len(); RepetitionsState { selected_set: None, correct_filters: vec![true; count], state, } } } impl RepetitionsState { pub fn update(&mut self, message: RepetitionsMessage) -> Task { let mut state = self.state.lock().unwrap(); match message { Next => {} Back => {} GoToRepetition => {} GoToHistory => {} GoToSettings => {} CreateSet => { let index = state.card_sets.len() + 1; state .card_sets .push(CardSetSettings::with_name(format!("Card set #{}", index))); self.correct_filters.push(true); } DeleteSet => { let set = state.card_sets.remove(self.selected_set.unwrap()); self.correct_filters.remove(self.selected_set.unwrap()); self.selected_set = None; delete_set(&set, &state.connection); } SelectSet(index) => { self.selected_set = Some(index); let mut set = state.card_sets.get(index).unwrap().clone(); set.update_worst_words(&state); state.card_sets[index] = set; } SetName(new) => { state.card_sets[self.selected_set.unwrap()].name = new; } Save => { for i in 0..state.card_sets.len() { self.correct_filters[i] = state.card_sets[i].check_filter(); } let set = &mut state.card_sets[self.selected_set.unwrap()].clone(); update_card_set(set, &state.connection); state.card_sets[self.selected_set.unwrap()] = set.clone(); } SetForward(new) => { state.card_sets[self.selected_set.unwrap()].forward = new; } SetBackward(new) => { state.card_sets[self.selected_set.unwrap()].backward = new; } SetFilter(new) => { state.card_sets[self.selected_set.unwrap()].filter = new; } TryFilter => { let set = state.card_sets.get(self.selected_set.unwrap()).unwrap(); let count = set.get_word_list(&state).len(); state.card_sets[self.selected_set.unwrap()].count = Some(count); } SetOpenMode(mode) => { state .card_sets .get_mut(self.selected_set.unwrap()) .unwrap() .open_mode = mode; } } Task::none() } pub fn view(&self) -> Element<'_, RepetitionsMessage> { back_overlay( row![ column![ scrollable(self.sets_list()).height(Fill), button("Добавить") .style(jl_button) .width(Fill) .on_press(CreateSet), ] .spacing(DEFAULT_SPACING) .width(Length::FillPortion(1)), self.selected_set_view(), ] .align_y(Center) .spacing(DEFAULT_SPACING) .width(Fill) .height(Fill) .into(), Back, ) } fn launch_delete_button(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { if set.id != 0 { return row![ space().width(Fill), button(text!("Начать повторение").width(200).center()) .style(cta_button) .on_press(GoToRepetition) ] .into(); } button("Удалить").style(danger).on_press(DeleteSet).into() } fn selected_set_view(&self) -> Element<'_, RepetitionsMessage> { if let Some(index) = self.selected_set { let set; { let state = self.state.lock().unwrap(); if state.card_sets.len() <= index { return space().width(Length::FillPortion(3)).into(); } set = state.card_sets[index].clone(); } return column![ scrollable( column![ column![ text!("Название набора"), row![ text_input("", &set.name).on_input(SetName), button(svg(Handle::from_memory(SETTINGS_ICON))) .width(43) .on_press(GoToSettings), ] .spacing(HALF_SPACING) ] .spacing(QUARTER_SPACING), { if set.id == 0 { column![ column![ text!("Передняя сторона"), text_input("", &set.forward).on_input(SetForward), ] .spacing(QUARTER_SPACING), column![ text!("Задняя сторона"), text_input("", &set.backward).on_input(SetBackward), ] .spacing(QUARTER_SPACING), column![ text!("Фильтр"), text_input("", &set.filter).on_input(SetFilter), button("Проверить фильтр") .style(jl_button) .on_press(TryFilter), self.count_view(&set), ] .spacing(QUARTER_SPACING), ] .spacing(DEFAULT_SPACING) } else { self.filled_set_data_view(&set) } }, radio( "Обычный режим", SetOrderMode::Default, Some(set.open_mode), SetOpenMode ), self.words_words_view(&set), button("История").style(jl_button).on_press(GoToHistory), ] .spacing(DEFAULT_SPACING) .padding(Padding { top: 0.0, right: DEFAULT_SPACING, bottom: 0.0, left: 0.0, }) ) .height(Fill), row![ button("Сохранить").style(jl_button).on_press(Save), self.launch_delete_button(&set) ] .spacing(DEFAULT_SPACING), ] .spacing(DEFAULT_SPACING) .width(Length::FillPortion(3)) .into(); } space().width(Length::FillPortion(3)).into() } fn filled_set_data_view(&self, set: &CardSetSettings) -> Column<'_, RepetitionsMessage> { column![self.activity_bar(set)].align_x(Center) .width(Fill) .spacing(DEFAULT_SPACING) .into() } fn activity_bar(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { let state = self.state.lock().unwrap(); let history = state.activity.get(&set.id); let mut counts: Vec = vec![0; 30 * 7]; let now = Local::now().date_naive(); if let Some(history) = history { for (date, activity) in history { let distance = *date - now; let index = counts.len() as i64 - distance.num_days() - 1; if index < 0 { continue; } counts[index as usize] = *activity; } } let mut row = Row::new() .padding(Padding { top: 0.0, right: 0.0, bottom: 0.0, left: DEFAULT_SPACING, }) .spacing(QUARTER_SPACING); const MAX_DAY_COUNT: f32 = 128.0; let mut iter = counts.into_iter(); for i in 0..30 { let mut column = Column::new().spacing(QUARTER_SPACING); for j in 0..7 { let value = iter.next().unwrap() as f32; let k = (value / MAX_DAY_COUNT).min(1.0) * 0.9 + 0.1; let date = now.clone().checked_sub_days(Days::new(30 * 7 - i * 7 - j - 1)).unwrap(); column = column.push(tooltip( iced::widget::container(space().height(15).width(15)).style( move |x: &Theme| container::Style { text_color: None, background: Some(Background::Color(x.palette().primary.scale_alpha(k))), border: Border { color: Default::default(), width: 0.0, radius: Radius::new(5), }, shadow: Default::default(), snap: false, }, ), text!("{} {} карточки", date.format("%Y.%m.%d"), value), Top, )); } row = row.push(column); } scrollable(column!["Карта активности", row].spacing(QUARTER_SPACING).align_x(Center)).into() } fn words_words_view(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { column![ text!("Худшие слова"), container(scrollable(self.worst_words_list(&set)).height(200)).style(bordered_box), radio( "Начать с плохих слов", SetOrderMode::TrainWorstFirst, Some(set.open_mode), SetOpenMode ), radio( "Полностью случайно", SetOrderMode::FullRandom, Some(set.open_mode), SetOpenMode ) ] .spacing(DEFAULT_SPACING) .into() } fn worst_words_list(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { let mut column = Column::new(); for word in set.worst_words_list.clone().unwrap() { column = column.push(text!("{} | {}", &word.key, &word.value)); } column.into() } fn count_view(&self, set: &CardSetSettings) -> Element<'_, RepetitionsMessage> { if let Some(count) = set.count { return text!("Количество слов: {}", count).into(); } space().into() } fn sets_list(&self) -> Column<'_, RepetitionsMessage> { let mut column = Column::new(); let mut i = 0; let sets = &self.state.lock().unwrap().card_sets; for set in sets { column = column.push( button(text!("{}", set.name.clone())) .on_press_with(move || SelectSet(i.clone())) .style(move |_x: &Theme, status: Status| Style { background: if status == Status::Hovered { Some(Background::Color(Color::WHITE.scale_alpha(0.2))) } else { None }, text_color: if self.correct_filters[i.clone()] { _x.palette().primary } else { _x.palette().warning }, border: Border { color: Default::default(), width: 0.0, radius: BUTTON_RADIUS.into(), }, shadow: Shadow::default(), snap: false, }), ); i += 1; } column } } #[derive(Clone)] pub enum RepetitionsMessage { Next, Back, GoToRepetition, CreateSet, DeleteSet, SetName(String), SelectSet(usize), Save, SetForward(String), SetBackward(String), SetFilter(String), TryFilter, SetOpenMode(SetOrderMode), GoToHistory, GoToSettings, } #[derive(Clone)] pub struct CardSetSettings { pub id: u32, pub name: String, pub forward: String, pub backward: String, pub filter: String, pub count: Option, pub worst_words_list: Option>, pub open_mode: SetOrderMode, } impl CardSetSettings { fn with_name(name: String) -> CardSetSettings { CardSetSettings { id: 0, name, forward: "".to_string(), backward: "".to_string(), filter: "true".to_string(), count: None, worst_words_list: None, open_mode: SetOrderMode::Default, } } fn check_filter(&self) -> bool { let engine = Engine::new(); let ast = engine.compile(&self.filter); ast.is_ok() } pub fn get_word_list(&self, state: &AppState) -> Vec { let mut list = vec![]; let engine = Engine::new(); let ast = engine.compile(&self.filter); if ast.is_err() { return list; } let ast = ast.unwrap(); let groups = &state.word_groups; for word in &state.dictionary { let mut more = rhai::Map::new(); for iced in &word.additional { more.insert(iced.0.clone().into(), iced.1.clone().into()); } let mut scope = Scope::new(); scope .push_constant("key", word.key.clone()) .push_constant("value", word.value.clone()) .push_constant("tags", word.tags.clone()) .push_constant("more", more) .push_constant( "group", groups .iter() .find(|g| g.id == word.group_id) .cloned() .unwrap() .name, ); let result = engine.eval_ast_with_scope::(&mut scope, &ast); if result.is_ok() && result.unwrap() { list.push(word.clone()); } } list } pub fn require_speech(&self) -> bool { self.forward == "speech" || self.backward == "speech" } fn update_worst_words(&mut self, state: &AppState) { if let Some(_) = self.worst_words_list { return; } let connection = &state.connection; let mut stats = load_stats_of_set(self, connection); stats.sort_by_key(|s| s.calculated_score() as i32); let avg = stats.iter().map(|s| s.calculated_score()).sum::() / stats.len() as f32; let avg = avg * 0.7; let bad: Vec = stats .iter() .take_while(|word| word.calculated_score() < avg) .map(|stat| { state.dictionary[state .dictionary .binary_search_by_key(&stat.word_id, |x| x.id) .unwrap()] .clone() }) .collect(); self.worst_words_list = Some(bad.clone()); } }