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 @@
+
\ 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 {})
- }
})
}