diff --git a/src/ui/install_wivrn_box.rs b/src/ui/install_wivrn_box.rs index ddf37f7..cf63f7c 100644 --- a/src/ui/install_wivrn_box.rs +++ b/src/ui/install_wivrn_box.rs @@ -1,4 +1,4 @@ -use super::alert::alert; +use super::{alert::alert, ADB_ERR_NO_DEV}; use crate::{ async_process::async_process, depcheck::common::dep_adb, @@ -9,8 +9,6 @@ use gtk::prelude::*; use relm4::{new_action_group, new_stateless_action, prelude::*}; use std::fs::remove_file; -const ADB_ERR_NO_DEV: &str = "no devices/emulators found"; - const WIVRN_LATEST_RELEASE_APK_URL: &str = "https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk"; diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 106f99c..cf66649 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -10,6 +10,7 @@ use super::{ steam_launch_options_box::{SteamLaunchOptionsBox, SteamLaunchOptionsBoxMsg}, steamvr_calibration_box::{SteamVrCalibrationBox, SteamVrCalibrationBoxMsg}, util::{limit_dropdown_width, warning_heading}, + wivrn_wired_start_box::{WivrnWiredStartBox, WivrnWiredStartBoxInit, WivrnWiredStartBoxMsg}, }; use crate::{ config::Config, @@ -44,6 +45,8 @@ pub struct MainView { #[tracker::do_not_track] install_wivrn_box: AsyncController, #[tracker::do_not_track] + wivrn_wired_start_box: AsyncController, + #[tracker::do_not_track] steam_launch_options_box: Controller, #[tracker::do_not_track] devices_box: Controller, @@ -364,6 +367,7 @@ impl SimpleComponent for MainView { }, model.steam_launch_options_box.widget(), + model.wivrn_wired_start_box.widget(), model.install_wivrn_box.widget(), model.steamvr_calibration_box.widget(), @@ -494,6 +498,9 @@ impl SimpleComponent for MainView { self.install_wivrn_box .sender() .emit(InstallWivrnBoxMsg::UpdateSelectedProfile(prof.clone())); + self.wivrn_wired_start_box + .sender() + .emit(WivrnWiredStartBoxMsg::UpdateSelectedProfile(prof.clone())); } Self::Input::UpdateProfiles(profiles, config) => { self.set_profiles(profiles); @@ -789,6 +796,12 @@ impl SimpleComponent for MainView { root_win: init.root_win.clone(), }) .detach(), + wivrn_wired_start_box: WivrnWiredStartBox::builder() + .launch(WivrnWiredStartBoxInit { + selected_profile: init.selected_profile.clone(), + root_win: init.root_win.clone(), + }) + .detach(), devices_box: DevicesBox::builder().launch(()).detach(), selected_profile: init.selected_profile.clone(), profile_not_editable_dialog, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3cf52a0..1d34c35 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -19,7 +19,9 @@ mod steamvr_calibration_box; mod term_widget; mod util; mod wivrn_conf_editor; -pub mod wivrn_encoder_presets_win; +mod wivrn_encoder_presets_win; +mod wivrn_wired_start_box; pub const SENDER_IO_ERR_MSG: &str = "relm4 sender i/o failed"; pub const ADW_DIALOG_WIDTH: i32 = 600; +pub const ADB_ERR_NO_DEV: &str = "no devices/emulators found"; diff --git a/src/ui/wivrn_wired_start_box.rs b/src/ui/wivrn_wired_start_box.rs new file mode 100644 index 0000000..94799f0 --- /dev/null +++ b/src/ui/wivrn_wired_start_box.rs @@ -0,0 +1,184 @@ +use super::{alert::alert, ADB_ERR_NO_DEV}; +use crate::{ + async_process::async_process, + constants::APP_NAME, + depcheck::common::dep_adb, + profile::{Profile, XRServiceType}, +}; +use gtk::prelude::*; +use relm4::prelude::*; + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum StartClientStatus { + Success, + Done(Option), + InProgress, +} + +#[tracker::track] +pub struct WivrnWiredStartBox { + selected_profile: Profile, + start_client_status: StartClientStatus, + #[tracker::do_not_track] + root_win: gtk::Window, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum WivrnWiredStartBoxMsg { + UpdateSelectedProfile(Profile), + /// prepares state for async action, calls DoStartClient + StartWivrnClient, + /// actually sends the adb commands to start the client + DoStartClient, +} + +#[derive(Debug)] +pub struct WivrnWiredStartBoxInit { + pub selected_profile: Profile, + pub root_win: gtk::Window, +} + +#[relm4::component(pub async)] +impl AsyncComponent for WivrnWiredStartBox { + type Init = WivrnWiredStartBoxInit; + type Input = WivrnWiredStartBoxMsg; + type Output = (); + type CommandOutput = (); + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 12, + add_css_class: "card", + add_css_class: "padded", + #[track = "model.changed(Self::selected_profile())"] + set_visible: model.selected_profile.xrservice_type == XRServiceType::Wivrn, + gtk::Label { + add_css_class: "heading", + set_hexpand: true, + set_xalign: 0.0, + set_label: "Start WiVRn Client (Wired)", + set_wrap: true, + set_wrap_mode: gtk::pango::WrapMode::Word, + }, + gtk::Label { + add_css_class: "dim-label", + set_hexpand: true, + set_label: concat!( + "Start the WiVRn client on your Android headset. This only ", + "works in wired connection mode." + ), + set_xalign: 0.0, + set_wrap: true, + set_wrap_mode: gtk::pango::WrapMode::Word, + }, + gtk::Button { + add_css_class: "suggested-action", + set_label: "Start WiVRn Client", + set_halign: gtk::Align::Start, + #[track = "model.changed(Self::start_client_status())"] + set_sensitive: model.start_client_status != StartClientStatus::InProgress, + connect_clicked[sender] => move |_| { + sender.input(Self::Input::StartWivrnClient) + }, + }, + gtk::Label { + add_css_class: "error", + set_xalign: 0.0, + set_wrap: true, + set_wrap_mode: gtk::pango::WrapMode::Word, + #[track = "model.changed(Self::start_client_status())"] + set_visible: matches!(&model.start_client_status, StartClientStatus::Done(Some(_))), + #[track = "model.changed(Self::start_client_status())"] + set_label: match &model.start_client_status { + StartClientStatus::Done(Some(err)) => err.as_str(), + _ => "", + }, + }, + } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + _root: &Self::Root, + ) { + self.reset(); + + match message { + Self::Input::UpdateSelectedProfile(p) => self.set_selected_profile(p), + Self::Input::StartWivrnClient => { + if !dep_adb().check() { + alert("ADB is not installed", Some(&format!("Please install ADB on your computer to start the WiVRn client from {}.", APP_NAME)), Some(&self.root_win)); + return; + } + self.set_start_client_status(StartClientStatus::InProgress); + sender.input(Self::Input::DoStartClient); + } + Self::Input::DoStartClient => { + let n_status = match async_process( + "sh", + Some(&[ + "-c", + concat!( + "adb reverse tcp:9757 tcp:9757 && ", + "adb shell am force-stop org.meumeu.wivrn && ", + // wait for force-stop + "sleep 1 && ", + "adb shell am start -a android.intent.action.VIEW -d \"wivrn+tcp://127.0.0.1\" org.meumeu.wivrn" + ) + ]), + None + ).await { + Ok(out) if out.exit_code == 0 => + StartClientStatus::Success + , + Ok(out) => { + if out.stdout.contains(ADB_ERR_NO_DEV) + || out.stderr.contains(ADB_ERR_NO_DEV) + { + StartClientStatus::Done(Some( + concat!( + "No devices connected. Make sure your headset is connected ", + "to this computer and USB debugging is turned on." + ) + .into(), + )) + } else { + eprintln!("Error: ADB failed with code {}.\nstdout:\n{}\n======\nstderr:\n{}", out.exit_code, out.stdout, out.stderr); + StartClientStatus::Done(Some( + format!("ADB exited with code \"{}\"", out.exit_code) + )) + } + }, + Err(e) => { + eprintln!("Error: failed to run ADB: {e}"); + StartClientStatus::Done(Some( + "Failed to run ADB".into() + )) + }, + }; + self.set_start_client_status(n_status); + } + } + } + + async fn init( + init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let model = Self { + selected_profile: init.selected_profile, + start_client_status: StartClientStatus::Done(None), + root_win: init.root_win, + tracker: 0, + }; + + let widgets = view_output!(); + + AsyncComponentParts { model, widgets } + } +}