use serde::{Deserialize, Serialize};
use yewdux::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum NotificationLevel {
#[default]
Info,
Success,
Warning,
Error,
}
impl NotificationLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Success => "success",
Self::Warning => "warning",
Self::Error => "error",
}
}
pub fn icon(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Success => "check-circle",
Self::Warning => "alert-triangle",
Self::Error => "x-circle",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Notification {
pub id: String,
pub title: String,
pub message: String,
pub level: NotificationLevel,
pub auto_dismiss_ms: Option<u32>,
pub dismissible: bool,
pub action_text: Option<String>,
pub timestamp: String,
}
impl Notification {
pub fn info(title: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(title, message, NotificationLevel::Info)
}
pub fn success(title: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(title, message, NotificationLevel::Success)
}
pub fn warning(title: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(title, message, NotificationLevel::Warning)
}
pub fn error(title: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(title, message, NotificationLevel::Error)
}
fn new(title: impl Into<String>, message: impl Into<String>, level: NotificationLevel) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
title: title.into(),
message: message.into(),
level,
auto_dismiss_ms: Some(5000),
dismissible: true,
action_text: None,
timestamp: chrono::Utc::now().to_rfc3339(),
}
}
pub fn persistent(mut self) -> Self {
self.auto_dismiss_ms = None;
self
}
pub fn with_action(mut self, text: impl Into<String>) -> Self {
self.action_text = Some(text.into());
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ModalState {
pub open: bool,
pub modal_type: Option<ModalType>,
pub data: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ModalType {
Confirm,
InstallSkill,
UninstallSkill,
ConfigureInstance,
ExecutionDetails,
Export,
Import,
About,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct CommandPaletteState {
pub open: bool,
pub query: String,
pub selected_index: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Store)]
pub struct UiStore {
pub sidebar_open: bool,
pub sidebar_collapsed: bool,
pub notifications: Vec<Notification>,
pub modal: ModalState,
pub command_palette: CommandPaletteState,
pub focused_element: Option<String>,
pub dark_mode: bool,
pub loading_overlay: bool,
pub loading_message: Option<String>,
pub is_mobile: bool,
pub is_touch_device: bool,
}
impl UiStore {
const MAX_NOTIFICATIONS: usize = 5;
pub fn visible_notifications(&self) -> &[Notification] {
let end = self.notifications.len().min(Self::MAX_NOTIFICATIONS);
&self.notifications[..end]
}
pub fn has_modal(&self) -> bool {
self.modal.open
}
pub fn is_command_palette_open(&self) -> bool {
self.command_palette.open
}
}
pub enum UiAction {
ToggleSidebar,
SetSidebarOpen(bool),
ToggleSidebarCollapsed,
SetSidebarCollapsed(bool),
AddNotification(Notification),
DismissNotification(String),
ClearNotifications,
OpenModal(ModalType, Option<String>),
CloseModal,
OpenCommandPalette,
CloseCommandPalette,
SetCommandPaletteQuery(String),
SelectCommandPaletteItem(usize),
CommandPaletteUp,
CommandPaletteDown,
ShowLoading(Option<String>),
HideLoading,
SetIsMobile(bool),
SetIsTouchDevice(bool),
SetDarkMode(bool),
SetFocusedElement(Option<String>),
}
impl Reducer<UiStore> for UiAction {
fn apply(self, mut store: std::rc::Rc<UiStore>) -> std::rc::Rc<UiStore> {
let state = std::rc::Rc::make_mut(&mut store);
match self {
UiAction::ToggleSidebar => {
state.sidebar_open = !state.sidebar_open;
}
UiAction::SetSidebarOpen(open) => {
state.sidebar_open = open;
}
UiAction::ToggleSidebarCollapsed => {
state.sidebar_collapsed = !state.sidebar_collapsed;
}
UiAction::SetSidebarCollapsed(collapsed) => {
state.sidebar_collapsed = collapsed;
}
UiAction::AddNotification(notification) => {
state.notifications.insert(0, notification);
if state.notifications.len() > 20 {
state.notifications.truncate(20);
}
}
UiAction::DismissNotification(id) => {
state.notifications.retain(|n| n.id != id);
}
UiAction::ClearNotifications => {
state.notifications.clear();
}
UiAction::OpenModal(modal_type, data) => {
state.modal = ModalState {
open: true,
modal_type: Some(modal_type),
data,
};
}
UiAction::CloseModal => {
state.modal = ModalState::default();
}
UiAction::OpenCommandPalette => {
state.command_palette = CommandPaletteState {
open: true,
query: String::new(),
selected_index: 0,
};
}
UiAction::CloseCommandPalette => {
state.command_palette = CommandPaletteState::default();
}
UiAction::SetCommandPaletteQuery(query) => {
state.command_palette.query = query;
state.command_palette.selected_index = 0;
}
UiAction::SelectCommandPaletteItem(index) => {
state.command_palette.selected_index = index;
}
UiAction::CommandPaletteUp => {
if state.command_palette.selected_index > 0 {
state.command_palette.selected_index -= 1;
}
}
UiAction::CommandPaletteDown => {
state.command_palette.selected_index += 1;
}
UiAction::ShowLoading(message) => {
state.loading_overlay = true;
state.loading_message = message;
}
UiAction::HideLoading => {
state.loading_overlay = false;
state.loading_message = None;
}
UiAction::SetIsMobile(is_mobile) => {
state.is_mobile = is_mobile;
if is_mobile {
state.sidebar_open = false;
}
}
UiAction::SetIsTouchDevice(is_touch) => {
state.is_touch_device = is_touch;
}
UiAction::SetDarkMode(dark) => {
state.dark_mode = dark;
}
UiAction::SetFocusedElement(element) => {
state.focused_element = element;
}
}
store
}
}