Work on user stats page

This commit is contained in:
Alexander Rosenberg 2023-10-27 02:35:42 -07:00
parent 04fbed2c3a
commit 8754520d10
Signed by: Zander671
GPG Key ID: 5FD0394ADBD72730
5 changed files with 176 additions and 20 deletions

View File

@ -205,10 +205,18 @@ template $LoginWindow : ApplicationWindow {
} }
} }
Label {
label: "Forgot your password? Reset it <a href=\"https://app.simplelogin.io/auth/forgot_password\">here</a>.";
margin-bottom: 5;
use-markup: true;
valign: start;
vexpand: true;
yalign: 0.0;
}
Label { Label {
label: "Don\'t have an account? You can register <a href=\"https://app.simplelogin.io/auth/register\">here</a>."; label: "Don\'t have an account? You can register <a href=\"https://app.simplelogin.io/auth/register\">here</a>.";
margin-bottom: 10; margin-bottom: 10;
use-markup: true; use-markup: true;
valign: start; valign: start;
vexpand: true; vexpand: true;

View File

@ -5,6 +5,7 @@ template $SettingsPanel : Box {
hexpand: true; hexpand: true;
child: Box content_wrapper { child: Box content_wrapper {
visible: false;
hexpand: true; hexpand: true;
vexpand: true; vexpand: true;
@ -43,6 +44,7 @@ template $SettingsPanel : Box {
Entry name_label { Entry name_label {
editable: false; editable: false;
can-focus: false;
has-frame: false; has-frame: false;
hexpand: true; hexpand: true;
input-hints: no_emoji | no_spellcheck; input-hints: no_emoji | no_spellcheck;

View File

@ -88,7 +88,8 @@ mod imp {
impl Default for Application { impl Default for Application {
fn default() -> Self { fn default() -> Self {
let settings = RefCell::new(gio::Settings::new(crate::APP_ID)); let settings = RefCell::new(gio::Settings::new(crate::APP_ID));
let api_key: Option<String> = settings.borrow().value("api-key").get(); let api_key: Option<String> =
settings.borrow().value("api-key").get::<Option<String>>().unwrap();
Self { Self {
settings, settings,
context: RefCell::new(sl::Context::new(api_key.as_deref())), context: RefCell::new(sl::Context::new(api_key.as_deref())),

View File

@ -59,6 +59,28 @@ mod imp {
#[gtk::template_callbacks] #[gtk::template_callbacks]
impl SettingsPanel { 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] #[template_callback]
fn clear_api_key_clicked(&self, _: &gtk::Button) { fn clear_api_key_clicked(&self, _: &gtk::Button) {
let main_loop = glib::MainContext::default(); 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] #[template_callback]
fn logout_clicked(&self, _: &gtk::Button) { fn logout_clicked(&self, _: &gtk::Button) {
let main_loop = glib::MainContext::default(); let main_loop = glib::MainContext::default();
@ -125,6 +130,54 @@ mod imp {
fn close_error_clicked(&self, _: &gtk::Button) { fn close_error_clicked(&self, _: &gtk::Button) {
self.set_error(None); 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() {
"<No name>"
} 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] #[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 {} impl BoxImpl for SettingsPanel {}
} }

View File

@ -59,6 +59,30 @@ struct MfaResponse {
email: String, 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<String>,
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 { pub enum ErrorKind {
State, State,
Json, Json,
@ -179,6 +203,23 @@ impl Context {
.body(body.to_owned())).await .body(body.to_owned())).await
} }
async fn patch_request(
&mut self, endpoint: &str, body: Option<&str>,
headers: Option<&[(&str, &str)]>
) -> Result<String, Error> {
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<T>( pub fn obj_to_json<T>(
obj: &T obj: &T
) -> Result<String, Error> where T: serde::Serialize { ) -> Result<String, Error> where T: serde::Serialize {
@ -250,4 +291,41 @@ impl Context {
None => Err(Error::new(ErrorKind::State, "Not logged in")), None => Err(Error::new(ErrorKind::State, "Not logged in")),
} }
} }
pub async fn get_user_info(&mut self) -> Result<UserInfo, Error> {
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<UserInfo, Error> {
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<Stats, Error> {
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")),
}
}
} }