diff --git a/Cargo.toml b/Cargo.toml index be469de..ef41711 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,9 @@ log = "0.4.20" tracing = "0.1.40" futures-util = "0.3.29" futures = "0.3.29" +rand = "0.8.5" +reqwest = "0.11.24" +constcat = "0.5.0" [build] target = "x86_64-unknown-linux-gnu" diff --git a/images/login-profile-placeholder.svg b/images/login-profile-placeholder.svg new file mode 100644 index 0000000..935196f --- /dev/null +++ b/images/login-profile-placeholder.svg @@ -0,0 +1 @@ +"Shapes" by "Florian Körner", licensed under "CC0 1.0". / Remix of the original. - Created with dicebear.comShapesFlorian Körnerhttps://www.dicebear.com \ No newline at end of file diff --git a/src/components/login.rs b/src/components/login.rs index a1a5b4b..4dbbd64 100644 --- a/src/components/login.rs +++ b/src/components/login.rs @@ -1,131 +1,110 @@ -use std::str::FromStr; - +use constcat::concat as const_concat; use dioxus::prelude::*; use fermi::*; -use tracing::debug; +use rand::distributions::{Alphanumeric, DistString}; +use tracing::{debug, error, warn}; -use crate::base::SESSION; -use crate::components::avatar_selector::AvatarSelector; -use crate::components::header::Header; +use crate::base::{Session, SESSION}; + +use super::button::{LoginButton, RegisterButton}; +use super::spinner::Spinner; +use super::text_field::TextField; + +use super::wallpaper::Wallpaper; + +include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); turf::style_sheet!("src/components/login.scss"); -static EMPTY_PLACEHOLDER: &str = "Tmp placeholder"; +const SEP: &str = ","; -pub fn Login(cx: Scope) -> Element { - debug!("Login rendering"); +const BACKGROUND_COLORS_STR: &str = const_concat!( + style::COLOR_PRIMARY_150, + SEP, + style::COLOR_PRIMARY_140, + SEP, + style::COLOR_SECONDARY_150, + SEP, + style::COLOR_SECONDARY_140, + SEP, + style::COLOR_TERNARY_150, + SEP, + style::COLOR_TERNARY_140, +); - let session = use_atom_ref(cx, &SESSION); +const SHAPE_1_COLORS_STR: &str = const_concat!( + style::COLOR_PRIMARY_120, + SEP, + style::COLOR_PRIMARY_110, + SEP, + style::COLOR_PRIMARY_100, + SEP, + style::COLOR_PRIMARY_90, + SEP, + style::COLOR_PRIMARY_80, +); - let invalid_login = use_state(cx, || false); +const SHAPE_2_COLORS_STR: &str = const_concat!( + style::COLOR_SECONDARY_120, + SEP, + style::COLOR_SECONDARY_110, + SEP, + style::COLOR_SECONDARY_100, + SEP, + style::COLOR_SECONDARY_90, + SEP, + style::COLOR_SECONDARY_80, +); - let login = use_ref(cx, Login::new); +const SHAPE_3_COLORS_STR: &str = const_concat!( + style::COLOR_TERNARY_120, + SEP, + style::COLOR_TERNARY_110, + SEP, + style::COLOR_TERNARY_100, + SEP, + style::COLOR_TERNARY_90, + SEP, + style::COLOR_TERNARY_80, +); - let password_class = if **invalid_login { - ClassName::INVALID_INPUT - } else { - "" +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); + } }; - - let run_matrix_client = move |_| { - cx.spawn({ - - to_owned![session, login]; - - async move { - let login_ref = login.read(); - - session.write().update( - login_ref.homeserver_url.clone(), - login_ref.email.clone(), - login_ref.password.clone(), - ); - } - }) - }; - - let login_ref = login.read(); - let placeholder = EMPTY_PLACEHOLDER.to_string(); - let homeserver_url_value = login_ref.homeserver_url.as_ref().unwrap_or(&placeholder); - let email_value = login_ref.email.as_ref().unwrap_or(&placeholder); - let password_value = login_ref.password.as_ref().unwrap_or(&placeholder); - - cx.render(rsx! { - style { STYLE_SHEET }, - - div { - class: ClassName::ROOT, - - div { - class: ClassName::HEADER, - Header {}, - }, - - div { - class: ClassName::BODY, - div { - class: ClassName::AVATAR_SELECTOR_CONTAINER, - AvatarSelector {}, - }, - - p { - "Matrix homeserver:" - }, - input { - id: "input-homeserver-url", - r#type: "text", - name: "homeserver URL", - value: "{homeserver_url_value}", - oninput: move |evt| login.write().homeserver_url = Some(evt.value.clone()), - }, - - p { - "E-mail address:" - }, - input { - id: "login-input-email", - r#type: "text", - name: "email", - value: "{email_value}", - oninput: move |evt| login.write().email = Some(evt.value.clone()), - }, - p { - "Password:" - }, - input { - class: "{password_class}", - id: "login-input-password", - r#type: "password", - name: "Password", - value: "{password_value}", - oninput: move |evt| { - login.write().password = Some(evt.value.clone()); - invalid_login.set(false); - }, - }, - - div { - class: ClassName::FOOTER_BUTTONS, - input { - class: ClassName::BUTTON, - onclick: run_matrix_client, - r#type: "submit", - value: "sign in", - }, - }, - }, - }, - }) + res } -#[derive(Debug)] -struct Login { +struct LoginData { homeserver_url: Option, email: Option, password: Option, } -impl Login { +impl LoginData { fn new() -> Self { Self { homeserver_url: None, @@ -134,3 +113,163 @@ impl Login { } } } + +async fn on_login(session_ref: &UseAtomRef, login_ref: &UseRef) { + let login = login_ref.read(); + + session_ref.write().update( + login.homeserver_url.clone(), + login.email.clone(), + login.password.clone(), + ); +} + +#[derive(Props)] +pub struct LoginProps<'a> { + dicebear_hostname: Option<&'a str>, +} + +pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> { + debug!("Login rendering"); + + let session = use_atom_ref(cx, &SESSION); + + let login = use_ref(cx, LoginData::new); + let login_ref = login.read(); + let homeserver_url = login_ref.homeserver_url.as_deref().unwrap_or(""); + let email = login_ref.email.as_deref().unwrap_or(""); + let password = login_ref.password.as_deref().unwrap_or(""); + + // TODO: Enable the spinner for registration and login steps. + let spinner_animated = use_state(cx, || false); + + let url = cx + .props + .dicebear_hostname + .unwrap_or("dicebear.tools.adrien.run") + .to_string(); + + let random_avatar_future = + use_future( + cx, + &url, + |url| async move { generate_random_avatar(&url).await }, + ); + + let avatar = match random_avatar_future.value() { + Some(Some(svg)) => { + rsx!(div { + class: ClassName::CONTENT, + dangerous_inner_html: svg.as_str(), + }) + } + Some(None) | None => { + warn!("No profile image set or generated, display the placeholder"); + rsx!(div { + class: ClassName::CONTENT, + img { + src: "./images/login-profile-placeholder.svg" + } + }) + } + }; + + if **spinner_animated && session.read().is_logged { + debug!("Stop spinner"); + spinner_animated.set(false); + } + + let on_login = move |_| { + cx.spawn({ + + to_owned![login, session, spinner_animated]; + + async move { + spinner_animated.set(true); + on_login(&session, &login).await; + } + }) + }; + + cx.render(rsx! { + style { STYLE_SHEET }, + + Wallpaper {}, + + div { + class: ClassName::ROOT, + + div { + class: ClassName::FORM, + + div { + class: ClassName::PHOTO, + + onclick: move |_| { + random_avatar_future.restart() + }, + + {avatar}, + }, + + div { + class: ClassName::HOMESERVER, + TextField { + id: "hs_url", + r#type: "text", + placeholder: "Homeserver URL", + value: "{homeserver_url}", + oninput: move |evt: FormEvent| login.write().homeserver_url = Some(evt.value.clone()), + }, + }, + + div { + class: ClassName::EMAIL, + TextField { + id: "email", + r#type: "email", + placeholder: "Email", + value: "{email}", + is_value_valid: false, + oninput: move |evt: FormEvent| login.write().email = Some(evt.value.clone()), + }, + }, + + div { + class: ClassName::PASSWORD, + TextField { + id: "password", + r#type: "password", + placeholder: "Password", + value: "{password}", + oninput: move |evt: FormEvent| login.write().password = Some(evt.value.clone()), + }, + }, + + div { + class: ClassName::SPINNER, + Spinner { + animate: **spinner_animated, + }, + }, + + div { + class: ClassName::REGISTER, + RegisterButton { + id: "register", + // TODO: Handle the registration process + }, + }, + + div { + class: ClassName::LOGIN, + LoginButton { + id: "login", + focus: true, + onclick: on_login, + }, + }, + }, + }, + }) +} diff --git a/src/components/login.scss b/src/components/login.scss index bbb2496..0bc5243 100644 --- a/src/components/login.scss +++ b/src/components/login.scss @@ -1,53 +1,111 @@ -@import "../_base.scss"; +@import "../_base.scss" +@import "./spinner.scss" .root { - width: 90%; - height: 98%; + height: 100%; + width: 100%; display: flex; - flex-direction: column; align-items: center; + justify-content: center; - padding: 5%; - padding-top: 2%; + position: relative; + top: -100vh; - background: linear-gradient(rgb(138, 191, 209), rgb(236, 246, 249) 10%); + .form { + $height: 95%; + height: $height; - .header { - height: 5%; - width: 100%; - } + max-height: $form-max-height; + aspect-ratio: $form-aspect-ratio; - .body { - height: 50%; - width: 50%; - max-width: 400px; + border: $border-big; + border-color: $color-primary-90; + border-radius: $border-radius; - display: flex; - flex-direction: column; - justify-content: center; + background-color: $greyscale-0; - padding-bottom: 3%; + display: grid; - .invalidInput { - border-color: red; - } + $padding-col: 5%; + $button-height: 8%; + $button-overlap: 5%; + $profile-img-width: 40%; + $edit-padding: 7.5%; + $photo-padding: 17.5%; - .avatar-selector-container { - height: 30%; - width: 100%; + $padding-row: calc(5% * $form-aspect-ratio); + $spinner-col-width: calc(0% + ($button-overlap * 2)); + $profile-img-height: calc($profile-img-width * $form-aspect-ratio); + $profile-img-ext-width: calc((($profile-img-width - $spinner-col-width) / 2) - $button-overlap); + $center-width: calc((50% - $padding-col - $edit-padding - $photo-padding - $button-overlap - + $profile-img-ext-width) * 2); + $spinner-height: calc(($spinner-col-width + $center-width) * $form-aspect-ratio / $logo-aspect-ratio); - padding-left: 25%; - } + grid-template-columns: $padding-col $edit-padding $photo-padding + $button-overlap $profile-img-ext-width $center-width $profile-img-ext-width $button-overlap + $photo-padding $edit-padding $padding-col; + grid-template-rows: $padding-row $profile-img-height auto 5% 5% 5% 5% 5% 8.5% $spinner-height 8.5% $button-height $padding-row; + grid-template-areas: + ". . . . . . . . . . ." + ". . . photo photo photo photo photo . . ." + ". . . . . . . . . . ." + ". . homeserver homeserver homeserver homeserver homeserver homeserver homeserver . ." + ". . . . . . . . . . ." + ". . username username username username username username username . ." + ". . . . . . . . . . ." + ". . status status status status status status status . ." + ". . . . . . . . . . ." + ". . . . spinner spinner spinner . . . ." + ". . . . . . . . . . ." + ". register register register register . login login login login ." + ". . . . . . . . . . ." + ; + .photo { + grid-area: photo; - .footerButtons { - width: 100%; + display: flex; + align-items: center; + justify-content: center; - padding-top: 5%; + border: $border-normal; + border-radius: $border-radius; - display: flex; - justify-content: center; - } - } + overflow: hidden; + + .content { + height: calc(100% + (2 * $border-big-width)); + aspect-ratio: 1; + } + } + + .homeserver { + grid-area: homeserver; + } + + .email { + grid-area: username; + } + + .password { + grid-area: status; + } + + .spinner { + grid-area: spinner; + } + + button { + width: 100%; + } + + .register { + grid-area: register; + } + + .login { + grid-area: login; + } + } } diff --git a/src/components/main_window.rs b/src/components/main_window.rs index d1055dd..bc0e673 100644 --- a/src/components/main_window.rs +++ b/src/components/main_window.rs @@ -4,7 +4,6 @@ use tracing::debug; use crate::base::SESSION; use crate::components::contacts_window::ContactsWindow; -use crate::components::login::Login; pub fn MainWindow(cx: Scope) -> Element { debug!("MainWindow rendering"); @@ -16,8 +15,5 @@ pub fn MainWindow(cx: Scope) -> Element { if is_logged { rsx!(ContactsWindow {}) } - else { - rsx!(Login {}) - } }) }