Compare commits
8 commits
0623efa360
...
5fc628a54d
Author | SHA1 | Date | |
---|---|---|---|
|
5fc628a54d | ||
|
cf986694eb | ||
|
4d0f8f57ce | ||
|
31698154e0 | ||
|
b9fb7fcea4 | ||
|
87befa2f13 | ||
|
d122cfc065 | ||
|
ff66184471 |
14 changed files with 877 additions and 490 deletions
|
@ -1,3 +1,3 @@
|
||||||
app_id = "de.j4ne.Paket"
|
app_id = "de.j4ne.Paket"
|
||||||
|
|
||||||
icons = ["plus", "minus", "package-x-generic", "mail"]
|
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy"]
|
|
@ -1,16 +1,22 @@
|
||||||
use super::utils::CodeVerfier;
|
|
||||||
use super::dhl_claims::DHLClaimsOptional;
|
use super::dhl_claims::DHLClaimsOptional;
|
||||||
|
use super::utils::CodeVerfier;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
pub fn client_id() -> &'static str {
|
pub fn client_id() -> &'static str {
|
||||||
"42ec7de4-e357-4c5d-aa63-f6aae5ca4d8f"
|
"42ec7de4-e357-4c5d-aa63-f6aae5ca4d8f"
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redirect_uri() -> &'static str {
|
pub fn redirect_uri_login() -> &'static str {
|
||||||
"dhllogin://de.dhl.paket/login"
|
"dhllogin://de.dhl.paket/login"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn redirect_uri_logout() -> &'static str {
|
||||||
|
"dhllogout://de.dhl.paket/logout"
|
||||||
|
}
|
||||||
|
|
||||||
pub mod token {
|
pub mod token {
|
||||||
|
use crate::login::DHLIdToken;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn refresh_token_form(refresh_token: &str) -> Vec<(&str, &str)> {
|
pub fn refresh_token_form(refresh_token: &str) -> Vec<(&str, &str)> {
|
||||||
|
@ -20,7 +26,7 @@ pub mod token {
|
||||||
("client_id", client_id()),
|
("client_id", client_id()),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authorization_code_form(
|
pub fn authorization_code_form(
|
||||||
authorization_code: String,
|
authorization_code: String,
|
||||||
code_verfier: &CodeVerfier,
|
code_verfier: &CodeVerfier,
|
||||||
|
@ -28,21 +34,33 @@ pub mod token {
|
||||||
vec![
|
vec![
|
||||||
("code".to_string(), authorization_code),
|
("code".to_string(), authorization_code),
|
||||||
("grant_type".to_string(), "authorization_code".to_string()),
|
("grant_type".to_string(), "authorization_code".to_string()),
|
||||||
("redirect_uri".to_string(), redirect_uri().to_string()),
|
("redirect_uri".to_string(), redirect_uri_login().to_string()),
|
||||||
("code_verifier".to_string(), code_verfier.code_verfier()),
|
("code_verifier".to_string(), code_verfier.code_verfier()),
|
||||||
("client_id".to_string(), client_id().to_string()),
|
("client_id".to_string(), client_id().to_string()),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn revoke_form(
|
||||||
|
token: String
|
||||||
|
) -> Vec<(String, String)> {
|
||||||
|
vec![
|
||||||
|
("token".into(), token),
|
||||||
|
("client_id".into(), client_id().into()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_agent() -> &'static str {
|
pub fn user_agent() -> &'static str {
|
||||||
"Dalvik/2.1.0 (Linux; U; Android 11; OnePlus 6T Build/RQ3A.211001.001)"
|
"Dalvik/2.1.0 (Linux; U; Android 11; OnePlus 6T Build/RQ3A.211001.001)"
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn endpoint() -> &'static str {
|
pub fn endpoint() -> &'static str {
|
||||||
"https://login.dhl.de/af5f9bb6-27ad-4af4-9445-008e7a5cddb8/login/token"
|
"https://login.dhl.de/af5f9bb6-27ad-4af4-9445-008e7a5cddb8/login/token"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn endpoint_revoke() -> &'static str {
|
||||||
|
"https://login.dhl.de/af5f9bb6-27ad-4af4-9445-008e7a5cddb8/login/token/revoke"
|
||||||
|
}
|
||||||
|
|
||||||
pub fn headers() -> reqwest::header::HeaderMap {
|
pub fn headers() -> reqwest::header::HeaderMap {
|
||||||
let aaa = vec![
|
let aaa = vec![
|
||||||
("Content-Type", "application/x-www-form-urlencoded"),
|
("Content-Type", "application/x-www-form-urlencoded"),
|
||||||
|
@ -52,19 +70,43 @@ pub mod token {
|
||||||
("Connection", "Keep-Alive"),
|
("Connection", "Keep-Alive"),
|
||||||
("Accept-Encoding", "gzip"),
|
("Accept-Encoding", "gzip"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut map = reqwest::header::HeaderMap::new();
|
let mut map = reqwest::header::HeaderMap::new();
|
||||||
for bbb in aaa {
|
for bbb in aaa {
|
||||||
map.append(bbb.0, bbb.1.parse().unwrap());
|
map.append(bbb.0, bbb.1.parse().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod logout {
|
||||||
|
use crate::{constants::web_user_agent, login::{
|
||||||
|
constants::{client_id, redirect_uri_logout},
|
||||||
|
DHLIdToken,
|
||||||
|
}};
|
||||||
|
|
||||||
|
pub fn form(id_token: &DHLIdToken) -> Vec<(String, String)> {
|
||||||
|
vec![
|
||||||
|
("id_token_hint".into(), id_token.to_string()),
|
||||||
|
("state".into(), "esnGubTtYjK5ImleO84prQ".into()),
|
||||||
|
("client_id".into(), client_id().into()),
|
||||||
|
("redirect_uri".into(), redirect_uri_logout().into()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn endpoint() -> &'static str {
|
||||||
|
"https://login.dhl.de/af5f9bb6-27ad-4af4-9445-008e7a5cddb8/auth-ui/logout"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_agent() -> String {
|
||||||
|
web_user_agent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod webbrowser_authorize {
|
pub mod webbrowser_authorize {
|
||||||
use crate::constants::web_user_agent;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::constants::web_user_agent;
|
||||||
|
|
||||||
pub fn user_agent() -> String {
|
pub fn user_agent() -> String {
|
||||||
web_user_agent()
|
web_user_agent()
|
||||||
|
@ -112,7 +154,7 @@ pub mod webbrowser_authorize {
|
||||||
|
|
||||||
pub fn authorize_query(nonce: &String, code_verfier: &CodeVerfier) -> Vec<(String, String)> {
|
pub fn authorize_query(nonce: &String, code_verfier: &CodeVerfier) -> Vec<(String, String)> {
|
||||||
vec![
|
vec![
|
||||||
("redirect_uri".to_string(), redirect_uri().to_string()),
|
("redirect_uri".to_string(), redirect_uri_login().to_string()),
|
||||||
("client_id".to_string(), client_id().to_string()),
|
("client_id".to_string(), client_id().to_string()),
|
||||||
("response_type".to_string(), "code".to_string()),
|
("response_type".to_string(), "code".to_string()),
|
||||||
("prompt".to_string(), "login".to_string()),
|
("prompt".to_string(), "login".to_string()),
|
||||||
|
|
|
@ -4,12 +4,14 @@ mod openid_response;
|
||||||
pub mod openid_token;
|
pub mod openid_token;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
use constants::redirect_uri_logout;
|
||||||
|
|
||||||
pub use self::dhl_claims::{DHLCs, DHLIdToken};
|
pub use self::dhl_claims::{DHLCs, DHLIdToken};
|
||||||
pub use self::openid_response::{RefreshToken, TokenResponse};
|
pub use self::openid_response::{RefreshToken, TokenResponse};
|
||||||
pub use self::utils::{create_nonce, CodeVerfier};
|
pub use self::utils::{create_nonce, CodeVerfier};
|
||||||
|
|
||||||
use super::common::APIResult;
|
use super::common::APIResult;
|
||||||
use crate::LibraryResult;
|
use crate::{LibraryError, LibraryResult};
|
||||||
|
|
||||||
pub struct OpenIdClient {
|
pub struct OpenIdClient {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
|
@ -67,4 +69,43 @@ impl OpenIdClient {
|
||||||
|
|
||||||
Ok(parse_json_response_from_apiresult!(res, TokenResponse))
|
Ok(parse_json_response_from_apiresult!(res, TokenResponse))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Unauthorized not correct error response
|
||||||
|
pub async fn logout(&self, dhli_token: &DHLIdToken) -> LibraryResult<()> {
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.get(constants::logout::endpoint())
|
||||||
|
.form(constants::logout::form(dhli_token).as_slice())
|
||||||
|
.header("Host", "login.dhl.de")
|
||||||
|
.header("User-Agent", constants::logout::user_agent())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
let res = parse_response_internal!(res);
|
||||||
|
|
||||||
|
if let Some(value) = res.headers().get("Location") {
|
||||||
|
if value.to_str().unwrap().starts_with(redirect_uri_logout()) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(crate::LibraryError::Unauthorized)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(crate::LibraryError::Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Unauthorized not correct error response
|
||||||
|
pub async fn revoke(&self, refresh_token: &RefreshToken) -> LibraryResult<()> {
|
||||||
|
let res = request_post!(self.client,
|
||||||
|
(constants::token::endpoint()),
|
||||||
|
.form(constants::token::revoke_form(refresh_token.to_string()).as_slice())
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.status() == 200 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(LibraryError::Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ pub enum CustomerDataService {
|
||||||
#[serde(rename = "POSTFILIALE_DIREKT")]
|
#[serde(rename = "POSTFILIALE_DIREKT")]
|
||||||
PostfilialeDirekt,
|
PostfilialeDirekt,
|
||||||
#[serde(rename = "DIGIBEN")]
|
#[serde(rename = "DIGIBEN")]
|
||||||
Digiben,
|
DigitaleBenachrichtigung,
|
||||||
#[serde(rename = "GERAET_AKTIVIERT")]
|
#[serde(rename = "GERAET_AKTIVIERT")]
|
||||||
GeraetAktiviert,
|
GeraetAktiviert,
|
||||||
#[serde(rename = "BRIEFANKUENDIGUNG")]
|
#[serde(rename = "BRIEFANKUENDIGUNG")]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{login::DHLIdToken, LibraryError, LibraryResult};
|
use crate::{constants::web_user_agent, login::DHLIdToken, LibraryResult};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TrackingParams {
|
pub struct TrackingParams {
|
||||||
|
@ -19,30 +19,38 @@ impl crate::WebClient {
|
||||||
&self,
|
&self,
|
||||||
params: TrackingParams,
|
params: TrackingParams,
|
||||||
ids: Vec<String>,
|
ids: Vec<String>,
|
||||||
dhli: Option<&DHLIdToken>,
|
dhli: &DHLIdToken,
|
||||||
) -> LibraryResult<Vec<Shipment>> {
|
) -> LibraryResult<Vec<Shipment>> {
|
||||||
let api_key = crate::www::api_key_header();
|
let api_key = crate::www::api_key_header();
|
||||||
let res = if let Some(dhli) = dhli {
|
let cookie_value = crate::utils::CookieHeaderValueBuilder::new()
|
||||||
let cookie_value = crate::utils::CookieHeaderValueBuilder::new()
|
.add_dhli(&dhli)
|
||||||
.add_dhli(&dhli)
|
.add_dhlcs(&dhli)
|
||||||
.add_dhlcs(&dhli)
|
.build_string();
|
||||||
.build_string();
|
|
||||||
request!(
|
let res = request!(
|
||||||
self.web_client,
|
self.web_client,
|
||||||
endpoint_data_search,
|
endpoint_data_search,
|
||||||
query(&query_parameters_data_search(¶ms, ids)),
|
query(&query_parameters_data_search(¶ms, ids)),
|
||||||
header(api_key.0, api_key.1),
|
header(api_key.0, api_key.1),
|
||||||
header("cookie", cookie_value),
|
header("cookie", cookie_value),
|
||||||
header("x-requested-with", "de.dhl.paket"),
|
header("accept-language", "de-DE;q=1.0"),
|
||||||
header("sec-ch-ua-platform", "\"Android\""),
|
header("x-requested-with", "de.dhl.paket"),
|
||||||
header("sec-ch-ua", r#""Chromium";v="122", "Not(A:Brand";v="24", "Android WebView";v="122""#),
|
header("sec-ch-ua-platform", "\"Android\""),
|
||||||
header("sec-ch-ua-mobile", "?1"),
|
header(
|
||||||
header("content-type", "application/json"),
|
"sec-ch-ua",
|
||||||
header("accept", "application/json")
|
r#""Chromium";v="122", "Not(A:Brand";v="24", "Android WebView";v="122""#
|
||||||
)
|
),
|
||||||
} else {
|
header("sec-ch-ua-mobile", "?1"),
|
||||||
return Err(LibraryError::InvalidArgument("only supported with a logged-in session".to_string()));
|
header("accept", "application/json"),
|
||||||
};
|
header("content-type", "application/json"),
|
||||||
|
header(
|
||||||
|
"referer",
|
||||||
|
"https://www.dhl.de/int-static/pdapp/spa/prod/ver5-SPA-VERFOLGEN.html"
|
||||||
|
),
|
||||||
|
header("verfolgen-wg", "0"),
|
||||||
|
header("user-agent", web_user_agent()),
|
||||||
|
headers(crate::www::web_headers())
|
||||||
|
);
|
||||||
|
|
||||||
let resp = parse_json_response!(res, Response);
|
let resp = parse_json_response!(res, Response);
|
||||||
resp.into()
|
resp.into()
|
||||||
|
@ -418,7 +426,7 @@ fn query_parameters_data_search(
|
||||||
let mut out = vec![
|
let mut out = vec![
|
||||||
("noRedirect".to_string(), "true".to_string()),
|
("noRedirect".to_string(), "true".to_string()),
|
||||||
("cid".to_string(), "app".to_string()),
|
("cid".to_string(), "app".to_string()),
|
||||||
];
|
];
|
||||||
|
|
||||||
if let Some(lang) = params.language.as_ref() {
|
if let Some(lang) = params.language.as_ref() {
|
||||||
out.push(("language".to_string(), lang.clone()));
|
out.push(("language".to_string(), lang.clone()));
|
||||||
|
|
|
@ -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-macros = { version = "0.9", git = "https://github.com/Relm4/Relm4.git" }
|
||||||
relm4-icons = { version = "0.9", git = "https://github.com/Relm4/icons.git" }
|
relm4-icons = { version = "0.9", git = "https://github.com/Relm4/icons.git" }
|
||||||
tracker = "0.2"
|
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" }
|
webkit = { package = "webkit6", version = "0.4" }
|
||||||
reqwest = "0.12"
|
reqwest = "0.12"
|
||||||
libpaket = { path = "../libpaket" }
|
libpaket = { path = "../libpaket" }
|
||||||
glycin = { version = "2.0.0-beta", features = ["gdk4"] }
|
glycin = { version = "2.0.0-beta", features = ["gdk4"] }
|
||||||
oo7 = { version = "0.3" }
|
oo7 = { version = "0.3" }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
gtk = { package = "gtk4", version = "0.9", features = ["v4_16"]}
|
gtk = { package = "gtk4", version = "0.9", features = ["v4_16"]}
|
248
paket/src/account.rs
Normal file
248
paket/src/account.rs
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
use adw::prelude::*;
|
||||||
|
use libpaket::{stammdaten::CustomerDataFull, LibraryError, LibraryResult};
|
||||||
|
use relm4::{Component, ComponentParts};
|
||||||
|
|
||||||
|
use crate::{send_log_out, 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),
|
||||||
|
LogOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = >k::ScrolledWindow {
|
||||||
|
set_propagate_natural_width: true,
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::Clamp {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_margin_start: 12,
|
||||||
|
set_margin_end: 12,
|
||||||
|
|
||||||
|
set_margin_top: 12,
|
||||||
|
set_margin_bottom: 12,
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_header_suffix = >k::Box {
|
||||||
|
add_css_class: relm4::css::LINKED,
|
||||||
|
|
||||||
|
append = >k::Button {
|
||||||
|
add_css_class: relm4::css::DESTRUCTIVE_ACTION,
|
||||||
|
set_label: "Log out",
|
||||||
|
|
||||||
|
connect_clicked => AccountInput::LogOut,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Postnumber
|
||||||
|
add = &adw::ActionRow {
|
||||||
|
add_css_class: relm4::css::NUMERIC,
|
||||||
|
set_subtitle: "Postnummer",
|
||||||
|
|
||||||
|
#[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(),
|
||||||
|
|
||||||
|
add_suffix = >k::Button {
|
||||||
|
set_vexpand: false,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AccountInput::LogOut => {
|
||||||
|
send_log_out();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
use libpaket::LibraryError;
|
use libpaket::LibraryError;
|
||||||
use relm4::{adw, gtk, gtk::gdk, gtk::gio, gtk::glib};
|
use relm4::{adw, gtk, gtk::gdk, gtk::gio};
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
|
|
@ -117,6 +117,7 @@ pub struct AdvicesView {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AdvicesViewInput {
|
pub enum AdvicesViewInput {
|
||||||
Fetch,
|
Fetch,
|
||||||
|
Reset,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -197,7 +198,13 @@ impl AsyncComponent for AdvicesView {
|
||||||
sender: AsyncComponentSender<Self>,
|
sender: AsyncComponentSender<Self>,
|
||||||
root: &Self::Root,
|
root: &Self::Root,
|
||||||
) {
|
) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
|
AdvicesViewInput::Reset => {
|
||||||
|
self.set_state(AdvicesViewState::Loading);
|
||||||
|
self.factory.guard().clear();
|
||||||
|
},
|
||||||
AdvicesViewInput::Fetch => {
|
AdvicesViewInput::Fetch => {
|
||||||
self.set_state(AdvicesViewState::Loading);
|
self.set_state(AdvicesViewState::Loading);
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ use relm4::{main_adw_application, prelude::*, AsyncComponentSender, RELM_THREADS
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum AppState {
|
enum AppState {
|
||||||
Loading,
|
Loading,
|
||||||
RequiresLogIn,
|
RequiresLogin,
|
||||||
FatalError,
|
|
||||||
Ready,
|
Ready,
|
||||||
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -20,13 +20,11 @@ struct AppError {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum AppInput {
|
enum AppInput {
|
||||||
ErrorOccoured(AppError),
|
AddBreakpoint(adw::Breakpoint),
|
||||||
FatalErrorOccoured(AppError),
|
|
||||||
SwitchToLogin,
|
SwitchToLogin,
|
||||||
SwitchToLoading,
|
SwitchToLoading,
|
||||||
SwitchToReady,
|
SwitchToReady,
|
||||||
NetworkFail,
|
FatalErr(AppError),
|
||||||
Notification(String, u32),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
|
@ -50,13 +48,6 @@ impl AsyncComponent for App {
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
main_window = adw::ApplicationWindow::new(&main_adw_application()) {
|
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_height: 600,
|
||||||
set_default_width: 800,
|
set_default_width: 800,
|
||||||
set_width_request: 300,
|
set_width_request: 300,
|
||||||
|
@ -64,92 +55,35 @@ impl AsyncComponent for App {
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_content = &adw::ViewStack {
|
set_content = &adw::ViewStack {
|
||||||
#[name = "page_prepare"]
|
#[name = "page_loading"]
|
||||||
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"]
|
|
||||||
add = &adw::Bin {
|
add = &adw::Bin {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::NavigationView {
|
set_child = &adw::Spinner {}
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
#[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: {
|
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
|
page
|
||||||
},
|
}
|
||||||
#[track(model.changed(App::state()) && model.state != AppState::Ready)]
|
|
||||||
set_visible_child: {
|
|
||||||
let page: &adw::ToolbarView = page_prepare.as_ref();
|
|
||||||
page
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -167,7 +101,7 @@ impl AsyncComponent for App {
|
||||||
.forward(sender.input_sender(), convert_ready_response);
|
.forward(sender.input_sender(), convert_ready_response);
|
||||||
|
|
||||||
let login = Login::builder()
|
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);
|
.forward(sender.input_sender(), convert_login_response);
|
||||||
|
|
||||||
let model = App {
|
let model = App {
|
||||||
|
@ -178,8 +112,8 @@ impl AsyncComponent for App {
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let ready_view_stack = model.ready.widget();
|
|
||||||
let page_login = model.login.widget();
|
let page_login = model.login.widget();
|
||||||
|
let page_ready = model.ready.widget();
|
||||||
|
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
@ -195,57 +129,23 @@ impl AsyncComponent for App {
|
||||||
) -> Self::Output {
|
) -> Self::Output {
|
||||||
self.reset();
|
self.reset();
|
||||||
match message {
|
match message {
|
||||||
AppInput::ErrorOccoured(error) => {
|
AppInput::AddBreakpoint(breakpoint) => {
|
||||||
let dialog: adw::AlertDialog = adw::AlertDialog::builder()
|
root.add_breakpoint(breakpoint);
|
||||||
.title(error.short)
|
},
|
||||||
.body(error.long)
|
|
||||||
.build();
|
|
||||||
dialog.present(Some(root));
|
|
||||||
}
|
|
||||||
AppInput::SwitchToLoading => {
|
AppInput::SwitchToLoading => {
|
||||||
self.set_state(AppState::Loading);
|
self.set_state(AppState::Loading);
|
||||||
}
|
}
|
||||||
AppInput::SwitchToLogin => {
|
AppInput::SwitchToLogin => {
|
||||||
self.set_state(AppState::RequiresLogIn);
|
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);
|
|
||||||
}
|
}
|
||||||
AppInput::SwitchToReady => {
|
AppInput::SwitchToReady => {
|
||||||
self.set_state(AppState::Ready);
|
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);
|
self.update_view(widgets, sender);
|
||||||
}
|
}
|
||||||
|
@ -255,31 +155,15 @@ fn convert_login_response(response: LoginOutput) -> AppInput {
|
||||||
match response {
|
match response {
|
||||||
LoginOutput::RequiresLogin => AppInput::SwitchToLogin,
|
LoginOutput::RequiresLogin => AppInput::SwitchToLogin,
|
||||||
LoginOutput::RequiresLoading => AppInput::SwitchToLoading,
|
LoginOutput::RequiresLoading => AppInput::SwitchToLoading,
|
||||||
LoginOutput::Error(err) => AppInput::ErrorOccoured(AppError {
|
LoginOutput::Error(library_error) => AppInput::FatalErr(AppError { short: "Unhandled API error".to_string(), long: library_error.to_string() }),
|
||||||
short: "An authorization error occured :(".to_string(),
|
LoginOutput::KeyringError(error) => AppInput::FatalErr(AppError { short: "Keyring usage failed".to_string(), long: error.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(),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
||||||
match response {
|
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::Ready => AppInput::SwitchToReady,
|
||||||
|
ReadyOutput::AddBreakpoint(breakpoint) => AppInput::AddBreakpoint(breakpoint),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod account;
|
||||||
pub mod advice;
|
pub mod advice;
|
||||||
pub mod advices;
|
pub mod advices;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
|
@ -6,3 +7,8 @@ pub mod ready;
|
||||||
pub mod tracking;
|
pub mod tracking;
|
||||||
|
|
||||||
pub use login::LoginSharedState;
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ static KEYRING: OnceLock<oo7::Keyring> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginInput {
|
pub enum LoginInput {
|
||||||
|
LogOut,
|
||||||
NeedsLogin,
|
NeedsLogin,
|
||||||
NeedsRefresh,
|
NeedsRefresh,
|
||||||
ReceivedAuthCode(String),
|
ReceivedAuthCode(String),
|
||||||
|
@ -66,7 +67,6 @@ pub struct Login {
|
||||||
pub enum LoginOutput {
|
pub enum LoginOutput {
|
||||||
RequiresLogin,
|
RequiresLogin,
|
||||||
RequiresLoading,
|
RequiresLoading,
|
||||||
NetworkFail,
|
|
||||||
Error(libpaket::LibraryError),
|
Error(libpaket::LibraryError),
|
||||||
KeyringError(oo7::Error),
|
KeyringError(oo7::Error),
|
||||||
}
|
}
|
||||||
|
@ -195,6 +195,41 @@ impl AsyncComponent for Login {
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
|
LoginInput::LogOut => {
|
||||||
|
sender.input(LoginInput::NeedsLogin);
|
||||||
|
{
|
||||||
|
let token = get_id_token(&self.shared_id_token).await;
|
||||||
|
if let Some(token) = token {
|
||||||
|
sender.command(|_, shutdown| {
|
||||||
|
shutdown
|
||||||
|
.register(async move {
|
||||||
|
let client = OpenIdClient::new();
|
||||||
|
let _ = client.logout(&token).await;
|
||||||
|
})
|
||||||
|
.drop_on_shutdown()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let token = self.shared_id_token.lock().await;
|
||||||
|
*token.write() = None;
|
||||||
|
}
|
||||||
|
if let Some(refresh_token) = self.refresh_token.clone() {
|
||||||
|
sender.command(|out, shutdown| {
|
||||||
|
shutdown
|
||||||
|
.register(async move {
|
||||||
|
let client = OpenIdClient::new();
|
||||||
|
let _ = client.revoke(&refresh_token).await;
|
||||||
|
})
|
||||||
|
.drop_on_shutdown()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.refresh_token = None;
|
||||||
|
let keyring = KEYRING.get().unwrap();
|
||||||
|
let _ = keyring
|
||||||
|
.delete(&HashMap::from([("app", crate::constants::APP_ID)]))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
LoginInput::NeedsRefresh => {
|
LoginInput::NeedsRefresh => {
|
||||||
let refresh_token = self.refresh_token.as_ref().unwrap().clone();
|
let refresh_token = self.refresh_token.as_ref().unwrap().clone();
|
||||||
sender.oneshot_command(async {
|
sender.oneshot_command(async {
|
||||||
|
@ -322,9 +357,7 @@ impl Login {
|
||||||
libpaket::LibraryError::InvalidArgument(_) => {
|
libpaket::LibraryError::InvalidArgument(_) => {
|
||||||
panic!("{}", res);
|
panic!("{}", res);
|
||||||
}
|
}
|
||||||
libpaket::LibraryError::NetworkFetch => {
|
libpaket::LibraryError::NetworkFetch => {}
|
||||||
sender.output(LoginOutput::NetworkFail).unwrap();
|
|
||||||
}
|
|
||||||
libpaket::LibraryError::DecodeError(_) => {
|
libpaket::LibraryError::DecodeError(_) => {
|
||||||
sender.output(LoginOutput::Error(res)).unwrap();
|
sender.output(LoginOutput::Error(res)).unwrap();
|
||||||
}
|
}
|
||||||
|
@ -346,7 +379,7 @@ macro_rules! received {
|
||||||
async fn $func_name$args -> LoginCommand {
|
async fn $func_name$args -> LoginCommand {
|
||||||
let client = OpenIdClient::new();
|
let client = OpenIdClient::new();
|
||||||
let mut err = LibraryError::NetworkFetch;
|
let mut err = LibraryError::NetworkFetch;
|
||||||
for _ in 0..6 {
|
while err == LibraryError::NetworkFetch {
|
||||||
let result: Result<TokenResponse, LibraryError> = client
|
let result: Result<TokenResponse, LibraryError> = client
|
||||||
.$calling$calling_args
|
.$calling$calling_args
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -1,57 +1,46 @@
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use libpaket::{
|
use relm4::prelude::*;
|
||||||
self,
|
|
||||||
tracking::{Shipment, TrackingParams},
|
|
||||||
LibraryError, LibraryResult,
|
|
||||||
};
|
|
||||||
use relm4::{adw, factory::FactoryHashMap, prelude::*};
|
|
||||||
|
|
||||||
use crate::advices::{AdvicesView, AdvicesViewInput};
|
use crate::{
|
||||||
|
account::{AccountOutput, AccountServices, AccountView},
|
||||||
|
advices::{AdvicesView, AdvicesViewInput},
|
||||||
|
tracking::{TrackingInput, TrackingOutput, TrackingView},
|
||||||
|
};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct Ready {
|
pub struct Ready {
|
||||||
#[do_not_track]
|
logged_in: bool,
|
||||||
login: crate::LoginSharedState,
|
|
||||||
activate: bool,
|
|
||||||
have_service_advices: bool,
|
have_service_advices: bool,
|
||||||
have_service_tracking: bool,
|
have_service_tracking: bool,
|
||||||
|
|
||||||
|
#[do_not_track]
|
||||||
|
account_component: Controller<AccountView>,
|
||||||
#[do_not_track]
|
#[do_not_track]
|
||||||
advices_component: AsyncController<AdvicesView>,
|
advices_component: AsyncController<AdvicesView>,
|
||||||
#[do_not_track]
|
#[do_not_track]
|
||||||
tracking_factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
|
tracking_component: Controller<TrackingView>,
|
||||||
|
#[do_not_track]
|
||||||
|
toast_overlay: adw::ToastOverlay,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ReadyOutput {
|
pub enum ReadyOutput {
|
||||||
Ready,
|
Ready,
|
||||||
Error(LibraryError),
|
AddBreakpoint(adw::Breakpoint),
|
||||||
FatalError(LibraryError),
|
|
||||||
Notification(String),
|
|
||||||
NoServicesEnabled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ReadyCmds {
|
pub enum ReadyCmds {
|
||||||
LoggedIn,
|
LoggedIn,
|
||||||
LoggedOut,
|
LoggedOut,
|
||||||
GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>),
|
|
||||||
GotTracking(LibraryResult<Vec<Shipment>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Services {
|
|
||||||
Advices,
|
|
||||||
SendungVerfolgung,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ReadyInput {
|
pub enum ReadyInput {
|
||||||
Activate,
|
LoggedIn,
|
||||||
Deactivate,
|
LoggedOut,
|
||||||
HaveService(Services),
|
HaveService(AccountServices),
|
||||||
ServiceBorked(Services),
|
ServiceBorked(AccountServices),
|
||||||
SearchTracking(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(pub)]
|
#[relm4::component(pub)]
|
||||||
|
@ -63,61 +52,73 @@ impl Component for Ready {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
adw::ViewStack {
|
adw::Bin {
|
||||||
add = &model.advices_component.widget().clone() -> gtk::ScrolledWindow {
|
#[wrap(Some)]
|
||||||
#[track(model.changed_have_service_advices())]
|
set_child = &adw::NavigationView {
|
||||||
set_visible: model.have_service_advices,
|
add = &adw::NavigationPage {
|
||||||
|
set_title: "",
|
||||||
|
|
||||||
} -> 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 {
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = >k::Box {
|
set_child = &adw::ToolbarView {
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
add_top_bar = ready_headerbar = &adw::HeaderBar {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_title_widget = ready_switchertop = &adw::ViewSwitcher{
|
||||||
|
set_policy: adw::ViewSwitcherPolicy::Wide,
|
||||||
|
set_stack: Some(&ready_view_stack),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
gtk::Box {
|
#[wrap(Some)]
|
||||||
set_orientation: gtk::Orientation::Horizontal,
|
set_content = &model.toast_overlay.clone() -> adw::ToastOverlay {
|
||||||
set_margin_all: 8,
|
#[wrap(Some)]
|
||||||
add_css_class: relm4::css::TOOLBAR,
|
#[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"]
|
} -> page_advices: adw::ViewStackPage {
|
||||||
gtk::Entry {
|
set_title: Some("Mail notification"),
|
||||||
set_input_hints: gtk::InputHints::PRIVATE,
|
set_name: Some("page_advices"),
|
||||||
set_hexpand: true,
|
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,
|
||||||
|
},
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
#[name = "tracking_entry_button"]
|
|
||||||
gtk::Button {}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
#[local_ref]
|
add_bottom_bar = ready_switcherbar = &adw::ViewSwitcherBar {
|
||||||
tracking_box -> gtk::Box {
|
set_stack: Some(&ready_view_stack),
|
||||||
set_spacing: 8
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
} -> 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(
|
fn init(
|
||||||
|
@ -125,55 +126,52 @@ impl Component for Ready {
|
||||||
root: Self::Root,
|
root: Self::Root,
|
||||||
sender: ComponentSender<Self>,
|
sender: ComponentSender<Self>,
|
||||||
) -> ComponentParts<Self> {
|
) -> ComponentParts<Self> {
|
||||||
let tracking_factory = FactoryHashMap::builder().launch_default().detach();
|
|
||||||
let advices_component = AdvicesView::builder().launch(init.clone()).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 account_component = AccountView::builder()
|
||||||
|
.launch(init.clone())
|
||||||
|
.forward(&sender.input_sender(), convert_account_output);
|
||||||
|
|
||||||
|
let toast_overlay = adw::ToastOverlay::new();
|
||||||
|
|
||||||
let model = Ready {
|
let model = Ready {
|
||||||
have_service_advices: false,
|
have_service_advices: false,
|
||||||
have_service_tracking: true,
|
have_service_tracking: true,
|
||||||
login: init.clone(),
|
logged_in: false,
|
||||||
activate: false,
|
|
||||||
tracking_factory,
|
account_component,
|
||||||
advices_component,
|
advices_component,
|
||||||
|
tracking_component,
|
||||||
|
toast_overlay,
|
||||||
|
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let tracking_box = model.tracking_factory.widget();
|
let breakpoint = adw::Breakpoint::new(adw::BreakpointCondition::new_length(
|
||||||
|
adw::BreakpointConditionLengthType::MaxWidth,
|
||||||
|
550.0,
|
||||||
|
adw::LengthUnit::Sp,
|
||||||
|
));
|
||||||
|
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
{
|
|
||||||
let login = model.login.clone();
|
breakpoint.add_setter(
|
||||||
sender.command(move |out, shutdown| {
|
widgets.ready_headerbar.widget_ref(),
|
||||||
shutdown
|
"show-title",
|
||||||
.register(async move {
|
Some(>k::glib::Value::from(false)),
|
||||||
let login = { login.clone().as_ref().lock().await.clone() };
|
);
|
||||||
let (sender, receiver) = relm4::channel::<ReadyCmds>();
|
breakpoint.add_setter(
|
||||||
login.subscribe(&sender, |model| match model {
|
widgets.ready_switcherbar.widget_ref(),
|
||||||
Some(_) => ReadyCmds::LoggedIn,
|
"reveal",
|
||||||
None => ReadyCmds::LoggedOut,
|
Some(>k::glib::Value::from(true)),
|
||||||
});
|
);
|
||||||
loop {
|
sender
|
||||||
out.send(receiver.recv().await.unwrap()).unwrap();
|
.output(ReadyOutput::AddBreakpoint(breakpoint))
|
||||||
}
|
.unwrap();
|
||||||
})
|
|
||||||
.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 }
|
||||||
}
|
}
|
||||||
|
@ -182,167 +180,65 @@ impl Component for Ready {
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
ReadyInput::Activate => {
|
ReadyInput::LoggedIn => {
|
||||||
self.set_activate(true);
|
self.set_logged_in(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();
|
|
||||||
ReadyCmds::GotCustomerDataFull(client.customer_data_full(&token).await)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ReadyInput::SearchTracking(value) => {
|
ReadyInput::LoggedOut => {
|
||||||
sender.oneshot_command(async move {
|
self.set_logged_in(false);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
ReadyInput::HaveService(service) => match service {
|
ReadyInput::HaveService(service) => match service {
|
||||||
Services::Advices => {
|
AccountServices::Advices => {
|
||||||
self.set_have_service_advices(true);
|
self.set_have_service_advices(true);
|
||||||
self.advices_component.emit(AdvicesViewInput::Fetch);
|
self.advices_component.emit(AdvicesViewInput::Fetch);
|
||||||
}
|
}
|
||||||
Services::SendungVerfolgung => {
|
AccountServices::SendungVerfolgung => {
|
||||||
self.set_have_service_tracking(true);
|
self.set_have_service_tracking(true);
|
||||||
let token = self.login.clone();
|
self.tracking_component.emit(TrackingInput::Search(None))
|
||||||
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::ServiceBorked(service) => match service {
|
ReadyInput::ServiceBorked(service) => match service {
|
||||||
Services::Advices => self.set_have_service_advices(false),
|
AccountServices::Advices => {
|
||||||
Services::SendungVerfolgung => self.set_have_service_tracking(false),
|
self.toast_overlay.add_toast(
|
||||||
|
adw::Toast::builder()
|
||||||
|
.title("Service borked: Mail notifications")
|
||||||
|
.timeout(30)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
self.set_have_service_advices(false);
|
||||||
|
}
|
||||||
|
AccountServices::SendungVerfolgung => {
|
||||||
|
self.toast_overlay.add_toast(
|
||||||
|
adw::Toast::builder()
|
||||||
|
.title("Service borked: Shipment tracking")
|
||||||
|
.timeout(30)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
self.set_have_service_tracking(false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
fn update_cmd(
|
if self.changed_logged_in() {
|
||||||
&mut self,
|
if self.logged_in {
|
||||||
message: Self::CommandOutput,
|
sender.output(ReadyOutput::Ready).unwrap();
|
||||||
sender: ComponentSender<Self>,
|
} else {
|
||||||
_: &Self::Root,
|
self.advices_component.emit(AdvicesViewInput::Reset);
|
||||||
) {
|
self.tracking_component.emit(TrackingInput::Reset);
|
||||||
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) => 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_tracking_output(value: TrackingOutput) -> ReadyInput {
|
||||||
|
match value {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,236 @@
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use libpaket::tracking::Shipment;
|
use libpaket::tracking::{Shipment, TrackingParams};
|
||||||
use relm4::factory::FactoryComponent;
|
use libpaket::{LibraryError, LibraryResult};
|
||||||
|
use relm4::factory::{FactoryComponent, FactoryHashMap};
|
||||||
use relm4::prelude::*;
|
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),
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
set_propagate_natural_width: true,
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::Clamp {
|
||||||
|
set_maximum_size: 1800,
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_margin_start: 6,
|
||||||
|
set_margin_end: 6,
|
||||||
|
|
||||||
|
set_margin_bottom: 12,
|
||||||
|
|
||||||
|
adw::Clamp {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_margin_all: 2,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
|
||||||
|
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::Reset => {
|
||||||
|
self.factory.clear();
|
||||||
|
}
|
||||||
|
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: Shipment,
|
||||||
|
|
||||||
// model abstraction
|
// model abstraction
|
||||||
|
@ -14,7 +240,7 @@ pub struct ShipmentView {
|
||||||
expanded: bool,
|
expanded: bool,
|
||||||
|
|
||||||
// workarounds
|
// workarounds
|
||||||
list_box_history: gtk::ListBox,
|
list_box_history: gtk::Box,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -22,7 +248,7 @@ pub enum ViewInput {
|
||||||
ToggleExpand,
|
ToggleExpand,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::factory(pub)]
|
#[relm4::factory]
|
||||||
impl FactoryComponent for ShipmentView {
|
impl FactoryComponent for ShipmentView {
|
||||||
type CommandOutput = ();
|
type CommandOutput = ();
|
||||||
type Init = Shipment;
|
type Init = Shipment;
|
||||||
|
@ -38,15 +264,7 @@ impl FactoryComponent for ShipmentView {
|
||||||
set_hexpand: true,
|
set_hexpand: true,
|
||||||
set_margin_all: 8,
|
set_margin_all: 8,
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
inline_css: "border-radius: 12px 12px 0px 0px",
|
||||||
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),
|
|
||||||
},
|
|
||||||
|
|
||||||
// title box
|
// title box
|
||||||
gtk::Box {
|
gtk::Box {
|
||||||
|
@ -70,6 +288,7 @@ impl FactoryComponent for ShipmentView {
|
||||||
gtk::Label {
|
gtk::Label {
|
||||||
add_css_class: relm4::css::HEADING,
|
add_css_class: relm4::css::HEADING,
|
||||||
set_halign: gtk::Align::Start,
|
set_halign: gtk::Align::Start,
|
||||||
|
set_wrap: true,
|
||||||
|
|
||||||
set_label: {
|
set_label: {
|
||||||
// TODO: gettext
|
// TODO: gettext
|
||||||
|
@ -106,27 +325,29 @@ impl FactoryComponent for ShipmentView {
|
||||||
}
|
}
|
||||||
}, // title box end
|
}, // 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 {
|
gtk::Revealer {
|
||||||
#[watch]
|
#[watch]
|
||||||
set_reveal_child: self.expanded,
|
set_reveal_child: self.expanded,
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = >k::Box {
|
set_child = >k::Box {
|
||||||
|
set_margin_all: 8,
|
||||||
|
|
||||||
// history viewstack
|
// history viewstack
|
||||||
adw::StatusPage {
|
gtk::Label {
|
||||||
set_visible: !self.have_events,
|
set_visible: !self.have_events,
|
||||||
|
|
||||||
set_title: "No events",
|
set_label: "No events",
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
append = &self.list_box_history.clone() {
|
append = &self.list_box_history.clone() {
|
||||||
set_visible: self.have_events,
|
set_visible: self.have_events,
|
||||||
|
|
||||||
add_css_class: relm4::css::BOXED_LIST,
|
|
||||||
|
|
||||||
set_selection_mode: gtk::SelectionMode::None,
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -142,7 +363,10 @@ impl FactoryComponent for ShipmentView {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let have_events = init.history.events.as_ref().is_some_and(|a| a.len() > 0);
|
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 {
|
let _self = ShipmentView {
|
||||||
have_events,
|
have_events,
|
||||||
|
@ -169,8 +393,6 @@ impl FactoryComponent for ShipmentView {
|
||||||
|
|
||||||
let boxie = gtk::Box::builder()
|
let boxie = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Horizontal)
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
.margin_start(8)
|
|
||||||
.margin_end(8)
|
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue