268 lines
7.5 KiB
Rust
268 lines
7.5 KiB
Rust
use std::fmt;
|
|
use std::future::Future;
|
|
use std::sync::OnceLock;
|
|
use std::{collections::HashMap, future::IntoFuture};
|
|
|
|
use rand::distributions::{Alphanumeric, DistString};
|
|
use reqwest::Result as RequestResult;
|
|
use tracing::error;
|
|
|
|
#[cfg(feature = "desktop")]
|
|
use tokio::fs::read_to_string;
|
|
|
|
#[cfg(feature = "web")]
|
|
use web_sys;
|
|
|
|
#[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<u32>,
|
|
eyes: Vec<u32>,
|
|
lips: Vec<u32>,
|
|
}
|
|
|
|
fn avatar_variants() -> &'static HashMap<AvatarFeeling, DicebearConfig<'static>> {
|
|
static VARIANTS: OnceLock<HashMap<AvatarFeeling, DicebearConfig>> = 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::<Vec<String>>()
|
|
.join(",")
|
|
}
|
|
|
|
async fn fetch_text(req: String) -> RequestResult<String> {
|
|
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<Box<impl Future<Output = Option<String>>>>,
|
|
) -> 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}/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(feature = "desktop")]
|
|
fn gen_placeholder_fetcher(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
|
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
|
|
}
|
|
}
|
|
})
|
|
}
|
|
#[cfg(feature = "web")]
|
|
fn gen_placeholder_fetcher<'a>(path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
|
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
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
#[cfg(not(any(feature = "desktop", feature = "web")))]
|
|
fn gen_placeholder_fetcher<'a>(_path: &'static str) -> Box<impl Future<Output = Option<String>>> {
|
|
Box::new(async move { 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::<String>::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::<String>::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
|
|
}
|