feat: paket: refactor views and cleanups
This commit is contained in:
parent
d122cfc065
commit
87befa2f13
6 changed files with 398 additions and 375 deletions
|
@ -1,3 +1,3 @@
|
|||
app_id = "de.j4ne.Paket"
|
||||
|
||||
icons = ["plus", "minus", "package-x-generic", "mail"]
|
||||
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large"]
|
|
@ -12,7 +12,7 @@ relm4-components = { version = "0.9", git = "https://github.com/Relm4/Relm4.git"
|
|||
relm4-macros = { version = "0.9", git = "https://github.com/Relm4/Relm4.git" }
|
||||
relm4-icons = { version = "0.9", git = "https://github.com/Relm4/icons.git" }
|
||||
tracker = "0.2"
|
||||
adw = {package = "libadwaita", version = "0.7", features = [ "v1_5" ]}
|
||||
adw = {package = "libadwaita", version = "0.7", features = [ "v1_6" ]}
|
||||
webkit = { package = "webkit6", version = "0.4" }
|
||||
reqwest = "0.12"
|
||||
libpaket = { path = "../libpaket" }
|
||||
|
|
|
@ -7,9 +7,9 @@ use relm4::{main_adw_application, prelude::*, AsyncComponentSender, RELM_THREADS
|
|||
#[derive(Debug, PartialEq)]
|
||||
enum AppState {
|
||||
Loading,
|
||||
RequiresLogIn,
|
||||
FatalError,
|
||||
RequiresLogin,
|
||||
Ready,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -20,13 +20,11 @@ struct AppError {
|
|||
|
||||
#[derive(Debug)]
|
||||
enum AppInput {
|
||||
ErrorOccoured(AppError),
|
||||
FatalErrorOccoured(AppError),
|
||||
AddBreakpoint(adw::Breakpoint),
|
||||
SwitchToLogin,
|
||||
SwitchToLoading,
|
||||
SwitchToReady,
|
||||
NetworkFail,
|
||||
Notification(String, u32),
|
||||
FatalErr(AppError),
|
||||
}
|
||||
|
||||
#[tracker::track]
|
||||
|
@ -50,13 +48,6 @@ impl AsyncComponent for App {
|
|||
view! {
|
||||
#[root]
|
||||
main_window = adw::ApplicationWindow::new(&main_adw_application()) {
|
||||
add_breakpoint = adw::Breakpoint::new(
|
||||
adw::BreakpointCondition::new_length(adw::BreakpointConditionLengthType::MaxWidth, 550.0, adw::LengthUnit::Sp)
|
||||
) {
|
||||
add_setter: (&ready_headerbar, "show-title", Some(&glib::Value::from(false))),
|
||||
add_setter: (&ready_switcherbar, "reveal", Some(&glib::Value::from(true)))
|
||||
},
|
||||
|
||||
set_default_height: 600,
|
||||
set_default_width: 800,
|
||||
set_width_request: 300,
|
||||
|
@ -64,92 +55,35 @@ impl AsyncComponent for App {
|
|||
|
||||
#[wrap(Some)]
|
||||
set_content = &adw::ViewStack {
|
||||
#[name = "page_prepare"]
|
||||
add = &adw::ToolbarView {
|
||||
add_top_bar = &adw::HeaderBar {},
|
||||
|
||||
#[wrap(Some)]
|
||||
set_content = prepare_toast_overlay = &adw::ToastOverlay {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ViewStack {
|
||||
#[name = "page_loading"]
|
||||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
|
||||
gtk::Spinner {
|
||||
start: (),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/* Will be filled in init with a webkit */
|
||||
#[local_ref]
|
||||
add = page_login -> adw::Bin {},
|
||||
|
||||
#[name = "page_fatal"]
|
||||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = fatal_status_page = &adw::StatusPage {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
#[track(model.changed(App::state()) && model.state != AppState::Ready)]
|
||||
set_visible_child: {
|
||||
let page: &adw::Bin = match model.state {
|
||||
AppState::Loading => page_loading.as_ref(),
|
||||
AppState::RequiresLogIn => page_login.as_ref(),
|
||||
AppState::FatalError => page_fatal.as_ref(),
|
||||
AppState::Ready => panic!(),
|
||||
};
|
||||
page
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
#[name = "page_ready"]
|
||||
#[name = "page_loading"]
|
||||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::NavigationView {
|
||||
add = &adw::NavigationPage {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ToolbarView {
|
||||
add_top_bar = ready_headerbar = &adw::HeaderBar {
|
||||
#[wrap(Some)]
|
||||
set_title_widget = ready_switchertop = &adw::ViewSwitcher{
|
||||
set_stack: Some(ready_view_stack),
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
#[wrap(Some)]
|
||||
set_content = ready_toast_overlay = &adw::ToastOverlay {
|
||||
set_child: Some(ready_view_stack),
|
||||
},
|
||||
|
||||
add_bottom_bar = ready_switcherbar = &adw::ViewSwitcherBar {
|
||||
set_stack: Some(ready_view_stack),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
set_child = &adw::Spinner {}
|
||||
},
|
||||
|
||||
#[track(model.changed(App::state()) && model.state == AppState::Ready)]
|
||||
#[local_ref]
|
||||
add = page_login -> adw::Bin {},
|
||||
|
||||
#[local_ref]
|
||||
add = page_ready -> adw::Bin {},
|
||||
|
||||
#[name = "page_error"]
|
||||
add = &adw::Bin {
|
||||
#[name = "page_error_status"]
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::StatusPage {}
|
||||
},
|
||||
|
||||
#[track(model.changed(App::state()))]
|
||||
set_visible_child: {
|
||||
let page: &adw::Bin = page_ready.as_ref();
|
||||
let page: &adw::Bin = match model.state {
|
||||
AppState::Loading => page_loading.as_ref(),
|
||||
AppState::RequiresLogin => page_login.as_ref(),
|
||||
AppState::Ready => page_ready.as_ref(),
|
||||
AppState::Error => page_error.as_ref(),
|
||||
};
|
||||
page
|
||||
},
|
||||
#[track(model.changed(App::state()) && model.state != AppState::Ready)]
|
||||
set_visible_child: {
|
||||
let page: &adw::ToolbarView = page_prepare.as_ref();
|
||||
page
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -178,8 +112,8 @@ impl AsyncComponent for App {
|
|||
tracker: 0,
|
||||
};
|
||||
|
||||
let ready_view_stack = model.ready.widget();
|
||||
let page_login = model.login.widget();
|
||||
let page_ready = model.ready.widget();
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
|
@ -195,57 +129,23 @@ impl AsyncComponent for App {
|
|||
) -> Self::Output {
|
||||
self.reset();
|
||||
match message {
|
||||
AppInput::ErrorOccoured(error) => {
|
||||
let dialog: adw::AlertDialog = adw::AlertDialog::builder()
|
||||
.title(error.short)
|
||||
.body(error.long)
|
||||
.build();
|
||||
dialog.present(Some(root));
|
||||
}
|
||||
AppInput::AddBreakpoint(breakpoint) => {
|
||||
root.add_breakpoint(breakpoint);
|
||||
},
|
||||
AppInput::SwitchToLoading => {
|
||||
self.set_state(AppState::Loading);
|
||||
}
|
||||
AppInput::SwitchToLogin => {
|
||||
self.set_state(AppState::RequiresLogIn);
|
||||
}
|
||||
AppInput::NetworkFail => {
|
||||
self.set__network_fail(true);
|
||||
if self.changed__network_fail() {
|
||||
sender.input(AppInput::Notification(
|
||||
"The internet connection is unstable.".to_string(),
|
||||
10,
|
||||
));
|
||||
}
|
||||
}
|
||||
AppInput::Notification(notification, timeout) => {
|
||||
let toast_overlay = match self.state {
|
||||
AppState::Loading => &widgets.prepare_toast_overlay,
|
||||
AppState::RequiresLogIn => &widgets.prepare_toast_overlay,
|
||||
AppState::Ready => &widgets.ready_toast_overlay,
|
||||
AppState::FatalError => &widgets.prepare_toast_overlay,
|
||||
};
|
||||
|
||||
toast_overlay.add_toast(
|
||||
adw::Toast::builder()
|
||||
.title(notification.as_str())
|
||||
.timeout(timeout)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
AppInput::FatalErrorOccoured(error) => {
|
||||
widgets.fatal_status_page.set_title(&error.short);
|
||||
widgets.fatal_status_page.set_description(Some(
|
||||
format!(
|
||||
"{}\nThis error is fatal, the app can't continue.",
|
||||
&error.long
|
||||
)
|
||||
.as_str(),
|
||||
));
|
||||
self.set_state(AppState::FatalError);
|
||||
self.set_state(AppState::RequiresLogin);
|
||||
}
|
||||
AppInput::SwitchToReady => {
|
||||
self.set_state(AppState::Ready);
|
||||
}
|
||||
AppInput::FatalErr(err) => {
|
||||
widgets.page_error_status.set_title(&err.short);
|
||||
widgets.page_error_status.set_description(Some(&err.long));
|
||||
self.set_state(AppState::Error);
|
||||
}
|
||||
}
|
||||
self.update_view(widgets, sender);
|
||||
}
|
||||
|
@ -255,31 +155,15 @@ fn convert_login_response(response: LoginOutput) -> AppInput {
|
|||
match response {
|
||||
LoginOutput::RequiresLogin => AppInput::SwitchToLogin,
|
||||
LoginOutput::RequiresLoading => AppInput::SwitchToLoading,
|
||||
LoginOutput::Error(err) => AppInput::ErrorOccoured(AppError {
|
||||
short: "An authorization error occured :(".to_string(),
|
||||
long: err.to_string(),
|
||||
}),
|
||||
LoginOutput::NetworkFail => AppInput::NetworkFail,
|
||||
LoginOutput::KeyringError(err) => AppInput::FatalErrorOccoured(AppError {
|
||||
short: "Unable to operate on the keyring :(".to_string(),
|
||||
long: err.to_string(),
|
||||
}),
|
||||
LoginOutput::Error(library_error) => AppInput::FatalErr(AppError { short: "Unhandled API error".to_string(), long: library_error.to_string() }),
|
||||
LoginOutput::KeyringError(error) => AppInput::FatalErr(AppError { short: "Keyring usage failed".to_string(), long: error.to_string() }),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
||||
match response {
|
||||
ReadyOutput::FatalError(err) => AppInput::FatalErrorOccoured(AppError {
|
||||
short: "Unexpted error occured.".to_string(),
|
||||
long: err.to_string(),
|
||||
}),
|
||||
ReadyOutput::NoServicesEnabled => AppInput::FatalErrorOccoured(AppError {
|
||||
short: "You can't use this app".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::Notification(value) => AppInput::Notification(value, 60),
|
||||
ReadyOutput::Ready => AppInput::SwitchToReady,
|
||||
ReadyOutput::AddBreakpoint(breakpoint) => AppInput::AddBreakpoint(breakpoint),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,6 @@ pub struct Login {
|
|||
pub enum LoginOutput {
|
||||
RequiresLogin,
|
||||
RequiresLoading,
|
||||
NetworkFail,
|
||||
Error(libpaket::LibraryError),
|
||||
KeyringError(oo7::Error),
|
||||
}
|
||||
|
@ -322,9 +321,7 @@ impl Login {
|
|||
libpaket::LibraryError::InvalidArgument(_) => {
|
||||
panic!("{}", res);
|
||||
}
|
||||
libpaket::LibraryError::NetworkFetch => {
|
||||
sender.output(LoginOutput::NetworkFail).unwrap();
|
||||
}
|
||||
libpaket::LibraryError::NetworkFetch => {}
|
||||
libpaket::LibraryError::DecodeError(_) => {
|
||||
sender.output(LoginOutput::Error(res)).unwrap();
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use adw::prelude::*;
|
||||
use libpaket::{
|
||||
self,
|
||||
tracking::{Shipment, TrackingParams},
|
||||
LibraryError, LibraryResult,
|
||||
};
|
||||
use relm4::{adw, factory::FactoryHashMap, prelude::*};
|
||||
use libpaket::{stammdaten::CustomerDataFull, LibraryError, LibraryResult};
|
||||
use relm4::prelude::*;
|
||||
|
||||
use crate::advices::{AdvicesView, AdvicesViewInput};
|
||||
use crate::{
|
||||
advices::{AdvicesView, AdvicesViewInput},
|
||||
tracking::{TrackingInput, TrackingOutput, TrackingView},
|
||||
};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct Ready {
|
||||
|
@ -19,16 +18,15 @@ pub struct Ready {
|
|||
#[do_not_track]
|
||||
advices_component: AsyncController<AdvicesView>,
|
||||
#[do_not_track]
|
||||
tracking_factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
|
||||
tracking_component: Controller<TrackingView>,
|
||||
#[do_not_track]
|
||||
toast_overlay: adw::ToastOverlay,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReadyOutput {
|
||||
Ready,
|
||||
Error(LibraryError),
|
||||
FatalError(LibraryError),
|
||||
Notification(String),
|
||||
NoServicesEnabled,
|
||||
AddBreakpoint(adw::Breakpoint),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -36,7 +34,6 @@ pub enum ReadyCmds {
|
|||
LoggedIn,
|
||||
LoggedOut,
|
||||
GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>),
|
||||
GotTracking(LibraryResult<Vec<Shipment>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -51,7 +48,6 @@ pub enum ReadyInput {
|
|||
Deactivate,
|
||||
HaveService(Services),
|
||||
ServiceBorked(Services),
|
||||
SearchTracking(String),
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
|
@ -63,61 +59,63 @@ impl Component for Ready {
|
|||
|
||||
view! {
|
||||
#[root]
|
||||
adw::ViewStack {
|
||||
add = &model.advices_component.widget().clone() -> gtk::ScrolledWindow {
|
||||
#[track(model.changed_have_service_advices())]
|
||||
set_visible: model.have_service_advices,
|
||||
|
||||
} -> page_advices: adw::ViewStackPage {
|
||||
set_title: Some("Mail notification"),
|
||||
set_name: Some("page_advices"),
|
||||
set_icon_name: Some(relm4_icons::icon_names::MAIL),
|
||||
|
||||
#[track(model.changed_have_service_advices())]
|
||||
set_visible: model.have_service_advices,
|
||||
},
|
||||
|
||||
add = &adw::Bin {
|
||||
#[track(model.changed_have_service_tracking())]
|
||||
set_visible: model.have_service_tracking,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = >k::ScrolledWindow {
|
||||
adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::NavigationView {
|
||||
add = &adw::NavigationPage {
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_child = &adw::ToolbarView {
|
||||
add_top_bar = ready_headerbar = &adw::HeaderBar {
|
||||
#[wrap(Some)]
|
||||
set_title_widget = ready_switchertop = &adw::ViewSwitcher{
|
||||
set_stack: Some(&ready_view_stack),
|
||||
}
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_margin_all: 8,
|
||||
add_css_class: relm4::css::TOOLBAR,
|
||||
#[wrap(Some)]
|
||||
set_content = &model.toast_overlay.clone() -> adw::ToastOverlay {
|
||||
#[wrap(Some)]
|
||||
#[name = "ready_view_stack"]
|
||||
set_child = &adw::ViewStack {
|
||||
add = &model.advices_component.widget().clone() -> gtk::ScrolledWindow {
|
||||
#[track(model.changed_have_service_advices())]
|
||||
set_visible: model.have_service_advices,
|
||||
|
||||
#[name = "tracking_entry"]
|
||||
gtk::Entry {
|
||||
set_input_hints: gtk::InputHints::PRIVATE,
|
||||
set_hexpand: true,
|
||||
} -> page_advices: adw::ViewStackPage {
|
||||
set_title: Some("Mail notification"),
|
||||
set_name: Some("page_advices"),
|
||||
set_icon_name: Some(relm4_icons::icon_names::MAIL),
|
||||
|
||||
#[track(model.changed_have_service_advices())]
|
||||
set_visible: model.have_service_advices,
|
||||
},
|
||||
|
||||
add = &model.tracking_component.widget().clone() -> adw::ToastOverlay {
|
||||
#[track(model.changed_have_service_tracking())]
|
||||
set_visible: model.have_service_tracking,
|
||||
|
||||
|
||||
} -> page_tracking: adw::ViewStackPage {
|
||||
set_title: Some("Shipment tracking"),
|
||||
set_name: Some("page_tracking"),
|
||||
set_icon_name: Some(relm4_icons::icon_names::PACKAGE_X_GENERIC),
|
||||
|
||||
#[track(model.changed_have_service_tracking())]
|
||||
set_visible: model.have_service_tracking,
|
||||
},
|
||||
},
|
||||
|
||||
#[name = "tracking_entry_button"]
|
||||
gtk::Button {}
|
||||
},
|
||||
|
||||
|
||||
#[local_ref]
|
||||
tracking_box -> gtk::Box {
|
||||
set_spacing: 8
|
||||
},
|
||||
add_bottom_bar = ready_switcherbar = &adw::ViewSwitcherBar {
|
||||
set_stack: Some(&ready_view_stack),
|
||||
}
|
||||
}
|
||||
}
|
||||
} -> page_tracking: adw::ViewStackPage {
|
||||
set_title: Some("Shipment tracking"),
|
||||
set_name: Some("page_tracking"),
|
||||
set_icon_name: Some(relm4_icons::icon_names::PACKAGE_X_GENERIC),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
#[track(model.changed_have_service_tracking())]
|
||||
set_visible: model.have_service_tracking,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
|
@ -125,22 +123,25 @@ impl Component for Ready {
|
|||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let tracking_factory = FactoryHashMap::builder().launch_default().detach();
|
||||
let advices_component = AdvicesView::builder().launch(init.clone()).detach();
|
||||
|
||||
let tracking_component = TrackingView::builder()
|
||||
.launch(init.clone())
|
||||
.forward(&sender.input_sender(), convert_tracking_output);
|
||||
|
||||
let toast_overlay = adw::ToastOverlay::new();
|
||||
|
||||
let model = Ready {
|
||||
have_service_advices: false,
|
||||
have_service_tracking: true,
|
||||
login: init.clone(),
|
||||
activate: false,
|
||||
tracking_factory,
|
||||
advices_component,
|
||||
tracking_component,
|
||||
toast_overlay,
|
||||
tracker: 0,
|
||||
};
|
||||
|
||||
let tracking_box = model.tracking_factory.widget();
|
||||
|
||||
let widgets = view_output!();
|
||||
{
|
||||
let login = model.login.clone();
|
||||
sender.command(move |out, shutdown| {
|
||||
|
@ -159,21 +160,32 @@ impl Component for Ready {
|
|||
.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("");
|
||||
});
|
||||
}
|
||||
|
||||
let breakpoint_0 = adw::Breakpoint::new(adw::BreakpointCondition::new_length(
|
||||
adw::BreakpointConditionLengthType::MaxWidth,
|
||||
480.0,
|
||||
adw::LengthUnit::Sp,
|
||||
));
|
||||
let breakpoint_1 = adw::Breakpoint::new(adw::BreakpointCondition::new_length(
|
||||
adw::BreakpointConditionLengthType::MaxWidth,
|
||||
480.0,
|
||||
adw::LengthUnit::Sp,
|
||||
));
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
breakpoint_0.add_setter(widgets.ready_headerbar.widget_ref(), "title-widget", None);
|
||||
sender
|
||||
.output(ReadyOutput::AddBreakpoint(breakpoint_0))
|
||||
.unwrap();
|
||||
breakpoint_1.add_setter(
|
||||
widgets.ready_switcherbar.widget_ref(),
|
||||
"reveal",
|
||||
Some(>k::glib::Value::from(true)),
|
||||
);
|
||||
sender
|
||||
.output(ReadyOutput::AddBreakpoint(breakpoint_1))
|
||||
.unwrap();
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
@ -189,28 +201,17 @@ impl Component for Ready {
|
|||
sender.oneshot_command(async move {
|
||||
let token = crate::login::get_id_token(&token).await.unwrap();
|
||||
let client = libpaket::StammdatenClient::new();
|
||||
ReadyCmds::GotCustomerDataFull(client.customer_data_full(&token).await)
|
||||
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::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 => {
|
||||
self.set_activate(false);
|
||||
}
|
||||
|
@ -221,28 +222,28 @@ impl Component for Ready {
|
|||
}
|
||||
Services::SendungVerfolgung => {
|
||||
self.set_have_service_tracking(true);
|
||||
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,
|
||||
)
|
||||
});
|
||||
self.tracking_component.emit(TrackingInput::Search(None))
|
||||
}
|
||||
},
|
||||
ReadyInput::ServiceBorked(service) => match service {
|
||||
Services::Advices => self.set_have_service_advices(false),
|
||||
Services::SendungVerfolgung => self.set_have_service_tracking(false),
|
||||
Services::Advices => {
|
||||
self.toast_overlay.add_toast(
|
||||
adw::Toast::builder()
|
||||
.title("Service borked: Mail notifications")
|
||||
.timeout(30)
|
||||
.build(),
|
||||
);
|
||||
self.set_have_service_advices(false);
|
||||
}
|
||||
Services::SendungVerfolgung => {
|
||||
self.toast_overlay.add_toast(
|
||||
adw::Toast::builder()
|
||||
.title("Service borked: Shipment tracking")
|
||||
.timeout(30)
|
||||
.build(),
|
||||
);
|
||||
self.set_have_service_tracking(false);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -273,76 +274,14 @@ impl Component for Ready {
|
|||
|
||||
sender.output(ReadyOutput::Ready).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) => {
|
||||
if err == LibraryError::APIChange {
|
||||
println!("Upstream API for parcel tracking broke");
|
||||
sender.input(ReadyInput::ServiceBorked(Services::SendungVerfolgung));
|
||||
sender
|
||||
.output(ReadyOutput::Notification(
|
||||
"Shipment Tracking API has changed. Deactivating that service."
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
} else {
|
||||
sender.output(ReadyOutput::Error(err)).unwrap();
|
||||
}
|
||||
}
|
||||
Err(err) => todo!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_tracking_output(value: TrackingOutput) -> ReadyInput {
|
||||
match value {
|
||||
TrackingOutput::Borked => ReadyInput::ServiceBorked(Services::SendungVerfolgung),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,218 @@
|
|||
use adw::prelude::*;
|
||||
use libpaket::tracking::Shipment;
|
||||
use relm4::factory::FactoryComponent;
|
||||
use libpaket::tracking::{Shipment, TrackingParams};
|
||||
use libpaket::{LibraryError, LibraryResult};
|
||||
use relm4::factory::{FactoryComponent, FactoryHashMap};
|
||||
use relm4::prelude::*;
|
||||
use relm4::{adw, gtk};
|
||||
|
||||
pub struct ShipmentView {
|
||||
use crate::login::get_id_token;
|
||||
use crate::LoginSharedState;
|
||||
|
||||
pub struct TrackingView {
|
||||
factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
|
||||
login: LoginSharedState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TrackingInput {
|
||||
Search(Option<String>),
|
||||
Notification(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TrackingCmds {
|
||||
GotTracking(LibraryResult<Vec<Shipment>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TrackingOutput {
|
||||
Borked,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl Component for TrackingView {
|
||||
type Init = LoginSharedState;
|
||||
type Input = TrackingInput;
|
||||
type Output = TrackingOutput;
|
||||
type CommandOutput = TrackingCmds;
|
||||
|
||||
view! {
|
||||
#[root]
|
||||
adw::ToastOverlay {
|
||||
#[wrap(Some)]
|
||||
set_child = >k::ScrolledWindow {
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_margin_all: 2,
|
||||
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 {
|
||||
set_icon_name: relm4_icons::icon_names::LOUPE_LARGE
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
#[local_ref]
|
||||
tracking_box -> gtk::Box {
|
||||
set_spacing: 8
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let factory = FactoryHashMap::builder().launch_default().detach();
|
||||
|
||||
let model = TrackingView {
|
||||
factory,
|
||||
login: init,
|
||||
};
|
||||
|
||||
let tracking_box = model.factory.widget();
|
||||
let widgets = view_output!();
|
||||
|
||||
{
|
||||
let sender = sender.clone();
|
||||
widgets.tracking_entry.connect_activate(move |entry| {
|
||||
sender.input(TrackingInput::Search(Some(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(TrackingInput::Search(Some(entry.text().into())));
|
||||
entry.set_text("");
|
||||
});
|
||||
}
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
|
||||
match message {
|
||||
TrackingInput::Search(value) => {
|
||||
// Some input validation
|
||||
if let Some(value) = &value {
|
||||
// https://www.pakete-verfolgen.de/dhl-sendungsnummer/
|
||||
if value.len() < 8 {
|
||||
sender.input(TrackingInput::Notification(
|
||||
"The id is too short to be valid.".to_string(),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let token = self.login.clone();
|
||||
sender.oneshot_command(async move {
|
||||
let token = get_id_token(&token).await.unwrap();
|
||||
let client = libpaket::WebClient::new();
|
||||
let mut vec = Vec::new();
|
||||
if let Some(value) = value {
|
||||
vec.push(value);
|
||||
}
|
||||
TrackingCmds::GotTracking(
|
||||
client
|
||||
.tracking_search(
|
||||
TrackingParams {
|
||||
language: Some("de".to_string()),
|
||||
},
|
||||
vec,
|
||||
&token,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
});
|
||||
}
|
||||
TrackingInput::Notification(value) => {
|
||||
root.add_toast(adw::Toast::builder().timeout(15).title(value).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_cmd(
|
||||
&mut self,
|
||||
message: Self::CommandOutput,
|
||||
sender: ComponentSender<Self>,
|
||||
root: &Self::Root,
|
||||
) {
|
||||
match message {
|
||||
TrackingCmds::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.input(TrackingInput::Notification(format!(
|
||||
"The id is invalid ({})",
|
||||
item.id
|
||||
)));
|
||||
} else if err.letter_not_found {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
"The letter wasn't found ({})",
|
||||
item.id
|
||||
)));
|
||||
} else if err.id_not_searchable {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
"The id is not searchable ({})",
|
||||
item.id
|
||||
)));
|
||||
} else if err.data_to_old {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
"No data available with id ({}) (data expired)",
|
||||
item.id
|
||||
)));
|
||||
} else if err.not_from_dhl {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
"The id is not from DHL ({})",
|
||||
item.id
|
||||
)));
|
||||
} else if err.no_data_available {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
"No data available with id ({})",
|
||||
item.id
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
self.factory.insert(item.id.clone(), item);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if err == LibraryError::APIChange {
|
||||
println!("Upstream API for parcel tracking broke");
|
||||
sender.output(TrackingOutput::Borked).unwrap();
|
||||
} else {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
"Unknown Error: {}",
|
||||
err.to_string()
|
||||
)))
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
struct ShipmentView {
|
||||
model: Shipment,
|
||||
|
||||
// model abstraction
|
||||
|
@ -14,7 +222,7 @@ pub struct ShipmentView {
|
|||
expanded: bool,
|
||||
|
||||
// workarounds
|
||||
list_box_history: gtk::ListBox,
|
||||
list_box_history: gtk::Box,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -22,7 +230,7 @@ pub enum ViewInput {
|
|||
ToggleExpand,
|
||||
}
|
||||
|
||||
#[relm4::factory(pub)]
|
||||
#[relm4::factory]
|
||||
impl FactoryComponent for ShipmentView {
|
||||
type CommandOutput = ();
|
||||
type Init = Shipment;
|
||||
|
@ -38,15 +246,7 @@ impl FactoryComponent for ShipmentView {
|
|||
set_hexpand: true,
|
||||
set_margin_all: 8,
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
|
||||
gtk::ProgressBar {
|
||||
add_css_class: relm4::css::OSD,
|
||||
set_margin_start: 8,
|
||||
set_margin_end: 8,
|
||||
set_margin_bottom: 1,
|
||||
|
||||
set_fraction: f64::from(self.model.history.maximal_fortschritt) / f64::from(self.model.history.fortschritt),
|
||||
},
|
||||
inline_css: "border-radius: 12px 12px 0px 0px",
|
||||
|
||||
// title box
|
||||
gtk::Box {
|
||||
|
@ -106,27 +306,29 @@ impl FactoryComponent for ShipmentView {
|
|||
}
|
||||
}, // title box end
|
||||
|
||||
gtk::ProgressBar {
|
||||
add_css_class: relm4::css::OSD,
|
||||
|
||||
set_fraction: f64::from(self.model.history.maximal_fortschritt) / f64::from(self.model.history.fortschritt),
|
||||
},
|
||||
|
||||
gtk::Revealer {
|
||||
#[watch]
|
||||
set_reveal_child: self.expanded,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Box {
|
||||
set_margin_all: 8,
|
||||
|
||||
// history viewstack
|
||||
adw::StatusPage {
|
||||
gtk::Label {
|
||||
set_visible: !self.have_events,
|
||||
|
||||
set_title: "No events",
|
||||
|
||||
set_label: "No events",
|
||||
},
|
||||
|
||||
append = &self.list_box_history.clone() {
|
||||
set_visible: self.have_events,
|
||||
|
||||
add_css_class: relm4::css::BOXED_LIST,
|
||||
|
||||
set_selection_mode: gtk::SelectionMode::None,
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
@ -142,7 +344,10 @@ impl FactoryComponent for ShipmentView {
|
|||
) -> Self {
|
||||
let have_events = init.history.events.as_ref().is_some_and(|a| a.len() > 0);
|
||||
|
||||
let list_box_history = gtk::ListBox::new();
|
||||
let list_box_history = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
.build();
|
||||
|
||||
let _self = ShipmentView {
|
||||
have_events,
|
||||
|
@ -169,8 +374,6 @@ impl FactoryComponent for ShipmentView {
|
|||
|
||||
let boxie = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.spacing(8)
|
||||
.build();
|
||||
|
||||
|
|
Loading…
Reference in a new issue