From 31698154e031d9cf492125e206d578f5fb8ff252 Mon Sep 17 00:00:00 2001 From: jane400 Date: Thu, 19 Sep 2024 20:17:51 +0200 Subject: [PATCH] feat: paket: account page --- icons.toml | 2 +- paket/src/account.rs | 214 +++++++++++++++++++++++++++++++++++++++++ paket/src/bin/paket.rs | 2 +- paket/src/lib.rs | 6 ++ paket/src/login.rs | 13 +++ paket/src/ready.rs | 139 +++++++++----------------- paket/src/tracking.rs | 1 - 7 files changed, 283 insertions(+), 94 deletions(-) create mode 100644 paket/src/account.rs diff --git a/icons.toml b/icons.toml index 71854d1..87593b8 100644 --- a/icons.toml +++ b/icons.toml @@ -1,3 +1,3 @@ app_id = "de.j4ne.Paket" -icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large"] \ No newline at end of file +icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy"] \ No newline at end of file diff --git a/paket/src/account.rs b/paket/src/account.rs new file mode 100644 index 0000000..4ebdf6b --- /dev/null +++ b/paket/src/account.rs @@ -0,0 +1,214 @@ +use adw::prelude::*; +use gtk::gdk; +use libpaket::{stammdaten::CustomerDataFull, LibraryError, LibraryResult}; +use relm4::{Component, ComponentParts}; + +use crate::LoginSharedState; + +#[tracker::track] +pub struct AccountView { + logged_in: bool, + + #[do_not_track] + login: LoginSharedState, + #[no_eq] + customer_data_full: Option, +} + +// We have to handle both events, as we want to reset everything, if we log out. +#[derive(Debug)] +pub enum AccountCmd { + LoggedIn, + LoggedOut, + GotCustomerDataFull(LibraryResult), +} + +#[derive(Debug)] +pub enum CopyTargets { + PostNumber, +} + +#[derive(Debug)] +pub enum AccountInput { + Copy(CopyTargets), +} + +#[derive(Debug)] +pub enum AccountServices { + Advices, + SendungVerfolgung, +} + +#[derive(Debug)] +pub enum AccountOutput { + LoggedOut, + LoggedIn, + HaveService(AccountServices), +} + +macro_rules! get_str_from_customer_data { + ($model: ident, $id: ident) => {{ + || -> Option { + let data = $model.customer_data_full.as_ref()?; + Some(data.common.$id.clone()) + }() + }}; +} + +#[relm4::component(pub)] +impl Component for AccountView { + type Input = AccountInput; + type Output = AccountOutput; + type Init = LoginSharedState; + type CommandOutput = AccountCmd; + + view! { + #[root] + adw::Bin { + #[wrap(Some)] + set_child = >k::ListBox { + // General infos + append = &adw::PreferencesGroup { + #[track(model.changed_customer_data_full() && model.get_customer_data_full().is_some())] + set_title?: get_str_from_customer_data!(model, display_name).as_ref(), + + // Postnumber + add = &adw::ActionRow { + #[track(model.changed_customer_data_full() && model.get_customer_data_full().is_some())] + set_title?: get_str_from_customer_data!(model, post_number).as_ref(), + + set_subtitle: "Postnummer", + + add_suffix = >k::Button { + #[wrap(Some)] + set_child = &adw::ButtonContent { + set_icon_name: relm4_icons::icon_names::COPY, + set_label: "Copy", + }, + + connect_clicked => AccountInput::Copy(CopyTargets::PostNumber), + }, + } + } + }, + } + } + + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = AccountView { + logged_in: false, + login: init, + tracker: 0, + customer_data_full: None, + }; + + { + let login = model.login.clone(); + sender.command(move |out, shutdown| { + shutdown + .register(async move { + let login = { login.clone().as_ref().lock().await.clone() }; + let (sender, receiver) = relm4::channel::(); + login.subscribe(&sender, |model| match model { + Some(_) => AccountCmd::LoggedIn, + None => AccountCmd::LoggedOut, + }); + loop { + out.send(receiver.recv().await.unwrap()).unwrap(); + } + }) + .drop_on_shutdown() + }); + } + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update( + &mut self, + message: Self::Input, + sender: relm4::ComponentSender, + root: &Self::Root, + ) { + self.reset(); + + match message { + AccountInput::Copy(target) => { + let value = match target { + CopyTargets::PostNumber => { + get_str_from_customer_data!(self, post_number) + } + }; + + if let Some(value) = value { + let display = root.display(); + let clipboard = display.clipboard(); + clipboard.set_text(value.as_str()); + } + } + }; + } + + fn update_cmd( + &mut self, + message: Self::CommandOutput, + sender: relm4::ComponentSender, + root: &Self::Root, + ) { + self.reset(); + + match message { + AccountCmd::LoggedIn => { + self.set_logged_in(true); + } + AccountCmd::LoggedOut => { + self.set_logged_in(false); + } + AccountCmd::GotCustomerDataFull(data) => match data { + Ok(data) => { + for service in &data.common.services { + match service { + libpaket::stammdaten::CustomerDataService::Packstation => (), + libpaket::stammdaten::CustomerDataService::Paketankuendigung => sender + .output(AccountOutput::HaveService( + AccountServices::SendungVerfolgung, + )) + .unwrap(), + libpaket::stammdaten::CustomerDataService::Briefankuendigung => sender + .output(AccountOutput::HaveService(AccountServices::Advices)) + .unwrap(), + _ => (), + } + } + self.set_customer_data_full(Some(data)); + } + Err(err) => todo!(), + }, + } + + if self.changed_logged_in() { + if self.logged_in { + sender.output(AccountOutput::LoggedIn).unwrap(); + let token = self.login.clone(); + sender.oneshot_command(async move { + let token = crate::login::get_id_token(&token).await.unwrap(); + let client = libpaket::StammdatenClient::new(); + let mut res: LibraryResult = Err(LibraryError::NetworkFetch); + while res.is_err() && *res.as_ref().err().unwrap() == LibraryError::NetworkFetch + { + res = client.customer_data_full(&token).await; + } + AccountCmd::GotCustomerDataFull(res) + }); + } else { + sender.output(AccountOutput::LoggedOut).unwrap(); + } + } + } +} diff --git a/paket/src/bin/paket.rs b/paket/src/bin/paket.rs index 9e68826..5f7d857 100644 --- a/paket/src/bin/paket.rs +++ b/paket/src/bin/paket.rs @@ -101,7 +101,7 @@ impl AsyncComponent for App { .forward(sender.input_sender(), convert_ready_response); let login = Login::builder() - .launch(login_shared_state.clone()) + .launch_with_broker(login_shared_state.clone(), &paket::LOGIN_BROKER) .forward(sender.input_sender(), convert_login_response); let model = App { diff --git a/paket/src/lib.rs b/paket/src/lib.rs index adf372c..058fda3 100644 --- a/paket/src/lib.rs +++ b/paket/src/lib.rs @@ -1,3 +1,4 @@ +pub mod account; pub mod advice; pub mod advices; pub mod constants; @@ -6,3 +7,8 @@ pub mod ready; pub mod tracking; pub use login::LoginSharedState; + +pub static LOGIN_BROKER: relm4::MessageBroker = relm4::MessageBroker::new(); +pub fn send_log_out() { + LOGIN_BROKER.send(login::LoginInput::LogOut); +} diff --git a/paket/src/login.rs b/paket/src/login.rs index 9c27c6b..c9d791a 100644 --- a/paket/src/login.rs +++ b/paket/src/login.rs @@ -21,6 +21,7 @@ static KEYRING: OnceLock = OnceLock::new(); #[derive(Debug)] pub enum LoginInput { + LogOut, NeedsLogin, NeedsRefresh, ReceivedAuthCode(String), @@ -194,6 +195,18 @@ impl AsyncComponent for Login { self.reset(); match message { + LoginInput::LogOut => { + self.refresh_token = None; + sender.output(LoginOutput::RequiresLogin).unwrap(); + { + let token = self.shared_id_token.lock().await; + *token.write() = None; + } + let keyring = KEYRING.get().unwrap(); + let _ = keyring + .delete(&HashMap::from([("app", crate::constants::APP_ID)])) + .await; + } LoginInput::NeedsRefresh => { let refresh_token = self.refresh_token.as_ref().unwrap().clone(); sender.oneshot_command(async { diff --git a/paket/src/ready.rs b/paket/src/ready.rs index ea6c6d1..6486eaa 100644 --- a/paket/src/ready.rs +++ b/paket/src/ready.rs @@ -1,20 +1,20 @@ use adw::prelude::*; -use libpaket::{stammdaten::CustomerDataFull, LibraryError, LibraryResult}; use relm4::prelude::*; use crate::{ + account::{AccountOutput, AccountServices, AccountView}, advices::{AdvicesView, AdvicesViewInput}, tracking::{TrackingInput, TrackingOutput, TrackingView}, }; #[tracker::track] pub struct Ready { - #[do_not_track] - login: crate::LoginSharedState, - activate: bool, + logged_in: bool, have_service_advices: bool, have_service_tracking: bool, + #[do_not_track] + account_component: Controller, #[do_not_track] advices_component: AsyncController, #[do_not_track] @@ -33,21 +33,14 @@ pub enum ReadyOutput { pub enum ReadyCmds { LoggedIn, LoggedOut, - GotCustomerDataFull(LibraryResult), -} - -#[derive(Debug)] -pub enum Services { - Advices, - SendungVerfolgung, } #[derive(Debug)] pub enum ReadyInput { - Activate, - Deactivate, - HaveService(Services), - ServiceBorked(Services), + LoggedIn, + LoggedOut, + HaveService(AccountServices), + ServiceBorked(AccountServices), } #[relm4::component(pub)] @@ -106,6 +99,13 @@ impl Component for Ready { #[track(model.changed_have_service_tracking())] set_visible: model.have_service_tracking, }, + + add = &model.account_component.widget().clone() -> adw::Bin { + } -> page_account: adw::ViewStackPage { + set_title: Some("Account"), + set_name: Some("page_account"), + set_icon_name: Some(relm4_icons::icon_names::PERSON) + } }, }, @@ -132,41 +132,28 @@ impl Component for Ready { .launch(init.clone()) .forward(&sender.input_sender(), convert_tracking_output); + let account_component = AccountView::builder() + .launch(init.clone()) + .forward(&sender.input_sender(), convert_account_output); + let toast_overlay = adw::ToastOverlay::new(); let model = Ready { have_service_advices: false, have_service_tracking: true, - login: init.clone(), - activate: false, + logged_in: false, + + account_component, advices_component, tracking_component, toast_overlay, + tracker: 0, }; - { - let login = model.login.clone(); - sender.command(move |out, shutdown| { - shutdown - .register(async move { - let login = { login.clone().as_ref().lock().await.clone() }; - let (sender, receiver) = relm4::channel::(); - login.subscribe(&sender, |model| match model { - Some(_) => ReadyCmds::LoggedIn, - None => ReadyCmds::LoggedOut, - }); - loop { - out.send(receiver.recv().await.unwrap()).unwrap(); - } - }) - .drop_on_shutdown() - }); - } - let breakpoint = adw::Breakpoint::new(adw::BreakpointCondition::new_length( adw::BreakpointConditionLengthType::MaxWidth, - 450.0, + 550.0, adw::LengthUnit::Sp, )); @@ -193,39 +180,24 @@ impl Component for Ready { self.reset(); match message { - ReadyInput::Activate => { - self.set_activate(true); - if self.changed_activate() { - let token = self.login.clone(); - sender.oneshot_command(async move { - let token = crate::login::get_id_token(&token).await.unwrap(); - let client = libpaket::StammdatenClient::new(); - let mut res: LibraryResult = - Err(LibraryError::NetworkFetch); - while res.is_err() - && *res.as_ref().err().unwrap() == LibraryError::NetworkFetch - { - res = client.customer_data_full(&token).await; - } - ReadyCmds::GotCustomerDataFull(res) - }); - } + ReadyInput::LoggedIn => { + self.set_logged_in(true); } - ReadyInput::Deactivate => { - self.set_activate(false); + ReadyInput::LoggedOut => { + self.set_logged_in(false); } ReadyInput::HaveService(service) => match service { - Services::Advices => { + AccountServices::Advices => { self.set_have_service_advices(true); self.advices_component.emit(AdvicesViewInput::Fetch); } - Services::SendungVerfolgung => { + AccountServices::SendungVerfolgung => { self.set_have_service_tracking(true); self.tracking_component.emit(TrackingInput::Search(None)) } }, ReadyInput::ServiceBorked(service) => match service { - Services::Advices => { + AccountServices::Advices => { self.toast_overlay.add_toast( adw::Toast::builder() .title("Service borked: Mail notifications") @@ -234,7 +206,7 @@ impl Component for Ready { ); self.set_have_service_advices(false); } - Services::SendungVerfolgung => { + AccountServices::SendungVerfolgung => { self.toast_overlay.add_toast( adw::Toast::builder() .title("Service borked: Shipment tracking") @@ -244,43 +216,28 @@ impl Component for Ready { self.set_have_service_tracking(false); } }, - } - } + }; - fn update_cmd( - &mut self, - message: Self::CommandOutput, - sender: ComponentSender, - _: &Self::Root, - ) { - match message { - ReadyCmds::LoggedIn => sender.input(ReadyInput::Activate), - ReadyCmds::LoggedOut => sender.input(ReadyInput::Deactivate), - ReadyCmds::GotCustomerDataFull(res) => match res { - Ok(res) => { - for service in &res.common.services { - match service { - libpaket::stammdaten::CustomerDataService::Packstation => (), - libpaket::stammdaten::CustomerDataService::Paketankuendigung => { - sender.input(ReadyInput::HaveService(Services::SendungVerfolgung)) - } - libpaket::stammdaten::CustomerDataService::Briefankuendigung => { - sender.input(ReadyInput::HaveService(Services::Advices)) - } - _ => (), - } - } - - sender.output(ReadyOutput::Ready).unwrap() - } - Err(err) => todo!(), - }, + if self.changed_logged_in() { + if self.logged_in { + sender.output(ReadyOutput::Ready).unwrap(); + } else { + todo!(); + } } } } fn convert_tracking_output(value: TrackingOutput) -> ReadyInput { match value { - TrackingOutput::Borked => ReadyInput::ServiceBorked(Services::SendungVerfolgung), + TrackingOutput::Borked => ReadyInput::ServiceBorked(AccountServices::SendungVerfolgung), + } +} + +fn convert_account_output(value: AccountOutput) -> ReadyInput { + match value { + AccountOutput::LoggedOut => ReadyInput::LoggedOut, + AccountOutput::LoggedIn => ReadyInput::LoggedIn, + AccountOutput::HaveService(service) => ReadyInput::HaveService(service), } } diff --git a/paket/src/tracking.rs b/paket/src/tracking.rs index 53c4d19..d306486 100644 --- a/paket/src/tracking.rs +++ b/paket/src/tracking.rs @@ -3,7 +3,6 @@ use libpaket::tracking::{Shipment, TrackingParams}; use libpaket::{LibraryError, LibraryResult}; use relm4::factory::{FactoryComponent, FactoryHashMap}; use relm4::prelude::*; -use relm4::{adw, gtk}; use crate::login::get_id_token; use crate::LoginSharedState;