#![allow(non_snake_case)]
mod id;
use std::collections::BTreeMap;
use dioxus::prelude::*;
use id::ID;
#[derive(Debug, Clone)]
struct ToastManagerItem {
info: ToastInfo,
hide_after: Option<i64>,
}
#[derive(Debug)]
pub struct ToastManager {
list: BTreeMap<usize, ToastManagerItem>,
maximum_toast: u8,
id_manager: ID,
}
impl ToastManager {
pub fn new(maximum_toast: u8) -> Self {
Self {
list: BTreeMap::new(),
maximum_toast,
id_manager: ID::new(),
}
}
pub fn popup(&mut self, info: ToastInfo) -> usize {
let toast_id = self.id_manager.add();
if self.list.len() >= self.maximum_toast.into() {
if let Some(result) = self.list.first_key_value() {
let id = *result.0;
println!("Deleted Toast ID: {:?}", id);
self.list.remove(&id);
}
}
let hide_after = info
.hide_after
.map(|duration| chrono::Local::now().timestamp() + duration as i64);
self.list
.insert(toast_id, ToastManagerItem { info, hide_after });
toast_id
}
pub fn remove(&mut self, id: usize) {
self.list.remove(&id);
}
pub fn clear(&mut self) {
self.list.clear();
}
}
impl Default for ToastManager {
fn default() -> Self {
Self {
list: Default::default(),
maximum_toast: 6,
id_manager: ID::new(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Position {
BottomLeft,
BottomRight,
TopLeft,
TopRight,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Icon {
Success,
Warning,
Error,
Info,
}
#[derive(Debug, Clone)]
pub struct ToastInfo {
pub heading: Option<String>,
pub context: String,
pub allow_toast_close: bool,
pub position: Position,
pub icon: Option<Icon>,
pub hide_after: Option<usize>,
}
impl ToastInfo {
pub fn simple(text: &str) -> Self {
Self {
heading: None,
context: text.to_string(),
allow_toast_close: true,
position: Position::BottomLeft,
icon: None,
hide_after: Some(6),
}
}
pub fn success(text: &str, heading: &str) -> Self {
Self {
heading: Some(heading.to_string()),
context: text.to_string(),
allow_toast_close: true,
position: Position::BottomLeft,
icon: Some(Icon::Success),
hide_after: Some(6),
}
}
pub fn warning(text: &str, heading: &str) -> Self {
Self {
heading: Some(heading.to_string()),
context: text.to_string(),
allow_toast_close: true,
position: Position::BottomLeft,
icon: Some(Icon::Warning),
hide_after: Some(6),
}
}
pub fn info(text: &str, heading: &str) -> Self {
Self {
heading: Some(heading.to_string()),
context: text.to_string(),
allow_toast_close: true,
position: Position::BottomLeft,
icon: Some(Icon::Info),
hide_after: Some(6),
}
}
pub fn error(text: &str, heading: &str) -> Self {
Self {
heading: Some(heading.to_string()),
context: text.to_string(),
allow_toast_close: true,
position: Position::BottomLeft,
icon: Some(Icon::Error),
hide_after: Some(6),
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ToastFrameProps {
manager: Signal<ToastManager>,
style: Option<&'static str>,
}
pub fn ToastFrame(props: ToastFrameProps) -> Element {
let mut manager = props.manager;
let toast_list = &manager.read().list;
let mut bottom_left_ele: Vec<VNode> = vec![];
let mut bottom_right_ele: Vec<VNode> = vec![];
let mut top_left_ele: Vec<VNode> = vec![];
let mut top_right_ele: Vec<VNode> = vec![];
for (id, item) in toast_list.iter() {
let current_id = *id;
let icon_class = if let Some(icon) = &item.info.icon {
let mut class = String::from("has-icon ");
match icon {
Icon::Success => class.push_str("icon-success"),
Icon::Warning => class.push_str("icon-warning"),
Icon::Error => class.push_str("icon-error"),
Icon::Info => class.push_str("icon-info"),
}
class
} else {
String::new()
};
let element = rsx! {
div { class: "toast-single {icon_class}", id: "{id}",
if item.info.allow_toast_close {
div {
class: "close-toast-single",
onclick: move |_| {
manager.write().list.remove(¤t_id);
},
"×"
}
} else {
div {}
}
if let Some(v) = &item.info.heading {
h2 { class: "toast-heading", "{v}" }
} else {
div {}
}
span { dangerous_inner_html: "{item.info.context}" }
}
};
if item.info.position == Position::BottomLeft {
bottom_left_ele.push(element?);
} else if item.info.position == Position::BottomRight {
bottom_right_ele.push(element?);
} else if item.info.position == Position::TopLeft {
top_left_ele.push(element?);
} else if item.info.position == Position::TopRight {
top_right_ele.push(element?);
}
}
let _ = use_resource(move || async move {
loop {
let now = chrono::Local::now().timestamp();
manager.write().list.retain(|_, item| {
if let Some(hide_after) = item.hide_after {
now < hide_after
} else {
true
}
});
time_sleep(100).await;
}
});
let style = props.style.unwrap_or(include_str!("./assets/toast.css"));
rsx! {
div { class: "toast-scope",
style { { style } }
div { class: "toast-wrap bottom-left", id: "wrap-bottom-left",
{ bottom_left_ele.into_iter() }
}
div { class: "toast-wrap bottom-right", id: "wrap-bottom-right",
{ bottom_right_ele.into_iter() }
}
div { class: "toast-wrap top-left", id: "wrap-top-left", { top_left_ele.into_iter() } }
div { class: "toast-wrap top-right", id: "wrap-top-right", { top_right_ele.into_iter() } }
}
}
}
#[cfg(feature = "web")]
async fn time_sleep(interval: usize) {
gloo_timers::future::TimeoutFuture::new(interval as u32).await;
}
#[cfg(feature = "desktop")]
async fn time_sleep(interval: usize) {
tokio::time::sleep(tokio::time::Duration::from_millis(interval as u64)).await;
}