Add registration dialog

This commit is contained in:
Elias Projahn 2021-01-30 23:16:44 +01:00
parent c9d9c1bc24
commit 1f90f6108e
9 changed files with 530 additions and 22 deletions

View file

@ -15,6 +15,7 @@
<file preprocess="xml-stripblanks">ui/preferences.ui</file> <file preprocess="xml-stripblanks">ui/preferences.ui</file>
<file preprocess="xml-stripblanks">ui/recording_editor.ui</file> <file preprocess="xml-stripblanks">ui/recording_editor.ui</file>
<file preprocess="xml-stripblanks">ui/recording_screen.ui</file> <file preprocess="xml-stripblanks">ui/recording_screen.ui</file>
<file preprocess="xml-stripblanks">ui/register_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/selector.ui</file> <file preprocess="xml-stripblanks">ui/selector.ui</file>
<file preprocess="xml-stripblanks">ui/server_dialog.ui</file> <file preprocess="xml-stripblanks">ui/server_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/source_selector.ui</file> <file preprocess="xml-stripblanks">ui/source_selector.ui</file>

View file

@ -47,8 +47,20 @@
<property name="margin-end">12</property> <property name="margin-end">12</property>
<property name="margin-top">18</property> <property name="margin-top">18</property>
<property name="margin-bottom">12</property> <property name="margin-bottom">12</property>
<property name="maximum-size">500</property> <property name="maximum-size">800</property>
<property name="tightening-threshold">300</property> <child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Login to existing account</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child> <child>
<object class="GtkFrame"> <object class="GtkFrame">
<property name="valign">start</property> <property name="valign">start</property>
@ -87,6 +99,40 @@
</child> </child>
</object> </object>
</child> </child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Create a new account</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="valign">start</property>
<child>
<object class="GtkListBox">
<property name="selection-mode">none</property>
<child>
<object class="AdwActionRow">
<property name="activatable">True</property>
<property name="title" translatable="yes">Register a new account</property>
<property name="activatable-widget">register_button</property>
<child>
<object class="GtkButton" id="register_button">
<property name="label" translatable="yes">Start</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>

233
res/ui/register_dialog.ui Normal file
View file

@ -0,0 +1,233 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<object class="GtkStack" id="widget">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar">
<property name="show-start-title-buttons">false</property>
<property name="show-end-title-buttons">false</property>
<property name="title-widget">
<object class="GtkLabel">
<property name="label" translatable="yes">Register</property>
<style>
<class name="title"/>
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkSpinner">
<property name="spinning">true</property>
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="halign">center</property>
<property name="valign">center</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">content</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar">
<property name="show-start-title-buttons">false</property>
<property name="show-end-title-buttons">false</property>
<property name="title-widget">
<object class="GtkLabel">
</object>
</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkButton" id="register_button">
<property name="label" translatable="yes">Create account</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="revealed">False</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<child>
<object class="AdwClamp">
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">18</property>
<property name="margin-bottom">12</property>
<property name="maximum-size">800</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Personal data</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="valign">start</property>
<child>
<object class="GtkListBox">
<property name="selection-mode">none</property>
<child>
<object class="AdwActionRow">
<property name="activatable">True</property>
<property name="title" translatable="yes">Username</property>
<property name="activatable-widget">username_entry</property>
<child>
<object class="GtkEntry" id="username_entry">
<property name="valign">center</property>
<property name="hexpand">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="activatable">True</property>
<property name="title" translatable="yes">E-mail (optional)</property>
<property name="activatable-widget">email_entry</property>
<child>
<object class="GtkEntry" id="email_entry">
<property name="valign">center</property>
<property name="hexpand">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Password</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="valign">start</property>
<child>
<object class="GtkListBox">
<property name="selection-mode">none</property>
<child>
<object class="AdwActionRow">
<property name="activatable">True</property>
<property name="title" translatable="yes">Password</property>
<property name="activatable-widget">password_entry</property>
<child>
<object class="GtkEntry" id="password_entry">
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="visibility">False</property>
<property name="input-purpose">password</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="activatable">True</property>
<property name="title" translatable="yes">Repeat password</property>
<property name="activatable-widget">repeat_password_entry</property>
<child>
<object class="GtkEntry" id="repeat_password_entry">
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="visibility">False</property>
<property name="input-purpose">password</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Captcha</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="valign">start</property>
<child>
<object class="GtkListBox">
<property name="selection-mode">none</property>
<child>
<object class="AdwActionRow" id="captcha_row">
<property name="title-lines">0</property>
<property name="subtitle" translatable="yes">Feel free to look for the answer online!</property>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="activatable">True</property>
<property name="title" translatable="yes">Your answer</property>
<property name="activatable-widget">captcha_entry</property>
<child>
<object class="GtkEntry" id="captcha_entry">
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="visibility">False</property>
<property name="activates-default">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</interface>

View file

@ -22,6 +22,9 @@ pub use persons::*;
pub mod recordings; pub mod recordings;
pub use recordings::*; pub use recordings::*;
pub mod register;
pub use register::*;
pub mod works; pub mod works;
pub use works::*; pub use works::*;

View file

@ -0,0 +1,49 @@
use super::Backend;
use anyhow::{anyhow, Result};
use isahc::http::StatusCode;
use isahc::prelude::*;
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Response body data for captcha requests.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Captcha {
pub id: String,
pub question: String,
}
/// Request body data for user registration.
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct UserRegistration {
pub username: String,
pub password: String,
pub email: Option<String>,
pub captcha_id: String,
pub answer: String,
}
impl Backend {
/// Request a new captcha for registration.
pub async fn get_captcha(&self) -> Result<Captcha> {
let body = self.get("captcha").await?;
let captcha = serde_json::from_str(&body)?;
Ok(captcha)
}
/// Register a new user and return whether the process suceeded. This will
/// not store the new login credentials.
pub async fn register(&self, data: UserRegistration) -> Result<bool> {
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
let mut response = Request::post(format!("{}/users", server_url))
.timeout(Duration::from_secs(10))
.header("Content-Type", "application/json")
.body(serde_json::to_string(&data)?)?
.send_async()
.await?;
Ok(response.status() == StatusCode::OK)
}
}

View file

@ -1,3 +1,4 @@
use super::RegisterDialog;
use crate::backend::{Backend, LoginData}; use crate::backend::{Backend, LoginData};
use crate::widgets::{Navigator, NavigatorScreen}; use crate::widgets::{Navigator, NavigatorScreen};
use glib::clone; use glib::clone;
@ -29,6 +30,7 @@ impl LoginDialog {
get_widget!(builder, gtk::Button, login_button); get_widget!(builder, gtk::Button, login_button);
get_widget!(builder, gtk::Entry, username_entry); get_widget!(builder, gtk::Entry, username_entry);
get_widget!(builder, gtk::Entry, password_entry); get_widget!(builder, gtk::Entry, password_entry);
get_widget!(builder, gtk::Button, register_button);
let this = Rc::new(Self { let this = Rc::new(Self {
backend, backend,
@ -77,6 +79,26 @@ impl LoginDialog {
}); });
})); }));
register_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let dialog = RegisterDialog::new(this.backend.clone());
dialog.set_selected_cb(clone!(@strong this => move |data| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(data);
}
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
navigator.push(dialog);
}
}));
this this
} }

View file

@ -7,5 +7,8 @@ pub use login_dialog::*;
pub mod preferences; pub mod preferences;
pub use preferences::*; pub use preferences::*;
pub mod register;
pub use register::*;
pub mod server_dialog; pub mod server_dialog;
pub use server_dialog::*; pub use server_dialog::*;

149
src/dialogs/register.rs Normal file
View file

@ -0,0 +1,149 @@
use crate::backend::{Backend, LoginData, UserRegistration};
use crate::widgets::{Navigator, NavigatorScreen};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating a new user account.
pub struct RegisterDialog {
backend: Rc<Backend>,
widget: gtk::Stack,
username_entry: gtk::Entry,
email_entry: gtk::Entry,
password_entry: gtk::Entry,
repeat_password_entry: gtk::Entry,
captcha_row: libadwaita::ActionRow,
captcha_entry: gtk::Entry,
captcha_id: RefCell<Option<String>>,
selected_cb: RefCell<Option<Box<dyn Fn(LoginData)>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RegisterDialog {
/// Create a new register dialog.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/register_dialog.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, register_button);
get_widget!(builder, gtk::Entry, username_entry);
get_widget!(builder, gtk::Entry, email_entry);
get_widget!(builder, gtk::Entry, password_entry);
get_widget!(builder, gtk::Entry, repeat_password_entry);
get_widget!(builder, libadwaita::ActionRow, captcha_row);
get_widget!(builder, gtk::Entry, captcha_entry);
let this = Rc::new(Self {
backend,
widget,
username_entry,
email_entry,
password_entry,
repeat_password_entry,
captcha_row,
captcha_entry,
captcha_id: RefCell::new(None),
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
register_button.connect_clicked(clone!(@strong this => move |_| {
let password = this.password_entry.get_text().unwrap().to_string();
let repeat = this.repeat_password_entry.get_text().unwrap().to_string();
if (password != repeat) {
// TODO: Show error and validate other input.
} else {
this.widget.set_visible_child_name("loading");
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let username = clone.username_entry.get_text().unwrap().to_string();
let email = clone.email_entry.get_text().unwrap().to_string();
let captcha_id = clone.captcha_id.borrow().clone().unwrap();
let answer = clone.captcha_entry.get_text().unwrap().to_string();
let email = if email.len() == 0 {
None
} else {
Some(email)
};
let registration = UserRegistration {
username: username.clone(),
password: password.clone(),
email,
captcha_id,
answer,
};
// TODO: Handle errors.
if clone.backend.register(registration).await.unwrap() {
if let Some(cb) = &*clone.selected_cb.borrow() {
let data = LoginData {
username,
password,
};
cb(data);
}
let navigator = clone.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
} else {
clone.widget.set_visible_child_name("content");
}
});
}
}));
// Initialize
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let captcha = clone.backend.get_captcha().await.unwrap();
clone.captcha_row.set_title(Some(&captcha.question));
clone.captcha_id.replace(Some(captcha.id));
clone.widget.set_visible_child_name("content");
});
this
}
/// The closure to call when the login succeded.
pub fn set_selected_cb<F: Fn(LoginData) + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for RegisterDialog {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -39,6 +39,7 @@ sources = files(
'backend/client/mod.rs', 'backend/client/mod.rs',
'backend/client/persons.rs', 'backend/client/persons.rs',
'backend/client/recordings.rs', 'backend/client/recordings.rs',
'backend/client/register.rs',
'backend/client/works.rs', 'backend/client/works.rs',
'backend/library.rs', 'backend/library.rs',
'backend/mod.rs', 'backend/mod.rs',
@ -56,6 +57,7 @@ sources = files(
'dialogs/login_dialog.rs', 'dialogs/login_dialog.rs',
'dialogs/mod.rs', 'dialogs/mod.rs',
'dialogs/preferences.rs', 'dialogs/preferences.rs',
'dialogs/register.rs',
'dialogs/server_dialog.rs', 'dialogs/server_dialog.rs',
'editors/ensemble.rs', 'editors/ensemble.rs',
'editors/instrument.rs', 'editors/instrument.rs',