use std::fmt; use std::future::Future; use std::sync::OnceLock; use std::{collections::HashMap, future::IntoFuture}; use rand::distr::{Alphanumeric, SampleString}; use reqwest::Result as RequestResult; use tracing::error; cfg_if! { if #[cfg(target_family = "wasm")] { use web_sys; } else { use tokio::fs::read_to_string; } } #[derive(Eq, Hash, PartialEq)] 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 avatar_variants() -> &'static HashMap> { static VARIANTS: OnceLock> = OnceLock::new(); VARIANTS.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 { match reqwest::get(req).await?.error_for_status() { Ok(res) => res.text().await, Err(err) => Err(err), } } async fn fetch_dicebear_svg( r#type: &DicebearType, req_fields: &[String], placeholder_fetcher: Option>>>, ) -> String { // TODO: Use configuration file let url = "dicebear.tools.adrien.run"; let seed = Alphanumeric.sample_string(&mut rand::rng(), 16); let type_str = r#type.to_string(); let url = format!( "https://{url}/8.x/{type_str}/svg?seed={seed}&randomizeIds=true{}{}", 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); if let Some(placeholder_fetcher) = placeholder_fetcher { placeholder_fetcher.into_future().await } else { None } } }; text.unwrap_or("".to_string()) } cfg_if! { if #[cfg(target_family = "wasm")] { fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box>> { Box::new(async move { let url = format!("{}{}", web_sys::window().unwrap().origin(), path); match fetch_text(url).await { Ok(content) => Some(content), Err(err) => { error!("Error during {path} fetching: {}", err.to_string()); None } } }) } } else { fn gen_placeholder_fetcher(path: &'static str) -> Box>> { let path = format!("./public/{}", &path); Box::new(async move { match read_to_string(&path).await { Ok(content) => Some(content), Err(err) => { error!( "Error during the access to the {path} file: {}", err.to_string() ); None } } }) } } } pub async fn generate_random_svg_avatar<'a>(config: Option<&'a AvatarConfig<'a>>) -> String { let (variant, feeling) = match config { Some(config) => (avatar_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(gen_placeholder_fetcher(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)); } let placeholder_path = "/images/login-profile-placeholder.svg"; fetch_dicebear_svg( &DicebearType::Shapes, &req_fields, Some(gen_placeholder_fetcher(placeholder_path)), ) .await }