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",
|
"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", "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?
|
# TODO: Consolidate?
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
random-string = "1.1.0"
|
random-string = "1.1.0"
|
||||||
reqwest = { workspace = true }
|
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "http2"] }
|
||||||
secrecy = { workspace = true}
|
secrecy = { version = "0.8.0", features = ["serde"] }
|
||||||
serde = { workspace = true }
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = "1.0.111"
|
||||||
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("unable to decode: {0}")]
|
#[error("internal 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,53 +1,41 @@
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use ed25519_dalek::Signer;
|
|
||||||
use ed25519_dalek::SigningKey;
|
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use secrecy::zeroize::Zeroize;
|
|
||||||
use secrecy::ExposeSecret;
|
|
||||||
use secrecy::SecretBox;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use ed25519_dalek::SigningKey;
|
||||||
|
use ed25519_dalek::Signer;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CustomerKeySeed {
|
pub struct CustomerKeySeed {
|
||||||
pub postnumber: String,
|
pub postnumber: String,
|
||||||
pub seed: SecretBox<Seed>,
|
pub seed: Seed,
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub device_id: Option<String>,
|
pub device_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Zeroize for CustomerKeySeed {
|
pub struct Seed {
|
||||||
fn zeroize(&mut self) {
|
bytes: Vec<u8>,
|
||||||
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.0[..]).try_into().unwrap()
|
(&self.bytes[..]).try_into().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +43,7 @@ impl CustomerKeySeed {
|
||||||
pub fn new(postnumber: String) -> Self {
|
pub fn new(postnumber: String) -> Self {
|
||||||
CustomerKeySeed {
|
CustomerKeySeed {
|
||||||
postnumber,
|
postnumber,
|
||||||
seed: SecretBox::new(Box::new(Seed::random())),
|
seed: Seed::random(),
|
||||||
uuid: uuid::Uuid::new_v4(),
|
uuid: uuid::Uuid::new_v4(),
|
||||||
device_id: None,
|
device_id: None,
|
||||||
}
|
}
|
||||||
|
@ -64,7 +52,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: SecretBox::new(Box::new(Seed::from(seed))),
|
seed: Seed::from_bytes(seed),
|
||||||
uuid: uuid.clone(),
|
uuid: uuid.clone(),
|
||||||
device_id: Some(device_id),
|
device_id: Some(device_id),
|
||||||
}
|
}
|
||||||
|
@ -77,15 +65,17 @@ 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.expose_secret().as_bytes());
|
let signing_key = SigningKey::from_bytes(self.seed.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.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_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,11 +4,8 @@ use sha2::Sha256;
|
||||||
|
|
||||||
use crate::{LibraryResult, LibraryError};
|
use crate::{LibraryResult, LibraryError};
|
||||||
|
|
||||||
pub struct RegToken(Vec<u8>);
|
pub struct RegToken {
|
||||||
impl secrecy::zeroize::Zeroize for RegToken {
|
token: Vec<u8>,
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.0.zeroize();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegToken {
|
impl RegToken {
|
||||||
|
@ -35,15 +32,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.0[32..])
|
general_purpose::STANDARD.encode(&self.token[32..])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hmac(&self) -> SimpleHmac<Sha256> {
|
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");
|
.expect("HMAC can take key of any size");
|
||||||
|
|
||||||
mac
|
mac
|
||||||
|
|
|
@ -7,19 +7,14 @@ 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 = { workspace = true }
|
relm4 = { version = "0.9", features = [ "libadwaita", "macros" ], git = "https://github.com/Relm4/Relm4.git" }
|
||||||
relm4-icons = { version = "0.9" }
|
relm4-icons = { version = "0.9", git = "https://github.com/Relm4/icons.git" }
|
||||||
tracker = "0.2"
|
tracker = "0.2"
|
||||||
adw = { package = "libadwaita", version = "0.7", features = ["v1_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,7 +37,6 @@ pub enum AccountInput {
|
||||||
pub enum AccountServices {
|
pub enum AccountServices {
|
||||||
Advices,
|
Advices,
|
||||||
SendungVerfolgung,
|
SendungVerfolgung,
|
||||||
PackstationAvailable,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -207,26 +206,9 @@ 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 => sender
|
libpaket::stammdaten::CustomerDataService::Packstation => (),
|
||||||
.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,41 +56,31 @@ impl AsyncComponent for App {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_content = &adw::ViewStack {
|
set_content = &adw::ViewStack {
|
||||||
#[name = "page_loading"]
|
#[name = "page_loading"]
|
||||||
add = &adw::ToolbarView {
|
add = &adw::Bin {
|
||||||
add_top_bar = &adw::HeaderBar {},
|
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_content = &adw::Bin {
|
set_child = &adw::Spinner {}
|
||||||
#[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::NavigationView {},
|
add = page_ready -> adw::Bin {},
|
||||||
|
|
||||||
#[name = "page_error"]
|
#[name = "page_error"]
|
||||||
add = &adw::ToolbarView {
|
add = &adw::Bin {
|
||||||
add_top_bar = &adw::HeaderBar {},
|
#[name = "page_error_status"]
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_content = &adw::Bin {
|
set_child = &adw::StatusPage {}
|
||||||
#[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 = match model.state {
|
let page: &adw::Bin = match model.state {
|
||||||
AppState::Loading => page_loading.widget_ref(),
|
AppState::Loading => page_loading.as_ref(),
|
||||||
AppState::RequiresLogin => page_login.widget_ref(),
|
AppState::RequiresLogin => page_login.as_ref(),
|
||||||
AppState::Ready => page_ready.widget_ref(),
|
AppState::Ready => page_ready.as_ref(),
|
||||||
AppState::Error => page_error.widget_ref(),
|
AppState::Error => page_error.as_ref(),
|
||||||
};
|
};
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
|
@ -140,8 +130,8 @@ impl AsyncComponent for App {
|
||||||
self.reset();
|
self.reset();
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -165,14 +155,8 @@ 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 {
|
LoginOutput::Error(library_error) => AppInput::FatalErr(AppError { short: "Unhandled API error".to_string(), long: library_error.to_string() }),
|
||||||
short: "Unhandled API error".to_string(),
|
LoginOutput::KeyringError(error) => AppInput::FatalErr(AppError { short: "Keyring usage failed".to_string(), long: 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() {
|
fn main() {
|
||||||
RELM_THREADS.set(4).unwrap();
|
RELM_THREADS.set(4).unwrap();
|
||||||
aperture::init(paket::constants::APP_ID);
|
gtk::init().unwrap();
|
||||||
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/");
|
||||||
|
|
|
@ -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 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,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 adw::prelude::*;
|
||||||
use libpaket::{
|
use libpaket::{
|
||||||
|
@ -12,7 +17,7 @@ use relm4::{
|
||||||
};
|
};
|
||||||
use webkit::{prelude::WebViewExt, URIRequest, WebContext, WebView};
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum LoginInput {
|
pub enum LoginInput {
|
||||||
|
@ -74,19 +79,8 @@ pub enum LoginCommand {
|
||||||
NeedsRefresh,
|
NeedsRefresh,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! keyring_result_get {
|
const KEYRING_ATTRIBUTES: [(&str, &str); 2] =
|
||||||
($sender: ident, $caller: expr, $code: expr) => {{
|
[("app", crate::constants::APP_ID), ("type", "refresh_token")];
|
||||||
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 {
|
||||||
|
@ -181,24 +175,47 @@ impl AsyncComponent for Login {
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = keyring_result_get!(sender, oo7::Keyring::new().await, |keyring| {
|
let result = oo7::Keyring::new().await;
|
||||||
crate::keyring::KEYRING.set(keyring).unwrap();
|
match result {
|
||||||
});
|
Ok(keyring) => {
|
||||||
|
KEYRING.set(keyring).unwrap();
|
||||||
if keyring_is_available() {
|
if let Err(err) = KEYRING.get().unwrap().unlock().await {
|
||||||
let refresh_token =
|
sender
|
||||||
keyring_result_get!(sender, keyring_get_refresh_token().await, move |value| {
|
.output(LoginOutput::KeyringError(err))
|
||||||
return value;
|
.expect("sender not worky");
|
||||||
});
|
|
||||||
if let Ok(value) = refresh_token {
|
|
||||||
model.refresh_token = value;
|
|
||||||
if model.refresh_token.is_some() {
|
|
||||||
sender.input(LoginInput::NeedsRefresh);
|
|
||||||
} else {
|
} 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();
|
let webcontext = WebContext::builder().build();
|
||||||
{
|
{
|
||||||
|
@ -256,7 +273,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(|_, shutdown| {
|
sender.command(|out, shutdown| {
|
||||||
shutdown
|
shutdown
|
||||||
.register(async move {
|
.register(async move {
|
||||||
let client = OpenIdClient::new();
|
let client = OpenIdClient::new();
|
||||||
|
@ -266,7 +283,10 @@ impl AsyncComponent for Login {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.refresh_token = None;
|
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 => {
|
LoginInput::NeedsRefresh => {
|
||||||
let refresh_token = self.refresh_token.as_ref().unwrap().clone();
|
let refresh_token = self.refresh_token.as_ref().unwrap().clone();
|
||||||
|
@ -368,21 +388,27 @@ 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
|
||||||
|
|
|
@ -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 relm4::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
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]
|
#[tracker::track]
|
||||||
|
@ -10,15 +12,12 @@ 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,
|
||||||
|
@ -40,7 +39,6 @@ pub enum ReadyCmds {
|
||||||
pub enum ReadyInput {
|
pub enum ReadyInput {
|
||||||
LoggedIn,
|
LoggedIn,
|
||||||
LoggedOut,
|
LoggedOut,
|
||||||
NavigationPageTemp(adw::NavigationPage),
|
|
||||||
HaveService(AccountServices),
|
HaveService(AccountServices),
|
||||||
ServiceBorked(AccountServices),
|
ServiceBorked(AccountServices),
|
||||||
}
|
}
|
||||||
|
@ -54,77 +52,73 @@ impl Component for Ready {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
&adw::NavigationView {
|
adw::Bin {
|
||||||
add = &adw::NavigationPage {
|
#[wrap(Some)]
|
||||||
set_title: "",
|
set_child = &adw::NavigationView {
|
||||||
|
add = &adw::NavigationPage {
|
||||||
#[wrap(Some)]
|
set_title: "",
|
||||||
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)]
|
#[wrap(Some)]
|
||||||
set_content = &model.toast_overlay.clone() -> adw::ToastOverlay {
|
set_child = &adw::ToolbarView {
|
||||||
#[wrap(Some)]
|
add_top_bar = ready_headerbar = &adw::HeaderBar {
|
||||||
#[name = "ready_view_stack"]
|
#[wrap(Some)]
|
||||||
set_child = &adw::ViewStack {
|
set_title_widget = ready_switchertop = &adw::ViewSwitcher{
|
||||||
add = &model.advices_component.widget().clone() -> gtk::ScrolledWindow {
|
set_policy: adw::ViewSwitcherPolicy::Wide,
|
||||||
#[track(model.changed_have_service_advices())]
|
set_stack: Some(&ready_view_stack),
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
#[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 {
|
} -> page_tracking: adw::ViewStackPage {
|
||||||
set_stack: Some(&ready_view_stack),
|
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(
|
fn init(
|
||||||
|
@ -136,28 +130,22 @@ 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,
|
||||||
|
@ -188,7 +176,7 @@ impl Component for Ready {
|
||||||
ComponentParts { model, widgets }
|
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();
|
self.reset();
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
|
@ -207,10 +195,6 @@ 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 => {
|
||||||
|
@ -221,7 +205,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()
|
||||||
|
@ -230,12 +214,8 @@ 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() {
|
||||||
|
@ -243,11 +223,7 @@ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -266,9 +242,3 @@ 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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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]
|
#[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