From 87730dbc43a8996f9f7dce187671a586cc204acf Mon Sep 17 00:00:00 2001 From: jane400 Date: Wed, 2 Oct 2024 17:40:48 +0200 Subject: [PATCH] feat: initial packstation registration flow --- icons.toml | 2 +- paket/src/account.rs | 20 ++- paket/src/bin/paket.rs | 2 +- paket/src/lib.rs | 2 + paket/src/packstation.rs | 294 +++++++++++++++++++++++++++++++++++++ paket/src/ready.rs | 170 +++++++++++++--------- paket/src/scanner.rs | 304 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 721 insertions(+), 73 deletions(-) create mode 100644 paket/src/packstation.rs create mode 100644 paket/src/scanner.rs diff --git a/icons.toml b/icons.toml index 87593b8..6a2ae04 100644 --- a/icons.toml +++ b/icons.toml @@ -1,3 +1,3 @@ app_id = "de.j4ne.Paket" -icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy"] \ No newline at end of file +icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy", "qr-code-scanner"] diff --git a/paket/src/account.rs b/paket/src/account.rs index 40dafff..7bd6a96 100644 --- a/paket/src/account.rs +++ b/paket/src/account.rs @@ -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, diff --git a/paket/src/bin/paket.rs b/paket/src/bin/paket.rs index 5f7d857..ff0c5c3 100644 --- a/paket/src/bin/paket.rs +++ b/paket/src/bin/paket.rs @@ -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/"); diff --git a/paket/src/lib.rs b/paket/src/lib.rs index c116a5f..7d607fc 100644 --- a/paket/src/lib.rs +++ b/paket/src/lib.rs @@ -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; diff --git a/paket/src/packstation.rs b/paket/src/packstation.rs new file mode 100644 index 0000000..43bc136 --- /dev/null +++ b/paket/src/packstation.rs @@ -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, + #[do_not_track] + login: LoginSharedState, + + #[do_not_track] + scanner: AsyncController, +} + +#[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 Packstation service 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, + ) -> ComponentParts { + 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, + 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, + 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, + ) -> libpaket::LibraryResult> { + 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), + } +} diff --git a/paket/src/ready.rs b/paket/src/ready.rs index fecb19a..1b07b78 100644 --- a/paket/src/ready.rs +++ b/paket/src/ready.rs @@ -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, #[do_not_track] advices_component: AsyncController, #[do_not_track] + packstation_component: Controller, + #[do_not_track] tracking_component: Controller, #[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,73 +54,77 @@ impl Component for Ready { view! { #[root] - adw::Bin { - #[wrap(Some)] - set_child = &adw::NavigationView { - add = &adw::NavigationPage { - set_title: "", - - #[wrap(Some)] - set_child = &adw::ToolbarView { - add_top_bar = ready_headerbar = &adw::HeaderBar { - #[wrap(Some)] - set_title_widget = ready_switchertop = &adw::ViewSwitcher{ - set_policy: adw::ViewSwitcherPolicy::Wide, - set_stack: Some(&ready_view_stack), - } - }, + &adw::NavigationView { + add = &adw::NavigationPage { + set_title: "", + #[wrap(Some)] + set_child = &adw::ToolbarView { + add_top_bar = ready_headerbar = &adw::HeaderBar { #[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.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_title_widget = ready_switchertop = &adw::ViewSwitcher{ + set_policy: adw::ViewSwitcherPolicy::Wide, set_stack: Some(&ready_view_stack), } + }, + + #[wrap(Some)] + set_content = &model.toast_overlay.clone() -> adw::ToastOverlay { + #[wrap(Some)] + #[name = "ready_view_stack"] + set_child = &adw::ViewStack { + add = &model.advices_component.widget().clone() -> gtk::ScrolledWindow { + #[track(model.changed_have_service_advices())] + set_visible: model.have_service_advices, + + } -> page_advices: adw::ViewStackPage { + set_title: Some("Mail notification"), + set_name: Some("page_advices"), + set_icon_name: Some(relm4_icons::icon_names::MAIL), + + #[track(model.changed_have_service_advices())] + set_visible: model.have_service_advices, + }, + + add = &model.tracking_component.widget().clone() -> adw::ToastOverlay { + #[track(model.changed_have_service_tracking())] + set_visible: model.have_service_tracking, + } -> 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( @@ -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::Root) { + fn update(&mut self, message: Self::Input, sender: ComponentSender, 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), + } +} diff --git a/paket/src/scanner.rs b/paket/src/scanner.rs new file mode 100644 index 0000000..c2c8777 --- /dev/null +++ b/paket/src/scanner.rs @@ -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) -> 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, + #[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, + ) -> AsyncComponentParts { + 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, + ) { + 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::() + .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::() + .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 for ErrorInternal { + fn from(value: aperture::PipewireError) -> Self { + ErrorInternal::Pipewire(value) + } + } + + impl From for ErrorInternal { + fn from(value: aperture::ProviderError) -> Self { + ErrorInternal::Provider(value) + } + } +}