From e1bfd9494dbe0c0f6bffd9da7ba19f30e06c3b62 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Sun, 23 Aug 2026 21:00:48 +0300 Subject: [PATCH] Notification base --- src/main.rs | 1 + src/navigation.rs | 32 +++++++--- src/notification.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++ src/sync.rs | 2 + 4 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 src/notification.rs diff --git a/src/main.rs b/src/main.rs index 204e308..eb205f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ pub mod styling; mod sync; mod word; mod writing; +pub mod notification; use crate::RootMessage::Keyboard; use crate::data_provider::card_sets::load_sets; diff --git a/src/navigation.rs b/src/navigation.rs index 19d9db8..e1dcbe9 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,14 +1,15 @@ use crate::data_provider::history::{get_history_of_set, history_dir}; use crate::data_provider::sqlite::{create_db, default_connection}; use crate::data_provider::web_api::{get_local_version, get_web_version, load_data, set_local_version}; -use crate::dictionary::{DictionaryMessage, DictionaryState, app_data_dir}; +use crate::dictionary::{app_data_dir, DictionaryMessage, DictionaryState}; use crate::dictionary_quiz::{DictionaryQuizMessage, DictionaryQuizState}; use crate::history::{HistoryMessage, HistoryState}; use crate::import::{ImportMessage, ImportState}; use crate::lang::Id; use crate::message_navigation; use crate::navigation::Page::*; -use crate::navigation::RootMessage::{DataLoaded, Keyboard, UpdateData}; +use crate::navigation::RootMessage::{DataLoaded, Keyboard, PushNotification, UpdateData, Notification}; +use crate::notification::{NotificationRequest, NotificationManager, NotificationMessage}; use crate::quiz::{QuizMessage, QuizState}; use crate::randomizer::{RandomizerMessage, RandomizerState}; use crate::repetition::{RepetitionMessage, RepetitionState}; @@ -21,16 +22,18 @@ use crate::sync::{SyncMessage, SyncState}; use crate::view_navigation; use crate::word::{WordMessage, WordState}; use crate::writing::{WritingMessage, WritingState}; -use crate::{AppState, fill_state}; +use crate::{fill_state, AppState}; use chrono::NaiveDate; use hashbrown::HashMap; use iced::keyboard::Event; +use iced::widget::stack; use iced::{Element, Task}; use parking_lot::Mutex; use reqwest::Error; use rusqlite::Connection; use std::sync::Arc; use std::time::Instant; +use iced_core::Length::Fill; impl Default for ScreenState { fn default() -> Self { @@ -40,6 +43,7 @@ impl Default for ScreenState { ScreenState { stack: vec![Selector(SelectorState::new(state.clone()))], app_state: state.clone(), + notification_manager: NotificationManager::new(), } } } @@ -61,6 +65,8 @@ pub enum RootMessage { Import(ImportMessage), Keyboard(Event), DataLoaded(HashMap>), + PushNotification(NotificationRequest), + Notification(NotificationMessage), None, UpdateData, } @@ -85,6 +91,7 @@ pub enum Page { pub struct ScreenState { stack: Vec, app_state: Arc>, + notification_manager: NotificationManager } impl ScreenState { @@ -187,6 +194,15 @@ impl ScreenState { fill_state(&mut state); return Task::none(); } + + if let Notification(message) = message { + return self.notification_manager.update(message); + } + + if let PushNotification(notification) = &message { + return self.notification_manager.push(notification.clone()) + } + let task: (Task, bool) = self.navigate_or_update(message); if task.1 { @@ -233,6 +249,7 @@ impl ScreenState { } pub fn view(&self) -> Element<'_, RootMessage> { + println!("View update"); let time = Instant::now(); let view = view_navigation!( @@ -252,8 +269,10 @@ impl ScreenState { Import ); + let notifications = self.notification_manager.draw().map(RootMessage::Notification); + println!("View took {:?}", time.elapsed()); - view + stack![view, notifications].width(Fill).height(Fill).into() } fn handle_keyboard(&mut self, message: Event) -> Task { @@ -281,7 +300,6 @@ macro_rules! view_navigation { } } } - #[macro_export] macro_rules! state_update { ($message:expr, $stack:expr, $($e:ident), *) => { @@ -300,7 +318,6 @@ macro_rules! state_update { } } } - #[macro_export] macro_rules! message_navigation { ($msg:expr, $stack:expr, $state:expr) => { @@ -321,7 +338,6 @@ macro_rules! message_navigation { } }; } - #[macro_export] macro_rules! state_navigate_handle { ($stack:expr, $($e:ident), *) => { @@ -335,14 +351,12 @@ macro_rules! state_navigate_handle { } } } - pub trait NavigatedPage { fn navigate(&mut self, message: &T) -> Option; fn navigated(&mut self); fn update(&mut self, message: T) -> Task; fn view(&self) -> Element<'_, T>; } - pub trait KeyPressedPage { fn press(&mut self, message: &Event) -> Task; } diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..70111a7 --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,151 @@ +use crate::navigation::RootMessage; +use crate::styling::{BUTTON_RADIUS, HALF_SPACING}; +use chrono::Utc; +use iced::widget::container::Style; +use iced::widget::{column, container, scrollable, text, Column}; +use iced::{Element, Task}; +use iced_core::alignment::Horizontal::Right; +use iced_core::alignment::Vertical; +use iced_core::Background::Color; +use iced_core::Border; +use iced_core::Length::Fill; + +pub struct NotificationManager { + list: Vec<(u32, NotificationRequest, u32)>, + notification_lifetime: u32, + next_id: u32, +} + +#[derive(Clone)] +pub enum NotificationMessage { + Remove(u32), +} + +impl Default for NotificationManager { + fn default() -> Self { + Self::new() + } +} + +impl NotificationManager { + pub fn new() -> Self { + Self { + list: Vec::new(), + notification_lifetime: 10, + next_id: 1, + } + } + + pub fn draw(&self) -> Element<'_, NotificationMessage> { + let mut column = Column::new().spacing(HALF_SPACING); + + for (id, notif, time) in &self.list { + column = column.push(Self::draw_notif(id, notif, time)); + } + + container(scrollable(column)).width(Fill).height(Fill) + .align_x(Right) + .align_y(Vertical::Top).padding(HALF_SPACING) + .into() + } + + pub fn push(&mut self, notification: NotificationRequest) -> Task { + let id = self.next_id; + self.list.push(( + id, + notification, + Utc::now().timestamp() as u32 + self.notification_lifetime, + )); + self.next_id += 1; + let message = NotificationMessage::Remove(id); + let time = self.notification_lifetime; + Task::perform( + async move { + tokio::time::sleep(core::time::Duration::from_secs(time as u64)).await; + }, + |_| RootMessage::Notification(message), + ) + } + + pub fn update(&mut self, message: NotificationMessage) -> Task { + match message { + NotificationMessage::Remove(id) => { + if let Some(index) = self.list.iter().position(|item| item.0 == id) { + self.list.remove(index); + } + } + } + Task::none() + } + + fn draw_notif<'a>( + id: &u32, + notif: &NotificationRequest, + _time: &u32, + ) -> Element<'a, NotificationMessage> { + let color_id; + let content = match notif { + NotificationRequest::Info(data) => { + color_id = 1; + Self::draw_data(data) + } + NotificationRequest::Warning(data) => { + color_id = 2; + Self::draw_data(data) + } + NotificationRequest::Error(data) => { + color_id = 3; + Self::draw_data(data) + } + }; + + let container = container(column![ + iced::widget::button("✕") + .on_press(NotificationMessage::Remove(*id)) + .style(iced::widget::button::text), + content + ].align_x(Right).padding(HALF_SPACING)); + container + .style(move |theme| { + let color = match color_id { + 1 => theme.palette().success, + 2 => theme.palette().warning, + 3 => theme.palette().danger, + _ => theme.palette().warning, + }; + Style { + text_color: None, + background: Some(Color(color.scale_alpha(0.3))), + border: Border::default().width(1).color(color).rounded(BUTTON_RADIUS), + shadow: Default::default(), + snap: false, + } + }) + .into() + } + + fn draw_data<'a>(data: &NotificationData) -> Element<'a, NotificationMessage> { + match data { + NotificationData::DataLoaded => { + text!("Данные успешно скачаны") + } + NotificationData::ImportError => { + text!("Ошибка при импорте данных из Anki") + } + } + .into() + } +} + +#[derive(Clone)] +pub enum NotificationRequest { + Info(NotificationData), + Warning(NotificationData), + Error(NotificationData), +} + +#[derive(Clone)] +pub enum NotificationData { + DataLoaded, + ImportError, +} diff --git a/src/sync.rs b/src/sync.rs index db05690..1733211 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -12,6 +12,7 @@ use iced::{Center, Element, Fill, Font, Length, Task}; use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; +use crate::notification::{NotificationRequest, NotificationData}; #[derive(Clone)] pub enum SyncMessage { @@ -128,6 +129,7 @@ impl NavigatedPage for SyncState { state.decks = updated_state.decks; state.dictionary = updated_state.dictionary; state.word_groups = updated_state.word_groups; + return Task::done(RootMessage::PushNotification(NotificationRequest::Info(NotificationData::DataLoaded))) } Disable => {} DisableSync => {