diff --git a/simplelogin-gui/data/login_window.blp b/simplelogin-gui/data/login_window.blp index 38b38d8..57a6e00 100644 --- a/simplelogin-gui/data/login_window.blp +++ b/simplelogin-gui/data/login_window.blp @@ -205,10 +205,18 @@ template $LoginWindow : ApplicationWindow { } } + Label { + label: "Forgot your password? Reset it here."; + margin-bottom: 5; + use-markup: true; + valign: start; + vexpand: true; + yalign: 0.0; + } Label { label: "Don\'t have an account? You can register here."; - margin-bottom: 10; + margin-bottom: 10; use-markup: true; valign: start; vexpand: true; diff --git a/simplelogin-gui/data/settings_panel.blp b/simplelogin-gui/data/settings_panel.blp index 857c1ea..9abb9c3 100644 --- a/simplelogin-gui/data/settings_panel.blp +++ b/simplelogin-gui/data/settings_panel.blp @@ -5,6 +5,7 @@ template $SettingsPanel : Box { hexpand: true; child: Box content_wrapper { + visible: false; hexpand: true; vexpand: true; @@ -43,6 +44,7 @@ template $SettingsPanel : Box { Entry name_label { editable: false; + can-focus: false; has-frame: false; hexpand: true; input-hints: no_emoji | no_spellcheck; diff --git a/simplelogin-gui/src/application.rs b/simplelogin-gui/src/application.rs index 6f02428..0d28eaf 100644 --- a/simplelogin-gui/src/application.rs +++ b/simplelogin-gui/src/application.rs @@ -88,7 +88,8 @@ mod imp { impl Default for Application { fn default() -> Self { let settings = RefCell::new(gio::Settings::new(crate::APP_ID)); - let api_key: Option = settings.borrow().value("api-key").get(); + let api_key: Option = + settings.borrow().value("api-key").get::>().unwrap(); Self { settings, context: RefCell::new(sl::Context::new(api_key.as_deref())), diff --git a/simplelogin-gui/src/settings_panel.rs b/simplelogin-gui/src/settings_panel.rs index 90eae90..f8ef46a 100644 --- a/simplelogin-gui/src/settings_panel.rs +++ b/simplelogin-gui/src/settings_panel.rs @@ -59,6 +59,28 @@ mod imp { #[gtk::template_callbacks] impl SettingsPanel { + fn set_is_loading(&self, loading: bool) { + if loading { + self.set_error(None); + } else if !self.is_error() { + self.content_wrapper.get().set_sensitive(true); + } + self.loading_overlay.get().set_visible(loading); + } + + fn is_error(&self) -> bool { + self.error_overlay.get().is_visible() + } + + fn set_error(&self, error: Option<&str>) { + if let Some(e) = error { + self.set_is_loading(false); + self.error_label.get().set_text(e); + } + self.content_wrapper.get().set_sensitive(error.is_none()); + self.error_overlay.get().set_visible(error.is_some()); + } + #[template_callback] fn clear_api_key_clicked(&self, _: >k::Button) { let main_loop = glib::MainContext::default(); @@ -79,23 +101,6 @@ mod imp { })); } - fn set_is_loading(&self, loading: bool) { - if loading { - self.set_error(None); - } - self.loading_overlay.get().set_visible(loading); - self.content_wrapper.get().set_sensitive(!loading); - } - - fn set_error(&self, error: Option<&str>) { - if let Some(e) = error { - self.set_is_loading(false); - self.error_label.get().set_text(e); - } - self.content_wrapper.get().set_sensitive(error.is_none()); - self.error_overlay.get().set_visible(error.is_some()); - } - #[template_callback] fn logout_clicked(&self, _: >k::Button) { let main_loop = glib::MainContext::default(); @@ -125,6 +130,54 @@ mod imp { fn close_error_clicked(&self, _: >k::Button) { self.set_error(None); } + + async fn refresh_stats(&self) -> bool { + let app = self.application.borrow(); + let mut ctx = app.context().borrow_mut(); + match ctx.stats().await { + Ok(stats) => { + self.stats_label.get().set_text(&format!( + "Aliases: {}, Blocked: {}, Forwarded: {}, Replied: {}", + stats.nb_alias, stats.nb_block, stats.nb_forward, stats.nb_reply + )); + false + }, + Err(e) => { + self.set_error(Some(e.to_string().as_str())); + true + }, + } + } + + async fn refresh_user_info(&self) -> bool { + let app = self.application.borrow(); + let mut ctx = app.context().borrow_mut(); + match ctx.get_user_info().await { + Ok(user_info) => { + self.name_label.get().set_text( + if user_info.name.is_empty() { + "" + } else { + &user_info.name + } + ); + self.email_label.get().set_text(&user_info.email); + self.status_label.get().set_text( + if user_info.is_premium { + "Premium" + } else if user_info.in_trial { + "Premium (Trial)" + } else { + "Free" + }); + false + }, + Err(e) => { + self.set_error(Some(e.to_string().as_str())); + true + }, + } + } } #[glib::object_subclass] @@ -160,7 +213,21 @@ mod imp { } } - impl WidgetImpl for SettingsPanel {} + impl WidgetImpl for SettingsPanel { + fn map(&self) { + self.parent_map(); + let main_loop = glib::MainContext::default(); + main_loop.spawn_local(clone!(@weak self as this => async move { + this.set_is_loading(true); + if !this.refresh_user_info().await { + this.refresh_stats().await; + } + this.set_is_loading(false); + this.content_wrapper.get().set_visible(true); + })); + } + } + impl BoxImpl for SettingsPanel {} } diff --git a/simplelogin/src/lib.rs b/simplelogin/src/lib.rs index 3b044ba..138e4b2 100644 --- a/simplelogin/src/lib.rs +++ b/simplelogin/src/lib.rs @@ -59,6 +59,30 @@ struct MfaResponse { email: String, } +#[derive(Serialize)] +struct UserInfoRequest<'a> { + api_key: &'a str, + profile_picture: Option<&'a str>, +} + +#[derive(Debug, Deserialize)] +pub struct UserInfo { + pub name: String, + pub is_premium: bool, + pub email: String, + pub in_trial: bool, + pub profile_picture_url: Option, + pub max_alias_free_plan: u32, +} + +#[derive(Debug, Deserialize)] +pub struct Stats { + pub nb_alias: u32, + pub nb_block: u32, + pub nb_forward: u32, + pub nb_reply: u32, +} + pub enum ErrorKind { State, Json, @@ -179,6 +203,23 @@ impl Context { .body(body.to_owned())).await } + async fn patch_request( + &mut self, endpoint: &str, body: Option<&str>, + headers: Option<&[(&str, &str)]> + ) -> Result { + let mut builder = self.client.patch(self.url.to_owned() + endpoint); + if let Some(body_str) = body { + builder = builder.header("Content-Type", "application/json") + .body(body_str.to_owned()); + } + if let Some(headers_arr) = headers { + for header in headers_arr { + builder = builder.header(header.0, header.1); + } + } + Self::perform_request(builder).await + } + pub fn obj_to_json( obj: &T ) -> Result where T: serde::Serialize { @@ -250,4 +291,41 @@ impl Context { None => Err(Error::new(ErrorKind::State, "Not logged in")), } } + + pub async fn get_user_info(&mut self) -> Result { + match self.api_key.as_ref() { + Some(api_key) => { + let res = self.get_request("api/user_info", + &[("Authentication", api_key.clone().as_str())]).await?; + Ok(Self::json_to_obj(&res)?) + }, + None => Err(Error::new(ErrorKind::State, "Not logged in")), + } + } + + pub async fn update_user_info( + &mut self, profile_picture: Option<&str> + ) -> Result { + match self.api_key.as_ref() { + Some(api_key) => { + let req_json = Self::obj_to_json( + &UserInfoRequest {api_key, profile_picture})?; + let res = self.patch_request("api/user_info", + Some(&req_json), None).await?; + Ok(Self::json_to_obj(&res)?) + }, + None => Err(Error::new(ErrorKind::State, "Not logged in")), + } + } + + pub async fn stats(&mut self) -> Result { + match self.api_key.as_ref() { + Some(api_key) => { + let res = self.get_request("api/stats", + &[("Authentication", api_key.clone().as_str())]).await?; + Ok(Self::json_to_obj(&res)?) + }, + None => Err(Error::new(ErrorKind::State, "Not logged in")), + } + } }