✨ Add Login component
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::hash_map::Values;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use const_format::formatcp;
|
use const_format::formatcp;
|
||||||
@@ -13,8 +12,9 @@ use zxcvbn::zxcvbn;
|
|||||||
use crate::base::{Session, SESSION};
|
use crate::base::{Session, SESSION};
|
||||||
|
|
||||||
use super::button::{LoginButton, RegisterButton};
|
use super::button::{LoginButton, RegisterButton};
|
||||||
|
use super::modal::{Modal, Severity};
|
||||||
use super::spinner::Spinner;
|
use super::spinner::Spinner;
|
||||||
use super::text_input::{TextInput, TextInputState};
|
use super::text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState};
|
||||||
|
|
||||||
use super::wallpaper::Wallpaper;
|
use super::wallpaper::Wallpaper;
|
||||||
|
|
||||||
@@ -100,75 +100,237 @@ const REGISTER_ID_PLACEHOLDER: &'static str = "Email";
|
|||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
enum Process {
|
enum Process {
|
||||||
LOGIN,
|
Login,
|
||||||
REGISTRATION,
|
Registration,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_data(data: &Data, process: &Process) -> Result<(), ValidationError> {
|
trait OnValidationError {
|
||||||
match process {
|
fn on_validation_error(&self, error: &ValidationError) {
|
||||||
Process::REGISTRATION => {
|
let code = error.code.to_string();
|
||||||
let mut is_confirm_password_empty = true;
|
let msg = match code.as_str() {
|
||||||
if let Some(confirm_password) = &data.confirm_password {
|
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
|
||||||
if confirm_password.len() > 0 {
|
_ => None,
|
||||||
is_confirm_password_empty = false;
|
};
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
self.invalidate(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn reset(&self);
|
||||||
|
fn invalidate(&self, helper_text: String);
|
||||||
|
fn box_clone(&self) -> Box<dyn OnValidationError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Box<dyn OnValidationError> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
self.box_clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TextInputHandler {
|
||||||
|
state_ref: UseRef<TextInputState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextInputHandler {}
|
||||||
|
|
||||||
|
impl OnValidationError for TextInputHandler {
|
||||||
|
fn reset(&self) {
|
||||||
|
self.state_ref.write().reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&self, helper_text: String) {
|
||||||
|
self.state_ref.write().invalidate(helper_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_clone(&self) -> Box<dyn OnValidationError> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct UrlInputHandler {
|
||||||
|
state_ref: UseRef<TextInputState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UrlInputHandler {
|
||||||
|
pub fn new(state_ref: UseRef<TextInputState>) -> Self {
|
||||||
|
Self { state_ref }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnValidationError for UrlInputHandler {
|
||||||
|
fn on_validation_error(&self, error: &ValidationError) {
|
||||||
|
let code = error.code.to_string();
|
||||||
|
let msg = match code.as_str() {
|
||||||
|
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
|
||||||
|
INVALID_URL_ERROR_NAME => Some(INVALID_URL_ERROR_HELPER_TEXT),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
self.invalidate(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&self) {
|
||||||
|
self.state_ref.write().reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&self, helper_text: String) {
|
||||||
|
self.state_ref.write().invalidate(helper_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_clone(&self) -> Box<dyn OnValidationError> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct EmailInputHandler {
|
||||||
|
state_ref: UseRef<TextInputState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailInputHandler {
|
||||||
|
pub fn new(state_ref: UseRef<TextInputState>) -> Self {
|
||||||
|
Self { state_ref }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnValidationError for EmailInputHandler {
|
||||||
|
fn on_validation_error(&self, error: &ValidationError) {
|
||||||
|
let code = error.code.to_string();
|
||||||
|
let msg = match code.as_str() {
|
||||||
|
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
|
||||||
|
INVALID_EMAIL_ERROR_NAME => Some(INVALID_EMAIL_ERROR_HELPER_TEXT),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
self.invalidate(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn reset(&self) {
|
||||||
|
self.state_ref.write().reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&self, helper_text: String) {
|
||||||
|
self.state_ref.write().invalidate(helper_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_clone(&self) -> Box<dyn OnValidationError> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PasswordInputHandler {
|
||||||
|
state_ref: UseRef<PasswordInputState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PasswordInputHandler {
|
||||||
|
pub fn new(state_ref: UseRef<PasswordInputState>) -> Self {
|
||||||
|
Self { state_ref }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnValidationError for PasswordInputHandler {
|
||||||
|
fn on_validation_error(&self, error: &ValidationError) {
|
||||||
|
let code = error.code.to_string();
|
||||||
|
let msg = match code.as_str() {
|
||||||
|
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
|
||||||
|
INVALID_EMAIL_ERROR_NAME => Some(INVALID_EMAIL_ERROR_HELPER_TEXT),
|
||||||
|
TOO_WEAK_PASSWORD_ERROR_NAME => {
|
||||||
|
let mut score = 0.0;
|
||||||
|
if let Some(guesses_log10) = error.params.get("guesses_log10") {
|
||||||
|
if let Some(guesses_log10) = guesses_log10.as_f64() {
|
||||||
|
score = guesses_log10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.state_ref.write().score = score;
|
||||||
|
Some(TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
self.invalidate(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&self) {
|
||||||
|
self.state_ref.write().reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&self, helper_text: String) {
|
||||||
|
self.state_ref.write().invalidate(helper_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_clone(&self) -> Box<dyn OnValidationError> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_validation_errors(
|
||||||
|
field_errors: &HashMap<&str, &Vec<ValidationError>>,
|
||||||
|
handlers: &InputHandlers,
|
||||||
|
) {
|
||||||
|
for (field_name, errors) in field_errors {
|
||||||
|
if let Some(handler) = handlers.get(field_name) {
|
||||||
|
errors
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|e| handler.on_validation_error(e));
|
||||||
|
} else if *field_name == "__all__" {
|
||||||
|
for error in *errors {
|
||||||
|
let code = error.code.to_string();
|
||||||
|
match code.as_str() {
|
||||||
|
FIELDS_MISMATCH_ERROR_NAME => {
|
||||||
|
let values = error.params.values();
|
||||||
|
|
||||||
|
let field_names = values
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for field_name in field_names.iter() {
|
||||||
|
if let Some(handler) = handlers.get(*field_name) {
|
||||||
|
let other_field_names = field_names
|
||||||
|
.iter()
|
||||||
|
.filter(|f| **f != *field_name)
|
||||||
|
.map(|f| *f)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let other_field_names_len = other_field_names.len();
|
||||||
|
|
||||||
|
let formatted = format!(
|
||||||
|
"This field must match with {}{}{} field{}",
|
||||||
|
other_field_names[0..other_field_names_len - 1].join(", "),
|
||||||
|
if other_field_names_len > 1 {
|
||||||
|
" and "
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
other_field_names[other_field_names_len - 1],
|
||||||
|
if other_field_names_len > 1 { "s" } else { "" },
|
||||||
|
);
|
||||||
|
|
||||||
|
handler.invalidate(formatted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRED_ERROR_NAME => {
|
||||||
|
let Some(field_value) = error.params.get("field_name") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(field_name) = field_value.as_str() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(handler) = handlers.get(field_name) {
|
||||||
|
handler.on_validation_error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => todo!("{:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if is_confirm_password_empty {
|
} else {
|
||||||
let mut err = ValidationError::new(&REQUIRED_ERROR_NAME);
|
error!("No validation state found for \"{field_name}\" field name");
|
||||||
err.add_param(Cow::from("field_name"), &CONFIRM_PASSWORD_FIELD_NAME);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.password != data.confirm_password {
|
|
||||||
let mut err = ValidationError::new(FIELDS_MISMATCH_ERROR_NAME);
|
|
||||||
err.add_param(Cow::from("field_name_1"), &PASSWORD_FIELD_NAME);
|
|
||||||
err.add_param(Cow::from("field_name_2"), &CONFIRM_PASSWORD_FIELD_NAME);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Process::LOGIN => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_id(id: &Option<String>, process: &Process) -> Result<(), ValidationError> {
|
|
||||||
if *process == Process::REGISTRATION {
|
|
||||||
if !id.validate_email() {
|
|
||||||
let err = ValidationError::new(&INVALID_EMAIL_ERROR_NAME);
|
|
||||||
return Err(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_password(password: &Option<String>, process: &Process) -> Result<(), ValidationError> {
|
|
||||||
if *process == Process::REGISTRATION {
|
|
||||||
if let Some(password) = password {
|
|
||||||
let estimate = zxcvbn(password, &[]).unwrap();
|
|
||||||
|
|
||||||
let score = estimate.score();
|
|
||||||
|
|
||||||
// TODO: Give the limit using the configuration
|
|
||||||
if score <= 2 {
|
|
||||||
let mut err = ValidationError::new(TOO_WEAK_PASSWORD_ERROR_NAME);
|
|
||||||
err.add_param(Cow::from("score"), &score);
|
|
||||||
|
|
||||||
if let Some(feedback) = estimate.feedback() {
|
|
||||||
let suggestions = feedback
|
|
||||||
.suggestions()
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
err.add_param(Cow::from("suggestions"), &suggestions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Validate)]
|
#[derive(Debug, Validate)]
|
||||||
@@ -199,13 +361,119 @@ impl Data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_data(data: &Data, process: &Process) -> Result<(), ValidationError> {
|
||||||
|
match process {
|
||||||
|
Process::Registration => {
|
||||||
|
let mut is_confirm_password_empty = true;
|
||||||
|
if let Some(confirm_password) = &data.confirm_password {
|
||||||
|
if confirm_password.len() > 0 {
|
||||||
|
is_confirm_password_empty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_confirm_password_empty {
|
||||||
|
let mut err = ValidationError::new(&REQUIRED_ERROR_NAME);
|
||||||
|
err.add_param(Cow::from("field_name"), &CONFIRM_PASSWORD_FIELD_NAME);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.password != data.confirm_password {
|
||||||
|
let mut err = ValidationError::new(FIELDS_MISMATCH_ERROR_NAME);
|
||||||
|
err.add_param(Cow::from("field_name_1"), &PASSWORD_FIELD_NAME);
|
||||||
|
err.add_param(Cow::from("field_name_2"), &CONFIRM_PASSWORD_FIELD_NAME);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Process::Login => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_id(id: &Option<String>, process: &Process) -> Result<(), ValidationError> {
|
||||||
|
if *process == Process::Registration {
|
||||||
|
if !id.validate_email() {
|
||||||
|
let err = ValidationError::new(&INVALID_EMAIL_ERROR_NAME);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PasswordValidationResult {
|
||||||
|
score: u8,
|
||||||
|
rating: f64, // 0 <= rating <= 1
|
||||||
|
suggestions: Vec<String>,
|
||||||
|
}
|
||||||
|
impl PasswordValidationResult {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PasswordValidationResult {
|
||||||
|
score: 0,
|
||||||
|
rating: 0.0,
|
||||||
|
suggestions: Vec::<String>::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_password_score(
|
||||||
|
password: &String,
|
||||||
|
with_suggestions: Option<bool>,
|
||||||
|
) -> Option<PasswordValidationResult> {
|
||||||
|
let Ok(estimate) = zxcvbn(password, &[]) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = PasswordValidationResult::new();
|
||||||
|
result.score = estimate.score();
|
||||||
|
|
||||||
|
let mut rating = estimate.guesses_log10() * 4.5 / 100.0;
|
||||||
|
if rating > 1.0 {
|
||||||
|
rating = 1.0;
|
||||||
|
};
|
||||||
|
result.rating = rating;
|
||||||
|
|
||||||
|
let with_suggestions = with_suggestions.unwrap_or(false);
|
||||||
|
if with_suggestions {
|
||||||
|
if let Some(feedback) = estimate.feedback() {
|
||||||
|
for suggestion in feedback.suggestions() {
|
||||||
|
result.suggestions.push(suggestion.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_password(password: &Option<String>, process: &Process) -> Result<(), ValidationError> {
|
||||||
|
if *process == Process::Registration {
|
||||||
|
if let Some(password) = password {
|
||||||
|
if let Some(result) = compute_password_score(password, Some(true)) {
|
||||||
|
// TODO: To configuration?
|
||||||
|
if result.score <= 2 {
|
||||||
|
let mut err = ValidationError::new(TOO_WEAK_PASSWORD_ERROR_NAME);
|
||||||
|
err.add_param(Cow::from("score"), &result.score);
|
||||||
|
err.add_param(Cow::from("rating"), &result.rating);
|
||||||
|
err.add_param(Cow::from("suggestions"), &result.suggestions);
|
||||||
|
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Unable to compute the password score");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Password parameter is None");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn on_login(
|
fn on_login(
|
||||||
session_ref: &UseAtomRef<Session>,
|
session_ref: &UseAtomRef<Session>,
|
||||||
data_ref: &UseRef<Data>,
|
data_ref: &UseRef<Data>,
|
||||||
) -> Result<(), ValidationErrors> {
|
) -> Result<(), ValidationErrors> {
|
||||||
let login = data_ref.read();
|
let login = data_ref.read();
|
||||||
|
|
||||||
match login.validate_with_args(&Process::LOGIN) {
|
match login.validate_with_args(&Process::Login) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
session_ref.write().update(
|
session_ref.write().update(
|
||||||
login.homeserver_url.clone(),
|
login.homeserver_url.clone(),
|
||||||
@@ -224,7 +492,7 @@ fn on_register(
|
|||||||
) -> Result<(), ValidationErrors> {
|
) -> Result<(), ValidationErrors> {
|
||||||
let login = data_ref.read();
|
let login = data_ref.read();
|
||||||
|
|
||||||
match login.validate_with_args(&Process::REGISTRATION) {
|
match login.validate_with_args(&Process::Registration) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
error!("TODO: Manage registration process");
|
error!("TODO: Manage registration process");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -233,96 +501,104 @@ fn on_register(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_validation_errors(
|
#[derive(Clone)]
|
||||||
field_errors: &HashMap<&str, &Vec<ValidationError>>,
|
struct InputHandlers<'a> {
|
||||||
by_field_name_states: &HashMap<&'static str, &UseRef<TextInputState>>,
|
handlers: HashMap<&'a str, Box<dyn OnValidationError>>,
|
||||||
) {
|
}
|
||||||
for (field_name, errors) in field_errors {
|
impl<'a> InputHandlers<'a> {
|
||||||
if let Some(state) = by_field_name_states.get(field_name) {
|
fn new() -> Self {
|
||||||
for error in *errors {
|
Self {
|
||||||
let code = error.code.to_string();
|
handlers: HashMap::new(),
|
||||||
match code.as_str() {
|
}
|
||||||
REQUIRED_ERROR_NAME => {
|
}
|
||||||
state
|
fn insert<T: 'static + OnValidationError>(&mut self, name: &'a str, handler: T) {
|
||||||
.write()
|
self.handlers.insert(name, Box::new(handler));
|
||||||
.invalidate(REQUIRED_ERROR_HELPER_TEXT.to_string());
|
}
|
||||||
}
|
|
||||||
INVALID_URL_ERROR_NAME => {
|
|
||||||
state
|
|
||||||
.write()
|
|
||||||
.invalidate(INVALID_URL_ERROR_HELPER_TEXT.to_string());
|
|
||||||
}
|
|
||||||
INVALID_EMAIL_ERROR_NAME => {
|
|
||||||
state
|
|
||||||
.write()
|
|
||||||
.invalidate(INVALID_EMAIL_ERROR_HELPER_TEXT.to_string());
|
|
||||||
}
|
|
||||||
TOO_WEAK_PASSWORD_ERROR_NAME => {
|
|
||||||
state
|
|
||||||
.write()
|
|
||||||
.invalidate(TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT.to_string());
|
|
||||||
}
|
|
||||||
other => todo!("{:?}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if *field_name == "__all__" {
|
|
||||||
for error in *errors {
|
|
||||||
let code = error.code.to_string();
|
|
||||||
match code.as_str() {
|
|
||||||
FIELDS_MISMATCH_ERROR_NAME => {
|
|
||||||
let values = error.params.values();
|
|
||||||
let field_names = values
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|v| v.as_str())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
for field_name in field_names.iter() {
|
|
||||||
if let Some(state) = by_field_name_states.get(*field_name) {
|
|
||||||
let other_field_names = field_names
|
|
||||||
.iter()
|
|
||||||
.filter(|f| **f != *field_name)
|
|
||||||
.map(|f| *f)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let other_field_names_len = other_field_names.len();
|
|
||||||
let formatted = format!(
|
|
||||||
"This field must match with {}{}{} field{}",
|
|
||||||
other_field_names[0..other_field_names_len - 1].join(", "),
|
|
||||||
if other_field_names_len > 1 {
|
|
||||||
" and "
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
other_field_names[other_field_names_len - 1],
|
|
||||||
if other_field_names_len > 1 { "s" } else { "" },
|
|
||||||
);
|
|
||||||
|
|
||||||
if state.read().is_valid {
|
fn get(&self, name: &'a str) -> Option<&dyn OnValidationError> {
|
||||||
state.write().invalidate(formatted);
|
if let Some(handler) = self.handlers.get(name) {
|
||||||
}
|
return Some(handler.as_ref());
|
||||||
}
|
}
|
||||||
}
|
None
|
||||||
}
|
}
|
||||||
REQUIRED_ERROR_NAME => {
|
fn reset_handlers(&self) {
|
||||||
if let Some(field_value) = error.params.get("field_name") {
|
self.handlers.values().for_each(|h| h.reset());
|
||||||
if let Some(field_name) = field_value.as_str() {
|
}
|
||||||
if let Some(state) = by_field_name_states.get(field_name) {
|
}
|
||||||
state
|
|
||||||
.write()
|
macro_rules! on_input {
|
||||||
.invalidate(REQUIRED_ERROR_HELPER_TEXT.to_string());
|
($data_ref:ident, $data_field:ident) => {
|
||||||
}
|
move |evt: FormEvent| {
|
||||||
}
|
$data_ref.write().$data_field = if evt.value.len() > 0 {
|
||||||
}
|
Some(evt.value.clone())
|
||||||
}
|
} else {
|
||||||
other => todo!("{:?}", other),
|
None
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! refresh_password_state {
|
||||||
|
($data:ident, $data_field:ident, $state:ident) => {
|
||||||
|
let mut rating = 0.0;
|
||||||
|
if let Some(password) = &$data.$data_field {
|
||||||
|
if let Some(result) = compute_password_score(password, None) {
|
||||||
|
rating = result.rating;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$state.write_silent().score = rating;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_modal<'a, 'b>(
|
||||||
|
config: &'b PasswordSuggestionsModalConfig<'a>,
|
||||||
|
on_confirm: impl Fn(Event<MouseData>) + 'b,
|
||||||
|
) -> LazyNodes<'a, 'b>
|
||||||
|
where
|
||||||
|
'b: 'a,
|
||||||
|
{
|
||||||
|
let suggestions = config.suggestions.get(PASSWORD_FIELD_NAME);
|
||||||
|
|
||||||
|
let mut rendered_suggestions = Vec::<LazyNodes>::new();
|
||||||
|
if let Some(suggestions) = suggestions {
|
||||||
|
if suggestions.len() == 1 {
|
||||||
|
rendered_suggestions.push(rsx!(suggestions[0].as_str()));
|
||||||
} else {
|
} else {
|
||||||
error!("No validation state found for \"{field_name}\" field name");
|
suggestions
|
||||||
|
.iter()
|
||||||
|
.for_each(|s| rendered_suggestions.push(rsx!(li { s.as_str() })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
Modal {
|
||||||
|
severity: config.severity.clone(),
|
||||||
|
title: config.title.as_ref(),
|
||||||
|
on_confirm: on_confirm,
|
||||||
|
|
||||||
|
div {
|
||||||
|
rendered_suggestions.into_iter()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_states(states: Values<&'static str, &UseRef<TextInputState>>) {
|
type Suggestions<'a> = HashMap<&'a str, Vec<String>>;
|
||||||
let _ = states.for_each(|s| s.write().reset());
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PasswordSuggestionsModalConfig<'a> {
|
||||||
|
severity: Severity,
|
||||||
|
title: String,
|
||||||
|
suggestions: Suggestions<'a>,
|
||||||
|
}
|
||||||
|
impl<'a> PasswordSuggestionsModalConfig<'a> {
|
||||||
|
fn new(severity: Option<Severity>, title: String) -> Self {
|
||||||
|
Self {
|
||||||
|
severity: severity.unwrap_or(Severity::Critical),
|
||||||
|
title,
|
||||||
|
suggestions: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Props)]
|
#[derive(Props)]
|
||||||
@@ -337,24 +613,33 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
|||||||
|
|
||||||
let data_ref = use_ref(cx, Data::new);
|
let data_ref = use_ref(cx, Data::new);
|
||||||
|
|
||||||
let current_process = use_state(cx, || Process::LOGIN);
|
let current_process = use_state(cx, || Process::Login);
|
||||||
|
|
||||||
let data = data_ref.read();
|
let data = data_ref.read();
|
||||||
|
|
||||||
let homeserver_url = data.homeserver_url.as_deref().unwrap_or("");
|
let homeserver_url = data.homeserver_url.as_deref().unwrap_or("");
|
||||||
let homeserver_url_state = use_ref(cx, TextInputState::new);
|
let homeserver_url_state = use_ref(cx, TextInputState::new);
|
||||||
let id = data.id.as_deref().unwrap_or("");
|
let id = data.id.as_deref().unwrap_or("");
|
||||||
let id_state = use_ref(cx, TextInputState::new);
|
let id_state = use_ref(cx, TextInputState::new);
|
||||||
let password = data.password.as_deref().unwrap_or("");
|
let password = data.password.as_deref().unwrap_or("");
|
||||||
let password_state = use_ref(cx, TextInputState::new);
|
let password_state = use_ref(cx, PasswordInputState::new);
|
||||||
let confirm_password = data.confirm_password.as_deref().unwrap_or("");
|
let confirm_password = data.confirm_password.as_deref().unwrap_or("");
|
||||||
let confirm_password_state = use_ref(cx, TextInputState::new);
|
let confirm_password_state = use_ref(cx, PasswordInputState::new);
|
||||||
|
|
||||||
let by_field_name_states: HashMap<&'static str, &UseRef<TextInputState>> = HashMap::from([
|
let mut handlers = InputHandlers::new();
|
||||||
(HOMESERVER_FIELD_NAME, homeserver_url_state),
|
handlers.insert(
|
||||||
(ID_FIELD_NAME, id_state),
|
HOMESERVER_FIELD_NAME,
|
||||||
(PASSWORD_FIELD_NAME, password_state),
|
UrlInputHandler::new(homeserver_url_state.clone()),
|
||||||
(CONFIRM_PASSWORD_FIELD_NAME, confirm_password_state),
|
);
|
||||||
]);
|
handlers.insert(ID_FIELD_NAME, EmailInputHandler::new(id_state.clone()));
|
||||||
|
handlers.insert(
|
||||||
|
PASSWORD_FIELD_NAME,
|
||||||
|
PasswordInputHandler::new(password_state.clone()),
|
||||||
|
);
|
||||||
|
handlers.insert(
|
||||||
|
CONFIRM_PASSWORD_FIELD_NAME,
|
||||||
|
PasswordInputHandler::new(confirm_password_state.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
let spinner_animated = use_state(cx, || false);
|
let spinner_animated = use_state(cx, || false);
|
||||||
let id_placeholder = use_state(cx, || LOGIN_ID_PLACEHOLDER);
|
let id_placeholder = use_state(cx, || LOGIN_ID_PLACEHOLDER);
|
||||||
@@ -395,14 +680,25 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
|||||||
spinner_animated.set(false);
|
spinner_animated.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let on_login = {
|
refresh_password_state!(data, password, password_state);
|
||||||
to_owned![by_field_name_states];
|
refresh_password_state!(data, confirm_password, confirm_password_state);
|
||||||
|
|
||||||
|
let modal_configs = use_ref(cx, Vec::<PasswordSuggestionsModalConfig>::new);
|
||||||
|
let modal_config = use_state(cx, || None::<PasswordSuggestionsModalConfig>);
|
||||||
|
|
||||||
|
if modal_configs.read().len() > 0 && modal_config.is_none() {
|
||||||
|
modal_config.set(modal_configs.write_silent().pop());
|
||||||
|
}
|
||||||
|
|
||||||
|
let on_clicked_login = {
|
||||||
|
to_owned![handlers];
|
||||||
|
|
||||||
move |_| {
|
move |_| {
|
||||||
reset_states(by_field_name_states.values());
|
|
||||||
|
|
||||||
if **current_process == Process::REGISTRATION {
|
handlers.reset_handlers();
|
||||||
current_process.set(Process::LOGIN);
|
|
||||||
|
if **current_process == Process::Registration {
|
||||||
|
current_process.set(Process::Login);
|
||||||
data_ref.write().id = None;
|
data_ref.write().id = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -411,55 +707,101 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
|||||||
|
|
||||||
if let Err(errors) = on_login(&session, &data_ref) {
|
if let Err(errors) = on_login(&session, &data_ref) {
|
||||||
let field_errors = errors.field_errors();
|
let field_errors = errors.field_errors();
|
||||||
on_validation_errors(&field_errors, &by_field_name_states);
|
on_validation_errors(&field_errors, &handlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
spinner_animated.set(false);
|
spinner_animated.set(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let on_register = {
|
let on_clicked_register = {
|
||||||
to_owned![by_field_name_states];
|
to_owned![handlers, modal_configs];
|
||||||
|
|
||||||
move |_| {
|
move |_| {
|
||||||
reset_states(by_field_name_states.values());
|
if **current_process == Process::Login {
|
||||||
|
current_process.set(Process::Registration);
|
||||||
if **current_process == Process::LOGIN {
|
|
||||||
current_process.set(Process::REGISTRATION);
|
|
||||||
data_ref.write().id = None;
|
data_ref.write().id = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlers.reset_handlers();
|
||||||
|
|
||||||
spinner_animated.set(true);
|
spinner_animated.set(true);
|
||||||
|
|
||||||
if let Err(errors) = on_register(&session, &data_ref) {
|
if let Err(errors) = on_register(&session, &data_ref) {
|
||||||
|
let field_name = PASSWORD_FIELD_NAME;
|
||||||
|
|
||||||
let field_errors = errors.field_errors();
|
let field_errors = errors.field_errors();
|
||||||
on_validation_errors(&field_errors, &by_field_name_states);
|
if let Some(errors) = field_errors.get(field_name) {
|
||||||
|
let mut suggestions_msgs = Vec::<String>::new();
|
||||||
|
errors
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.code == TOO_WEAK_PASSWORD_ERROR_NAME)
|
||||||
|
.for_each(|e| {
|
||||||
|
if let Some(suggestions_value) = e.params.get("suggestions") {
|
||||||
|
if let Some(suggestion) = suggestions_value.as_array() {
|
||||||
|
let msgs = suggestion.iter().map(|msg| {
|
||||||
|
let msg = msg.to_string();
|
||||||
|
let without_quote_marks = &msg[1..msg.len() - 1];
|
||||||
|
without_quote_marks.to_string()
|
||||||
|
});
|
||||||
|
suggestions_msgs.extend(msgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if suggestions_msgs.len() > 0 {
|
||||||
|
let mut modal_config = PasswordSuggestionsModalConfig::new(
|
||||||
|
None,
|
||||||
|
"Let's talk about your password".to_string(),
|
||||||
|
);
|
||||||
|
modal_config
|
||||||
|
.suggestions
|
||||||
|
.insert(field_name, suggestions_msgs);
|
||||||
|
|
||||||
|
modal_configs.write().push(modal_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_validation_errors(&field_errors, &handlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
spinner_animated.set(false);
|
spinner_animated.set(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut form_classes: [&str; 2] = [ClassName::FORM, ""];
|
let mut form_classes: [&str; 2] = [ClassName::LOGIN_FORM, ""];
|
||||||
let mut confirm_password_classes: [&str; 2] = [ClassName::CONFIRM_PASSWORD, ""];
|
let mut password_classes: [&str; 2] = [ClassName::LOGIN_FORM_PASSWORD, ""];
|
||||||
|
let mut confirm_password_classes: [&str; 2] = [ClassName::LOGIN_FORM_CONFIRM_PASSWORD, ""];
|
||||||
|
|
||||||
match **current_process {
|
match **current_process {
|
||||||
Process::REGISTRATION => {
|
Process::Registration => {
|
||||||
form_classes[1] = ClassName::REGISTER;
|
form_classes[1] = ClassName::REGISTER;
|
||||||
|
password_classes[1] = ClassName::SHOW;
|
||||||
confirm_password_classes[1] = ClassName::SHOW;
|
confirm_password_classes[1] = ClassName::SHOW;
|
||||||
|
|
||||||
if **id_placeholder != REGISTER_ID_PLACEHOLDER {
|
if **id_placeholder != REGISTER_ID_PLACEHOLDER {
|
||||||
id_placeholder.set(REGISTER_ID_PLACEHOLDER);
|
id_placeholder.set(REGISTER_ID_PLACEHOLDER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Process::LOGIN => {
|
Process::Login => {
|
||||||
if **id_placeholder != LOGIN_ID_PLACEHOLDER {
|
if **id_placeholder != LOGIN_ID_PLACEHOLDER {
|
||||||
id_placeholder.set(LOGIN_ID_PLACEHOLDER);
|
id_placeholder.set(LOGIN_ID_PLACEHOLDER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let on_modal_confirm = move |_: Event<MouseData>| {
|
||||||
|
modal_config.set(None);
|
||||||
|
};
|
||||||
|
let rendered_modal = if let Some(modal_config) = modal_config.get() {
|
||||||
|
Some(render!(generate_modal(modal_config, on_modal_confirm)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let form_classes_str = form_classes.join(" ");
|
let form_classes_str = form_classes.join(" ");
|
||||||
|
let password_classes_str = password_classes.join(" ");
|
||||||
let confirm_password_classes_str = confirm_password_classes.join(" ");
|
let confirm_password_classes_str = confirm_password_classes.join(" ");
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
@@ -486,50 +828,42 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
|||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_FORM_HOMESERVER,
|
class: ClassName::LOGIN_FORM_HOMESERVER,
|
||||||
TextInput {
|
TextInput {
|
||||||
id: "hs_url",
|
|
||||||
r#type: "text",
|
|
||||||
placeholder: "Homeserver URL",
|
placeholder: "Homeserver URL",
|
||||||
value: "{homeserver_url}",
|
value: "{homeserver_url}",
|
||||||
state: homeserver_url_state,
|
state: homeserver_url_state,
|
||||||
oninput: move |evt: FormEvent| data_ref.write().homeserver_url = if evt.value.len() > 0 {
|
oninput: on_input![data_ref, homeserver_url],
|
||||||
Some(evt.value.clone()) } else { None },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_FORM_ID,
|
class: ClassName::LOGIN_FORM_ID,
|
||||||
TextInput {
|
TextInput {
|
||||||
r#type: "text",
|
|
||||||
placeholder: "{id_placeholder}",
|
placeholder: "{id_placeholder}",
|
||||||
value: "{id}",
|
value: "{id}",
|
||||||
state: id_state,
|
state: id_state,
|
||||||
oninput: move |evt: FormEvent| data_ref.write().id = if evt.value.len() > 0 {
|
oninput: on_input![data_ref, id],
|
||||||
Some(evt.value.clone()) } else { None },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: ClassName::PASSWORD,
|
class: "{password_classes_str}",
|
||||||
TextInput {
|
PasswordTextInput {
|
||||||
r#type: "password",
|
|
||||||
placeholder: "Password",
|
placeholder: "Password",
|
||||||
value: "{password}",
|
value: "{password}",
|
||||||
state: password_state,
|
state: password_state,
|
||||||
oninput: move |evt: FormEvent| data_ref.write().password = if evt.value.len() > 0 {
|
oninput: on_input![data_ref, password],
|
||||||
Some(evt.value.clone()) } else { None },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: "{confirm_password_classes_str}",
|
class: "{confirm_password_classes_str}",
|
||||||
TextInput {
|
PasswordTextInput {
|
||||||
r#type: "password",
|
|
||||||
placeholder: "Confirm Password",
|
placeholder: "Confirm Password",
|
||||||
value: "{confirm_password}",
|
value: "{confirm_password}",
|
||||||
state: confirm_password_state,
|
state: confirm_password_state,
|
||||||
oninput: move |evt: FormEvent| data_ref.write().confirm_password = if evt.value.len() > 0 {
|
oninput: on_input![data_ref, confirm_password],
|
||||||
Some(evt.value.clone()) } else { None },
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@@ -542,7 +876,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
|||||||
div {
|
div {
|
||||||
class: ClassName::LOGIN_FORM_REGISTER_BUTTON,
|
class: ClassName::LOGIN_FORM_REGISTER_BUTTON,
|
||||||
RegisterButton {
|
RegisterButton {
|
||||||
onclick: on_register,
|
onclick: on_clicked_register,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -550,10 +884,12 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
|||||||
class: ClassName::LOGIN_FORM_LOGIN_BUTTON,
|
class: ClassName::LOGIN_FORM_LOGIN_BUTTON,
|
||||||
LoginButton {
|
LoginButton {
|
||||||
focus: true,
|
focus: true,
|
||||||
onclick: on_login,
|
onclick: on_clicked_login,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
rendered_modal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -85,7 +85,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
height: calc(100% + (2 * $border-big-width));
|
height: calc(100% + (2 * $border-normal-width));
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,11 @@
|
|||||||
|
|
||||||
&__spinner {
|
&__spinner {
|
||||||
grid-area: spinner;
|
grid-area: spinner;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
41
src/main.rs
41
src/main.rs
@@ -7,10 +7,13 @@ pub mod utils;
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_desktop::Config;
|
use dioxus_desktop::Config;
|
||||||
use fermi::*;
|
use fermi::*;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
use tracing::{debug, Level};
|
use tracing::{debug, Level};
|
||||||
|
|
||||||
use crate::base::{login, sync_rooms, APP_SETTINGS, CHATS_WIN_INTERFACE, ROOMS, SESSION};
|
use crate::base::{login, sync_rooms, APP_SETTINGS, CHATS_WIN_INTERFACE, ROOMS, SESSION};
|
||||||
use crate::components::chats_window::{ChatsWindow, ChatsWindowProps};
|
use crate::components::chats_window::{ChatsWindow, ChatsWindowProps};
|
||||||
|
use crate::components::loading::LoadingPage;
|
||||||
|
use crate::components::login::Login;
|
||||||
use crate::components::main_window::MainWindow;
|
use crate::components::main_window::MainWindow;
|
||||||
|
|
||||||
mod base;
|
mod base;
|
||||||
@@ -25,6 +28,20 @@ fn App(cx: Scope) -> Element {
|
|||||||
let rooms_ref = use_atom_ref(cx, &ROOMS);
|
let rooms_ref = use_atom_ref(cx, &ROOMS);
|
||||||
let chats_win_interface_ref = use_atom_ref(cx, &CHATS_WIN_INTERFACE);
|
let chats_win_interface_ref = use_atom_ref(cx, &CHATS_WIN_INTERFACE);
|
||||||
|
|
||||||
|
let ready = use_state(cx, || false);
|
||||||
|
|
||||||
|
// Dummy timer simulating the loading of the application
|
||||||
|
let _: &Coroutine<()> = use_coroutine(cx, |_: UnboundedReceiver<_>| {
|
||||||
|
to_owned![ready];
|
||||||
|
async move {
|
||||||
|
debug!("Not ready");
|
||||||
|
sleep(Duration::from_secs(3)).await;
|
||||||
|
// sleep(Duration::from_secs(0)).await;
|
||||||
|
debug!("Ready");
|
||||||
|
ready.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let chats_win_state = use_state(cx, || None);
|
let chats_win_state = use_state(cx, || None);
|
||||||
|
|
||||||
let login_coro = use_coroutine(cx, |rx| {
|
let login_coro = use_coroutine(cx, |rx| {
|
||||||
@@ -88,13 +105,25 @@ fn App(cx: Scope) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.render(rsx! {
|
if **ready {
|
||||||
MainWindow {}
|
if session_ref.read().is_logged {
|
||||||
})
|
debug!("Should render the MainWindow component");
|
||||||
|
cx.render(rsx! {
|
||||||
|
MainWindow {},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cx.render(rsx! {
|
||||||
|
Login {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cx.render(rsx! {
|
||||||
|
LoadingPage {},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
fn main() {
|
||||||
async fn main() -> anyhow::Result<()> {
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
// .pretty()
|
// .pretty()
|
||||||
.with_max_level(Level::DEBUG)
|
.with_max_level(Level::DEBUG)
|
||||||
@@ -102,6 +131,4 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
dioxus_desktop::launch(App);
|
dioxus_desktop::launch(App);
|
||||||
// dioxus_web::launch(App);
|
// dioxus_web::launch(App);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user