M sdbclient/src/main.rs => sdbclient/src/main.rs +1 -16
@@ 28,14 28,6 @@ pub enum GameState {
Game,
}
-// Input structs
-/// Login inputs
-#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
-pub struct InputsUserLogin(String, String);
-/// Register inputs
-#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
-pub struct InputsUserRegister(String, String, String, String);
-
fn main() {
let mut app = App::new();
@@ 97,14 89,7 @@ fn main() {
})
.insert_resource(cfg::CfgHidden {
api_server: "http://localhost:8080/api".to_string(),
- })
- .insert_resource(InputsUserLogin("".to_string(), "".to_string()))
- .insert_resource(InputsUserRegister(
- "".to_string(),
- "".to_string(),
- "".to_string(),
- "".to_string(),
- ));
+ });
app.add_startup_system(setup).add_state(GameState::Splash);
A => +123 -0
@@ 0,0 1,123 @@
/*
* This file is part of sdbclient
* Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
*
* Licensed under GPL-3.0-only.
* See LICENSE for licensing information.
*/
use bevy::{
prelude::*,
tasks::{AsyncComputeTaskPool, Task},
};
use bevy_console::PrintConsoleLine;
use futures_lite::future;
use crate::{
api::{self, user::ResponseToken},
cfg::{self, CfgDirs, CfgHidden, CfgUser},
util,
};
use super::MenuState;
pub mod ui;
pub struct AccountLoginPlugin;
impl Plugin for AccountLoginPlugin {
fn build(&self, app: &mut App) {
app.add_state(LoginState::None)
// UI system
.insert_resource(ui::InputsUserLogin::new())
.add_system_set(
SystemSet::on_update(LoginState::Input).with_system(ui::account_login_ui),
)
// Login system, as in calling the API
.add_system_set(
SystemSet::on_enter(LoginState::LoggingIn).with_system(start_login_call),
)
.add_system_set(
SystemSet::on_update(LoginState::LoggingIn).with_system(handle_login_call),
);
}
}
/// Login State
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub enum LoginState {
None,
Input,
LoggingIn,
}
#[derive(Component)]
struct LoginCall(Task<ResponseToken>);
fn start_login_call(
mut commands: Commands,
cfg_hidden: Res<CfgHidden>,
inputs: Res<ui::InputsUserLogin>,
) {
let api_address = cfg_hidden.api_server.clone();
let i = inputs.clone();
let thread_pool = AsyncComputeTaskPool::get();
let task = thread_pool.spawn(async move {
let token_response = api::user::token(api_address, i.email, i.password);
token_response
});
commands.spawn(LoginCall(task));
}
fn handle_login_call(
mut commands: Commands,
mut login_call_tasks: Query<(Entity, &mut LoginCall)>,
mut inputs: ResMut<ui::InputsUserLogin>,
mut login_state: ResMut<State<LoginState>>,
mut menu_state: ResMut<State<MenuState>>,
mut cfg_user: ResMut<CfgUser>,
cfg_dirs: Res<CfgDirs>,
mut console: EventWriter<PrintConsoleLine>,
) {
let (entity, mut task) = login_call_tasks.single_mut();
if let Some(login_response) = future::block_on(future::poll_once(&mut task.0)) {
match login_response {
ResponseToken::Valid(res) => {
console.send(PrintConsoleLine::new(format!(
"Logged in with: {}",
inputs.email
)));
// TODO: We need to fetch the user details at some point.
*cfg_user = CfgUser {
logged_in: true,
user_token: res.token,
id: res.id,
username: "NOT SET".to_string(),
email: "NOT SET".to_string(),
};
util::sl::save(
cfg_dirs.0.config_dir().to_str().unwrap(),
cfg::FILE_CFG_USER,
&cfg_user.into_inner(),
console,
);
login_state.set(LoginState::None).unwrap();
menu_state.set(MenuState::AccountLoggedIn).unwrap();
}
ResponseToken::Error { error } => {
inputs.error = error;
login_state.set(LoginState::Input).unwrap();
}
}
// Remove the task, since it's done now
commands.entity(entity).remove::<LoginCall>();
commands.entity(entity).despawn_recursive();
}
}
R => +37 -6
@@ 9,13 9,34 @@
use bevy::prelude::*;
use bevy_egui::{egui, EguiContext};
use crate::{util::eguipwd, InputsUserLogin};
use crate::util::eguipwd;
use super::MenuState;
use crate::plugins::menu::MenuState;
use super::LoginState;
/// Login inputs
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct InputsUserLogin {
pub email: String,
pub password: String,
pub error: String,
}
impl InputsUserLogin {
pub fn new() -> Self {
Self {
email: "".to_string(),
password: "".to_string(),
error: "".to_string(),
}
}
}
pub fn account_login_ui(
mut egui_context: ResMut<EguiContext>,
mut menu_state: ResMut<State<MenuState>>,
mut login_state: ResMut<State<LoginState>>,
mut inputs: ResMut<InputsUserLogin>,
) {
egui::Window::new("Login")
@@ 23,20 44,30 @@ pub fn account_login_ui(
.show(egui_context.ctx_mut(), |ui| {
ui.horizontal(|ui| {
ui.label("Email: ");
ui.text_edit_singleline(&mut inputs.0);
ui.text_edit_singleline(&mut inputs.email);
});
ui.horizontal(|ui| {
ui.label("Password: ");
ui.add(eguipwd::password(&mut inputs.1));
ui.add(eguipwd::password(&mut inputs.password));
});
// Show an error if there is one
if !inputs.error.is_empty() {
ui.horizontal(|ui| {
ui.label(egui::RichText::new(inputs.error.clone()).color(egui::Color32::RED));
});
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
if ui.button("Cancel").clicked() {
menu_state.set(MenuState::Main).unwrap();
login_state.set(LoginState::None).unwrap();
menu_state.set(MenuState::AccountLoggedOut).unwrap();
}
if ui.button("Login").clicked() {
info!("todo");
login_state.set(LoginState::LoggingIn).unwrap();
// Reset error field
inputs.error = "".to_string();
}
})
});
A => +153 -0
@@ 0,0 1,153 @@
/*
* This file is part of sdbclient
* Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
*
* Licensed under GPL-3.0-only.
* See LICENSE for licensing information.
*/
use bevy::{
prelude::*,
tasks::{AsyncComputeTaskPool, Task},
};
use bevy_console::PrintConsoleLine;
use futures_lite::future;
use crate::{
api::{
self,
user::{ResponseRegister, ResponseToken},
},
cfg::{self, CfgDirs, CfgHidden, CfgUser},
util,
};
use super::MenuState;
pub mod ui;
pub struct AccountRegisterPlugin;
impl Plugin for AccountRegisterPlugin {
fn build(&self, app: &mut App) {
app.add_state(RegisterState::None)
// UI system
.insert_resource(ui::InputsUserRegister::new())
.add_system_set(
SystemSet::on_update(RegisterState::Input).with_system(ui::account_register_ui),
)
// Register system, as in calling the API
.add_system_set(
SystemSet::on_enter(RegisterState::Registering).with_system(start_register_call),
)
.add_system_set(
SystemSet::on_update(RegisterState::Registering).with_system(handle_register_call),
);
}
}
/// Register State
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub enum RegisterState {
None,
Input,
Registering,
}
struct RegisterCallResponse {
register: ResponseRegister,
token: ResponseToken,
}
#[derive(Component)]
struct RegisterCall(Task<RegisterCallResponse>);
fn start_register_call(
mut commands: Commands,
cfg_hidden: Res<CfgHidden>,
inputs: Res<ui::InputsUserRegister>,
) {
let api_address = cfg_hidden.api_server.clone();
let i = inputs.clone();
let thread_pool = AsyncComputeTaskPool::get();
let task = thread_pool.spawn(async move {
let register_response = api::user::register(
api_address.clone(),
i.username.clone(),
i.email.clone(),
i.password.clone(),
);
// TODO: This is bad, if the above fails, this will too. Or maybe that doesn't matter?
let token_response =
api::user::token(api_address.clone(), i.email.clone(), i.password.clone());
RegisterCallResponse {
register: register_response,
token: token_response,
}
});
commands.spawn(RegisterCall(task));
}
fn handle_register_call(
mut commands: Commands,
mut register_call_tasks: Query<(Entity, &mut RegisterCall)>,
mut inputs: ResMut<ui::InputsUserRegister>,
mut register_state: ResMut<State<RegisterState>>,
mut menu_state: ResMut<State<MenuState>>,
mut cfg_user: ResMut<CfgUser>,
cfg_dirs: Res<CfgDirs>,
mut console: EventWriter<PrintConsoleLine>,
) {
let (entity, mut task) = register_call_tasks.single_mut();
if let Some(register_call_response) = future::block_on(future::poll_once(&mut task.0)) {
match register_call_response.register {
ResponseRegister::Valid(register_res) => {
console.send(PrintConsoleLine::new(format!(
"Registered user: {}",
register_res.username,
)));
match register_call_response.token {
ResponseToken::Valid(token_res) => {
*cfg_user = CfgUser {
logged_in: true,
user_token: token_res.token,
id: register_res.id,
username: register_res.username,
email: register_res.email,
};
util::sl::save(
cfg_dirs.0.config_dir().to_str().unwrap(),
cfg::FILE_CFG_USER,
&cfg_user.into_inner(),
console,
);
}
ResponseToken::Error { error } => {
// TODO: Handle? Is it possible to even get here without the server shitting itself between the register and token calls?
// And if the server does indeed shit itself between those calls, the user can just login normally, so 🤷♀️
console.send(PrintConsoleLine::new(format!(
"Something went wrong with getting the user token after registering, got error: '{}'", error
)));
}
}
register_state.set(RegisterState::None).unwrap();
menu_state.set(MenuState::AccountLoggedIn).unwrap();
}
ResponseRegister::Error { error } => {
inputs.error = error;
register_state.set(RegisterState::Input).unwrap();
}
}
// Remove the task, since it's done now
commands.entity(entity).remove::<RegisterCall>();
commands.entity(entity).despawn_recursive();
}
}
R => +58 -10
@@ 9,13 9,40 @@
use bevy::prelude::*;
use bevy_egui::{egui, EguiContext};
use crate::{util::eguipwd, InputsUserRegister};
use crate::util::eguipwd;
use super::MenuState;
use crate::plugins::menu::MenuState;
use super::RegisterState;
/// Register inputs
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct InputsUserRegister {
pub username: String,
pub email: String,
pub password: String,
pub password_confirm: String,
pub passwords_match: bool,
pub error: String,
}
impl InputsUserRegister {
pub fn new() -> Self {
Self {
username: "".to_string(),
email: "".to_string(),
password: "".to_string(),
password_confirm: "".to_string(),
passwords_match: true,
error: "".to_string(),
}
}
}
pub fn account_register_ui(
mut egui_context: ResMut<EguiContext>,
mut menu_state: ResMut<State<MenuState>>,
mut register_state: ResMut<State<RegisterState>>,
mut inputs: ResMut<InputsUserRegister>,
) {
egui::Window::new("Register")
@@ 23,31 50,52 @@ pub fn account_register_ui(
.show(egui_context.ctx_mut(), |ui| {
ui.horizontal(|ui| {
ui.label("Username: ");
ui.text_edit_singleline(&mut inputs.0);
ui.text_edit_singleline(&mut inputs.username);
});
ui.horizontal(|ui| {
ui.label("Email: ");
ui.text_edit_singleline(&mut inputs.1);
ui.text_edit_singleline(&mut inputs.email);
});
ui.horizontal(|ui| {
ui.label("Password: ");
ui.add(eguipwd::password(&mut inputs.2));
ui.add(eguipwd::password(&mut inputs.password));
});
ui.horizontal(|ui| {
ui.label("Confirm password: ");
ui.add(eguipwd::password(&mut inputs.3));
ui.add(eguipwd::password(&mut inputs.password_confirm));
});
inputs.passwords_match = inputs.password == inputs.password_confirm;
// Show an error if there is one
if !inputs.passwords_match {
ui.horizontal(|ui| {
ui.label(
egui::RichText::new("passwords don't match").color(egui::Color32::RED),
);
});
} else if !inputs.error.is_empty() {
ui.horizontal(|ui| {
ui.label(egui::RichText::new(inputs.error.clone()).color(egui::Color32::RED));
});
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
if ui.button("Cancel").clicked() {
menu_state.set(MenuState::Main).unwrap();
}
if ui.button("Register").clicked() {
info!("todo");
register_state.set(RegisterState::None).unwrap();
menu_state.set(MenuState::AccountLoggedOut).unwrap();
}
ui.add_enabled_ui(inputs.passwords_match, |ui| {
if ui.button("Register").clicked() {
register_state.set(RegisterState::Registering).unwrap();
// Reset error field
inputs.error = "".to_string();
}
});
})
});
}
M => +37 -2
@@ 11,14 11,21 @@ use bevy::{
ui::{JustifyContent, Size, Style, Val},
};
use crate::cfg::CfgUser;
use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};
/// Tag component for tagging entities on settings menu screen
#[derive(Component)]
pub struct OnAccountLoggedInScreen;
pub fn account_loggedin_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
pub fn account_loggedin_setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
cfg_user: ResMut<CfgUser>,
) {
let font = asset_server.load("fonts/FiraMono-Regular.ttf");
let font_bold = asset_server.load("fonts/FiraMono-Bold.ttf");
let button_style = Style {
size: Size::new(Val::Px(200.0), Val::Px(65.0)),
@@ 63,6 70,34 @@ pub fn account_loggedin_setup(mut commands: Commands, asset_server: Res<AssetSer
..Default::default()
}),
);
parent.spawn(
TextBundle::from_section(
"Logged in as: ",
TextStyle {
font: font.clone(),
font_size: 25.0,
color: TEXT_COLOR,
},
)
.with_style(Style {
margin: UiRect::all(Val::Px(50.)),
..Default::default()
}),
);
parent.spawn(
TextBundle::from_section(
cfg_user.username.clone(),
TextStyle {
font: font_bold.clone(),
font_size: 40.0,
color: TEXT_COLOR,
},
)
.with_style(Style {
margin: UiRect::all(Val::Px(50.)),
..Default::default()
}),
);
parent
.spawn(NodeBundle {
style: Style {
@@ 75,7 110,7 @@ pub fn account_loggedin_setup(mut commands: Commands, asset_server: Res<AssetSer
})
.with_children(|parent| {
for (action, text) in [
(MenuButtonAction::AccountLogin, "Logout"),
(MenuButtonAction::AccountLogout, "Logout"),
(MenuButtonAction::BackToMainMenu, "Back"),
] {
parent
M => +18 -19
@@ 33,11 33,8 @@ use accountscreenloggedout::*;
mod accountscreenloggedin;
use accountscreenloggedin::*;
mod accountloginui;
use accountloginui::*;
mod accountregisterui;
use accountregisterui::*;
mod accountlogin;
mod accountregister;
const TEXT_COLOR: Color = Color::rgb(0.9, 0.9, 0.9);
@@ 70,11 67,11 @@ impl Plugin for MenuPlugin {
// Systems for account loggedin screen
.add_system_set(SystemSet::on_enter(MenuState::AccountLoggedIn).with_system(account_loggedin_setup))
.add_system_set(SystemSet::on_exit(MenuState::AccountLoggedIn).with_system(despawn_screen::<OnAccountLoggedInScreen>))
// Account login and register systems
.add_system_set(SystemSet::on_update(MenuState::AccountLogin).with_system(account_login_ui))
.add_system_set(SystemSet::on_update(MenuState::AccountRegister).with_system(account_register_ui))
// Common systems
.add_system_set(SystemSet::on_update(GameState::MainMenu).with_system(menu_action).with_system(button_system));
app.add_plugin(accountregister::AccountRegisterPlugin)
.add_plugin(accountlogin::AccountLoginPlugin);
}
}
@@ 97,14 94,6 @@ pub enum MenuState {
#[derive(Component)]
struct OnAccountScreen;
/// Tag component for tagging entities on login screen
#[derive(Component)]
struct OnLoginScreen;
/// Tag component for tagging entities on register screen
#[derive(Component)]
struct OnRegisterScreen;
const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25);
const HOVERED_PRESSED_BUTTON: Color = Color::rgb(0.25, 0.65, 0.25);
@@ 125,6 114,7 @@ enum MenuButtonAction {
Account,
AccountLogin,
AccountRegister,
AccountLogout,
BackToMainMenu,
BackToSettings,
Exit,
@@ 137,6 127,8 @@ fn menu_action(
>,
mut app_exit_events: EventWriter<AppExit>,
mut menu_state: ResMut<State<MenuState>>,
mut register_state: ResMut<State<accountregister::RegisterState>>,
mut login_state: ResMut<State<accountlogin::LoginState>>,
//mut game_state: ResMut<State<GameState>>,
cfg_user: Res<CfgUser>,
) {
@@ 144,7 136,7 @@ fn menu_action(
if *interaction == Interaction::Clicked {
match menu_button_action {
MenuButtonAction::Exit => app_exit_events.send(AppExit),
MenuButtonAction::Play => println!("todo"),
MenuButtonAction::Play => warn!("todo"),
MenuButtonAction::Settings => menu_state.set(MenuState::Settings).unwrap(),
MenuButtonAction::SettingsDisplay => {
menu_state.set(MenuState::SettingsDisplay).unwrap()
@@ 160,10 152,17 @@ fn menu_action(
menu_state.set(MenuState::AccountLoggedOut).unwrap()
}
}
MenuButtonAction::AccountLogin => menu_state.set(MenuState::AccountLogin).unwrap(),
MenuButtonAction::AccountLogin => {
menu_state.set(MenuState::AccountLogin).unwrap();
login_state.set(accountlogin::LoginState::Input).unwrap();
}
MenuButtonAction::AccountRegister => {
menu_state.set(MenuState::AccountRegister).unwrap()
menu_state.set(MenuState::AccountRegister).unwrap();
register_state
.set(accountregister::RegisterState::Input)
.unwrap();
}
MenuButtonAction::AccountLogout => warn!("todo"),
MenuButtonAction::BackToSettings => menu_state.set(MenuState::Settings).unwrap(),
MenuButtonAction::BackToMainMenu => menu_state.set(MenuState::Main).unwrap(),
}