diff --git a/src/components/login.rs b/src/components/login.rs index e0fda5c..87e5f58 100644 --- a/src/components/login.rs +++ b/src/components/login.rs @@ -5,12 +5,12 @@ use std::rc::Rc; use const_format::formatcp; use dioxus::prelude::*; -use rand::distributions::{Alphanumeric, DistString}; use tracing::{debug, error, warn}; use validator::{Validate, ValidateArgs, ValidateEmail, ValidationError, ValidationErrors}; use zxcvbn::zxcvbn; use crate::base::{Session, SESSION}; +use crate::data::datasources::random_svg_generators::{generate_random_svg_shape, ShapeConfig}; use super::button::{LoginButton, RegisterButton}; use super::modal::{Modal, Severity}; @@ -47,36 +47,6 @@ const SHAPE_2_COLORS_STR: &str = formatcp!( const SHAPE_3_COLORS_STR: &str = formatcp!( "{COLOR_TERNARY_120},{COLOR_TERNARY_110},{COLOR_TERNARY_100},{COLOR_TERNARY_90},{COLOR_TERNARY_80}"); -async fn generate_random_avatar(url: String) -> Option { - let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); - let req = format!( - "https://{url}/7.x/shapes/svg?\ - seed={seed}&\ - backgroundColor={BACKGROUND_COLORS_STR}&\ - shape1Color={SHAPE_1_COLORS_STR}&\ - shape2Color={SHAPE_2_COLORS_STR}&\ - shape3Color={SHAPE_3_COLORS_STR}" - ); - - let mut res: Option = None; - 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 -} - const REQUIRED_ERROR_NAME: &str = "required"; const REQUIRED_ERROR_HELPER_TEXT: &str = "This field must be completed"; @@ -591,12 +561,7 @@ impl<'a> PasswordSuggestionsModalConfig<'a> { } } -#[derive(Props, Clone, PartialEq)] -pub struct LoginProps { - dicebear_hostname: Option, -} - -pub fn Login(props: LoginProps) -> Element { +pub fn Login() -> Element { debug!("Login rendering"); let mut data = use_signal(Data::new); @@ -632,31 +597,23 @@ pub fn Login(props: LoginProps) -> Element { let mut spinner_animated = use_signal(|| false); let mut id_placeholder = use_signal(|| LOGIN_ID_PLACEHOLDER); - let url = props - .dicebear_hostname - .unwrap_or("dicebear.tools.adrien.run".to_string()); - - let mut random_avatar_future = use_resource(move || { - to_owned![url]; - async move { generate_random_avatar(url).await } + let mut random_avatar_future = use_resource(move || async move { + let shape_config = ShapeConfig::new( + BACKGROUND_COLORS_STR, + SHAPE_1_COLORS_STR, + SHAPE_2_COLORS_STR, + SHAPE_3_COLORS_STR, + ); + generate_random_svg_shape(Some(&shape_config)).await }); let avatar = match &*random_avatar_future.read_unchecked() { - Some(Some(svg)) => { - rsx!(div { + Some(svg) => Some(rsx! { + div { class: ClassName::LOGIN_FORM_PHOTO_CONTENT, dangerous_inner_html: svg.as_str(), - }) - } - Some(None) => { - warn!("No profile image set or generated, display the placeholder"); - rsx!(div { - class: ClassName::LOGIN_FORM_PHOTO_CONTENT, - img { - src: "./images/login-profile-placeholder.svg" - } - }) - } + } + }), None => None, }; diff --git a/src/components/modal.rs b/src/components/modal.rs index 1860868..9fe197e 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -1,13 +1,14 @@ 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}; +use crate::data::datasources::random_svg_generators::{ + generate_random_svg_avatar, AvatarConfig, AvatarFeeling, +}; + include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100}; @@ -20,23 +21,25 @@ pub enum Severity { 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, +fn avatar_configs() -> &'static HashMap> { + static HASHMAP: OnceLock> = OnceLock::new(); + HASHMAP.get_or_init(|| { + let mut configs = HashMap::new(); + configs.insert( + Severity::Critical, + AvatarConfig::new(AvatarFeeling::Alerting, COLOR_CRITICAL_100), + ); + configs.insert( + Severity::Warning, + AvatarConfig::new(AvatarFeeling::Warning, COLOR_WARNING_100), + ); + configs.insert( + Severity::Ok, + AvatarConfig::new(AvatarFeeling::Ok, COLOR_SUCCESS_100), + ); + configs + }) } #[derive(Props, Clone, PartialEq)] @@ -49,131 +52,20 @@ pub struct ModalProps { 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: &str, 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(props: ModalProps) -> Element { - // TODO: Use configuration file - let url = "dicebear.tools.adrien.run"; + let avatar_config = avatar_configs().get(&props.severity); let random_figure_future = - use_resource(move || async move { generate_random_figure(url, &props.severity).await }); + use_resource(move || async move { generate_random_svg_avatar(avatar_config).await }); - let figure = match &*random_figure_future.read_unchecked() { - Some(Some(svg)) => Some(rsx! { + let icon = match &*random_figure_future.read_unchecked() { + 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 &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, }; @@ -183,7 +75,7 @@ pub fn Modal(props: ModalProps) -> Element { Severity::Critical => ErrorButton, }; - figure.as_ref()?; + icon.as_ref()?; rsx! { style { {STYLE_SHEET} }, @@ -196,7 +88,7 @@ pub fn Modal(props: ModalProps) -> Element { div { class: ClassName::MODAL_CONTENT_ICON, - {figure} + {icon} }, div { diff --git a/src/components/modal.scss b/src/components/modal.scss index 5c3bc9f..e33b71a 100644 --- a/src/components/modal.scss +++ b/src/components/modal.scss @@ -70,6 +70,10 @@ $modal-max-height: 55vh; width: calc(100% + (2 * $border-normal-width)); height: calc(100% - (2 * $border-normal-width)); } + + svg { + width: 105%; + } } &__title { diff --git a/src/data/datasources/mod.rs b/src/data/datasources/mod.rs new file mode 100644 index 0000000..6e340f5 --- /dev/null +++ b/src/data/datasources/mod.rs @@ -0,0 +1 @@ +pub(crate) mod random_svg_generators; diff --git a/src/data/datasources/random_svg_generators.rs b/src/data/datasources/random_svg_generators.rs new file mode 100644 index 0000000..71e0dbb --- /dev/null +++ b/src/data/datasources/random_svg_generators.rs @@ -0,0 +1,231 @@ +use std::collections::HashMap; +use std::fmt; +use std::io::Result as IoResult; +use std::sync::OnceLock; + +use rand::distributions::{Alphanumeric, DistString}; +use reqwest::Result as RequestResult; +use tokio::fs::read_to_string; +use tracing::error; + +#[derive(Eq, PartialEq, Hash)] +pub enum AvatarFeeling { + Ok, + Warning, + Alerting, +} +impl fmt::Display for AvatarFeeling { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let repr = match self { + Self::Ok => "Ok", + Self::Warning => "Warning", + Self::Alerting => "Alerting", + }; + write!(f, "{repr}") + } +} + +pub struct AvatarConfig<'a> { + feeling: AvatarFeeling, + background_color: &'a str, +} +impl<'a> AvatarConfig<'a> { + pub fn new(feeling: AvatarFeeling, background_color: &'a str) -> Self { + Self { + feeling, + background_color, + } + } +} + +enum DicebearType { + Notionists, + Shapes, +} +impl fmt::Display for DicebearType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let repr = match self { + Self::Notionists => "notionists", + Self::Shapes => "shapes", + }; + write!(f, "{repr}") + } +} + +struct DicebearConfig<'a> { + gesture: &'a str, + browns: Vec, + eyes: Vec, + lips: Vec, +} + +fn dicebear_variants() -> &'static HashMap> { + static HASHMAP: OnceLock> = OnceLock::new(); + HASHMAP.get_or_init(|| { + let mut variants = HashMap::new(); + variants.insert( + AvatarFeeling::Alerting, + DicebearConfig { + gesture: "wavePointLongArms", + browns: vec![2, 6, 11, 13], + eyes: vec![2, 4], + lips: vec![1, 2, 7, 11, 19, 20, 24, 27], + }, + ); + variants.insert( + AvatarFeeling::Warning, + DicebearConfig { + gesture: "pointLongArm", + browns: vec![2, 5, 10, 13], + eyes: vec![1, 3], + lips: vec![1, 2, 4, 8, 10, 13, 18, 21, 29], + }, + ); + variants.insert( + AvatarFeeling::Ok, + DicebearConfig { + gesture: "okLongArm", + 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 fetch_text(req: String) -> RequestResult { + reqwest::get(req).await?.text().await +} + +async fn read_file(path: &str) -> IoResult { + read_to_string(path).await +} + +async fn fetch_dicebear_svg( + r#type: &DicebearType, + req_fields: &Vec, + placeholder_path: Option<&str>, +) -> String { + // TODO: Use configuration file + let url = "dicebear.tools.adrien.run"; + + let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + let type_str = r#type.to_string(); + let url = format!( + "https://{url}/7.x/{type_str}/svg?seed={seed}{}{}", + if !req_fields.is_empty() { "&" } else { " " }, + req_fields.join("&") + ); + + let text = match fetch_text(url).await { + Ok(text) => Some(text), + Err(err) => { + error!("Error during placeholder loading: {}", err); + + match placeholder_path { + Some(placeholder_path) => match read_file(placeholder_path).await { + Ok(content) => Some(content), + Err(err) => { + error!( + "Error during to read {placeholder_path} file: {}", + err.to_string() + ); + None + } + }, + None => None, + } + } + }; + + text.unwrap_or("".to_string()) +} + +pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String { + let (variant, feeling) = match config { + Some(config) => (dicebear_variants().get(&config.feeling), &config.feeling), + None => (None, &AvatarFeeling::Alerting), + }; + + let mut req_fields = Vec::::new(); + + if let Some(config) = config { + req_fields.push(format!("backgroundColor={}", config.background_color)); + } + + if let Some(variant) = variant { + req_fields.push(format!( + "gestureProbability=100&gesture={}", + &variant.gesture + )); + req_fields.push(format!( + "&browsProbability=100&brows={}", + render_dicebear_variants(&variant.browns) + )); + req_fields.push(format!( + "&eyesProbability=100&eyes={}", + render_dicebear_variants(&variant.eyes) + )); + req_fields.push(format!( + "&lipsProbability=100&lips={}", + render_dicebear_variants(&variant.lips) + )); + } + + let placeholder_path = match feeling { + AvatarFeeling::Ok => "./images/modal-default-ok-icon.svg", + AvatarFeeling::Warning => "./images/modal-default-warning-icon.svg", + AvatarFeeling::Alerting => "./images/modal-default-critical-icon.svg", + }; + + fetch_dicebear_svg( + &DicebearType::Notionists, + &req_fields, + Some(placeholder_path), + ) + .await +} + +pub struct ShapeConfig<'a> { + background_color: &'a str, + shape_1_color: &'a str, + shape_2_color: &'a str, + shape_3_color: &'a str, +} +impl<'a> ShapeConfig<'a> { + pub fn new( + background_color: &'a str, + shape_1_color: &'a str, + shape_2_color: &'a str, + shape_3_color: &'a str, + ) -> Self { + Self { + background_color, + shape_1_color, + shape_2_color, + shape_3_color, + } + } +} + +pub async fn generate_random_svg_shape<'a>(config: Option<&'a ShapeConfig<'a>>) -> String { + let mut req_fields = Vec::::new(); + + if let Some(config) = config { + req_fields.push(format!("backgroundColor={}", config.background_color)); + req_fields.push(format!("shape1Color={}", config.shape_1_color)); + req_fields.push(format!("shape2Color={}", config.shape_2_color)); + req_fields.push(format!("shape3Color={}", config.shape_3_color)); + } + + fetch_dicebear_svg(&DicebearType::Shapes, &req_fields, None).await +} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..e3d884c --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1 @@ +pub(crate) mod datasources; diff --git a/src/main.rs b/src/main.rs index 2bc55db..dce7f83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ #![allow(non_snake_case)] -pub mod components; -pub mod matrix_interface; -pub mod utils; +mod components; +mod data; +mod matrix_interface; +mod utils; use dioxus::prelude::*; use tokio::time::{sleep, Duration};