Compare commits

...

2 commits

Author SHA1 Message Date
jane400
cd2bc321cd feat: paket: sendungsverfolgung bringup 2024-08-22 13:20:00 +02:00
jane400
0e1167adb2 feat: visual improvments for advices 2024-08-22 13:18:01 +02:00
3 changed files with 225 additions and 41 deletions

View file

@ -1,13 +1,13 @@
use adw::{gio, glib};
use gtk::gdk;
use libpaket::advices::UatToken; use libpaket::advices::UatToken;
use libpaket::LibraryError; use libpaket::LibraryError;
use relm4::gtk; use relm4::gtk;
use gtk::gdk;
use adw::{gio, glib};
use relm4::prelude::*;
use adw::prelude::*; use adw::prelude::*;
use glib::prelude::*;
use gio::prelude::*; use gio::prelude::*;
use glib::prelude::*;
use relm4::prelude::*;
#[derive(Debug)] #[derive(Debug)]
pub struct AppAdviceMetadata { pub struct AppAdviceMetadata {
@ -42,16 +42,25 @@ impl FactoryComponent for AppAdvice {
add_overlay = &gtk::Spinner { add_overlay = &gtk::Spinner {
start: (), start: (),
set_align: gtk::Align::Center, set_align: gtk::Align::Center,
#[track(self.changed_texture())] #[track(self.changed_texture())]
set_visible: self.texture.is_none(), set_visible: self.texture.is_none(),
}, },
add_overlay = &gtk::Label { add_overlay = &gtk::Box {
set_halign: gtk::Align::Center, add_css_class: relm4::css::OSD,
set_valign: gtk::Align::Center, add_css_class: relm4::css::NUMERIC,
set_label: self.metadata.date.as_str(), set_valign: gtk::Align::End,
set_halign: gtk::Align::End,
set_margin_all: 8,
gtk::Label {
set_margin_all: 4,
set_label: self.metadata.date.as_str(),
},
}, },
#[wrap(Some)] #[wrap(Some)]
@ -71,9 +80,11 @@ impl FactoryComponent for AppAdvice {
let advice = _self.metadata.advice.clone(); let advice = _self.metadata.advice.clone();
let uat = value.1; let uat = value.1;
sender.oneshot_command(async move { sender.oneshot_command(async move {
let res = libpaket::advices::AdviceClient::new().fetch_advice_image(&advice, &uat).await; let res = libpaket::advices::AdviceClient::new()
.fetch_advice_image(&advice, &uat)
.await;
let res = match res { let res = match res {
Ok(res) => res, Ok(res) => res,
@ -83,13 +94,21 @@ impl FactoryComponent for AppAdvice {
let file = { let file = {
let (file, io_stream) = gio::File::new_tmp(None::<&std::path::Path>).unwrap(); let (file, io_stream) = gio::File::new_tmp(None::<&std::path::Path>).unwrap();
let output_stream = io_stream.output_stream(); let output_stream = io_stream.output_stream();
output_stream.write(res.as_slice(), None::<&gio::Cancellable>).unwrap(); output_stream
.write(res.as_slice(), None::<&gio::Cancellable>)
.unwrap();
file file
}; };
let image = glycin::Loader::new(file).load().await.expect("Image decoding failed"); let image = glycin::Loader::new(file)
let frame = image.next_frame().await.expect("Image frame decoding failed"); .load()
.await
.expect("Image decoding failed");
let frame = image
.next_frame()
.await
.expect("Image frame decoding failed");
AppAdviceCmds::GotTexture(frame.texture()) AppAdviceCmds::GotTexture(frame.texture())
}); });
@ -99,8 +118,7 @@ impl FactoryComponent for AppAdvice {
fn update_cmd(&mut self, message: Self::CommandOutput, sender: FactorySender<Self>) { fn update_cmd(&mut self, message: Self::CommandOutput, sender: FactorySender<Self>) {
match message { match message {
AppAdviceCmds::GotTexture(texture) => self.set_texture(Some(texture)), AppAdviceCmds::GotTexture(texture) => self.set_texture(Some(texture)),
AppAdviceCmds::Error(err) => todo!() AppAdviceCmds::Error(err) => todo!(),
}; };
} }
}
}

View file

@ -13,6 +13,7 @@ mod advices;
mod constants; mod constants;
mod login; mod login;
mod ready; mod ready;
mod tracking;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum AppState { enum AppState {
@ -282,6 +283,7 @@ fn convert_ready_response(response: ReadyOutput) -> AppInput {
long: "There is no feature on your account which is supported by this app. You need the offical app and register for one or more of:\n\"Briefasnkündigung\"".to_string(), long: "There is no feature on your account which is supported by this app. You need the offical app and register for one or more of:\n\"Briefasnkündigung\"".to_string(),
}), }),
ReadyOutput::Error(err) => AppInput::ErrorOccoured(AppError { short: "meow".to_string(), long: err.to_string() }), ReadyOutput::Error(err) => AppInput::ErrorOccoured(AppError { short: "meow".to_string(), long: err.to_string() }),
ReadyOutput::Notification(value) => AppInput::Notification(value, 60),
ReadyOutput::Ready => AppInput::SwitchToReady, ReadyOutput::Ready => AppInput::SwitchToReady,
} }
} }

View file

@ -1,14 +1,21 @@
// managed the various pages... // managed the various pages...
use std::hash::DefaultHasher;
use std::string;
use std::time::Duration; use std::time::Duration;
use adw::prelude::*; use adw::prelude::*;
use libpaket::{ use libpaket::{
self, self,
advices::{AdvicesList, UatToken}, advices::{AdvicesList, UatToken},
tracking::{Shipment, TrackingParams},
LibraryError, LibraryResult, LibraryError, LibraryResult,
}; };
use relm4::{adw, factory::FactoryVecDeque, prelude::*}; use relm4::{
adw,
factory::{FactoryHashMap, FactorySender, FactoryVecDeque},
prelude::*,
};
use crate::advices::AppAdviceMetadata; use crate::advices::AppAdviceMetadata;
@ -25,9 +32,13 @@ pub struct Ready {
login: crate::LoginSharedState, login: crate::LoginSharedState,
activate: bool, activate: bool,
have_service_advices: bool, have_service_advices: bool,
#[do_not_track] #[do_not_track]
advices_factory: FactoryVecDeque<crate::advices::AppAdvice>, advices_factory: FactoryVecDeque<crate::advices::AppAdvice>,
advices_state: ReadyAdvicesState, advices_state: ReadyAdvicesState,
#[do_not_track]
tracking_factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -35,6 +46,7 @@ pub enum ReadyOutput {
Ready, Ready,
Error(LibraryError), Error(LibraryError),
FatalError(LibraryError), FatalError(LibraryError),
Notification(String),
NoServicesEnabled, NoServicesEnabled,
} }
@ -45,6 +57,7 @@ pub enum ReadyCmds {
GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>), GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>),
RetryAdvices, RetryAdvices,
GotAdvices((LibraryResult<Vec<AppAdviceMetadata>>, Option<UatToken>)), GotAdvices((LibraryResult<Vec<AppAdviceMetadata>>, Option<UatToken>)),
GotTracking(LibraryResult<Vec<Shipment>>),
} }
#[derive(Debug)] #[derive(Debug)]
@ -52,6 +65,8 @@ pub enum ReadyInput {
Activate, Activate,
Deactivate, Deactivate,
HaveAdvicesService, HaveAdvicesService,
HavePaketankuendigungService,
SearchTracking(String),
} }
#[relm4::component(pub)] #[relm4::component(pub)]
@ -77,18 +92,23 @@ impl Component for Ready {
set_title: "No mail notifications available." set_title: "No mail notifications available."
}, },
#[name = "advices_page_have_some"] #[name = "advices_page_have_some"]
add = &gtk::Box { add = &adw::Clamp {
set_orientation: gtk::Orientation::Horizontal, #[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
#[local_ref] #[local_ref]
advices_carousel -> adw::Carousel { advices_carousel -> adw::Carousel {
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Vertical,
},
adw::CarouselIndicatorDots {
#[watch]
set_carousel: Some(advices_carousel),
set_orientation: gtk::Orientation::Vertical,
}
}, },
adw::CarouselIndicatorDots {
set_carousel: Some(advices_carousel),
}
}, },
#[track(model.changed_advices_state())] #[track(model.changed_advices_state())]
@ -103,9 +123,44 @@ impl Component for Ready {
}, },
} -> /*page_advices: adw::ViewStackPage*/ { } -> /*page_advices: adw::ViewStackPage*/ {
set_title: Some("Mail notification"), set_title: Some("Mail notification"),
set_name: Some("page_advices"),
/*#[track(model.changed_have_service_advices())] /*#[track(model.changed_have_service_advices())]
set_visible: model.have_service_advices,*/ set_visible: model.have_service_advices,*/
} },
add = &adw::Bin {
#[wrap(Some)]
set_child = &gtk::ScrolledWindow {
#[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_margin_all: 8,
add_css_class: relm4::css::TOOLBAR,
#[name = "tracking_entry"]
gtk::Entry {
set_input_hints: gtk::InputHints::PRIVATE,
set_hexpand: true,
},
#[name = "tracking_entry_button"]
gtk::Button {}
},
#[local_ref]
tracking_box -> gtk::Box {
set_spacing: 8
},
}
}
} -> /*page_tracking: adw::ViewStackPage*/ {
set_title: Some("Shipment tracking"),
set_name: Some("page_tracking"),
},
} }
} }
@ -114,9 +169,9 @@ impl Component for Ready {
root: Self::Root, root: Self::Root,
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let advices_factory = FactoryVecDeque::builder() let advices_factory = FactoryVecDeque::builder().launch_default().detach();
.launch(adw::Carousel::new())
.detach(); let tracking_factory = FactoryHashMap::builder().launch_default().detach();
let model = Ready { let model = Ready {
have_service_advices: false, have_service_advices: false,
@ -124,10 +179,12 @@ impl Component for Ready {
advices_state: ReadyAdvicesState::Loading, advices_state: ReadyAdvicesState::Loading,
login: init.clone(), login: init.clone(),
activate: false, activate: false,
tracking_factory,
tracker: 0, tracker: 0,
}; };
let advices_carousel = model.advices_factory.widget(); let advices_carousel = model.advices_factory.widget();
let tracking_box = model.tracking_factory.widget();
let widgets = view_output!(); let widgets = view_output!();
{ {
@ -148,6 +205,21 @@ impl Component for Ready {
.drop_on_shutdown() .drop_on_shutdown()
}); });
} }
{
let sender = sender.clone();
widgets.tracking_entry.connect_activate(move |entry| {
sender.input(ReadyInput::SearchTracking(entry.text().into()));
entry.set_text("");
});
}
{
let sender = sender.clone();
let entry = widgets.tracking_entry.clone();
widgets.tracking_entry_button.connect_clicked(move |_| {
sender.input(ReadyInput::SearchTracking(entry.text().into()));
entry.set_text("");
});
}
ComponentParts { model, widgets } ComponentParts { model, widgets }
} }
@ -155,11 +227,11 @@ impl Component for Ready {
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) { fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
self.reset(); self.reset();
let token = self.login.clone();
match message { match message {
ReadyInput::Activate => { ReadyInput::Activate => {
self.set_activate(true); self.set_activate(true);
if self.changed_activate() { if self.changed_activate() {
let token = self.login.clone();
sender.oneshot_command(async move { sender.oneshot_command(async move {
let token = crate::login::get_id_token(&token).await.unwrap(); let token = crate::login::get_id_token(&token).await.unwrap();
let client = libpaket::StammdatenClient::new(); let client = libpaket::StammdatenClient::new();
@ -167,10 +239,48 @@ impl Component for Ready {
}); });
} }
} }
ReadyInput::SearchTracking(value) => {
sender.oneshot_command(async move {
let client = libpaket::WebClient::new();
let mut vec = Vec::new();
vec.push(value);
ReadyCmds::GotTracking(
client
.tracking_search(
TrackingParams {
language: Some("de".to_string()),
},
vec,
None,
)
.await,
)
});
}
ReadyInput::Deactivate => { ReadyInput::Deactivate => {
self.set_activate(false); self.set_activate(false);
} }
ReadyInput::HavePaketankuendigungService => {
let token = self.login.clone();
sender.oneshot_command(async move {
// fetching advices
let dhli_token = crate::login::get_id_token(&token).await.unwrap();
let client = libpaket::WebClient::new();
ReadyCmds::GotTracking(
client
.tracking_search(
TrackingParams {
language: Some("de".to_string()),
},
Vec::new(),
Some(&dhli_token),
)
.await,
)
});
}
ReadyInput::HaveAdvicesService => { ReadyInput::HaveAdvicesService => {
let token = self.login.clone();
sender.oneshot_command(async move { sender.oneshot_command(async move {
// fetching advices // fetching advices
let dhli_token = crate::login::get_id_token(&token).await.unwrap(); let dhli_token = crate::login::get_id_token(&token).await.unwrap();
@ -217,28 +327,82 @@ impl Component for Ready {
ReadyCmds::LoggedOut => sender.input(ReadyInput::Deactivate), ReadyCmds::LoggedOut => sender.input(ReadyInput::Deactivate),
ReadyCmds::GotCustomerDataFull(res) => match res { ReadyCmds::GotCustomerDataFull(res) => match res {
Ok(res) => { Ok(res) => {
let mut a_service_was_activated = false;
for service in &res.common.services { for service in &res.common.services {
match service { match service {
libpaket::stammdaten::CustomerDataService::Packstation => (), libpaket::stammdaten::CustomerDataService::Packstation => (),
libpaket::stammdaten::CustomerDataService::Paketankuendigung => (), libpaket::stammdaten::CustomerDataService::Paketankuendigung => {
sender.input(ReadyInput::HavePaketankuendigungService);
}
libpaket::stammdaten::CustomerDataService::PostfilialeDirekt => (), libpaket::stammdaten::CustomerDataService::PostfilialeDirekt => (),
libpaket::stammdaten::CustomerDataService::Digiben => (), libpaket::stammdaten::CustomerDataService::Digiben => (),
libpaket::stammdaten::CustomerDataService::GeraetAktiviert => (), libpaket::stammdaten::CustomerDataService::GeraetAktiviert => (),
libpaket::stammdaten::CustomerDataService::Briefankuendigung => { libpaket::stammdaten::CustomerDataService::Briefankuendigung => {
a_service_was_activated = true;
sender.input(ReadyInput::HaveAdvicesService); sender.input(ReadyInput::HaveAdvicesService);
} }
} }
} }
if a_service_was_activated {
sender.output(ReadyOutput::Ready).unwrap() sender.output(ReadyOutput::Ready).unwrap()
} else {
sender.output(ReadyOutput::NoServicesEnabled).unwrap();
}
} }
Err(err) => sender.output(ReadyOutput::FatalError(err)).unwrap(), Err(err) => sender.output(ReadyOutput::FatalError(err)).unwrap(),
}, },
ReadyCmds::GotTracking(res) => match res {
Ok(shipment_vec) => {
for item in shipment_vec {
if let Some(err) = item.error {
// TODO: gettext
if err.id_invalid {
sender
.output(ReadyOutput::Notification(format!(
"The id is invalid ({})",
item.id
)))
.unwrap();
} else if err.letter_not_found {
sender
.output(ReadyOutput::Notification(format!(
"The letter wasn't found ({})",
item.id
)))
.unwrap();
} else if err.id_not_searchable {
sender
.output(ReadyOutput::Notification(format!(
"The id is not searchable ({})",
item.id
)))
.unwrap();
} else if err.data_to_old {
sender
.output(ReadyOutput::Notification(format!(
"No data available with id ({}) (data expired)",
item.id
)))
.unwrap();
} else if err.not_from_dhl {
sender
.output(ReadyOutput::Notification(format!(
"The id is not from DHL ({})",
item.id
)))
.unwrap();
} else if err.no_data_available {
sender
.output(ReadyOutput::Notification(format!(
"No data available with id ({})",
item.id
)))
.unwrap();
}
} else {
self.tracking_factory.insert(item.id.clone(), item);
}
}
}
Err(err) => {
sender.output(ReadyOutput::Error(err)).unwrap();
}
},
ReadyCmds::GotAdvices(res) => match res.0 { ReadyCmds::GotAdvices(res) => match res.0 {
Ok(advices_vec) => { Ok(advices_vec) => {
{ {