use crate::app::AppState; use crate::core::deck::DeckData; use crate::core::srs::{AppendMode, OrderMode}; use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData}; use crate::data::repositories::{BatchedRepository, Repository}; use crate::jl_button; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::MaybeError; use crate::pages::history::HistoryState; use crate::pages::repetition::RepetitionState; use crate::pages::repetition_settings::RepetitionSettingsState; use crate::pages::repetitions::RepetitionsMessage::*; use crate::push_error; use crate::push_error_task; use crate::styling::*; use chrono::{Days, Local, Utc}; use hashbrown::HashSet; use iced::widget::button::{danger, Status}; pub use iced::widget::button::{Catalog, Style}; use iced::widget::tooltip::Position::Top; use iced::widget::{ button, column, container, lazy, 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 std::collections::BTreeMap; use std::ops::Deref; use std::sync::Arc; pub struct RepetitionsState { selected_deck_handle: Option>, decks: BTreeMap, DeckRunData>, state: AppState, } 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 mut state_clone = self.state.clone(); let selected_set = self.selected_deck_mut().unwrap(); if selected_set.append_mode == AppendMode::Full { if let Err(_err) = Self::append_all_words(&mut state_clone, selected_set) { return None; } selected_set.existing_words_indices = selected_set.available_words_indices.clone(); } else { if selected_set.available_words_indices.is_empty() || selected_set.existing_words_indices.is_empty() { return None; } } if let Ok(repetition_state) = RepetitionState::new(selected_set.clone(), self.state.clone()) { return Some(Repetition(repetition_state)); } None } else if let GoToSettings = message { Some(RepetitionSettings(RepetitionSettingsState::new( self.selected_deck_handle.unwrap(), self.state.clone(), ))) } else { None } } fn navigated(&mut self) { if let Some(index) = self.selected_deck_handle { let decks = self.state.decks.read(); if let Some(deck) = decks.get_static(index) { { let stored = self.decks.get_mut(&index).unwrap(); stored.general_settings = deck; } drop(decks); self.fill_deck_word_indices(); } else { self.decks.remove(&index); drop(decks); self.clear_selection(); } } else { self.fill_deck_word_indices(); } } fn update(&mut self, message: RepetitionsMessage) -> Task { match message { Next => {} Back => {} GoToRepetition => {} GoToHistory => {} GoToSettings => {} CreateSet => { let index = self.decks.len() + 1; let general = DeckSettings::with_name(format!("Колода #{}", index)); let handle = self.state.decks.write().create(general.clone()); self.decks.insert( handle, DeckRunData { settings_handle: handle, general_settings: general, existing_words_indices: Default::default(), available_words_indices: Default::default(), indices_ready: false, append_mode: Default::default(), count: None, open_mode: OrderMode::Default, }, ); } DeleteSet => { let index = self.selected_deck_handle.unwrap(); self.clear_selection(); self.decks.remove(&index); if index.real() { let mut repo = self.state.decks.write(); if let Err(err) = repo.delete(index) { return push_error_task!(err); } } } 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.indices_ready { self.fill_deck_word_indices() } } Save => { let deck = self.selected_deck().unwrap().clone(); let handle = self.selected_deck_handle.as_mut().unwrap(); let mut repo = self.state.decks.write(); repo.set(handle, deck.general_settings.clone()); let old = *handle; if let Err(err) = repo.save(handle) { return push_error_task!(err); } let mut deck = self.decks.remove(&old).unwrap(); deck.settings_handle = *handle; self.decks.insert(*handle, deck); } 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 deck = self.selected_deck().unwrap(); let count = DeckData::list(deck, &self.state).len(); let deck = self.selected_deck_mut().unwrap(); deck.count = Some(count); } SetOpenMode(mode) => { let deck = self.selected_deck_mut().unwrap(); deck.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; 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 .iter() .filter(|c| !created_set.contains(c)) .take(count) .cloned() .collect(); } let deck = self.selected_deck_mut().unwrap(); deck.existing_words_indices.append(&mut adding.clone()); } let deck = self.selected_deck().unwrap().clone(); let state = &mut self.state; if let Err(err) = Self::append_words(state, &deck.general_settings, adding.into_iter()) { return push_error_task!(err); } } } Task::none() } fn view(&self) -> Element<'_, RepetitionsMessage> { back_overlay( row![ column![ scrollable(self.decks_list()).height(Fill), 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: AppState) -> RepetitionsState { let decks = Self::get_decks_from_state(&state); RepetitionsState { selected_deck_handle: None, decks, state, } } fn get_decks_from_state(state: &AppState) -> BTreeMap, DeckRunData> { state .decks .read() .iter() .enumerate() .map(|(index, deck)| { ( EntityHandle::new(deck.id, index, true), DeckRunData { settings_handle: EntityHandle::new(deck.id, index, true), general_settings: deck.clone(), existing_words_indices: Default::default(), available_words_indices: Default::default(), indices_ready: false, append_mode: Default::default(), count: None, open_mode: OrderMode::Default, }, ) }) .collect::>() } } impl RepetitionsState { fn launch_delete_button(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> { if deck.id.is_valid() { if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.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.settings_handle.real() { 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), 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 ), 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![ 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 history = self.state.history.read(); let history = history.activity_of_deck(deck.id); let mut counts: Vec = vec![0; 30 * 7]; let now = Local::now().date_naive(); for (date, activity) in history { let distance = now - date; let index = counts.len() as i64 - distance.num_days() - 1; 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_or_default(); 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: &DeckRunData) -> 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: &DeckRunData) -> 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.len(); let available = selected_deck.available_words_indices.len(); text!("{} слова добавлено из {}", now, available).into() } fn count_view(&self, deck: &DeckRunData) -> 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 (handle, deck) in self.decks.iter() { column = column.push( button(text!("{}", &deck.name)) .on_press_with(move || SelectSet(*handle)) .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: &mut AppState, deck: &DeckRunData) -> MaybeError { let existing = &deck.existing_words_indices; let mut created_set = HashSet::with_capacity(existing.len()); existing.iter().for_each(|c| { created_set.insert(c); }); let required = deck .available_words_indices .iter() .filter(|i| !created_set.contains(i)) .cloned(); Self::append_words(state, deck, required)?; Ok(()) } fn append_words( state: &mut AppState, deck: &DeckSettings, indices: impl Iterator>, ) -> MaybeError { let stats = &mut indices .map(|i| CardState { id: 0.into(), word_id: i.id, last_open: Utc::now(), score: 1, fsrs_opened: Utc::now().date_naive(), fsrs_memory: None, deck_id: deck.id, fsrs_open_day: Utc::now().date_naive(), }) .collect::>(); if !stats.is_empty() { let mut repo = state.card_states.write(); repo.create_now_many(stats.as_mut_slice())?; } Ok(()) } fn select_deck(&mut self, index: EntityHandle) { if self.decks.contains_key(&index) { self.selected_deck_handle = Some(index); } } fn selected_deck(&self) -> Option<&DeckRunData> { self.decks.get(&self.selected_deck_handle?) } fn selected_deck_mut(&mut self) -> Option<&mut DeckRunData> { self.decks.get_mut(&self.selected_deck_handle?) } fn clear_selection(&mut self) { self.selected_deck_handle = None; } fn fill_deck_word_indices(&mut self) { let card_states = self.state.card_states.read(); let deck_id = self.selected_deck().unwrap().id; let added = card_states .query_par(Arc::new(move |state| state.deck_id == deck_id)) .iter(&card_states) .map(|state| EntityHandle::from_id(state.word_id)) .collect::>(); let mut deck = self.selected_deck().unwrap().clone(); let total = DeckData::list(&deck, &self.state); deck.existing_words_indices = added; deck.available_words_indices = total; deck.indices_ready = true; self.decks.insert(self.selected_deck_handle.unwrap(), deck); } } #[derive(Clone)] pub enum RepetitionsMessage { Next, Back, GoToRepetition, CreateSet, DeleteSet, SetName(String), SelectSet(EntityHandle), Save, SetForward(String), SetBackward(String), SetFilter(String), TryFilter, SetOpenMode(OrderMode), SetAppendMode(AppendMode), GoToHistory, GoToSettings, AppendWords(usize), } #[derive(Clone)] pub struct DeckRunData { pub settings_handle: EntityHandle, pub general_settings: DeckSettings, existing_words_indices: Vec>, available_words_indices: Vec>, indices_ready: bool, pub append_mode: AppendMode, count: Option, pub open_mode: OrderMode, } impl Deref for DeckRunData { type Target = DeckSettings; fn deref(&self) -> &Self::Target { &self.general_settings } }