feat: paket: account page

This commit is contained in:
jane400 2024-09-19 20:17:51 +02:00 committed by jane400
parent b9fb7fcea4
commit 31698154e0
7 changed files with 283 additions and 94 deletions

View file

@ -1,3 +1,3 @@
app_id = "de.j4ne.Paket"
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large"]
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy"]

214
paket/src/account.rs Normal file
View file

@ -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<CustomerDataFull>,
}
// 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<CustomerDataFull>),
}
#[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<String> {
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 = &gtk::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 = &gtk::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<Self>,
) -> relm4::ComponentParts<Self> {
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::<AccountCmd>();
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<Self>,
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<Self>,
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<CustomerDataFull> = 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();
}
}
}
}

View file

@ -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 {

View file

@ -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<login::LoginInput> = relm4::MessageBroker::new();
pub fn send_log_out() {
LOGIN_BROKER.send(login::LoginInput::LogOut);
}

View file

@ -21,6 +21,7 @@ static KEYRING: OnceLock<oo7::Keyring> = 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 {

View file

@ -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<AccountView>,
#[do_not_track]
advices_component: AsyncController<AdvicesView>,
#[do_not_track]
@ -33,21 +33,14 @@ pub enum ReadyOutput {
pub enum ReadyCmds {
LoggedIn,
LoggedOut,
GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>),
}
#[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::<ReadyCmds>();
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<CustomerDataFull> =
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>,
_: &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),
}
}

View file

@ -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;