use crate::data_provider::card_sets::update_deck; use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck}; use crate::history::HistoryState; use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode}; 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, Utc}; use hashbrown::{HashMap, HashSet}; pub use iced::widget::button::{Catalog, Style}; use iced::widget::button::{Status, danger}; use iced::widget::tooltip::Position::Top; use iced::widget::{ Column, Row, button, column, container, lazy, radio, row, scrollable, space, svg, text, text_input, tooltip, }; use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme}; use iced_core::Padding; use iced_core::border::Radius; use iced_core::svg::Handle; use std::ops::Deref; use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct RepetitionsState { word_id_index_map: HashMap, selected_deck_index: Option, decks: Vec, state: Arc>, } impl NavigatedPage for RepetitionsState { fn navigate(&mut self, message: &RepetitionsMessage) -> Option { if let Back = message { Some(PreviousPage) } else if let GoToHistory = message { Some(History(HistoryState::new( self.selected_deck().unwrap().id, self.state.clone(), ))) } else if let GoToRepetition = message { let clone = self.state.clone(); let selected_set = self.selected_deck_mut().unwrap(); if selected_set.append_mode == AppendMode::Full { Self::append_all_words(&clone.lock().unwrap(), selected_set); selected_set.existing_words_indices = selected_set.available_words_indices.clone(); } else { if selected_set .available_words_indices .as_ref() .unwrap() .is_empty() || selected_set .existing_words_indices .as_ref() .unwrap() .is_empty() { return None; } } Some(Repetition(RepetitionState::new( selected_set.general_settings.clone(), clone, ))) } else if let GoToSettings = message { Some(RepetitionSettings(RepetitionSettingsState::new( self.selected_deck().unwrap().index.unwrap(), self.state.clone(), ))) } else { None } } fn navigated(&mut self) { let index = self.selected_deck_index.unwrap(); let state = self.state.lock().unwrap(); if let Some(global_deck) = state.decks.get(index) { if global_deck.id != self.selected_deck().unwrap().id { drop(state); self.decks.remove(index); self.clear_selection(); } } else { drop(state); self.decks.remove(index); self.clear_selection() } } fn update(&mut self, message: RepetitionsMessage) -> Task { match message { Next => {} Back => {} GoToRepetition => {} GoToHistory => {} GoToSettings => {} CreateSet => { let index = self.decks.len() + 1; self.decks.push(DeckViewData { general_settings: DeckSettings::with_name(format!("Колода #{}", index)), existing_words_indices: None, available_words_indices: None, append_mode: Default::default(), index: None, }); } DeleteSet => { let index = self.selected_deck_index.unwrap(); self.clear_selection(); let deck = self.decks.remove(index); if deck.id.is_valid() { let mut state = self.state.lock().unwrap(); state.decks.remove(index); self.decks = self .decks .clone() .into_iter() .map(|mut deck| { deck.index = state.decks.iter().position(|d| d.id == deck.id); deck }) .collect(); } } SetName(value) => { let deck = self.selected_deck_mut().unwrap(); deck.general_settings.name = value; } SelectSet(index) => { self.select_deck(index); let deck = self.selected_deck().unwrap(); if deck.existing_words_indices.is_none() { let state = self.state.lock().unwrap(); let added: Vec<_> = load_stats_of_deck(deck, &state.connection) .iter() .map(|c| self.word_id_index_map[&c.word_id]) .collect(); let total = deck.get_word_list(&state); drop(state); let deck = self.selected_deck_mut().unwrap(); deck.existing_words_indices = Some(added); deck.available_words_indices = Some(total); } } Save => { let mut deck = self.selected_deck().unwrap().clone(); let mut state = self.state.lock().unwrap(); { update_deck(&mut deck.general_settings, &state.connection); if let Some(index) = deck.index { state.decks[index] = deck.general_settings.clone(); } else { state.decks.push(deck.general_settings.clone()); deck.index = Some(state.decks.len() - 1); self.decks[self.selected_deck_index.unwrap()] = deck.clone(); } } } SetForward(value) => { let deck = self.selected_deck_mut().unwrap(); deck.general_settings.forward = value; } SetBackward(value) => { let deck = self.selected_deck_mut().unwrap(); deck.general_settings.backward = value; } SetFilter(value) => { let deck = self.selected_deck_mut().unwrap(); deck.general_settings.filter = value; } TryFilter => { let state = self.state.lock().unwrap(); let deck = self.selected_deck().unwrap(); let count = deck.get_word_list(&state).len(); drop(state); let deck = self.selected_deck_mut().unwrap(); deck.general_settings.count = Some(count); } SetOpenMode(mode) => { let deck = self.selected_deck_mut().unwrap(); deck.general_settings.open_mode = mode; } SetAppendMode(mode) => { let deck = self.selected_deck_mut().unwrap(); deck.append_mode = mode; } AppendWords(count) => { let adding: Vec; { { let deck = self.selected_deck().unwrap(); let existing_words_indices = deck.existing_words_indices.as_ref().unwrap(); let mut created_set = HashSet::with_capacity(existing_words_indices.len()); existing_words_indices.iter().for_each(|c| { created_set.insert(c); }); adding = deck .available_words_indices .as_ref() .unwrap() .iter() .filter(|c| !created_set.contains(c)) .take(count) .cloned() .collect(); } let deck = self.selected_deck_mut().unwrap(); deck.existing_words_indices .as_mut() .unwrap() .append(&mut adding.clone()); } let deck = self.selected_deck().unwrap(); let state = self.state.lock().unwrap(); Self::append_words(&state, &deck.general_settings, adding.into_iter()); } } Task::none() } fn view(&self) -> Element<'_, RepetitionsMessage> { back_overlay( row![ column![ scrollable(self.decks_list()).height(Fill), button("Добавить") .style(jl_button) .width(Fill) .on_press(CreateSet), ] .spacing(DEFAULT_SPACING) .width(Length::FillPortion(1)), self.selected_deck_view(), ] .align_y(Center) .spacing(DEFAULT_SPACING) .width(Fill) .height(Fill) .into(), Back, ) } } impl RepetitionsState { pub(crate) fn new(state: Arc>) -> RepetitionsState { let state_ = state.lock().unwrap(); let mut map = HashMap::with_capacity(state_.dictionary.len()); state_ .dictionary .iter() .enumerate() .for_each(|(index, word)| { map.insert(word.id, index); }); let decks = Self::get_decks_from_state(&state_); drop(state_); RepetitionsState { word_id_index_map: map, selected_deck_index: None, decks, state, } } fn get_decks_from_state(state: &AppState) -> Vec { state .decks .iter() .enumerate() .map(|(index, deck)| DeckViewData { general_settings: deck.clone(), existing_words_indices: None, available_words_indices: None, append_mode: Default::default(), index: Some(index), }) .collect() } } impl RepetitionsState { fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { if deck.id.is_valid() { if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.as_ref().unwrap().is_empty() { return text!("Добавьте карточки или измените фильтр").into(); } 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_deck_view(&self) -> Element<'_, RepetitionsMessage> { if let Some(deck) = &self.selected_deck() { return column![ scrollable( column![ column![ text!("Название набора"), row![ text_input("", &deck.name).on_input(SetName), button(svg(Handle::from_memory(SETTINGS_ICON))) .width(43) .on_press(GoToSettings), ] .spacing(HALF_SPACING) ] .spacing(QUARTER_SPACING), { if !deck.id.is_valid() { column![ column![ text!("Передняя сторона"), text_input("", &deck.forward).on_input(SetForward), ] .spacing(QUARTER_SPACING), column![ text!("Задняя сторона"), text_input("", &deck.backward).on_input(SetBackward), ] .spacing(QUARTER_SPACING), column![ text!("Фильтр"), text_input("", &deck.filter).on_input(SetFilter), button("Проверить фильтр") .style(jl_button) .on_press(TryFilter), self.count_view(deck), ] .spacing(QUARTER_SPACING), ] .spacing(DEFAULT_SPACING) } else { column![ self.filled_deck_data_view(deck), self.word_append_panel(deck), radio( "Обычный режим", OrderMode::Default, Some(deck.open_mode), SetOpenMode ), radio( "Начать с плохих слов", OrderMode::TrainWorstFirst, Some(deck.open_mode), SetOpenMode ), radio( "Полностью случайно", OrderMode::FullRandom, Some(deck.open_mode), SetOpenMode ), button("История").style(jl_button).on_press(GoToHistory), ] .spacing(HALF_SPACING) } }, // self.words_words_view(&set), ] .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(deck) ] .spacing(DEFAULT_SPACING) .align_y(Center), ] .spacing(DEFAULT_SPACING) .width(Length::FillPortion(2)) .into(); } space().width(Length::FillPortion(2)).into() } fn filled_deck_data_view(&self, deck: &DeckSettings) -> Column<'_, RepetitionsMessage> { column![self.activity_bar(deck), self.count_comparator_view()] .align_x(Center) .width(Fill) .spacing(DEFAULT_SPACING) } fn activity_bar(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { const MAX_DAY_COUNT: f32 = 128.0; let state = self.state.lock().unwrap(); let history = state.activity.get(&deck.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 = now - *date; let index = counts.len() as i64 - distance.num_days() - 1; if index == 210 { println!("invalid index") } if index < 0 { continue; } counts[index as usize] = *activity; } } lazy((counts, now), |(counts, now)| { let mut row = Row::new() .padding(Padding { top: 0.0, right: 0.0, bottom: 0.0, left: DEFAULT_SPACING, }) .spacing(QUARTER_SPACING); let mut iter = counts.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) .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 word_append_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { column![ text!("Режим добавления карточек"), row![ radio( "Добавлять все доступные", AppendMode::Full, Some(deck.append_mode), SetAppendMode ), radio( "Добавлять вручную", AppendMode::Manual, Some(deck.append_mode), SetAppendMode ) ] .spacing(HALF_SPACING), self.adder_panel(deck), ] .spacing(HALF_SPACING) .into() } fn adder_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { match deck.append_mode { AppendMode::Full => space().into(), AppendMode::Manual => row![ button("+5").on_press(AppendWords(5)), button("+10").on_press(AppendWords(10)), button("+15").on_press(AppendWords(15)), button("+20").on_press(AppendWords(20)) ] .spacing(HALF_SPACING) .into(), } } fn count_comparator_view(&self) -> Element<'_, RepetitionsMessage> { let selected_deck = self.selected_deck().unwrap(); let now = selected_deck.existing_words_indices.as_ref().unwrap().len(); let available = selected_deck .available_words_indices .as_ref() .unwrap() .len(); text!("{} слова добавлено из {}", now, available).into() } fn count_view(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { if let Some(count) = deck.count { return text!("Количество слов: {}", count).into(); } space().into() } fn decks_list(&self) -> Column<'_, RepetitionsMessage> { let mut column = Column::new(); for (i, deck) in self.decks.iter().enumerate() { column = column.push( button(text!("{}", &deck.name)) .on_press_with(move || SelectSet(i)) .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: _x.palette().primary, border: Border { color: Default::default(), width: 0.0, radius: BUTTON_RADIUS.into(), }, shadow: Shadow::default(), snap: false, }), ); } column } } impl RepetitionsState { fn append_all_words(state: &AppState, deck: &DeckViewData) { let existing = deck.existing_words_indices.as_ref().unwrap(); let mut created_set = HashSet::with_capacity(existing.len()); existing.iter().for_each(|c| { created_set.insert(c); }); let required = deck .available_words_indices .as_ref() .unwrap() .iter() .filter(|i| !created_set.contains(i)) .cloned(); Self::append_words(state, deck, required); } fn append_words(state: &AppState, deck: &DeckSettings, indices: impl Iterator) { let words = &state.dictionary; let stats = &mut indices .map(|i| { let word = words.get(i).unwrap(); CardStatistics { id: 0.into(), word_id: word.id, last_open: Utc::now(), score: 1, set_id: deck.id, } }) .collect::>(); if !stats.is_empty() { add_stat_list(stats, &state.connection); } } fn select_deck(&mut self, index: usize) { if self.decks.get(index).is_some() { self.selected_deck_index = Some(index); } } fn selected_deck(&self) -> Option<&DeckViewData> { self.decks.get(self.selected_deck_index?) } fn selected_deck_mut(&mut self) -> Option<&mut DeckViewData> { self.decks.get_mut(self.selected_deck_index?) } fn clear_selection(&mut self) { self.selected_deck_index = None; } } #[derive(Clone)] pub enum RepetitionsMessage { Next, Back, GoToRepetition, CreateSet, DeleteSet, SetName(String), SelectSet(usize), Save, SetForward(String), SetBackward(String), SetFilter(String), TryFilter, SetOpenMode(OrderMode), SetAppendMode(AppendMode), GoToHistory, GoToSettings, AppendWords(usize), } #[derive(Clone)] struct DeckViewData { general_settings: DeckSettings, existing_words_indices: Option>, available_words_indices: Option>, append_mode: AppendMode, index: Option, } impl Deref for DeckViewData { type Target = DeckSettings; fn deref(&self) -> &Self::Target { &self.general_settings } }