From 87befa2f13577aaaaf5f809901c5fffba3b4d9ed Mon Sep 17 00:00:00 2001 From: jane400 Date: Wed, 18 Sep 2024 17:07:51 +0200 Subject: [PATCH] feat: paket: refactor views and cleanups --- icons.toml | 2 +- paket/Cargo.toml | 4 +- paket/src/bin/paket.rs | 196 ++++++-------------------- paket/src/login.rs | 5 +- paket/src/ready.rs | 313 +++++++++++++++++------------------------ paket/src/tracking.rs | 253 +++++++++++++++++++++++++++++---- 6 files changed, 398 insertions(+), 375 deletions(-) diff --git a/icons.toml b/icons.toml index ded2116..71854d1 100644 --- a/icons.toml +++ b/icons.toml @@ -1,3 +1,3 @@ app_id = "de.j4ne.Paket" -icons = ["plus", "minus", "package-x-generic", "mail"] \ No newline at end of file +icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large"] \ No newline at end of file diff --git a/paket/Cargo.toml b/paket/Cargo.toml index e608b1c..4ef1db3 100644 --- a/paket/Cargo.toml +++ b/paket/Cargo.toml @@ -12,11 +12,11 @@ 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" } glycin = { version = "2.0.0-beta", features = ["gdk4"] } oo7 = { version = "0.3" } futures = "0.3" -gtk = { package = "gtk4", version = "0.9", features = ["v4_16"]} +gtk = { package = "gtk4", version = "0.9", features = ["v4_16"]} \ No newline at end of file diff --git a/paket/src/bin/paket.rs b/paket/src/bin/paket.rs index e14185c..9e68826 100644 --- a/paket/src/bin/paket.rs +++ b/paket/src/bin/paket.rs @@ -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), } } diff --git a/paket/src/login.rs b/paket/src/login.rs index bdc6015..9c27c6b 100644 --- a/paket/src/login.rs +++ b/paket/src/login.rs @@ -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(); } diff --git a/paket/src/ready.rs b/paket/src/ready.rs index 6d1fcab..26dbc60 100644 --- a/paket/src/ready.rs +++ b/paket/src/ready.rs @@ -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, #[do_not_track] - tracking_factory: FactoryHashMap, + tracking_component: Controller, + #[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), - GotTracking(LibraryResult>), } #[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, ) -> ComponentParts { - 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 = + 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), + } +} diff --git a/paket/src/tracking.rs b/paket/src/tracking.rs index 3fd8bf8..53c4d19 100644 --- a/paket/src/tracking.rs +++ b/paket/src/tracking.rs @@ -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, + login: LoginSharedState, +} + +#[derive(Debug)] +pub enum TrackingInput { + Search(Option), + Notification(String), +} + +#[derive(Debug)] +pub enum TrackingCmds { + GotTracking(LibraryResult>), +} + +#[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, + ) -> ComponentParts { + 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, 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, + 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();