Compare commits
11 commits
6d68c6500b
...
c99956583b
Author | SHA1 | Date | |
---|---|---|---|
|
c99956583b | ||
|
c1e91ff1bb | ||
|
e994bec86f | ||
|
c20a7a803a | ||
|
b6e0ef7638 | ||
|
11d7cb2ef4 | ||
|
d7e905f7eb | ||
|
afcfe0c9ef | ||
|
b320ca0c61 | ||
|
d5c2997c62 | ||
|
a8d82bb7b7 |
17 changed files with 1661 additions and 335 deletions
751
Cargo.lock
generated
751
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
@ -6,9 +6,19 @@ members = [
|
||||||
"paket",
|
"paket",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
authors = ["Jane Rachinger <libpaket@j4ne.de>"]
|
authors = ["Jane Rachinger <libpaket@j4ne.de>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
version = "0.1.0"
|
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"
|
app_id = "de.j4ne.Paket"
|
||||||
|
|
||||||
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy"]
|
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy", "qr-code-scanner"]
|
||||||
|
|
|
@ -14,10 +14,10 @@ num_enum = { version = "0.7", optional = true }
|
||||||
# TODO: Consolidate?
|
# TODO: Consolidate?
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
random-string = "1.1.0"
|
random-string = "1.1.0"
|
||||||
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "http2"] }
|
reqwest = { workspace = true }
|
||||||
secrecy = { version = "0.8.0", features = ["serde"] }
|
secrecy = { workspace = true}
|
||||||
serde = { version = "1.0.195", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1.0.111"
|
serde_json = { workspace = true }
|
||||||
serde_repr = { version = "0.1.18", optional = true }
|
serde_repr = { version = "0.1.18", optional = true }
|
||||||
serde_ignored = "0.1"
|
serde_ignored = "0.1"
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
|
@ -28,8 +28,8 @@ base64 = "0.22"
|
||||||
# sha2 also used in briefankuendigung and packstation_register_regtoken
|
# sha2 also used in briefankuendigung and packstation_register_regtoken
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
urlencoding = "2.1.3"
|
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"
|
serde_newtype = "0.1.1"
|
||||||
thiserror = "1.0.56"
|
thiserror = "1.0.56"
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ pub enum LibraryError {
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
#[error("invalid argument: {0}")]
|
#[error("invalid argument: {0}")]
|
||||||
InvalidArgument(String),
|
InvalidArgument(String),
|
||||||
#[error("internal error, unable to decode: {0}")]
|
#[error("unable to decode: {0}")]
|
||||||
DecodeError(String),
|
DecodeError(String),
|
||||||
#[error("upstream api was changed. not continuing")]
|
#[error("upstream api was changed. not continuing")]
|
||||||
APIChange,
|
APIChange,
|
||||||
|
|
|
@ -1,41 +1,53 @@
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use rand::prelude::*;
|
|
||||||
use uuid::Uuid;
|
|
||||||
use ed25519_dalek::SigningKey;
|
|
||||||
use ed25519_dalek::Signer;
|
use ed25519_dalek::Signer;
|
||||||
|
use ed25519_dalek::SigningKey;
|
||||||
|
use rand::prelude::*;
|
||||||
|
use secrecy::zeroize::Zeroize;
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
use secrecy::SecretBox;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct CustomerKeySeed {
|
pub struct CustomerKeySeed {
|
||||||
pub postnumber: String,
|
pub postnumber: String,
|
||||||
pub seed: Seed,
|
pub seed: SecretBox<Seed>,
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub device_id: Option<String>,
|
pub device_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Seed {
|
impl Zeroize for CustomerKeySeed {
|
||||||
bytes: Vec<u8>,
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Seed {
|
impl Seed {
|
||||||
pub(self) fn from_bytes(bytes: Vec<u8>) -> Self {
|
|
||||||
assert_eq!(bytes.len(), 32);
|
|
||||||
Seed {
|
|
||||||
bytes: bytes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn random() -> Self {
|
pub fn random() -> Self {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
let mut bytes: Vec<u8> = vec![0; 32];
|
let mut bytes: Vec<u8> = vec![0; 32];
|
||||||
|
|
||||||
rng.fill_bytes(bytes.as_mut_slice());
|
rng.fill_bytes(bytes.as_mut_slice());
|
||||||
|
Seed (bytes)
|
||||||
Seed { bytes: bytes }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||||
(&self.bytes[..]).try_into().unwrap()
|
(&self.0[..]).try_into().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +55,7 @@ impl CustomerKeySeed {
|
||||||
pub fn new(postnumber: String) -> Self {
|
pub fn new(postnumber: String) -> Self {
|
||||||
CustomerKeySeed {
|
CustomerKeySeed {
|
||||||
postnumber,
|
postnumber,
|
||||||
seed: Seed::random(),
|
seed: SecretBox::new(Box::new(Seed::random())),
|
||||||
uuid: uuid::Uuid::new_v4(),
|
uuid: uuid::Uuid::new_v4(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
}
|
}
|
||||||
|
@ -52,7 +64,7 @@ impl CustomerKeySeed {
|
||||||
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid, device_id: String) -> Self {
|
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid, device_id: String) -> Self {
|
||||||
CustomerKeySeed {
|
CustomerKeySeed {
|
||||||
postnumber: postnumber.clone(),
|
postnumber: postnumber.clone(),
|
||||||
seed: Seed::from_bytes(seed),
|
seed: SecretBox::new(Box::new(Seed::from(seed))),
|
||||||
uuid: uuid.clone(),
|
uuid: uuid.clone(),
|
||||||
device_id: Some(device_id),
|
device_id: Some(device_id),
|
||||||
}
|
}
|
||||||
|
@ -65,17 +77,15 @@ impl CustomerKeySeed {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn sign(&self, message: &[u8]) -> String {
|
pub(crate) fn sign(&self, message: &[u8]) -> String {
|
||||||
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());
|
let signing_key = SigningKey::from_bytes(self.seed.expose_secret().as_bytes());
|
||||||
|
|
||||||
let signature = signing_key.sign(message);
|
let signature = signing_key.sign(message);
|
||||||
|
|
||||||
let sig_str = general_purpose::STANDARD.encode(signature.to_bytes());
|
let sig_str = general_purpose::STANDARD.encode(signature.to_bytes());
|
||||||
|
|
||||||
format!("{}.{}", self.uuid.to_string(), sig_str)
|
format!("{}.{}", self.uuid.to_string(), sig_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_public_key(&self) -> String {
|
pub(crate) fn get_public_key(&self) -> String {
|
||||||
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());
|
let signing_key = SigningKey::from_bytes(self.seed.expose_secret().as_bytes());
|
||||||
let public_bytes = signing_key.verifying_key().to_bytes();
|
let public_bytes = signing_key.verifying_key().to_bytes();
|
||||||
let public_str = general_purpose::STANDARD.encode(public_bytes);
|
let public_str = general_purpose::STANDARD.encode(public_bytes);
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,11 @@ use sha2::Sha256;
|
||||||
|
|
||||||
use crate::{LibraryResult, LibraryError};
|
use crate::{LibraryResult, LibraryError};
|
||||||
|
|
||||||
pub struct RegToken {
|
pub struct RegToken(Vec<u8>);
|
||||||
token: Vec<u8>,
|
impl secrecy::zeroize::Zeroize for RegToken {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.0.zeroize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegToken {
|
impl RegToken {
|
||||||
|
@ -32,15 +35,15 @@ impl RegToken {
|
||||||
token.extend(vec![0; 32 - token.len()].iter())
|
token.extend(vec![0; 32 - token.len()].iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(RegToken { token })
|
Ok(RegToken(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn customer_password(&self) -> String {
|
pub fn customer_password(&self) -> String {
|
||||||
general_purpose::STANDARD.encode(&self.token[32..])
|
general_purpose::STANDARD.encode(&self.0[32..])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hmac(&self) -> SimpleHmac<Sha256> {
|
pub fn hmac(&self) -> SimpleHmac<Sha256> {
|
||||||
let mac = SimpleHmac::<Sha256>::new_from_slice(&self.token[0..32])
|
let mac = SimpleHmac::<Sha256>::new_from_slice(&self.0[0..32])
|
||||||
.expect("HMAC can take key of any size");
|
.expect("HMAC can take key of any size");
|
||||||
|
|
||||||
mac
|
mac
|
||||||
|
|
|
@ -7,14 +7,19 @@ version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# using git version, for https://github.com/Relm4/Relm4/pull/677
|
# using git version, for https://github.com/Relm4/Relm4/pull/677
|
||||||
relm4 = { version = "0.9", features = [ "libadwaita", "macros" ], git = "https://github.com/Relm4/Relm4.git" }
|
relm4 = { workspace = true }
|
||||||
relm4-icons = { version = "0.9", git = "https://github.com/Relm4/icons.git" }
|
relm4-icons = { version = "0.9" }
|
||||||
tracker = "0.2"
|
tracker = "0.2"
|
||||||
adw = {package = "libadwaita", version = "0.7", features = [ "v1_6" ]}
|
adw = { package = "libadwaita", version = "0.7", features = ["v1_6"] }
|
||||||
|
aperture = "0.7"
|
||||||
webkit = { package = "webkit6", version = "0.4" }
|
webkit = { package = "webkit6", version = "0.4" }
|
||||||
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"] }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
secrecy = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
|
|
@ -37,6 +37,7 @@ pub enum AccountInput {
|
||||||
pub enum AccountServices {
|
pub enum AccountServices {
|
||||||
Advices,
|
Advices,
|
||||||
SendungVerfolgung,
|
SendungVerfolgung,
|
||||||
|
PackstationAvailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -206,9 +207,26 @@ impl Component for AccountView {
|
||||||
}
|
}
|
||||||
AccountCmd::GotCustomerDataFull(data) => match data {
|
AccountCmd::GotCustomerDataFull(data) => match data {
|
||||||
Ok(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 {
|
for service in &data.common.services {
|
||||||
match service {
|
match service {
|
||||||
libpaket::stammdaten::CustomerDataService::Packstation => (),
|
libpaket::stammdaten::CustomerDataService::Packstation => sender
|
||||||
|
.output(AccountOutput::HaveService(
|
||||||
|
AccountServices::PackstationAvailable,
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
libpaket::stammdaten::CustomerDataService::Paketankuendigung => sender
|
libpaket::stammdaten::CustomerDataService::Paketankuendigung => sender
|
||||||
.output(AccountOutput::HaveService(
|
.output(AccountOutput::HaveService(
|
||||||
AccountServices::SendungVerfolgung,
|
AccountServices::SendungVerfolgung,
|
||||||
|
|
|
@ -56,31 +56,41 @@ impl AsyncComponent for App {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_content = &adw::ViewStack {
|
set_content = &adw::ViewStack {
|
||||||
#[name = "page_loading"]
|
#[name = "page_loading"]
|
||||||
add = &adw::Bin {
|
add = &adw::ToolbarView {
|
||||||
|
add_top_bar = &adw::HeaderBar {},
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::Spinner {}
|
set_content = &adw::Bin {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::Spinner {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
#[local_ref]
|
#[local_ref]
|
||||||
add = page_login -> adw::Bin {},
|
add = page_login -> adw::Bin {},
|
||||||
|
|
||||||
#[local_ref]
|
#[local_ref]
|
||||||
add = page_ready -> adw::Bin {},
|
add = page_ready -> adw::NavigationView {},
|
||||||
|
|
||||||
#[name = "page_error"]
|
#[name = "page_error"]
|
||||||
add = &adw::Bin {
|
add = &adw::ToolbarView {
|
||||||
#[name = "page_error_status"]
|
add_top_bar = &adw::HeaderBar {},
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::StatusPage {}
|
set_content = &adw::Bin {
|
||||||
|
#[name = "page_error_status"]
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::StatusPage {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
#[track(model.changed(App::state()))]
|
#[track(model.changed(App::state()))]
|
||||||
set_visible_child: {
|
set_visible_child: {
|
||||||
let page: &adw::Bin = match model.state {
|
let page = match model.state {
|
||||||
AppState::Loading => page_loading.as_ref(),
|
AppState::Loading => page_loading.widget_ref(),
|
||||||
AppState::RequiresLogin => page_login.as_ref(),
|
AppState::RequiresLogin => page_login.widget_ref(),
|
||||||
AppState::Ready => page_ready.as_ref(),
|
AppState::Ready => page_ready.widget_ref(),
|
||||||
AppState::Error => page_error.as_ref(),
|
AppState::Error => page_error.widget_ref(),
|
||||||
};
|
};
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
|
@ -131,7 +141,7 @@ impl AsyncComponent for App {
|
||||||
match message {
|
match message {
|
||||||
AppInput::AddBreakpoint(breakpoint) => {
|
AppInput::AddBreakpoint(breakpoint) => {
|
||||||
root.add_breakpoint(breakpoint);
|
root.add_breakpoint(breakpoint);
|
||||||
},
|
}
|
||||||
AppInput::SwitchToLoading => {
|
AppInput::SwitchToLoading => {
|
||||||
self.set_state(AppState::Loading);
|
self.set_state(AppState::Loading);
|
||||||
}
|
}
|
||||||
|
@ -155,8 +165,14 @@ 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(library_error) => AppInput::FatalErr(AppError { short: "Unhandled API error".to_string(), long: library_error.to_string() }),
|
LoginOutput::Error(library_error) => AppInput::FatalErr(AppError {
|
||||||
LoginOutput::KeyringError(error) => AppInput::FatalErr(AppError { short: "Keyring usage failed".to_string(), long: error.to_string() }),
|
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(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +185,7 @@ fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
RELM_THREADS.set(4).unwrap();
|
RELM_THREADS.set(4).unwrap();
|
||||||
gtk::init().unwrap();
|
aperture::init(paket::constants::APP_ID);
|
||||||
let display = gtk::gdk::Display::default().unwrap();
|
let display = gtk::gdk::Display::default().unwrap();
|
||||||
let theme = gtk::IconTheme::for_display(&display);
|
let theme = gtk::IconTheme::for_display(&display);
|
||||||
theme.add_resource_path("/de/j4ne/Paket/icons/");
|
theme.add_resource_path("/de/j4ne/Paket/icons/");
|
||||||
|
|
181
paket/src/keyring.rs
Normal file
181
paket/src/keyring.rs
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
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,8 +2,11 @@ pub mod account;
|
||||||
pub mod advice;
|
pub mod advice;
|
||||||
pub mod advices;
|
pub mod advices;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
|
pub mod keyring;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod packstation;
|
||||||
pub mod ready;
|
pub mod ready;
|
||||||
|
pub mod scanner;
|
||||||
pub mod tracking;
|
pub mod tracking;
|
||||||
|
|
||||||
pub use login::LoginSharedState;
|
pub use login::LoginSharedState;
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
use std::{
|
use std::{cell::RefCell, collections::HashMap, sync::Arc, time::Duration};
|
||||||
cell::RefCell,
|
|
||||||
collections::HashMap,
|
|
||||||
sync::{Arc, OnceLock},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use libpaket::{
|
use libpaket::{
|
||||||
|
@ -17,7 +12,7 @@ use relm4::{
|
||||||
};
|
};
|
||||||
use webkit::{prelude::WebViewExt, URIRequest, WebContext, WebView};
|
use webkit::{prelude::WebViewExt, URIRequest, WebContext, WebView};
|
||||||
|
|
||||||
static KEYRING: OnceLock<oo7::Keyring> = OnceLock::new();
|
use crate::keyring::{keyring_get_refresh_token, keyring_is_available, keyring_set_refresh_token};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginInput {
|
pub enum LoginInput {
|
||||||
|
@ -79,8 +74,19 @@ pub enum LoginCommand {
|
||||||
NeedsRefresh,
|
NeedsRefresh,
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEYRING_ATTRIBUTES: [(&str, &str); 2] =
|
macro_rules! keyring_result_get {
|
||||||
[("app", crate::constants::APP_ID), ("type", "refresh_token")];
|
($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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[relm4::component(async, pub)]
|
#[relm4::component(async, pub)]
|
||||||
impl AsyncComponent for Login {
|
impl AsyncComponent for Login {
|
||||||
|
@ -175,47 +181,24 @@ impl AsyncComponent for Login {
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = oo7::Keyring::new().await;
|
let _ = keyring_result_get!(sender, oo7::Keyring::new().await, |keyring| {
|
||||||
match result {
|
crate::keyring::KEYRING.set(keyring).unwrap();
|
||||||
Ok(keyring) => {
|
});
|
||||||
KEYRING.set(keyring).unwrap();
|
|
||||||
if let Err(err) = KEYRING.get().unwrap().unlock().await {
|
if keyring_is_available() {
|
||||||
sender
|
let refresh_token =
|
||||||
.output(LoginOutput::KeyringError(err))
|
keyring_result_get!(sender, keyring_get_refresh_token().await, move |value| {
|
||||||
.expect("sender not worky");
|
return value;
|
||||||
|
});
|
||||||
|
if let Ok(value) = refresh_token {
|
||||||
|
model.refresh_token = value;
|
||||||
|
if model.refresh_token.is_some() {
|
||||||
|
sender.input(LoginInput::NeedsRefresh);
|
||||||
} else {
|
} else {
|
||||||
let keyring = KEYRING.get().unwrap();
|
sender.input(LoginInput::NeedsLogin);
|
||||||
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();
|
let webcontext = WebContext::builder().build();
|
||||||
{
|
{
|
||||||
|
@ -273,7 +256,7 @@ impl AsyncComponent for Login {
|
||||||
*token.write() = None;
|
*token.write() = None;
|
||||||
}
|
}
|
||||||
if let Some(refresh_token) = self.refresh_token.clone() {
|
if let Some(refresh_token) = self.refresh_token.clone() {
|
||||||
sender.command(|out, shutdown| {
|
sender.command(|_, shutdown| {
|
||||||
shutdown
|
shutdown
|
||||||
.register(async move {
|
.register(async move {
|
||||||
let client = OpenIdClient::new();
|
let client = OpenIdClient::new();
|
||||||
|
@ -283,10 +266,7 @@ impl AsyncComponent for Login {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.refresh_token = None;
|
self.refresh_token = None;
|
||||||
let keyring = KEYRING.get().unwrap();
|
let _ = crate::keyring::keyring_delete_all_items().await;
|
||||||
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();
|
||||||
|
@ -388,27 +368,21 @@ impl Login {
|
||||||
.drop_on_shutdown()
|
.drop_on_shutdown()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let future = async {
|
|
||||||
self.refresh_token = Some(res.refresh_token);
|
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() {
|
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 credentials_model = self.shared_id_token.lock().await;
|
||||||
let mut credentials_model = credentials_model.write();
|
let mut credentials_model = credentials_model.write();
|
||||||
|
|
||||||
*credentials_model = Some(res.id_token);
|
*credentials_model = Some(res.id_token);
|
||||||
}
|
}
|
||||||
future.await;
|
|
||||||
}
|
}
|
||||||
Err(res) => {
|
Err(res) => {
|
||||||
// We disarm the webkit flow/aka breaking the application. We want to reduce invalid requests
|
// We disarm the webkit flow/aka breaking the application. We want to reduce invalid requests
|
||||||
|
|
294
paket/src/packstation.rs
Normal file
294
paket/src/packstation.rs
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
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,9 +2,7 @@ use adw::prelude::*;
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
account::{AccountOutput, AccountServices, AccountView},
|
account::{AccountOutput, AccountServices, AccountView}, advices::{AdvicesView, AdvicesViewInput}, packstation::{PackstationView, PackstationViewInput, PackstationViewOutput}, tracking::{TrackingInput, TrackingOutput, TrackingView}
|
||||||
advices::{AdvicesView, AdvicesViewInput},
|
|
||||||
tracking::{TrackingInput, TrackingOutput, TrackingView},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
|
@ -12,12 +10,15 @@ pub struct Ready {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
have_service_advices: bool,
|
have_service_advices: bool,
|
||||||
have_service_tracking: bool,
|
have_service_tracking: bool,
|
||||||
|
have_service_packstation: bool,
|
||||||
|
|
||||||
#[do_not_track]
|
#[do_not_track]
|
||||||
account_component: Controller<AccountView>,
|
account_component: Controller<AccountView>,
|
||||||
#[do_not_track]
|
#[do_not_track]
|
||||||
advices_component: AsyncController<AdvicesView>,
|
advices_component: AsyncController<AdvicesView>,
|
||||||
#[do_not_track]
|
#[do_not_track]
|
||||||
|
packstation_component: Controller<PackstationView>,
|
||||||
|
#[do_not_track]
|
||||||
tracking_component: Controller<TrackingView>,
|
tracking_component: Controller<TrackingView>,
|
||||||
#[do_not_track]
|
#[do_not_track]
|
||||||
toast_overlay: adw::ToastOverlay,
|
toast_overlay: adw::ToastOverlay,
|
||||||
|
@ -39,6 +40,7 @@ pub enum ReadyCmds {
|
||||||
pub enum ReadyInput {
|
pub enum ReadyInput {
|
||||||
LoggedIn,
|
LoggedIn,
|
||||||
LoggedOut,
|
LoggedOut,
|
||||||
|
NavigationPageTemp(adw::NavigationPage),
|
||||||
HaveService(AccountServices),
|
HaveService(AccountServices),
|
||||||
ServiceBorked(AccountServices),
|
ServiceBorked(AccountServices),
|
||||||
}
|
}
|
||||||
|
@ -52,73 +54,77 @@ impl Component for Ready {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
adw::Bin {
|
&adw::NavigationView {
|
||||||
#[wrap(Some)]
|
add = &adw::NavigationPage {
|
||||||
set_child = &adw::NavigationView {
|
set_title: "",
|
||||||
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),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::ToolbarView {
|
||||||
|
add_top_bar = ready_headerbar = &adw::HeaderBar {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_content = &model.toast_overlay.clone() -> adw::ToastOverlay {
|
set_title_widget = ready_switchertop = &adw::ViewSwitcher{
|
||||||
#[wrap(Some)]
|
set_policy: adw::ViewSwitcherPolicy::Wide,
|
||||||
#[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.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),
|
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,
|
||||||
|
} -> 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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
add_bottom_bar = ready_switcherbar = &adw::ViewSwitcherBar {
|
||||||
|
set_stack: Some(&ready_view_stack),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
|
@ -130,22 +136,28 @@ impl Component for Ready {
|
||||||
|
|
||||||
let tracking_component = TrackingView::builder()
|
let tracking_component = TrackingView::builder()
|
||||||
.launch(init.clone())
|
.launch(init.clone())
|
||||||
.forward(&sender.input_sender(), convert_tracking_output);
|
.forward(sender.input_sender(), convert_tracking_output);
|
||||||
|
|
||||||
let account_component = AccountView::builder()
|
let account_component = AccountView::builder()
|
||||||
.launch(init.clone())
|
.launch(init.clone())
|
||||||
.forward(&sender.input_sender(), convert_account_output);
|
.forward(sender.input_sender(), convert_account_output);
|
||||||
|
|
||||||
|
let packstation_component = PackstationView::builder()
|
||||||
|
.launch(init.clone())
|
||||||
|
.forward(sender.input_sender(), convert_packstation_output);
|
||||||
|
|
||||||
let toast_overlay = adw::ToastOverlay::new();
|
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,
|
||||||
|
have_service_packstation: false,
|
||||||
logged_in: false,
|
logged_in: false,
|
||||||
|
|
||||||
account_component,
|
account_component,
|
||||||
advices_component,
|
advices_component,
|
||||||
tracking_component,
|
tracking_component,
|
||||||
|
packstation_component,
|
||||||
toast_overlay,
|
toast_overlay,
|
||||||
|
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
|
@ -176,7 +188,7 @@ impl Component for Ready {
|
||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
|
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
|
@ -195,6 +207,10 @@ impl Component for Ready {
|
||||||
self.set_have_service_tracking(true);
|
self.set_have_service_tracking(true);
|
||||||
self.tracking_component.emit(TrackingInput::Search(None))
|
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 {
|
ReadyInput::ServiceBorked(service) => match service {
|
||||||
AccountServices::Advices => {
|
AccountServices::Advices => {
|
||||||
|
@ -205,7 +221,7 @@ impl Component for Ready {
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
self.set_have_service_advices(false);
|
self.set_have_service_advices(false);
|
||||||
}
|
},
|
||||||
AccountServices::SendungVerfolgung => {
|
AccountServices::SendungVerfolgung => {
|
||||||
self.toast_overlay.add_toast(
|
self.toast_overlay.add_toast(
|
||||||
adw::Toast::builder()
|
adw::Toast::builder()
|
||||||
|
@ -214,8 +230,12 @@ impl Component for Ready {
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
self.set_have_service_tracking(false);
|
self.set_have_service_tracking(false);
|
||||||
}
|
},
|
||||||
|
_ => (),
|
||||||
},
|
},
|
||||||
|
ReadyInput::NavigationPageTemp(page) => {
|
||||||
|
root.push(&page);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.changed_logged_in() {
|
if self.changed_logged_in() {
|
||||||
|
@ -223,7 +243,11 @@ impl Component for Ready {
|
||||||
sender.output(ReadyOutput::Ready).unwrap();
|
sender.output(ReadyOutput::Ready).unwrap();
|
||||||
} else {
|
} else {
|
||||||
self.advices_component.emit(AdvicesViewInput::Reset);
|
self.advices_component.emit(AdvicesViewInput::Reset);
|
||||||
|
self.packstation_component.emit(PackstationViewInput::Reset);
|
||||||
self.tracking_component.emit(TrackingInput::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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -242,3 +266,9 @@ fn convert_account_output(value: AccountOutput) -> ReadyInput {
|
||||||
AccountOutput::HaveService(service) => ReadyInput::HaveService(service),
|
AccountOutput::HaveService(service) => ReadyInput::HaveService(service),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_packstation_output(value: PackstationViewOutput) -> ReadyInput {
|
||||||
|
match value {
|
||||||
|
PackstationViewOutput::NavigationPage(navigation_page) => ReadyInput::NavigationPageTemp(navigation_page),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
304
paket/src/scanner.rs
Normal file
304
paket/src/scanner.rs
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
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,7 +78,8 @@ impl Component for TrackingView {
|
||||||
|
|
||||||
#[local_ref]
|
#[local_ref]
|
||||||
tracking_box -> gtk::Box {
|
tracking_box -> gtk::Box {
|
||||||
set_spacing: 8
|
set_spacing: 8,
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue