diff --git a/images/modal-default-critical-icon.svg b/images/modal-default-critical-icon.svg new file mode 100644 index 0000000..27c0b81 --- /dev/null +++ b/images/modal-default-critical-icon.svg @@ -0,0 +1 @@ +"Notionists" by "Zoish", licensed under "CC0 1.0". / Remix of the original. - Created with dicebear.comNotionistsZoishhttps://heyzoish.gumroad.com/l/notionists \ No newline at end of file diff --git a/images/modal-default-ok-icon.svg b/images/modal-default-ok-icon.svg new file mode 100644 index 0000000..bf525e9 --- /dev/null +++ b/images/modal-default-ok-icon.svg @@ -0,0 +1 @@ +"Notionists" by "Zoish", licensed under "CC0 1.0". / Remix of the original. - Created with dicebear.comNotionistsZoishhttps://heyzoish.gumroad.com/l/notionists \ No newline at end of file diff --git a/images/modal-default-warning-icon.svg b/images/modal-default-warning-icon.svg new file mode 100644 index 0000000..e312db4 --- /dev/null +++ b/images/modal-default-warning-icon.svg @@ -0,0 +1 @@ +"Notionists" by "Zoish", licensed under "CC0 1.0". / Remix of the original. - Created with dicebear.comNotionistsZoishhttps://heyzoish.gumroad.com/l/notionists \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index fadbf2d..a429ed3 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -7,6 +7,7 @@ pub mod icons; pub mod loading; pub mod login; pub mod main_window; +pub mod modal; pub mod spinner; pub mod text_input; pub mod wallpaper; diff --git a/src/components/modal.rs b/src/components/modal.rs new file mode 100644 index 0000000..96cdf86 --- /dev/null +++ b/src/components/modal.rs @@ -0,0 +1,231 @@ +use std::collections::HashMap; +use std::fmt; +use std::sync::OnceLock; + +use dioxus::prelude::*; +use rand::distributions::{Alphanumeric, DistString}; +use tracing::{error, warn}; + +use super::button::{ErrorButton, SuccessButton, WarningButton}; + +include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); + +use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100}; + +turf::style_sheet!("src/components/modal.scss"); + +#[derive(Clone, Eq, PartialEq, Hash)] +pub enum Severity { + Ok, + Warning, + Critical, +} +impl fmt::Display for Severity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let repr = match self { + Self::Ok => "Severity::Ok", + Self::Warning => "Severity::Warning", + Self::Critical => "Severity::Critical", + }; + write!(f, "{repr}") + } +} + +struct DicebearConfig<'a> { + gesture: &'a str, + color: &'a str, + browns: Vec, + eyes: Vec, + lips: Vec, +} + +#[derive(Props)] +pub struct ModalProps<'a> { + pub severity: Severity, + #[props(optional)] + pub title: Option<&'a str>, + pub children: Element<'a>, + #[props(optional)] + pub on_confirm: Option>, +} + +fn dicebear_variants() -> &'static HashMap> { + static HASHMAP: OnceLock> = OnceLock::new(); + HASHMAP.get_or_init(|| { + let mut variants = HashMap::new(); + variants.insert( + Severity::Critical, + DicebearConfig { + gesture: "wavePointLongArms", + color: COLOR_CRITICAL_100, + browns: vec![2, 6, 11, 13], + eyes: vec![2, 4], + lips: vec![1, 2, 7, 11, 19, 20, 24, 27], + }, + ); + variants.insert( + Severity::Warning, + DicebearConfig { + gesture: "pointLongArm", + color: COLOR_WARNING_100, + browns: vec![2, 5, 10, 13], + eyes: vec![1, 3], + lips: vec![1, 2, 4, 8, 10, 13, 18, 21, 29], + }, + ); + variants.insert( + Severity::Ok, + DicebearConfig { + gesture: "okLongArm", + color: COLOR_SUCCESS_100, + browns: vec![1, 3, 4, 7, 8, 9, 12], + eyes: vec![5], + lips: vec![3, 5, 9, 14, 17, 22, 23, 25, 30], + }, + ); + + variants + }) +} + +fn render_dicebear_variants(values: &[u32]) -> String { + values + .iter() + .map(|l| format!("variant{:02}", l)) + .collect::>() + .join(",") +} + +async fn generate_random_figure(url: &String, severity: Severity) -> Option { + let mut res: Option = None; + + let config = match dicebear_variants().get(&severity) { + Some(config) => config, + None => { + error!("No dicebear configuration found for \"{severity}\""); + return res; + } + }; + + let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + + let color = config.color; + let gesture = config.gesture; + let rendered_browns = render_dicebear_variants(&config.browns); + let rendered_eyes = render_dicebear_variants(&config.eyes); + let rendered_lips = render_dicebear_variants(&config.lips); + + let req = format!( + "https://{url}/7.x/notionists/svg?\ + seed={seed}&\ + backgroundColor={color}&\ + gestureProbability=100&gesture={gesture}&\ + browsProbability=100&brows={rendered_browns}&\ + eyesProbability=100&eyes={rendered_eyes}&\ + lipsProbability=100&lips={rendered_lips}" + ); + + match reqwest::get(req).await { + Ok(result) => { + match result.text().await { + Ok(svg) => { + res = Some(svg); + } + Err(err) => { + error!("Error during placeholder loading: {}", err); + } + }; + } + Err(err) => { + error!("Error during placeholder loading: {}", err); + } + }; + res +} + +#[component] +pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> { + // TODO: Use configuration file + let url = "dicebear.tools.adrien.run".to_string(); + + let severity = cx.props.severity.clone(); + + let random_figure_future = use_future(cx, &url, |url| async move { + generate_random_figure(&url, severity).await + }); + + let figure = match random_figure_future.value() { + Some(Some(svg)) => Some(rsx! { + div { + class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER, + dangerous_inner_html: svg.as_str(), + } + }), + Some(None) => { + warn!("No profile image set or generated, display the placeholder"); + let path = match cx.props.severity { + Severity::Ok => "./images/modal-default-ok-icon.svg", + Severity::Warning => "./images/modal-default-warning-icon.svg", + Severity::Critical => "./images/modal-default-critical-icon.svg", + }; + Some(rsx! { + div { + class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER, + img { + src: path + } + } + }) + } + None => None, + }; + + let button_class = match cx.props.severity { + Severity::Ok => SuccessButton, + Severity::Warning => WarningButton, + Severity::Critical => ErrorButton, + }; + + if figure.is_none() { + return None; + } + + cx.render(rsx! { + style { STYLE_SHEET }, + + div { + class: ClassName::MODAL, + + div { + class: ClassName::MODAL_CONTENT, + + div { + class: ClassName::MODAL_CONTENT_ICON, + {figure} + }, + + div { + class: ClassName::MODAL_CONTENT_TITLE, + cx.props.title, + }, + + div { + class: ClassName::MODAL_CONTENT_MSG, + + &cx.props.children, + }, + + div { + class: ClassName::MODAL_CONTENT_BUTTONS, + button_class { + onclick: move |evt| { + if let Some(cb) = &cx.props.on_confirm { + cb.call(evt); + } + }, + }, + }, + }, + }, + }) +} diff --git a/src/components/modal.scss b/src/components/modal.scss new file mode 100644 index 0000000..5c3bc9f --- /dev/null +++ b/src/components/modal.scss @@ -0,0 +1,107 @@ +@import "../_base.scss" + +$modal-min-height: 15vh; +$modal-max-height: 55vh; + +.modal { + width: 100%; + height: 100%; + + position: relative; + top: -100%; + margin-bottom: -100%; + + display: flex; + align-items: center; + justify-content: center; + + background: rgba(0, 0, 0, 0.5); + + &__content { + $content-width: 70vw; + $title-height: 3vh; + $space-height: 1vh; + $buttons-height: 5vh; + $icon-width: 10vw; + $msg-max-width: 50vw; + + $msg-min-height: calc($modal-min-height - $title-height - $space-height - $buttons-height); + $msg-max-height: calc($modal-max-height - $title-height - $space-height - $buttons-height); + + min-height: $modal-min-height; + max-height: $modal-max-height; + + width: min-content; + + display: grid; + grid-template-columns: $icon-width 1vw minmax(max-content, $msg-max-width); + grid-template-rows: $title-height $space-height max-content $space-height $buttons-height; + grid-template-areas: + "icon . title" + "icon . ." + "icon . msg" + ". . ." + "buttons buttons buttons" + ; + + padding: 2.5%; + + background: get-color(greyscale, 0); + + border: $border-normal; + border-radius: $border-radius; + border-color: get-color(greyscale, 90); + + &__icon { + grid-area: icon; + + aspect-ratio: 1; + + display: flex; + align-items: center; + justify-content: center; + + overflow: hidden; + + border: $border-normal; + border-radius: $border-radius; + + &__placeholder { + width: calc(100% + (2 * $border-normal-width)); + height: calc(100% - (2 * $border-normal-width)); + } + } + + &__title { + grid-area: title; + + width: max-content; + + font-size: calc($title-height - 0.5vh); + font-weight: bold; + } + + &__msg { + grid-area: msg; + + height: fit-content; + + min-height: $msg-min-height; + max-height: $msg-max-height; + max-width: $msg-max-width; + + overflow: scroll; + + font-size: 1.5vh; + color: get-color(greyscale, 80); + } + + &__buttons { + grid-area: buttons; + + display: flex; + justify-content: center; + align-items: center; + } + } +}