Compare commits
No commits in common. "c99956583baf2f58712c8fe4fd72a62aa10b985a" and "6d68c6500b8226e53b46abf30dcd12dd8773818c" have entirely different histories.
c99956583b
...
6d68c6500b
17 changed files with 330 additions and 1656 deletions
747
Cargo.lock
generated
747
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
@ -6,19 +6,9 @@ members = [
|
|||
"paket",
|
||||
]
|
||||
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Jane Rachinger <libpaket@j4ne.de>"]
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
version = "0.1.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
secrecy = { version = "0.10", features = ["serde"] }
|
||||
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "http2"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
relm4 = { git = "https://github.com/Relm4/Relm4.git", features = [
|
||||
"libadwaita",
|
||||
"macros",
|
||||
] }
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
app_id = "de.j4ne.Paket"
|
||||
|
||||
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy", "qr-code-scanner"]
|
||||
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy"]
|
|
@ -14,10 +14,10 @@ num_enum = { version = "0.7", optional = true }
|
|||
# TODO: Consolidate?
|
||||
rand = "0.8.5"
|
||||
random-string = "1.1.0"
|
||||
reqwest = { workspace = true }
|
||||
secrecy = { workspace = true}
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "http2"] }
|
||||
secrecy = { version = "0.8.0", features = ["serde"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
serde_repr = { version = "0.1.18", optional = true }
|
||||
serde_ignored = "0.1"
|
||||
url = "2.5.0"
|
||||
|
@ -28,8 +28,8 @@ base64 = "0.22"
|
|||
# sha2 also used in briefankuendigung and packstation_register_regtoken
|
||||
sha2 = "0.10.8"
|
||||
urlencoding = "2.1.3"
|
||||
uuid = { workspace = true, features = ["serde"], optional = true }
|
||||
|
||||
uuid = { version = "1.7.0", features = ["v4", "serde"], optional = true }
|
||||
serde_newtype = "0.1.1"
|
||||
thiserror = "1.0.56"
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ pub enum LibraryError {
|
|||
Unauthorized,
|
||||
#[error("invalid argument: {0}")]
|
||||
InvalidArgument(String),
|
||||
#[error("unable to decode: {0}")]
|
||||
#[error("internal error, unable to decode: {0}")]
|
||||
DecodeError(String),
|
||||
#[error("upstream api was changed. not continuing")]
|
||||
APIChange,
|
||||
|
|
|
@ -1,53 +1,41 @@
|
|||
use base64::{engine::general_purpose, Engine as _};
|
||||
use ed25519_dalek::Signer;
|
||||
use ed25519_dalek::SigningKey;
|
||||
use rand::prelude::*;
|
||||
use secrecy::zeroize::Zeroize;
|
||||
use secrecy::ExposeSecret;
|
||||
use secrecy::SecretBox;
|
||||
use uuid::Uuid;
|
||||
use ed25519_dalek::SigningKey;
|
||||
use ed25519_dalek::Signer;
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CustomerKeySeed {
|
||||
pub postnumber: String,
|
||||
pub seed: SecretBox<Seed>,
|
||||
pub seed: Seed,
|
||||
pub uuid: Uuid,
|
||||
pub device_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Zeroize for CustomerKeySeed {
|
||||
fn zeroize(&mut self) {
|
||||
self.postnumber.zeroize();
|
||||
self.seed.zeroize();
|
||||
self.uuid.into_bytes().zeroize();
|
||||
self.device_id.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Seed(Vec<u8>);
|
||||
impl Zeroize for Seed {
|
||||
fn zeroize(&mut self) {
|
||||
self.0.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Seed {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
assert_eq!(value.len(), 32);
|
||||
Seed(value)
|
||||
}
|
||||
pub struct Seed {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Seed {
|
||||
pub(self) fn from_bytes(bytes: Vec<u8>) -> Self {
|
||||
assert_eq!(bytes.len(), 32);
|
||||
Seed {
|
||||
bytes: bytes
|
||||
}
|
||||
}
|
||||
|
||||
pub fn random() -> Self {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let mut bytes: Vec<u8> = vec![0; 32];
|
||||
|
||||
rng.fill_bytes(bytes.as_mut_slice());
|
||||
Seed (bytes)
|
||||
|
||||
Seed { bytes: bytes }
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
(&self.0[..]).try_into().unwrap()
|
||||
(&self.bytes[..]).try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,7 +43,7 @@ impl CustomerKeySeed {
|
|||
pub fn new(postnumber: String) -> Self {
|
||||
CustomerKeySeed {
|
||||
postnumber,
|
||||
seed: SecretBox::new(Box::new(Seed::random())),
|
||||
seed: Seed::random(),
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
device_id: None,
|
||||
}
|
||||
|
@ -64,7 +52,7 @@ impl CustomerKeySeed {
|
|||
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid, device_id: String) -> Self {
|
||||
CustomerKeySeed {
|
||||
postnumber: postnumber.clone(),
|
||||
seed: SecretBox::new(Box::new(Seed::from(seed))),
|
||||
seed: Seed::from_bytes(seed),
|
||||
uuid: uuid.clone(),
|
||||
device_id: Some(device_id),
|
||||
}
|
||||
|
@ -77,15 +65,17 @@ impl CustomerKeySeed {
|
|||
}
|
||||
|
||||
pub(crate) fn sign(&self, message: &[u8]) -> String {
|
||||
let signing_key = SigningKey::from_bytes(self.seed.expose_secret().as_bytes());
|
||||
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());
|
||||
|
||||
let signature = signing_key.sign(message);
|
||||
|
||||
let sig_str = general_purpose::STANDARD.encode(signature.to_bytes());
|
||||
|
||||
format!("{}.{}", self.uuid.to_string(), sig_str)
|
||||
}
|
||||
|
||||
pub(crate) fn get_public_key(&self) -> String {
|
||||
let signing_key = SigningKey::from_bytes(self.seed.expose_secret().as_bytes());
|
||||
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());
|
||||
let public_bytes = signing_key.verifying_key().to_bytes();
|
||||
let public_str = general_purpose::STANDARD.encode(public_bytes);
|
||||
|
||||
|
|
|
@ -4,11 +4,8 @@ use sha2::Sha256;
|
|||
|
||||
use crate::{LibraryResult, LibraryError};
|
||||
|
||||
pub struct RegToken(Vec<u8>);
|
||||
impl secrecy::zeroize::Zeroize for RegToken {
|
||||
fn zeroize(&mut self) {
|
||||
self.0.zeroize();
|
||||
}
|
||||
pub struct RegToken {
|
||||
token: Vec<u8>,
|
||||
}
|
||||
|
||||
impl RegToken {
|
||||
|
@ -35,15 +32,15 @@ impl RegToken {
|
|||
token.extend(vec![0; 32 - token.len()].iter())
|
||||
}
|
||||
|
||||
Ok(RegToken(token))
|
||||
Ok(RegToken { token })
|
||||
}
|
||||
|
||||
pub fn customer_password(&self) -> String {
|
||||
general_purpose::STANDARD.encode(&self.0[32..])
|
||||
general_purpose::STANDARD.encode(&self.token[32..])
|
||||
}
|
||||
|
||||
pub fn hmac(&self) -> SimpleHmac<Sha256> {
|
||||
let mac = SimpleHmac::<Sha256>::new_from_slice(&self.0[0..32])
|
||||
let mac = SimpleHmac::<Sha256>::new_from_slice(&self.token[0..32])
|
||||
.expect("HMAC can take key of any size");
|
||||
|
||||
mac
|
||||
|
|
|
@ -7,19 +7,14 @@ version.workspace = true
|
|||
|
||||
[dependencies]
|
||||
# using git version, for https://github.com/Relm4/Relm4/pull/677
|
||||
relm4 = { workspace = true }
|
||||
relm4-icons = { version = "0.9" }
|
||||
relm4 = { version = "0.9", features = [ "libadwaita", "macros" ], 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_6"] }
|
||||
aperture = "0.7"
|
||||
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"] }
|
||||
reqwest = { workspace = true }
|
||||
secrecy = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
gtk = { package = "gtk4", version = "0.9", features = ["v4_16"]}
|
|
@ -37,7 +37,6 @@ pub enum AccountInput {
|
|||
pub enum AccountServices {
|
||||
Advices,
|
||||
SendungVerfolgung,
|
||||
PackstationAvailable,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -207,26 +206,9 @@ impl Component for AccountView {
|
|||
}
|
||||
AccountCmd::GotCustomerDataFull(data) => match data {
|
||||
Ok(data) => {
|
||||
if let Some(services) = &data.requested_services {
|
||||
for service in services {
|
||||
match service {
|
||||
libpaket::stammdaten::CustomerDataService::Packstation => {
|
||||
sender.output(AccountOutput::HaveService(
|
||||
AccountServices::PackstationAvailable,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
for service in &data.common.services {
|
||||
match service {
|
||||
libpaket::stammdaten::CustomerDataService::Packstation => sender
|
||||
.output(AccountOutput::HaveService(
|
||||
AccountServices::PackstationAvailable,
|
||||
))
|
||||
.unwrap(),
|
||||
libpaket::stammdaten::CustomerDataService::Packstation => (),
|
||||
libpaket::stammdaten::CustomerDataService::Paketankuendigung => sender
|
||||
.output(AccountOutput::HaveService(
|
||||
AccountServices::SendungVerfolgung,
|
||||
|
|
|
@ -56,41 +56,31 @@ impl AsyncComponent for App {
|
|||
#[wrap(Some)]
|
||||
set_content = &adw::ViewStack {
|
||||
#[name = "page_loading"]
|
||||
add = &adw::ToolbarView {
|
||||
add_top_bar = &adw::HeaderBar {},
|
||||
|
||||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_content = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::Spinner {}
|
||||
}
|
||||
set_child = &adw::Spinner {}
|
||||
},
|
||||
|
||||
#[local_ref]
|
||||
add = page_login -> adw::Bin {},
|
||||
|
||||
|
||||
#[local_ref]
|
||||
add = page_ready -> adw::NavigationView {},
|
||||
add = page_ready -> adw::Bin {},
|
||||
|
||||
#[name = "page_error"]
|
||||
add = &adw::ToolbarView {
|
||||
add_top_bar = &adw::HeaderBar {},
|
||||
|
||||
add = &adw::Bin {
|
||||
#[name = "page_error_status"]
|
||||
#[wrap(Some)]
|
||||
set_content = &adw::Bin {
|
||||
#[name = "page_error_status"]
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::StatusPage {}
|
||||
}
|
||||
set_child = &adw::StatusPage {}
|
||||
},
|
||||
|
||||
#[track(model.changed(App::state()))]
|
||||
set_visible_child: {
|
||||
let page = match model.state {
|
||||
AppState::Loading => page_loading.widget_ref(),
|
||||
AppState::RequiresLogin => page_login.widget_ref(),
|
||||
AppState::Ready => page_ready.widget_ref(),
|
||||
AppState::Error => page_error.widget_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
|
||||
}
|
||||
|
@ -140,8 +130,8 @@ impl AsyncComponent for App {
|
|||
self.reset();
|
||||
match message {
|
||||
AppInput::AddBreakpoint(breakpoint) => {
|
||||
root.add_breakpoint(breakpoint);
|
||||
}
|
||||
root.add_breakpoint(breakpoint);
|
||||
},
|
||||
AppInput::SwitchToLoading => {
|
||||
self.set_state(AppState::Loading);
|
||||
}
|
||||
|
@ -165,14 +155,8 @@ fn convert_login_response(response: LoginOutput) -> AppInput {
|
|||
match response {
|
||||
LoginOutput::RequiresLogin => AppInput::SwitchToLogin,
|
||||
LoginOutput::RequiresLoading => AppInput::SwitchToLoading,
|
||||
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(),
|
||||
}),
|
||||
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() }),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,7 +169,7 @@ fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
|||
|
||||
fn main() {
|
||||
RELM_THREADS.set(4).unwrap();
|
||||
aperture::init(paket::constants::APP_ID);
|
||||
gtk::init().unwrap();
|
||||
let display = gtk::gdk::Display::default().unwrap();
|
||||
let theme = gtk::IconTheme::for_display(&display);
|
||||
theme.add_resource_path("/de/j4ne/Paket/icons/");
|
||||
|
|
|
@ -1,181 +0,0 @@
|
|||
use std::{str::FromStr as _, sync::OnceLock};
|
||||
|
||||
use gtk::glib;
|
||||
use libpaket::{locker::crypto::CustomerKeySeed, login::RefreshToken, LibraryError};
|
||||
use secrecy::{zeroize::Zeroize, ExposeSecret, SecretBox};
|
||||
|
||||
pub static KEYRING: OnceLock<oo7::Keyring> = OnceLock::new();
|
||||
|
||||
fn get_keyring_base_attribute() -> (&'static str, &'static str) {
|
||||
("app", crate::constants::APP_ID)
|
||||
}
|
||||
|
||||
fn get_keyring_attributes_refresh_token() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
get_keyring_base_attribute(),
|
||||
("type", "refresh_token"),
|
||||
("version", "1"),
|
||||
]
|
||||
}
|
||||
|
||||
fn get_keyring_attributes_packstation() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
get_keyring_base_attribute(),
|
||||
("type", "packstation-gerät-secret"),
|
||||
("version", "1"),
|
||||
]
|
||||
}
|
||||
|
||||
pub async fn keyring_delete_all_items() {
|
||||
let keyring = get_keyring();
|
||||
|
||||
let attr1 = get_keyring_attributes_refresh_token();
|
||||
let attr2 = get_keyring_attributes_packstation();
|
||||
|
||||
let _ = futures::join!(keyring.delete(&attr1), keyring.delete(&attr2),);
|
||||
}
|
||||
|
||||
fn get_keyring<'a>() -> &'a oo7::Keyring {
|
||||
KEYRING.get().unwrap()
|
||||
}
|
||||
|
||||
pub async fn keyring_get_refresh_token() -> oo7::Result<Option<RefreshToken>> {
|
||||
let items = get_keyring()
|
||||
.search_items(&get_keyring_attributes_refresh_token())
|
||||
.await?;
|
||||
|
||||
if let Some(item) = items.get(0) {
|
||||
if item.is_locked().await? {
|
||||
item.unlock().await?;
|
||||
}
|
||||
let data = item.secret().await.unwrap();
|
||||
Ok(Some(
|
||||
RefreshToken::new(String::from_utf8(data.to_vec()).unwrap()).unwrap(),
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn keyring_set_refresh_token(value: String) -> oo7::Result<()> {
|
||||
get_keyring()
|
||||
.create_item(
|
||||
"Paket: Login credentials",
|
||||
&get_keyring_attributes_refresh_token(),
|
||||
value,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
struct PackstationSecrets {
|
||||
postnumber: String,
|
||||
seed: String,
|
||||
uuid: String,
|
||||
device_id: String,
|
||||
}
|
||||
|
||||
impl secrecy::SerializableSecret for PackstationSecrets {}
|
||||
|
||||
impl Zeroize for PackstationSecrets {
|
||||
fn zeroize(&mut self) {
|
||||
self.device_id.zeroize();
|
||||
self.postnumber.zeroize();
|
||||
self.seed.zeroize();
|
||||
self.uuid.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum KeyringError {
|
||||
OO7(oo7::Error),
|
||||
Libpaket(LibraryError),
|
||||
SerdeJson(serde_json::Error),
|
||||
Uuid(uuid::Error),
|
||||
}
|
||||
|
||||
type KeyringResult<T> = Result<T, KeyringError>;
|
||||
|
||||
impl From<oo7::Error> for KeyringError {
|
||||
fn from(value: oo7::Error) -> Self {
|
||||
KeyringError::OO7(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LibraryError> for KeyringError {
|
||||
fn from(value: LibraryError) -> Self {
|
||||
KeyringError::Libpaket(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for KeyringError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
Self::SerdeJson(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Error> for KeyringError {
|
||||
fn from(value: uuid::Error) -> Self {
|
||||
KeyringError::Uuid(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn keyring_get_packstation() -> KeyringResult<Option<CustomerKeySeed>> {
|
||||
let items = get_keyring()
|
||||
.search_items(&get_keyring_attributes_packstation())
|
||||
.await?;
|
||||
|
||||
if let Some(item) = items.get(0) {
|
||||
if item.is_locked().await? {
|
||||
item.unlock().await?;
|
||||
}
|
||||
let data = item.secret().await.unwrap();
|
||||
let data = serde_json::from_slice::<SecretBox<PackstationSecrets>>(data.as_slice())?;
|
||||
let data = data.expose_secret();
|
||||
|
||||
let uuid = uuid::Uuid::from_str(data.uuid.as_str())?;
|
||||
let seed = glib::base64_decode(&data.seed);
|
||||
Ok(Some(CustomerKeySeed::from(
|
||||
&data.postnumber,
|
||||
seed,
|
||||
&uuid,
|
||||
data.device_id.clone(),
|
||||
)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn keyring_set_packstation(data: &CustomerKeySeed) -> KeyringResult<()> {
|
||||
if data.device_id.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let seed = secrecy::SecretString::from(Into::<String>::into(glib::base64_encode(
|
||||
data.seed.expose_secret().as_bytes(),
|
||||
)));
|
||||
let uuid = data.uuid.to_string();
|
||||
let device_id = data.device_id.as_ref().unwrap().to_string();
|
||||
|
||||
let secret = SecretBox::new(Box::new(PackstationSecrets {
|
||||
postnumber: data.postnumber.clone(),
|
||||
seed: seed.expose_secret().to_string(),
|
||||
uuid,
|
||||
device_id,
|
||||
}));
|
||||
|
||||
let string = secrecy::SecretString::from(serde_json::to_string_pretty(&secret)?);
|
||||
|
||||
Ok(get_keyring()
|
||||
.create_item(
|
||||
"Paket: Device keys",
|
||||
&get_keyring_attributes_packstation(),
|
||||
string.expose_secret(),
|
||||
true,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn keyring_is_available() -> bool {
|
||||
KEYRING.get().is_some()
|
||||
}
|
|
@ -2,11 +2,8 @@ pub mod account;
|
|||
pub mod advice;
|
||||
pub mod advices;
|
||||
pub mod constants;
|
||||
pub mod keyring;
|
||||
pub mod login;
|
||||
pub mod packstation;
|
||||
pub mod ready;
|
||||
pub mod scanner;
|
||||
pub mod tracking;
|
||||
|
||||
pub use login::LoginSharedState;
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
use std::{cell::RefCell, collections::HashMap, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use adw::prelude::*;
|
||||
use libpaket::{
|
||||
|
@ -12,7 +17,7 @@ use relm4::{
|
|||
};
|
||||
use webkit::{prelude::WebViewExt, URIRequest, WebContext, WebView};
|
||||
|
||||
use crate::keyring::{keyring_get_refresh_token, keyring_is_available, keyring_set_refresh_token};
|
||||
static KEYRING: OnceLock<oo7::Keyring> = OnceLock::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginInput {
|
||||
|
@ -74,19 +79,8 @@ pub enum LoginCommand {
|
|||
NeedsRefresh,
|
||||
}
|
||||
|
||||
macro_rules! keyring_result_get {
|
||||
($sender: ident, $caller: expr, $code: expr) => {{
|
||||
let res = $caller;
|
||||
|
||||
match res {
|
||||
Ok(value) => Ok($code(value)),
|
||||
Err(err) => {
|
||||
$sender.output(LoginOutput::KeyringError(err)).unwrap();
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
const KEYRING_ATTRIBUTES: [(&str, &str); 2] =
|
||||
[("app", crate::constants::APP_ID), ("type", "refresh_token")];
|
||||
|
||||
#[relm4::component(async, pub)]
|
||||
impl AsyncComponent for Login {
|
||||
|
@ -181,24 +175,47 @@ impl AsyncComponent for Login {
|
|||
tracker: 0,
|
||||
};
|
||||
|
||||
let _ = keyring_result_get!(sender, oo7::Keyring::new().await, |keyring| {
|
||||
crate::keyring::KEYRING.set(keyring).unwrap();
|
||||
});
|
||||
|
||||
if keyring_is_available() {
|
||||
let refresh_token =
|
||||
keyring_result_get!(sender, keyring_get_refresh_token().await, move |value| {
|
||||
return value;
|
||||
});
|
||||
if let Ok(value) = refresh_token {
|
||||
model.refresh_token = value;
|
||||
if model.refresh_token.is_some() {
|
||||
sender.input(LoginInput::NeedsRefresh);
|
||||
let result = oo7::Keyring::new().await;
|
||||
match result {
|
||||
Ok(keyring) => {
|
||||
KEYRING.set(keyring).unwrap();
|
||||
if let Err(err) = KEYRING.get().unwrap().unlock().await {
|
||||
sender
|
||||
.output(LoginOutput::KeyringError(err))
|
||||
.expect("sender not worky");
|
||||
} else {
|
||||
sender.input(LoginInput::NeedsLogin);
|
||||
let keyring = KEYRING.get().unwrap();
|
||||
match keyring
|
||||
.search_items(&HashMap::from(KEYRING_ATTRIBUTES))
|
||||
.await
|
||||
{
|
||||
Ok(res) => {
|
||||
if res.len() > 0 {
|
||||
let item = &res[0];
|
||||
let refresh_token = item.secret().await.unwrap();
|
||||
let refresh_token =
|
||||
std::str::from_utf8(refresh_token.as_slice()).unwrap();
|
||||
model.refresh_token =
|
||||
Some(RefreshToken::new(refresh_token.to_string()).unwrap());
|
||||
sender.input(LoginInput::NeedsRefresh);
|
||||
} else {
|
||||
sender.input(LoginInput::NeedsLogin);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
sender
|
||||
.output(LoginOutput::KeyringError(err))
|
||||
.expect("sender not worky");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
sender
|
||||
.output(LoginOutput::KeyringError(err))
|
||||
.expect("sender not worky");
|
||||
}
|
||||
};
|
||||
|
||||
let webcontext = WebContext::builder().build();
|
||||
{
|
||||
|
@ -256,7 +273,7 @@ impl AsyncComponent for Login {
|
|||
*token.write() = None;
|
||||
}
|
||||
if let Some(refresh_token) = self.refresh_token.clone() {
|
||||
sender.command(|_, shutdown| {
|
||||
sender.command(|out, shutdown| {
|
||||
shutdown
|
||||
.register(async move {
|
||||
let client = OpenIdClient::new();
|
||||
|
@ -266,7 +283,10 @@ impl AsyncComponent for Login {
|
|||
});
|
||||
}
|
||||
self.refresh_token = None;
|
||||
let _ = crate::keyring::keyring_delete_all_items().await;
|
||||
let keyring = KEYRING.get().unwrap();
|
||||
let _ = keyring
|
||||
.delete(&HashMap::from([("app", crate::constants::APP_ID)]))
|
||||
.await;
|
||||
}
|
||||
LoginInput::NeedsRefresh => {
|
||||
let refresh_token = self.refresh_token.as_ref().unwrap().clone();
|
||||
|
@ -368,21 +388,27 @@ impl Login {
|
|||
.drop_on_shutdown()
|
||||
});
|
||||
}
|
||||
|
||||
self.refresh_token = Some(res.refresh_token);
|
||||
let future = async {
|
||||
self.refresh_token = Some(res.refresh_token);
|
||||
let keyring = KEYRING.get().unwrap();
|
||||
keyring
|
||||
.create_item(
|
||||
"Refresh Token",
|
||||
&HashMap::from(KEYRING_ATTRIBUTES),
|
||||
self.refresh_token.as_ref().unwrap().to_string(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
if !res.id_token.is_expired() {
|
||||
let _ = keyring_result_get!(
|
||||
sender,
|
||||
keyring_set_refresh_token(self.refresh_token.as_ref().unwrap().to_string())
|
||||
.await,
|
||||
|_| {}
|
||||
);
|
||||
let credentials_model = self.shared_id_token.lock().await;
|
||||
let mut credentials_model = credentials_model.write();
|
||||
|
||||
*credentials_model = Some(res.id_token);
|
||||
}
|
||||
future.await;
|
||||
}
|
||||
Err(res) => {
|
||||
// We disarm the webkit flow/aka breaking the application. We want to reduce invalid requests
|
||||
|
|
|
@ -1,294 +0,0 @@
|
|||
use libpaket::locker::crypto::CustomerKeySeed;
|
||||
use libpaket::locker::register::{APIRegisterError, DeviceMetadata, RegToken};
|
||||
use relm4::prelude::*;
|
||||
use relm4::{Component, ComponentParts, WidgetRef};
|
||||
use secrecy::{ExposeSecret, SecretBox};
|
||||
|
||||
use crate::keyring::{keyring_get_packstation, keyring_set_packstation};
|
||||
use crate::login::get_id_token;
|
||||
use crate::{
|
||||
scanner::{Scanner, ScannerOutput},
|
||||
LoginSharedState,
|
||||
};
|
||||
use adw::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum RegisterState {
|
||||
Beginning,
|
||||
Loading,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum State {
|
||||
Nothing,
|
||||
RegisterWizard(RegisterState),
|
||||
Loaded,
|
||||
}
|
||||
|
||||
#[tracker::track]
|
||||
pub struct PackstationView {
|
||||
state: State,
|
||||
|
||||
#[do_not_track]
|
||||
key: Option<CustomerKeySeed>,
|
||||
#[do_not_track]
|
||||
login: LoginSharedState,
|
||||
|
||||
#[do_not_track]
|
||||
scanner: AsyncController<Scanner>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PackstationViewInput {
|
||||
Init,
|
||||
GotQrValue(String),
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PackstationViewOutput {
|
||||
NavigationPage(adw::NavigationPage),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PackstationViewCommand {
|
||||
GotDeviceCredentials(CustomerKeySeed),
|
||||
GotLibraryError(libpaket::LibraryError),
|
||||
GotAPIRegisterError(APIRegisterError),
|
||||
GotNothing,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl Component for PackstationView {
|
||||
type Init = LoginSharedState;
|
||||
type Input = PackstationViewInput;
|
||||
type Output = PackstationViewOutput;
|
||||
type CommandOutput = PackstationViewCommand;
|
||||
|
||||
view! {
|
||||
#[root]
|
||||
adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ViewStack {
|
||||
add = page_loading = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::Spinner {}
|
||||
},
|
||||
|
||||
add = page_registration = &adw::ViewStack {
|
||||
#[name = "registration_page_beginning"]
|
||||
add = &adw::StatusPage {
|
||||
set_title: "Register your device",
|
||||
set_description: Some("Registration is required to interact with parcel lockers\nYou'll need camera access and the letter with the registration qr-code you received. If you don't have one, request the <a href=\"https://www.dhl.de/de/privatkunden/pakete-empfangen/an-einem-abholort-empfangen/packstation/packstation-registrierung.html\">Packstation service</a> and come back later."),
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::Clamp {
|
||||
set_maximum_size: 260,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Button {
|
||||
add_css_class: relm4::css::SUGGESTED_ACTION,
|
||||
add_css_class: relm4::css::PILL,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ButtonContent {
|
||||
set_label: "Register",
|
||||
set_icon_name: relm4_icons::icon_names::QR_CODE_SCANNER,
|
||||
},
|
||||
|
||||
connect_clicked[sender = sender.clone()] => move |_| {
|
||||
sender.output(PackstationViewOutput::NavigationPage(scanner_page.clone())).unwrap();
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
#[name = "registration_page_loading"]
|
||||
add = &adw::StatusPage {
|
||||
#[wrap(Some)]
|
||||
set_paintable = &adw::SpinnerPaintable::new(Some(registration_page_loading.widget_ref())) {},
|
||||
|
||||
set_title: "Registering your device"
|
||||
},
|
||||
|
||||
#[track(model.changed_state())]
|
||||
set_visible_child?: {
|
||||
match model.get_state() {
|
||||
State::RegisterWizard(register_state) => Some(match register_state {
|
||||
RegisterState::Beginning => registration_page_beginning.widget_ref(),
|
||||
RegisterState::Loading => registration_page_loading.widget_ref(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
add = page_loaded = &adw::Bin {
|
||||
|
||||
},
|
||||
|
||||
#[track(model.changed_state())]
|
||||
set_visible_child: {
|
||||
match model.get_state() {
|
||||
State::Nothing => page_loading.widget_ref(),
|
||||
State::RegisterWizard(_) => page_registration.widget_ref(),
|
||||
State::Loaded => page_loaded.widget_ref(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: relm4::ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let scanner = Scanner::builder()
|
||||
.launch(())
|
||||
.forward(sender.input_sender(), convert_scanner_output);
|
||||
|
||||
let model = PackstationView {
|
||||
state: State::Nothing,
|
||||
scanner,
|
||||
login: init,
|
||||
key: None,
|
||||
tracker: 0,
|
||||
};
|
||||
|
||||
let scanner_page = model.scanner.widget().clone();
|
||||
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 {
|
||||
PackstationViewInput::Init => {
|
||||
self.set_state(State::Nothing);
|
||||
sender.oneshot_command(async {
|
||||
let value = keyring_get_packstation().await.unwrap();
|
||||
match value {
|
||||
Some(value) => PackstationViewCommand::GotDeviceCredentials(value),
|
||||
None => PackstationViewCommand::GotNothing,
|
||||
}
|
||||
});
|
||||
self.set_state(State::RegisterWizard(RegisterState::Beginning))
|
||||
}
|
||||
PackstationViewInput::GotQrValue(value) => {
|
||||
self.set_state(State::RegisterWizard(RegisterState::Loading));
|
||||
let reg_token = match RegToken::parse_from_qrcode_uri(value.as_str()) {
|
||||
Ok(value) => secrecy::SecretBox::new(Box::new(value)),
|
||||
Err(_) => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let login = self.login.clone();
|
||||
sender.oneshot_command(async move {
|
||||
match Self::register_with_regtoken(login, reg_token).await {
|
||||
Ok(value) => match value {
|
||||
Ok(value) => {
|
||||
keyring_set_packstation(&value).await.unwrap();
|
||||
PackstationViewCommand::GotDeviceCredentials(value)
|
||||
},
|
||||
Err(err) => todo!(),
|
||||
},
|
||||
Err(err) => todo!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
PackstationViewInput::Reset => {
|
||||
self.set_state(State::Nothing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_cmd(
|
||||
&mut self,
|
||||
message: Self::CommandOutput,
|
||||
sender: ComponentSender<Self>,
|
||||
root: &Self::Root,
|
||||
) {
|
||||
match message {
|
||||
PackstationViewCommand::GotDeviceCredentials(secret_box) => {
|
||||
self.key = Some(secret_box);
|
||||
self.set_state(State::Loaded);
|
||||
}
|
||||
PackstationViewCommand::GotNothing => {
|
||||
self.set_state(State::RegisterWizard(RegisterState::Beginning));
|
||||
},
|
||||
PackstationViewCommand::GotAPIRegisterError(error) => {
|
||||
todo!()
|
||||
},
|
||||
PackstationViewCommand::GotLibraryError(error) => {
|
||||
match error {
|
||||
libpaket::LibraryError::Unauthorized => todo!(),
|
||||
libpaket::LibraryError::DecodeError(_) => todo!(),
|
||||
libpaket::LibraryError::APIChange => todo!(),
|
||||
libpaket::LibraryError::Deprecated => todo!(),
|
||||
|
||||
libpaket::LibraryError::InvalidArgument(error) => panic!("{}", error),
|
||||
libpaket::LibraryError::NetworkFetch => panic!(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PackstationView {
|
||||
async fn register_with_regtoken(
|
||||
login: LoginSharedState,
|
||||
regtoken: SecretBox<RegToken>,
|
||||
) -> libpaket::LibraryResult<Result<CustomerKeySeed, APIRegisterError>> {
|
||||
let client = libpaket::StammdatenClient::new();
|
||||
let payload = client
|
||||
.begin_registration(&get_id_token(&login).await.unwrap())
|
||||
.await?;
|
||||
|
||||
let mut customer_key_seed = CustomerKeySeed::new(
|
||||
get_id_token(&login)
|
||||
.await
|
||||
.unwrap()
|
||||
.get_post_number()
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let res = client
|
||||
.register_by_regtoken(
|
||||
&get_id_token(&login).await.unwrap(),
|
||||
&customer_key_seed,
|
||||
payload,
|
||||
DeviceMetadata {
|
||||
manufacturer_model: "Manufacturer".into(),
|
||||
manufacturer_name: "Name".into(),
|
||||
manufacturer_operating_system: "Android".into(),
|
||||
name: "Linux Mobile Device".into(),
|
||||
},
|
||||
regtoken.expose_secret(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(match res {
|
||||
libpaket::locker::register::DeviceRegistrationResponse::Error { id, description } => {
|
||||
Err(id)
|
||||
}
|
||||
libpaket::locker::register::DeviceRegistrationResponse::Okay(device) => {
|
||||
customer_key_seed.set_device_id(device.id);
|
||||
Ok(customer_key_seed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_scanner_output(value: ScannerOutput) -> PackstationViewInput {
|
||||
match value {
|
||||
ScannerOutput::CodeDetected(value) => PackstationViewInput::GotQrValue(value),
|
||||
}
|
||||
}
|
|
@ -2,7 +2,9 @@ use adw::prelude::*;
|
|||
use relm4::prelude::*;
|
||||
|
||||
use crate::{
|
||||
account::{AccountOutput, AccountServices, AccountView}, advices::{AdvicesView, AdvicesViewInput}, packstation::{PackstationView, PackstationViewInput, PackstationViewOutput}, tracking::{TrackingInput, TrackingOutput, TrackingView}
|
||||
account::{AccountOutput, AccountServices, AccountView},
|
||||
advices::{AdvicesView, AdvicesViewInput},
|
||||
tracking::{TrackingInput, TrackingOutput, TrackingView},
|
||||
};
|
||||
|
||||
#[tracker::track]
|
||||
|
@ -10,15 +12,12 @@ pub struct Ready {
|
|||
logged_in: bool,
|
||||
have_service_advices: bool,
|
||||
have_service_tracking: bool,
|
||||
have_service_packstation: bool,
|
||||
|
||||
#[do_not_track]
|
||||
account_component: Controller<AccountView>,
|
||||
#[do_not_track]
|
||||
advices_component: AsyncController<AdvicesView>,
|
||||
#[do_not_track]
|
||||
packstation_component: Controller<PackstationView>,
|
||||
#[do_not_track]
|
||||
tracking_component: Controller<TrackingView>,
|
||||
#[do_not_track]
|
||||
toast_overlay: adw::ToastOverlay,
|
||||
|
@ -40,7 +39,6 @@ pub enum ReadyCmds {
|
|||
pub enum ReadyInput {
|
||||
LoggedIn,
|
||||
LoggedOut,
|
||||
NavigationPageTemp(adw::NavigationPage),
|
||||
HaveService(AccountServices),
|
||||
ServiceBorked(AccountServices),
|
||||
}
|
||||
|
@ -54,77 +52,73 @@ impl Component for Ready {
|
|||
|
||||
view! {
|
||||
#[root]
|
||||
&adw::NavigationView {
|
||||
add = &adw::NavigationPage {
|
||||
set_title: "",
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ToolbarView {
|
||||
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),
|
||||
}
|
||||
},
|
||||
adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::NavigationView {
|
||||
add = &adw::NavigationPage {
|
||||
set_title: "",
|
||||
|
||||
#[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,
|
||||
|
||||
} -> 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,
|
||||
},
|
||||
|
||||
add = &model.packstation_component.widget().clone() -> adw::Bin {
|
||||
#[track(model.changed_have_service_packstation())]
|
||||
set_visible: model.have_service_packstation,
|
||||
} -> page_packstation: adw::ViewStackPage {
|
||||
set_title: Some("Packstation"),
|
||||
set_name: Some("page_packstation"),
|
||||
|
||||
#[track(model.changed_have_service_packstation())]
|
||||
set_visible: model.have_service_packstation,
|
||||
},
|
||||
|
||||
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)
|
||||
set_child = &adw::ToolbarView {
|
||||
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),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
#[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,
|
||||
|
||||
} -> 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,
|
||||
|
||||
|
||||
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,
|
||||
},
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
add_bottom_bar = ready_switcherbar = &adw::ViewSwitcherBar {
|
||||
set_stack: Some(&ready_view_stack),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
fn init(
|
||||
|
@ -136,28 +130,22 @@ impl Component for Ready {
|
|||
|
||||
let tracking_component = TrackingView::builder()
|
||||
.launch(init.clone())
|
||||
.forward(sender.input_sender(), convert_tracking_output);
|
||||
.forward(&sender.input_sender(), convert_tracking_output);
|
||||
|
||||
let account_component = AccountView::builder()
|
||||
.launch(init.clone())
|
||||
.forward(sender.input_sender(), convert_account_output);
|
||||
|
||||
let packstation_component = PackstationView::builder()
|
||||
.launch(init.clone())
|
||||
.forward(sender.input_sender(), convert_packstation_output);
|
||||
.forward(&sender.input_sender(), convert_account_output);
|
||||
|
||||
let toast_overlay = adw::ToastOverlay::new();
|
||||
|
||||
let model = Ready {
|
||||
have_service_advices: false,
|
||||
have_service_tracking: true,
|
||||
have_service_packstation: false,
|
||||
logged_in: false,
|
||||
|
||||
account_component,
|
||||
advices_component,
|
||||
tracking_component,
|
||||
packstation_component,
|
||||
toast_overlay,
|
||||
|
||||
tracker: 0,
|
||||
|
@ -188,7 +176,7 @@ impl Component for Ready {
|
|||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
|
@ -207,10 +195,6 @@ impl Component for Ready {
|
|||
self.set_have_service_tracking(true);
|
||||
self.tracking_component.emit(TrackingInput::Search(None))
|
||||
}
|
||||
AccountServices::PackstationAvailable => {
|
||||
self.set_have_service_packstation(true);
|
||||
self.packstation_component.emit(PackstationViewInput::Init);
|
||||
}
|
||||
},
|
||||
ReadyInput::ServiceBorked(service) => match service {
|
||||
AccountServices::Advices => {
|
||||
|
@ -221,7 +205,7 @@ impl Component for Ready {
|
|||
.build(),
|
||||
);
|
||||
self.set_have_service_advices(false);
|
||||
},
|
||||
}
|
||||
AccountServices::SendungVerfolgung => {
|
||||
self.toast_overlay.add_toast(
|
||||
adw::Toast::builder()
|
||||
|
@ -230,12 +214,8 @@ impl Component for Ready {
|
|||
.build(),
|
||||
);
|
||||
self.set_have_service_tracking(false);
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
},
|
||||
ReadyInput::NavigationPageTemp(page) => {
|
||||
root.push(&page);
|
||||
}
|
||||
};
|
||||
|
||||
if self.changed_logged_in() {
|
||||
|
@ -243,11 +223,7 @@ impl Component for Ready {
|
|||
sender.output(ReadyOutput::Ready).unwrap();
|
||||
} else {
|
||||
self.advices_component.emit(AdvicesViewInput::Reset);
|
||||
self.packstation_component.emit(PackstationViewInput::Reset);
|
||||
self.tracking_component.emit(TrackingInput::Reset);
|
||||
self.set_have_service_advices(false);
|
||||
self.set_have_service_tracking(true);
|
||||
self.set_have_service_packstation(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -266,9 +242,3 @@ fn convert_account_output(value: AccountOutput) -> ReadyInput {
|
|||
AccountOutput::HaveService(service) => ReadyInput::HaveService(service),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_packstation_output(value: PackstationViewOutput) -> ReadyInput {
|
||||
match value {
|
||||
PackstationViewOutput::NavigationPage(navigation_page) => ReadyInput::NavigationPageTemp(navigation_page),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,304 +0,0 @@
|
|||
pub use r#impl::{Scanner, ScannerOutput};
|
||||
|
||||
mod r#impl {
|
||||
use adw::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
use relm4::WidgetRef;
|
||||
|
||||
impl Scanner {
|
||||
async fn activate_internal_impl() -> Result<(), ErrorInternal> {
|
||||
let device_provider = aperture::DeviceProvider::instance();
|
||||
|
||||
device_provider.start()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn activate_internal(&mut self, sender: &AsyncComponentSender<Self>) -> bool {
|
||||
if !aperture::DeviceProvider::instance().started() {
|
||||
match Scanner::activate_internal_impl().await {
|
||||
Ok(_) => return true,
|
||||
Err(err) => {
|
||||
sender.input(ScannerInput::Error(err));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum State {
|
||||
Nothing,
|
||||
Error,
|
||||
InView,
|
||||
}
|
||||
|
||||
#[tracker::track]
|
||||
pub struct Scanner {
|
||||
state: State,
|
||||
in_activation: bool,
|
||||
|
||||
#[do_not_track]
|
||||
camera: Option<aperture::Camera>,
|
||||
#[do_not_track]
|
||||
view_finder: aperture::Viewfinder,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScannerOutput {
|
||||
CodeDetected(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScannerInput {
|
||||
// Externally
|
||||
Activate,
|
||||
Deactivate,
|
||||
|
||||
Error(ErrorInternal),
|
||||
|
||||
CamaraAdded(aperture::Camera),
|
||||
CamaraRemoved(aperture::Camera),
|
||||
CameraChoosen(aperture::Camera),
|
||||
|
||||
// View
|
||||
TransitionToInView,
|
||||
}
|
||||
|
||||
#[relm4::component(pub, async)]
|
||||
impl SimpleAsyncComponent for Scanner {
|
||||
type Input = ScannerInput;
|
||||
type Output = ScannerOutput;
|
||||
type Init = ();
|
||||
|
||||
view! {
|
||||
#[root]
|
||||
adw::NavigationPage {
|
||||
connect_parent_notify[sender = sender.clone()] => move |page| {
|
||||
let sender = sender.input_sender();
|
||||
if page.parent().is_none() {
|
||||
sender.emit(ScannerInput::Deactivate);
|
||||
} else {
|
||||
sender.emit(ScannerInput::Activate);
|
||||
}
|
||||
},
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ToolbarView {
|
||||
add_top_bar = &adw::HeaderBar {},
|
||||
|
||||
#[wrap(Some)]
|
||||
set_content = &adw::ViewStack {
|
||||
#[local_ref]
|
||||
add = &page_view_finder -> aperture::Viewfinder {
|
||||
set_detect_codes: true,
|
||||
|
||||
connect_code_detected[sender = sender.clone()] => move |_, code_type, value| {
|
||||
match code_type {
|
||||
aperture::CodeType::Qr => {
|
||||
let _ = sender.output(ScannerOutput::CodeDetected(value.to_string()));
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
},
|
||||
|
||||
connect_state_notify[sender = sender.clone()] => move |view_finder|{
|
||||
match view_finder.state() {
|
||||
aperture::ViewfinderState::Loading => {},
|
||||
aperture::ViewfinderState::Ready => {},
|
||||
aperture::ViewfinderState::NoCameras => {
|
||||
sender.input(ScannerInput::Deactivate);
|
||||
},
|
||||
aperture::ViewfinderState::Error => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
#[name = "page_error"]
|
||||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::StatusPage {
|
||||
set_title: "Error occured",
|
||||
},
|
||||
},
|
||||
|
||||
#[name = "page_no_cameras"]
|
||||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::StatusPage {
|
||||
set_title: "No cameras detected",
|
||||
},
|
||||
},
|
||||
|
||||
#[track(model.changed_state())]
|
||||
set_visible_child: {
|
||||
match model.state {
|
||||
State::Nothing => page_no_cameras.widget_ref(),
|
||||
State::InView => page_view_finder.widget_ref(),
|
||||
State::Error => page_error.widget_ref(),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: relm4::AsyncComponentSender<Self>,
|
||||
) -> AsyncComponentParts<Self> {
|
||||
let view_finder = aperture::Viewfinder::new();
|
||||
|
||||
let device_provider = aperture::DeviceProvider::instance();
|
||||
{
|
||||
let sender = sender.input_sender().clone();
|
||||
device_provider.connect_camera_removed(move |_, camera| {
|
||||
let _ = sender.send(ScannerInput::CamaraRemoved(camera.clone()));
|
||||
});
|
||||
}
|
||||
{
|
||||
let sender = sender.input_sender().clone();
|
||||
device_provider.connect_camera_added(move |_, camera| {
|
||||
let _ = sender.send(ScannerInput::CamaraAdded(camera.clone()));
|
||||
});
|
||||
}
|
||||
|
||||
let model = Scanner {
|
||||
state: State::Nothing,
|
||||
view_finder,
|
||||
camera: None,
|
||||
in_activation: false,
|
||||
tracker: 0,
|
||||
};
|
||||
|
||||
let page_view_finder = model.view_finder.clone();
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
AsyncComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&mut self,
|
||||
message: Self::Input,
|
||||
sender: relm4::AsyncComponentSender<Self>,
|
||||
) {
|
||||
match self.state {
|
||||
State::Error => {
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
match message {
|
||||
ScannerInput::CamaraRemoved(removed_camera) => {
|
||||
if let Some(camera) = self.camera.as_ref() {
|
||||
if removed_camera == *camera {
|
||||
self.camera = None;
|
||||
if *self.get_state() == State::InView {
|
||||
sender.input(ScannerInput::Deactivate);
|
||||
sender.input(ScannerInput::Activate);
|
||||
z }
|
||||
}
|
||||
}
|
||||
}
|
||||
ScannerInput::CamaraAdded(_) => {
|
||||
if self.camera.is_none() {
|
||||
self.view_finder.stop_stream();
|
||||
self.set_state(State::Nothing);
|
||||
sender.input(ScannerInput::Activate);
|
||||
}
|
||||
}
|
||||
ScannerInput::Error(err) => {
|
||||
println!("error: {:?}", err);
|
||||
self.set_state(State::Error);
|
||||
}
|
||||
ScannerInput::Activate => {
|
||||
if self.activate_internal(&sender).await {
|
||||
sender.input(ScannerInput::TransitionToInView);
|
||||
}
|
||||
}
|
||||
ScannerInput::Deactivate => {
|
||||
self.view_finder.stop_stream();
|
||||
self.set_state(State::Nothing);
|
||||
}
|
||||
ScannerInput::CameraChoosen(camera) => {
|
||||
self.camera = Some(camera);
|
||||
self.view_finder.stop_stream();
|
||||
sender.input(ScannerInput::TransitionToInView);
|
||||
}
|
||||
ScannerInput::TransitionToInView => {
|
||||
/*
|
||||
* If we don't have a camera selected, choose one with this pattern:
|
||||
* - first internal camera that is facing back
|
||||
* - first external camera
|
||||
* - first camera that was found
|
||||
*/
|
||||
let device_provider = aperture::DeviceProvider::instance();
|
||||
|
||||
if device_provider.started() {
|
||||
if self.camera.is_none() {
|
||||
let mut camera = device_provider
|
||||
.iter::<aperture::Camera>()
|
||||
.find(move |item| {
|
||||
let item = item.as_ref().unwrap();
|
||||
match item.location() {
|
||||
aperture::CameraLocation::Back => true,
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
.map(|res| res.unwrap());
|
||||
if camera.is_none() {
|
||||
device_provider
|
||||
.iter::<aperture::Camera>()
|
||||
.find(move |item| {
|
||||
let item = item.as_ref().unwrap();
|
||||
match item.location() {
|
||||
aperture::CameraLocation::External => true,
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
.map(|res| res.unwrap());
|
||||
}
|
||||
if camera.is_none() {
|
||||
camera = device_provider.camera(0);
|
||||
}
|
||||
|
||||
self.camera = camera;
|
||||
}
|
||||
|
||||
if self.camera.is_some() {
|
||||
self.view_finder.set_camera(self.camera.clone());
|
||||
self.view_finder.start_stream();
|
||||
self.set_state(State::InView);
|
||||
} else {
|
||||
self.set_state(State::Nothing);
|
||||
}
|
||||
} else {
|
||||
self.set_state(State::Nothing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ErrorInternal {
|
||||
Pipewire(aperture::PipewireError),
|
||||
Provider(aperture::ProviderError),
|
||||
}
|
||||
|
||||
impl From<aperture::PipewireError> for ErrorInternal {
|
||||
fn from(value: aperture::PipewireError) -> Self {
|
||||
ErrorInternal::Pipewire(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<aperture::ProviderError> for ErrorInternal {
|
||||
fn from(value: aperture::ProviderError) -> Self {
|
||||
ErrorInternal::Provider(value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -78,8 +78,7 @@ impl Component for TrackingView {
|
|||
|
||||
#[local_ref]
|
||||
tracking_box -> gtk::Box {
|
||||
set_spacing: 8,
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 8
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue