feat: initial packstation registration flow
This commit is contained in:
parent
11d7cb2ef4
commit
87730dbc43
7 changed files with 721 additions and 73 deletions
|
@ -1,3 +1,3 @@
|
|||
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"]
|
||||
|
|
|
@ -37,6 +37,7 @@ pub enum AccountInput {
|
|||
pub enum AccountServices {
|
||||
Advices,
|
||||
SendungVerfolgung,
|
||||
PackstationAvailable,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -206,9 +207,26 @@ impl Component for AccountView {
|
|||
}
|
||||
AccountCmd::GotCustomerDataFull(data) => match data {
|
||||
Ok(data) => {
|
||||
if let Some(services) = &data.requested_services {
|
||||
for service in services {
|
||||
match service {
|
||||
libpaket::stammdaten::CustomerDataService::Packstation => {
|
||||
sender.output(AccountOutput::HaveService(
|
||||
AccountServices::PackstationAvailable,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
for service in &data.common.services {
|
||||
match service {
|
||||
libpaket::stammdaten::CustomerDataService::Packstation => (),
|
||||
libpaket::stammdaten::CustomerDataService::Packstation => sender
|
||||
.output(AccountOutput::HaveService(
|
||||
AccountServices::PackstationAvailable,
|
||||
))
|
||||
.unwrap(),
|
||||
libpaket::stammdaten::CustomerDataService::Paketankuendigung => sender
|
||||
.output(AccountOutput::HaveService(
|
||||
AccountServices::SendungVerfolgung,
|
||||
|
|
|
@ -169,7 +169,7 @@ fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
|||
|
||||
fn main() {
|
||||
RELM_THREADS.set(4).unwrap();
|
||||
gtk::init().unwrap();
|
||||
aperture::init(paket::constants::APP_ID);
|
||||
let display = gtk::gdk::Display::default().unwrap();
|
||||
let theme = gtk::IconTheme::for_display(&display);
|
||||
theme.add_resource_path("/de/j4ne/Paket/icons/");
|
||||
|
|
|
@ -4,7 +4,9 @@ pub mod advices;
|
|||
pub mod constants;
|
||||
pub mod keyring;
|
||||
pub mod login;
|
||||
pub mod packstation;
|
||||
pub mod ready;
|
||||
pub mod scanner;
|
||||
pub mod tracking;
|
||||
|
||||
pub use login::LoginSharedState;
|
||||
|
|
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 crate::{
|
||||
account::{AccountOutput, AccountServices, AccountView},
|
||||
advices::{AdvicesView, AdvicesViewInput},
|
||||
tracking::{TrackingInput, TrackingOutput, TrackingView},
|
||||
account::{AccountOutput, AccountServices, AccountView}, advices::{AdvicesView, AdvicesViewInput}, packstation::{PackstationView, PackstationViewInput, PackstationViewOutput}, tracking::{TrackingInput, TrackingOutput, TrackingView}
|
||||
};
|
||||
|
||||
#[tracker::track]
|
||||
|
@ -12,12 +10,15 @@ pub struct Ready {
|
|||
logged_in: bool,
|
||||
have_service_advices: bool,
|
||||
have_service_tracking: bool,
|
||||
have_service_packstation: bool,
|
||||
|
||||
#[do_not_track]
|
||||
account_component: Controller<AccountView>,
|
||||
#[do_not_track]
|
||||
advices_component: AsyncController<AdvicesView>,
|
||||
#[do_not_track]
|
||||
packstation_component: Controller<PackstationView>,
|
||||
#[do_not_track]
|
||||
tracking_component: Controller<TrackingView>,
|
||||
#[do_not_track]
|
||||
toast_overlay: adw::ToastOverlay,
|
||||
|
@ -39,6 +40,7 @@ pub enum ReadyCmds {
|
|||
pub enum ReadyInput {
|
||||
LoggedIn,
|
||||
LoggedOut,
|
||||
NavigationPageTemp(adw::NavigationPage),
|
||||
HaveService(AccountServices),
|
||||
ServiceBorked(AccountServices),
|
||||
}
|
||||
|
@ -52,9 +54,7 @@ impl Component for Ready {
|
|||
|
||||
view! {
|
||||
#[root]
|
||||
adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::NavigationView {
|
||||
&adw::NavigationView {
|
||||
add = &adw::NavigationPage {
|
||||
set_title: "",
|
||||
|
||||
|
@ -89,8 +89,6 @@ impl Component for Ready {
|
|||
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"),
|
||||
|
@ -100,6 +98,17 @@ impl Component for Ready {
|
|||
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"),
|
||||
|
@ -116,9 +125,6 @@ impl Component for Ready {
|
|||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
fn init(
|
||||
|
@ -130,22 +136,28 @@ impl Component for Ready {
|
|||
|
||||
let tracking_component = TrackingView::builder()
|
||||
.launch(init.clone())
|
||||
.forward(&sender.input_sender(), convert_tracking_output);
|
||||
.forward(sender.input_sender(), convert_tracking_output);
|
||||
|
||||
let account_component = AccountView::builder()
|
||||
.launch(init.clone())
|
||||
.forward(&sender.input_sender(), convert_account_output);
|
||||
.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 model = Ready {
|
||||
have_service_advices: false,
|
||||
have_service_tracking: true,
|
||||
have_service_packstation: false,
|
||||
logged_in: false,
|
||||
|
||||
account_component,
|
||||
advices_component,
|
||||
tracking_component,
|
||||
packstation_component,
|
||||
toast_overlay,
|
||||
|
||||
tracker: 0,
|
||||
|
@ -176,7 +188,7 @@ impl Component for Ready {
|
|||
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();
|
||||
|
||||
match message {
|
||||
|
@ -195,6 +207,10 @@ impl Component for Ready {
|
|||
self.set_have_service_tracking(true);
|
||||
self.tracking_component.emit(TrackingInput::Search(None))
|
||||
}
|
||||
AccountServices::PackstationAvailable => {
|
||||
self.set_have_service_packstation(true);
|
||||
self.packstation_component.emit(PackstationViewInput::Init);
|
||||
}
|
||||
},
|
||||
ReadyInput::ServiceBorked(service) => match service {
|
||||
AccountServices::Advices => {
|
||||
|
@ -205,7 +221,7 @@ impl Component for Ready {
|
|||
.build(),
|
||||
);
|
||||
self.set_have_service_advices(false);
|
||||
}
|
||||
},
|
||||
AccountServices::SendungVerfolgung => {
|
||||
self.toast_overlay.add_toast(
|
||||
adw::Toast::builder()
|
||||
|
@ -214,8 +230,12 @@ impl Component for Ready {
|
|||
.build(),
|
||||
);
|
||||
self.set_have_service_tracking(false);
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
},
|
||||
ReadyInput::NavigationPageTemp(page) => {
|
||||
root.push(&page);
|
||||
}
|
||||
};
|
||||
|
||||
if self.changed_logged_in() {
|
||||
|
@ -223,7 +243,11 @@ impl Component for Ready {
|
|||
sender.output(ReadyOutput::Ready).unwrap();
|
||||
} else {
|
||||
self.advices_component.emit(AdvicesViewInput::Reset);
|
||||
self.packstation_component.emit(PackstationViewInput::Reset);
|
||||
self.tracking_component.emit(TrackingInput::Reset);
|
||||
self.set_have_service_advices(false);
|
||||
self.set_have_service_tracking(true);
|
||||
self.set_have_service_packstation(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,3 +266,9 @@ fn convert_account_output(value: AccountOutput) -> ReadyInput {
|
|||
AccountOutput::HaveService(service) => ReadyInput::HaveService(service),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_packstation_output(value: PackstationViewOutput) -> ReadyInput {
|
||||
match value {
|
||||
PackstationViewOutput::NavigationPage(navigation_page) => ReadyInput::NavigationPageTemp(navigation_page),
|
||||
}
|
||||
}
|
||||
|
|
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue