diff --git a/Cargo.lock b/Cargo.lock index 471d5f1..78a636d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1310,6 +1310,7 @@ version = "0.5.0" dependencies = [ "anyhow", "cc", + "cmake", "cpp", "cpp_build", "cstr", diff --git a/Cargo.toml b/Cargo.toml index c3bde2b..fd1b6cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,4 @@ cpp_build = "0.5.11" walkdir = "2.5.0" winres = "0.1.12" pkg-config = "0.3.33" +cmake = "0.1.58" diff --git a/build.rs b/build.rs index 56d391c..0ee830e 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright © 2021-2022 Adrian +use cmake::build; use std::env; use std::fmt::Write; use std::path::Path; @@ -14,6 +15,7 @@ fn compile_qml(dir: &str, qt_include_path: &str, qt_library_path: &str) { config.include(&format!("{}/QtQml", qt_include_path)); println!("cargo:rerun-if-changed=src/live_reload.cpp"); + println!("cargo:rerun-if-changed=src/utils.rs"); if cfg!(target_os = "macos") { config.include(format!("{}/QtCore.framework/Headers/", qt_library_path)); @@ -116,21 +118,79 @@ fn compile_qml(dir: &str, qt_include_path: &str, qt_library_path: &str) { println!("cargo:rustc-link-lib=static:+whole-archive=qmlcache"); } +// TODO: we may need to do moc +// fn find_moc(qt_library_path: &str) -> String { +// let qt_path = Path::new(qt_library_path).parent().unwrap(); +// let candidates = [ +// qt_path.join("libexec/moc"), +// qt_path.join("../macos/libexec/moc"), +// qt_path.join("../msvc2019_64/bin/moc"), +// ]; +// for c in candidates { +// if c.exists() { +// return c.to_string_lossy().to_string(); +// } +// } +// "moc".to_string() +// } + +// fn run_moc(moc: &str, header: &str, out_dir: &Path) -> String { +// let base = Path::new(header) +// .file_stem() +// .unwrap() +// .to_string_lossy() +// .to_string(); +// let moc_cpp = out_dir.join(format!("moc_{}.cpp", base)); +// assert!( +// Command::new(moc) +// .args([header, "-o", moc_cpp.to_str().unwrap()]) +// .status() +// .unwrap() +// .success() +// ); +// moc_cpp.to_string_lossy().to_string() +// } + fn compile_bridge(qt_include_path: &str) { println!("compile_bridge"); println!("cargo:rerun-if-changed=src/bridge.cpp"); println!("cargo:rerun-if-changed=src/include/bridge.h"); + println!("cargo:rerun-if-changed=src/networkdeviceprovider.h"); + println!("cargo:rerun-if-changed=src/core/services/avahi/avahi_service.h"); + println!("cargo:rerun-if-changed=src/core/services/avahi/avahi_service.cpp"); + let dir_var = env::var("OUT_DIR").unwrap(); + // let out_dir = Path::new(&dir_var); + // let moc = find_moc(&env::var("DEP_QT_LIBRARY_PATH").unwrap()); let mut cc_build = cc::Build::new(); cc_build .cpp(true) .file("src/bridge.cpp") + // .file("src/core/services/avahi/avahi_service.cpp") + // .include("src") .include(qt_include_path) .include(format!("{}/QtCore", qt_include_path)) .include(format!("{}/QtGui", qt_include_path)) .flag_if_supported("-std=c++17") .flag_if_supported("-Wno-deprecated-declarations"); + // for f in env::var("DEP_QT_COMPILE_FLAGS") + // .unwrap() + // .split_terminator(';') + // { + // cc_build.flag(f); + // } + + // for header in [ + // "src/networkdeviceprovider.h", + // "src/core/services/avahi/avahi_service.h", + // ] { + // if Path::new(header).exists() { + // let moc_cpp = run_moc(&moc, header, out_dir); + // cc_build.file(moc_cpp); + // } + // } + if let Ok(ffmpeg_dir) = std::env::var("FFMPEG_DIR") { cc_build.include(format!("{}/include", ffmpeg_dir)); println!("cargo:rustc-link-search={}/lib", ffmpeg_dir); @@ -148,15 +208,64 @@ fn compile_bridge(qt_include_path: &str) { let _ = pkg_config::Config::new().probe("libavcodec"); let _ = pkg_config::Config::new().probe("libavutil"); let _ = pkg_config::Config::new().probe("libswscale"); + let _ = pkg_config::Config::new().probe("avahi-client"); } cc_build.compile("bridge"); - for lib in ["avformat", "avcodec", "avutil", "swscale"] { + for lib in ["avformat", "avcodec", "avutil", "swscale", "avahi-client"] { println!("cargo:rustc-link-lib={}", lib); } } +fn compile_uxplay() { + let uxplay = cmake::Config::new("lib/uxplay") + .build_target("uxplay") + //no need for x11 + .define("NO_X11_DEPS", "ON") + .build(); + + let build = uxplay.display(); + + println!("cargo:rustc-link-search=native={build}/build"); + println!("cargo:rustc-link-search=native={build}/build/lib"); + println!("cargo:rustc-link-search=native={build}/build/renderers"); + println!("cargo:rustc-link-search=native={build}/build/lib/llhttp"); + println!("cargo:rustc-link-search=native={build}/build/lib/playfair"); + + println!("cargo:rustc-link-lib=static=uxplay"); + println!("cargo:rustc-link-lib=static=renderers"); + println!("cargo:rustc-link-lib=static=airplay"); + println!("cargo:rustc-link-lib=static=llhttp"); + println!("cargo:rustc-link-lib=static=playfair"); + + // TODO: don't depend on Qt6Core + pkg_config::Config::new().probe("Qt6Core").unwrap(); + + // gst stuff + for pkg in &[ + "gstreamer-1.0", + "gstreamer-app-1.0", + "gstreamer-video-1.0", + "gstreamer-audio-1.0", + ] { + pkg_config::Config::new().probe(pkg).unwrap(); + } + + pkg_config::Config::new().probe("openssl").unwrap(); + + // Linux uses avahi + pkg_config::Config::new().probe("avahi-compat-libdns_sd").unwrap(); + + // FIXME: macOS and Windows + // println!("cargo:rustc-link-lib=dns_sd"); + + // glib + pkg_config::Config::new().probe("glib-2.0").unwrap(); + pkg_config::Config::new().probe("gobject-2.0").unwrap(); + pkg_config::Config::new().probe("libplist-2.0").unwrap(); +} + fn main() { println!("cargo:rerun-if-changed=src/main.rs"); @@ -187,6 +296,24 @@ fn main() { config.flag(f); } + let mut add_pkg_includes = |pkg: &str| { + let lib = pkg_config::Config::new() + .cargo_metadata(false) + .probe(pkg) + .unwrap_or_else(|e| panic!("pkg-config probe failed for {pkg}: {e}")); + for p in lib.include_paths { + config.include(p); + } + }; + + add_pkg_includes("gstreamer-1.0"); + add_pkg_includes("gstreamer-app-1.0"); + add_pkg_includes("gstreamer-video-1.0"); + add_pkg_includes("gstreamer-audio-1.0"); + add_pkg_includes("glib-2.0"); + add_pkg_includes("gobject-2.0"); + + let mut public_include = |name| { if cfg!(target_os = "macos") { config.include(format!("{}/{}.framework/Headers/", qt_library_path, name)); @@ -328,4 +455,5 @@ fn main() { } compile_bridge(&qt_include_path); + compile_uxplay(); } diff --git a/lib/uxplay b/lib/uxplay index 758fec4..e98981e 160000 --- a/lib/uxplay +++ b/lib/uxplay @@ -1 +1 @@ -Subproject commit 758fec4be61f854dd22fe2c16b625ca3053adb4d +Subproject commit e98981ed0926c60e1f3426b41f657a871abf92c2 diff --git a/src/afc_services.rs b/src/afc_services.rs index 1d79285..22abbd6 100644 --- a/src/afc_services.rs +++ b/src/afc_services.rs @@ -1,9 +1,10 @@ -use crate::qt_threading::{QtThread, QtThreading}; -use crate::{APP_DEVICE_STATE, RUNTIME, run_sync}; +use crate::device_ctx; +use crate::qt_threading::QtThreading; +use crate::{RUNTIME, run_sync}; use idevice::{ - IdeviceService, afc::{AfcClient, opcode::AfcFopenMode}, }; +use macros::QtThreading; use once_cell::sync::Lazy; use qmetaobject::prelude::*; use qttypes::{QStringList, QVariantMap}; @@ -16,7 +17,7 @@ use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufWriter}; use tokio::net::TcpListener; use tokio::sync::Mutex; use tokio::sync::oneshot; -#[derive(QObject)] +#[derive(QObject, QtThreading)] pub struct AfcServices { base: qt_base_class!(trait QObject), afc: Arc>, @@ -31,20 +32,18 @@ pub struct AfcServices { ), start_video_stream: qt_method!(fn(&self, file_path: QString) -> QString), delete_path: qt_method!(fn(&self, path: QString) -> bool), -} - -impl QtThreading for AfcServices { - fn qt_thread(&self) -> crate::qt_threading::QtThread - where - Self: Sized, - { - QtThread::new(self) - } + //only required for hause_arrest afc + bundle_id: qt_property!(QString), } impl AfcServices { - /* udid is for debugging purposes */ - pub fn from_afc_client(afc_client: Arc>, udid: String) -> Self { + pub fn from_afc_client( + afc_client: Arc>, + /* udid is for debugging purposes */ + udid: String, + //only required for hause_arrest afc + bundle_id: Option, + ) -> Self { Self { afc: afc_client, udid: udid, @@ -57,6 +56,7 @@ impl AfcServices { check_is_dir_and_list_finished: Default::default(), start_video_stream: Default::default(), delete_path: Default::default(), + bundle_id: bundle_id.map_or_else(QString::default, QString::from), } } @@ -325,7 +325,7 @@ impl AfcServices { let udid_for_insert = udid.clone(); // FIXME: should we do it here ? let inserted = run_sync(async move { - let maybe_device = APP_DEVICE_STATE.lock().await.get(&udid_for_insert).cloned(); + let maybe_device = device_ctx::get_device_opt(udid_for_insert).await; let device = match maybe_device { Some(d) => d, None => return false, @@ -387,7 +387,7 @@ impl AfcServices { run_sync(async move { let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let maybe_device = device_ctx::get_device_opt(&udid).await; let device = match maybe_device { Some(d) => d, diff --git a/src/airplay.rs b/src/airplay.rs new file mode 100644 index 0000000..6c2e244 --- /dev/null +++ b/src/airplay.rs @@ -0,0 +1,78 @@ +use crate::{RUNTIME, qt_threading::QtThreading, qvariantmap_insert}; +use idevice::utils; +use ipatool::Account; +use ipatool::IpaTool; +use macros::QtThreading; +use qmetaobject::prelude::*; +use qttypes::{QStringList, QVariantMap}; + + +use std::ffi::CString; +use std::os::raw::c_void; +use std::os::raw::{c_char, c_int}; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; +use cpp::*; + +static VIDEO_ITEM_PTR : AtomicUsize = AtomicUsize::new(0); + +unsafe extern "C" { + fn init_uxplay(argc: c_int, argv: *mut *mut c_char) -> c_int; + + fn set_uxplay_gl_callbacks( + connection_cb: extern "C" fn(bool), + get_video_item_cb: extern "C" fn() -> *mut c_void, + ); +} + +extern "C" fn rust_uxplay_get_video_item() -> *mut c_void { + VIDEO_ITEM_PTR.load(Ordering::Acquire) as *mut c_void +} + + +extern "C" fn rust_uxplay_connection_cb(connected : bool) { + +} + + +#[derive(QObject, Default, QtThreading)] +pub struct Airplay { + base: qt_base_class!(trait QObject), + init : qt_method!(fn(&self, video_item : QVariant) -> bool), + load_gst_gl : qt_method!(fn(&self) -> bool), + connection_change : qt_signal!(connected: bool) +} + +impl Airplay { + + fn load_gst_gl(&self) -> bool { + crate::utils::force_load_gst_gl() + } + + fn init(&self, video_item: QVariant ) -> bool { + + let ptr = crate::utils::qvariant_to_ptr(video_item); + + VIDEO_ITEM_PTR.store(ptr as usize, Ordering::Release); + unsafe { + set_uxplay_gl_callbacks( + rust_uxplay_connection_cb, + rust_uxplay_get_video_item + ); + } + + std::thread::spawn(||{ + let arg0 = CString::new("uxplay").unwrap(); + let arg1 = CString::new("-p").unwrap(); + let arg2 =CString::new("-fps").unwrap(); + let arg3 =CString::new("60").unwrap(); + + let mut c_args = vec![arg0.as_ptr() as *mut c_char, arg1.as_ptr() as *mut c_char, arg2.as_ptr() as *mut c_char,arg3.as_ptr() as *mut c_char, std::ptr::null_mut()]; + + unsafe { + init_uxplay((c_args.len() - 1) as i32, c_args.as_mut_ptr()); + } + }); + true + } +} \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..5771b1a --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,50 @@ +pub const RECENTS_ALBUM_ID: i32 = -1; +pub const FAVS_ALBUM_ID: i32 = -2; + +pub static IOS_15_ALBUM_QUERY_STATEMENT: &str = "SELECT + ZGENERICALBUM.Z_PK, + ZGENERICALBUM.ZTITLE, + ZGENERICALBUM.ZCACHEDCOUNT, + ZASSET.ZDIRECTORY, + ZASSET.ZFILENAME + FROM ZGENERICALBUM + LEFT JOIN ZASSET ON ZGENERICALBUM.ZKEYASSET = ZASSET.Z_PK + WHERE ZGENERICALBUM.ZKEYASSET IS NOT NULL + "; + +pub static RECENTS_ALBUM_QUERY: &str = " + SELECT + ZASSET.ZFILENAME, + ZASSET.ZDIRECTORY, + (SELECT COUNT(*) FROM ZASSET) + FROM ZASSET + ORDER BY ZASSET.Z_PK DESC + LIMIT 1 +"; + +pub static RECENTS_QUERY: &str = " + SELECT + ZASSET.ZFILENAME, + ZASSET.ZDIRECTORY + FROM ZASSET ORDER BY ZASSET.Z_PK DESC +"; + +pub static FAVS_ALBUM_QUERY: &str = " + SELECT + ZASSET.ZFILENAME, + ZASSET.ZDIRECTORY, + (SELECT COUNT(*) FROM ZASSET WHERE ZASSET.ZFAVORITE = 1) + FROM ZASSET + WHERE ZASSET.ZFAVORITE = 1 + ORDER BY ZASSET.Z_PK DESC + LIMIT 1 +"; + +pub static FAVS_QUERY: &str = " + SELECT + ZASSET.ZFILENAME, + ZASSET.ZDIRECTORY + FROM ZASSET + WHERE ZASSET.ZFAVORITE = 1 + ORDER BY ZASSET.Z_PK DESC +"; diff --git a/src/core.rs b/src/core.rs index c367c2f..b81e38d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -30,8 +30,9 @@ use once_cell::sync::Lazy; use plist::{Dictionary, Value}; use qmetaobject::prelude::*; +use crate::device_ctx::{APP_DEVICE_STATE,DeviceServices}; use crate::{ - APP_DEVICE_STATE, APP_LABEL, DeviceServices, EV_CONNECTED, EV_DISCONNECTED, EV_FAIL, + APP_LABEL, EV_CONNECTED, EV_DISCONNECTED, EV_FAIL, EV_PAIRING_PENDING, POSSIBLE_ROOT, RUNTIME, qt_threading::{QtThread, QtThreading}, utils, @@ -45,7 +46,7 @@ pub struct Core { init_wireless_device: qt_method!(fn(&mut self, ip: QString, pairing_file: QString, mac_address: QString)), - // get_pairing_files : qt_method(fn remove_device(mut &self, udid: &QString)), + get_pairing_files : qt_method!(fn (&mut self) -> QVariantMap), // remove_device : qt_method!(fn (&mut self, udid: QString)), device_event: qt_signal!(event_type : u32, udid : QString , info : QVariantMap), init_failed: qt_signal!(mac_address : QString), @@ -569,6 +570,7 @@ async fn init_idescriptor_device< lockdown: Arc::new(Mutex::new(lc)), }; + // FIXME: use device_ctx { let mut state = APP_DEVICE_STATE.lock().await; if let Some(mut old) = state.insert(udid.to_string(), device_services) { diff --git a/src/device_ctx.rs b/src/device_ctx.rs new file mode 100644 index 0000000..86cddf7 --- /dev/null +++ b/src/device_ctx.rs @@ -0,0 +1,39 @@ +use idevice::{ + afc::AfcClient, diagnostics_relay::DiagnosticsRelayClient, lockdown::LockdownClient, +}; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use tokio::sync::oneshot; +use tokio::task::JoinHandle; + +#[derive(Clone)] +pub struct DeviceServices { + pub afc: Arc>, + pub afc2: Option>>, + pub diag: Arc>, + pub heartbeat_task: Option>>, + pub video_streams: Arc>>>, + pub provider: Arc>>, + pub lockdown: Arc>, +} + +// FIXME: shouldn't be public +pub static APP_DEVICE_STATE: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + +pub async fn get_device(udid: impl Into) -> anyhow::Result { + let udid_str = udid.into(); + let maybe_device = APP_DEVICE_STATE.lock().await.get(&udid_str).cloned(); + + match maybe_device { + Some(d) => Ok(d), + None => anyhow::bail!(format!("Device with udid {} does not exist", udid_str)), + } +} + +pub async fn get_device_opt(udid: impl Into) -> Option { + let udid_str = udid.into(); + APP_DEVICE_STATE.lock().await.get(&udid_str).cloned() +} diff --git a/src/hause_arrest.rs b/src/hause_arrest.rs deleted file mode 100644 index ab5e382..0000000 --- a/src/hause_arrest.rs +++ /dev/null @@ -1,374 +0,0 @@ -use crate::{APP_DEVICE_STATE, RUNTIME, afc, run_sync, utils}; -use cxx_qt::{CxxQtType, Threading}; -use cxx_qt_lib::{QByteArray, QMap, QMapPair_QString_QVariant, QString}; -use idevice::afc::{AfcClient, opcode::AfcFopenMode}; -use std::pin::Pin; -use std::sync::Arc; - -use tokio::{ - io::AsyncReadExt, - net::TcpListener, - sync::{Mutex, oneshot}, -}; - -#[cxx_qt::bridge(namespace = "CXX")] -mod qobject { - #[namespace = ""] - unsafe extern "C++" { - include!("cxx-qt-lib/qstring.h"); - include!("cxx-qt-lib/qlist.h"); - include!("cxx-qt-lib/qbytearray.h"); - include!("cxx-qt-lib/qmap.h"); - include!("cxx-qt-lib/qvariant.h"); - - type QString = cxx_qt_lib::QString; - type QMap_QString_QVariant = cxx_qt_lib::QMap; - type QByteArray = cxx_qt_lib::QByteArray; - - } - - extern "RustQt" { - #[qobject] - type HauseArrest = super::RHauseArrestBackend; - - #[qinvokable] - fn get_bundle_id(self: &HauseArrest) -> &QString; - - #[qinvokable] - fn init_session(self: Pin<&mut HauseArrest>); - #[qsignal] - fn init_session_finished(self: Pin<&mut HauseArrest>, success: bool); - - #[qinvokable] - fn check_is_dir_and_list(self: &HauseArrest, path: &QString); - #[qsignal] - fn check_is_dir_and_list_finished( - self: Pin<&mut HauseArrest>, - success: bool, - entries: &QMap_QString_QVariant, - ); - - #[qinvokable] - fn file_to_buffer(self: &HauseArrest, file_path: &QString) -> QByteArray; - - #[qinvokable] - fn start_video_stream(self: &HauseArrest, file_path: &QString) -> QString; - - #[qinvokable] - fn delete_path(self: &HauseArrest, path: &QString) -> bool; - } - - impl cxx_qt::Threading for HauseArrest {} - - impl cxx_qt::Constructor<(QString, QString), NewArguments = (QString, QString)> for HauseArrest {} -} - -#[derive(Default)] -pub struct RHauseArrestBackend { - udid: QString, - bundle_id: QString, - afc_handle: Option>>, -} - -impl cxx_qt::Constructor<(QString, QString)> for qobject::HauseArrest { - type BaseArguments = (); - type InitializeArguments = (); - type NewArguments = (QString, QString); - - fn route_arguments( - args: (QString, QString), - ) -> ( - Self::NewArguments, - Self::BaseArguments, - Self::InitializeArguments, - ) { - (args, (), ()) - } - - fn new(args: (QString, QString)) -> RHauseArrestBackend { - RHauseArrestBackend { - udid: args.0, - bundle_id: args.1, - afc_handle: None, - } - } -} - -impl qobject::HauseArrest { - fn get_bundle_id(&self) -> &QString { - use cxx_qt::CxxQtType; - &self.rust().bundle_id - } - - fn init_session(self: Pin<&mut Self>) { - let udid_str = self.udid.to_string(); - let bundle_id_str = self.bundle_id.to_string(); - let qt_thread = self.qt_thread(); - - RUNTIME.spawn(async move { - let afc_client = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(&udid_str).cloned(); - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("HouseArrest: Device {} not found", udid_str); - - qt_thread - .queue(move |q| { - q.init_session_finished(false); - }) - .ok(); - return; - } - }; - - let provider_guard = device.provider.lock().await; - - match utils::vend_app_documents(provider_guard.as_ref(), &bundle_id_str).await { - Ok(afc_client) => afc_client, - Err(e) => { - eprintln!( - "Failed to initialize HouseArrest session for {}: {}", - bundle_id_str, e - ); - qt_thread - .queue(move |q| { - q.init_session_finished(false); - }) - .ok(); - return; - } - } - }; - - qt_thread - .queue(move |mut qobject| { - qobject.as_mut().rust_mut().afc_handle = Some(Arc::new(Mutex::new(afc_client))); - }) - .ok(); - - qt_thread - .queue(move |q| { - q.init_session_finished(true); - }) - .ok(); - }); - } - - fn check_is_dir_and_list(self: &Self, path: &QString) { - let qt_t = self.qt_thread(); - let path_str = path.to_string(); - - let afc_opt = self.rust().afc_handle.clone(); - - RUNTIME.spawn(async move { - let qt_thread = qt_t.clone(); - - let Some(afc_handle) = afc_opt else { - eprintln!("HouseArrest: AfcClient not initialized"); - qt_thread - .queue(move |q| { - q.check_is_dir_and_list_finished( - false, - &QMap::::default(), - ); - }) - .ok(); - return; - }; - - let mut afc_client = afc_handle.lock().await; - let map_result = afc::check_is_dir_and_list(&mut *afc_client, path_str).await; - - qt_thread - .queue(move |q| { - q.check_is_dir_and_list_finished(true, &map_result); - }) - .ok(); - }); - } - - fn file_to_buffer(&self, album_path: &QString) -> QByteArray { - let album_path_string = album_path.to_string(); - let afc_opt = self.rust().afc_handle.clone(); - - let data: Vec = run_sync(async move { - let Some(afc_handle) = afc_opt else { - eprintln!("HouseArrest: AfcClient not initialized"); - return Vec::new(); - }; - - let mut afc_client = afc_handle.lock().await; - - let mut fd = match afc_client - .open(album_path_string.clone(), AfcFopenMode::RdOnly) - .await - { - Ok(f) => f, - Err(e) => { - eprintln!("file_to_buffer: failed to open {album_path_string}: {e}"); - return Vec::new(); - } - }; - - let mut buf = Vec::new(); - let mut chunk = vec![0u8; 8192]; - - loop { - let n = match fd.read(&mut chunk).await { - Ok(n) => n, - Err(e) => { - eprintln!("file_to_buffer: failed to read {album_path_string}: {e}"); - buf.clear(); - break; - } - }; - if n == 0 { - break; - } - buf.extend_from_slice(&chunk[..n]); - } - fd.close().await.ok(); - buf - }); - - if data.is_empty() { - QByteArray::default() - } else { - QByteArray::from(&data[..]) - } - } - - fn start_video_stream(&self, file_path: &QString) -> QString { - let afc_opt = self.rust().afc_handle.clone(); - - let Some(afc) = afc_opt else { - eprintln!("HouseArrest: AfcClient not initialized"); - return QString::default(); - }; - - let udid_str = self.udid.to_string(); - let path_str = file_path.to_string(); - let cloned_path = path_str.clone(); - - eprintln!( - "start_video_stream: request udid={} path={}", - udid_str, cloned_path - ); - - // bind ephemeral port on localhost - let listener = match std::net::TcpListener::bind("127.0.0.1:0") { - Ok(l) => l, - Err(e) => { - eprintln!("start_video_stream: bind failed: {e}"); - return QString::default(); - } - }; - let local_addr = match listener.local_addr() { - Ok(a) => a, - Err(e) => { - eprintln!("start_video_stream: local_addr failed: {e}"); - return QString::default(); - } - }; - listener.set_nonblocking(true).ok(); - - // create Tokio TcpListener inside runtime - let std_listener = { - let _guard = RUNTIME.handle().enter(); - match TcpListener::from_std(listener) { - Ok(l) => l, - Err(e) => { - eprintln!("start_video_stream: from_std failed: {e}"); - return QString::default(); - } - } - }; - - let port = local_addr.port(); - - let encoded = urlencoding::encode(&cloned_path); - let url = format!("http://127.0.0.1:{}/{}", port, encoded); - let url_clone = url.clone(); - let url_clone_for_log = url.clone(); - let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>(); - let udid_for_insert = udid_str.clone(); - let url_for_insert = url.clone(); - let inserted = run_sync(async move { - let maybe_device = APP_DEVICE_STATE.lock().await.get(&udid_for_insert).cloned(); - let device = match maybe_device { - Some(d) => d, - None => return false, - }; - - let mut video_streams = device.video_streams.lock().await; - video_streams.insert(url_for_insert, shutdown_tx); - true - }); - if !inserted { - eprintln!( - "start_video_stream: failed to insert video stream for udid={} path={}", - udid_str, cloned_path - ); - return QString::default(); - } - eprintln!( - "start_video_stream: serving {} for udid={} path={}", - url_clone, udid_str, cloned_path - ); - // accept-loop task - RUNTIME.spawn(async move { - loop { - tokio::select! { - _ = &mut shutdown_rx => { - // shutdown requested - eprintln!("start_video_stream: shutdown requested for {}", url_clone); - break; - } - accept_res = std_listener.accept() => { - let (socket, peer) = match accept_res { - Ok(s) => s, - Err(e) => { - eprintln!("start_video_stream: accept error: {e} on {}", url_clone); - break; - } - }; - eprintln!("start_video_stream: accepted connection from {} on {}", peer, url_clone); - - let path_clone = path_str.clone(); - let afc_for_conn = afc.clone(); - - tokio::spawn(async move { - let mut afc = afc_for_conn.lock().await; - afc::handle_http_connection(&mut afc, path_clone, socket).await; - }); - } - } - } - eprintln!("start_video_stream: accept-loop exiting for {}", url_clone); - }); - - QString::from(url_clone_for_log) - } - fn delete_path(&self, path: &QString) -> bool { - let path_str = path.to_string(); - let afc_opt = self.rust().afc_handle.clone(); - - run_sync(async move { - let Some(afc_handle) = afc_opt else { - eprintln!("HouseArrest: AfcClient not initialized"); - return false; - }; - - let mut afc_client = afc_handle.lock().await; - - match afc_client.remove(&path_str).await { - Ok(_) => true, - Err(e) => { - eprintln!("delete_path: failed to delete {}: {}", path_str, e); - false - } - } - }) - } -} diff --git a/src/image_loader.rs b/src/image_loader.rs index 835e3c1..12ad773 100644 --- a/src/image_loader.rs +++ b/src/image_loader.rs @@ -1,3 +1,7 @@ +use crate::RUNTIME; +use crate::device_ctx; +use crate::qt_threading::{QtThread, QtThreading}; +use crate::utils::{AfcReader, create_image_from_buffer, generate_thumbnail, is_video_file}; use idevice::afc::AfcClient; use idevice::afc::opcode::AfcFopenMode; use once_cell::sync::Lazy; @@ -15,10 +19,6 @@ use tokio::{ sync::{Notify, Semaphore}, }; -use crate::qt_threading::{QtThread, QtThreading}; -use crate::utils::{AfcReader, create_image_from_buffer, generate_thumbnail, is_video_file}; -use crate::{APP_DEVICE_STATE, RUNTIME}; - #[derive(Default, QObject)] pub struct ImageLoader { base: qt_base_class!(trait QObject), @@ -123,25 +123,7 @@ fn ensure_worker_started() { let _permit = permit; let res: anyhow::Result<()> = async { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(key.udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - // eprintln!( - // "image_loader::read_file_via_afc: device {udid} not found" - // ); - anyhow::bail!("No device"); - } - }; - - device.afc.clone() - }; + let afc_arc = device_ctx::get_device(key.udid.as_str()).await?.afc; let mut img = QImage::default(); if is_video_file(&key.path) { diff --git a/src/image_provider.rs b/src/image_provider.rs index 9145afc..1fc7d6a 100644 --- a/src/image_provider.rs +++ b/src/image_provider.rs @@ -82,11 +82,6 @@ impl QQuickImageProvider for ImageProvider { ); } - println!("index ={}", index); - - // self.loader - // .request_thumbnail(QString::from(udid), QString::from(path), index); - self.loader.pinned().clone().borrow_mut().request_thumbnail( QString::from(udid), QString::from(path), diff --git a/src/main.rs b/src/main.rs index 9388b46..66a9ac4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,9 @@ use crate::qquickimageprovider_imp::AddImageProvider; pub mod afc_services; pub mod apps; +pub mod constants; pub mod core; +pub mod device_ctx; pub mod image_cache; pub mod image_loader; pub mod image_provider; @@ -33,6 +35,7 @@ pub mod qt_threading; pub mod query_sqlite; pub mod service_factory; pub mod service_manager; +pub mod airplay; pub mod utils; pub const POSSIBLE_ROOT: &str = "../../../../"; @@ -55,22 +58,9 @@ cpp! {{ #include #include "src/live_reload.cpp" + // #include "src/networkdeviceprovider.h" }} -#[derive(Clone)] -pub struct DeviceServices { - pub afc: Arc>, - pub afc2: Option>>, - pub diag: Arc>, - pub heartbeat_task: Option>>, - pub video_streams: Arc>>>, - pub provider: Arc>>, - pub lockdown: Arc>, -} - -pub static APP_DEVICE_STATE: Lazy>> = - Lazy::new(|| Mutex::new(HashMap::new())); - static RUNTIME: Lazy = Lazy::new(|| { tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -78,6 +68,7 @@ static RUNTIME: Lazy = Lazy::new(|| { .unwrap() }); +// TODO: handle timeout here pub fn run_sync(fut: F) -> R where F: Future + Send + 'static, @@ -128,7 +119,8 @@ fn main() { // FIXME: fluentui example app was forcing OpenGL // but do we need this ? // #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - // QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); + // QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); + // QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); // #endif #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); @@ -198,7 +190,7 @@ fn main() { // #[cfg(not(compiled_qml))] // crate::resources_qml::rsrc_qml(); - qml_register_type::(cstr::cstr!("iDescriptor"), 1, 0, cstr::cstr!("Core")); + // qml_register_type::(cstr::cstr!("iDescriptor"), 1, 0, cstr::cstr!("Core")); qml_register_type::( cstr::cstr!("iDescriptor"), 1, @@ -206,9 +198,20 @@ fn main() { cstr::cstr!("Query"), ); + + // qml_register_type::( + // cstr::cstr!("iDescriptor"), + // 1, + // 0, + // cstr::cstr!("Airplay"), + // ); + let mut engine = QmlEngine::new(); // let mut dpi = cpp!(unsafe[] -> f64 as "double" { return QGuiApplication::primaryScreen()->logicalDotsPerInch() / 96.0; }); + let core_obj = QObjectBox::new(core::Core::default()); + engine.set_object_property("core".into(), core_obj.pinned()); + let obj = QObjectBox::new(image_loader::ImageLoader::default()); engine.set_object_property("imageLoader".into(), obj.pinned()); @@ -218,8 +221,17 @@ fn main() { let provider_ref_cell = QObjectBox::new(image_provider::ImageProvider::default(obj)); engine.add_image_provider("thumb", provider_ref_cell); + let airplay = QObjectBox::new(airplay::Airplay::default()); + engine.set_object_property("AirplayImp".into(), airplay.pinned()); + + + let engine_ptr = engine.cpp_ptr(); + // cpp!(unsafe [engine_ptr as "QQmlApplicationEngine *"] { + // engine_ptr->rootContext()->setContextProperty("NetworkDeviceProvider", NetworkDeviceProvider::sharedInstance()); + // }); + let service_factory = QObjectBox::new(crate::service_factory::ServiceFactory::new(engine_ptr)); engine.set_object_property("serviceFactory".into(), service_factory.pinned()); diff --git a/src/query_sqlite.rs b/src/query_sqlite.rs index 1792f34..a06ea1b 100644 --- a/src/query_sqlite.rs +++ b/src/query_sqlite.rs @@ -1,19 +1,15 @@ use qmetaobject::prelude::*; use qttypes::{QStringList, QVariantMap}; -use crate::qt_threading::{QtThread, QtThreading}; -use crate::{APP_DEVICE_STATE, RUNTIME, run_sync}; - -use idevice::afc::{AfcClient, opcode::AfcFopenMode}; -use idevice::{ - IdeviceError, IdeviceService, diagnostics_relay::DiagnosticsRelayClient, - house_arrest::HouseArrestClient, installation_proxy::InstallationProxyClient, - provider::IdeviceProvider, +use crate::constants::{ + FAVS_ALBUM_ID, FAVS_ALBUM_QUERY, FAVS_QUERY, IOS_15_ALBUM_QUERY_STATEMENT, RECENTS_ALBUM_ID, + RECENTS_ALBUM_QUERY, RECENTS_QUERY, }; -use once_cell::sync::Lazy; -use plist::Dictionary as PlistDictionary; -use plist_macro::plist; -use regex::Regex; +use crate::device_ctx; +use crate::qt_threading::{QtThread, QtThreading}; +use crate::utils::create_album_info; +use crate::{RUNTIME, run_sync}; +use idevice::afc::{AfcClient, opcode::AfcFopenMode}; use rusqlite::{Connection, Rows}; use serde_json::json; use std::default; @@ -53,7 +49,6 @@ impl QtThreading for Query { } impl Default for Query { - // qml_register_type calls ::default fn default() -> Self { let mut state = QVariantMap::default(); state.insert(QString::from("init"), QVariant::from(false)); @@ -83,18 +78,7 @@ impl Query { RUNTIME.spawn(async move { let res: Result<(), Box> = (async { let mut gallery_db_bytes = { - let afc_arc = { - let device = APP_DEVICE_STATE - .lock() - .await - .get(&udid_clone.to_string()) - .cloned(); - match device { - Some(d) => d.afc.clone(), - None => return Err("Device not found".into()), - } - }; - + let afc_arc = device_ctx::get_device(udid_clone).await?.afc; let mut afc = afc_arc.lock().await; let mut fd = afc .open("/PhotoData/Photos.sqlite", AfcFopenMode::RdOnly) @@ -170,7 +154,7 @@ impl Query { let con_arc = match &self.connection { Some(c) => c.clone(), None => { - println!("WTF NO CONN"); + println!("NO CONN"); return; } }; @@ -181,17 +165,7 @@ impl Query { let res: Result<(), Box> = (async { let conn = con_arc.lock().await; //recents album - let mut recents_stmt = conn.prepare( - " - SELECT - ZASSET.ZFILENAME as 'FNAME', - ZASSET.ZDIRECTORY as 'DIR', - (SELECT COUNT(*) FROM ZASSET) as 'COUNT' - FROM ZASSET - ORDER BY ZASSET.Z_PK DESC - LIMIT 1 - ", - )?; + let mut recents_stmt = conn.prepare(RECENTS_ALBUM_QUERY)?; let (fname, fdir, count) = recents_stmt.query_row([], |r| { let fname: String = r.get(0)?; @@ -200,9 +174,7 @@ impl Query { Ok((fname, fdir, count)) })?; - let recents_album_data = - json!({ "item_count" : count, "file_path" : format!("{}/{}",fdir,fname)}) - .to_string(); + let recents_album_data = create_album_info(RECENTS_ALBUM_ID, count, fdir, fname); albums.insert( QString::from("Recents"), @@ -210,18 +182,7 @@ impl Query { ); //favs - let mut favs_stmt = conn.prepare( - " - SELECT - ZASSET.ZFILENAME, - ZASSET.ZDIRECTORY, - (SELECT COUNT(*) FROM ZASSET WHERE ZASSET.ZFAVORITE = 1) - FROM ZASSET - WHERE ZASSET.ZFAVORITE = 1 - ORDER BY ZASSET.Z_PK DESC - LIMIT 1 - ", - )?; + let mut favs_stmt = conn.prepare(FAVS_ALBUM_QUERY)?; let (fname, fdir, count) = favs_stmt.query_row([], |r| { let fname: String = r.get(0)?; @@ -230,9 +191,7 @@ impl Query { Ok((fname, fdir, count)) })?; - let favs_album_data = - json!({"item_count" : count, "file_path" : format!("{}/{}",fdir,fname) }) - .to_string(); + let favs_album_data = create_album_info(FAVS_ALBUM_ID, count, fdir, fname); albums.insert( QString::from("Favorites"), @@ -255,18 +214,7 @@ impl Query { // ", // )?; - let mut stmt = conn.prepare( - "SELECT - ZGENERICALBUM.Z_PK, - ZGENERICALBUM.ZTITLE, - ZGENERICALBUM.ZCACHEDCOUNT, - ZASSET.ZDIRECTORY, - ZASSET.ZFILENAME - FROM ZGENERICALBUM - LEFT JOIN ZASSET ON ZGENERICALBUM.ZKEYASSET = ZASSET.Z_PK - WHERE ZGENERICALBUM.ZKEYASSET IS NOT NULL - ", - )?; + let mut stmt = conn.prepare(IOS_15_ALBUM_QUERY_STATEMENT)?; let rows_iter = stmt.query_map([], |row| { let album_id: i32 = row.get(0)?; @@ -280,12 +228,8 @@ impl Query { for row_res in rows_iter { let (album_id, title, item_count, asset_dir, asset_file_name) = row_res?; - let album_data = crate::utils::create_album_info( - album_id, - item_count, - asset_dir, - asset_file_name, - ); + let album_data = + create_album_info(album_id, item_count, asset_dir, asset_file_name); albums.insert( QString::from(title), @@ -323,6 +267,18 @@ impl Query { Some(c) => c.clone(), None => return, }; + + match id { + FAVS_ALBUM_ID => { + return self.query_favs(); + } + RECENTS_ALBUM_ID => { + return self.query_recents(); + } + /* continue if something else */ + _ => {} + }; + let q_thread = self.qt_thread(); RUNTIME.spawn(async move { @@ -361,16 +317,106 @@ impl Query { q.album_queried(id, list); }); } - Err(_) => { - println!("Error querying album") + Err(e) => { + println!("Error querying album {}", e.to_string()) } } }); } - fn query_favs(self: Pin<&mut Self>) {} + fn query_favs(&mut self) { + let q_thread = self.qt_thread(); + let con_arc = match &self.connection { + Some(c) => c.clone(), + None => { + println!("NO CONN"); + return; + } + }; - fn query_recents(self: Pin<&mut Self>) {} + RUNTIME.spawn(async move { + let res: anyhow::Result = async { + let con = con_arc.lock().await; + let mut list: QStringList = QStringList::default(); + + //favs album + let mut favs_stmt = con.prepare(FAVS_QUERY)?; + + let favs_iter = favs_stmt.query_map([], |r| { + let fname: String = r.get(0)?; + let fdir: String = r.get(1)?; + Ok((fname, fdir)) + })?; + + for fav_item in favs_iter { + let (fname, fdir) = fav_item?; + list.push(QString::from(format!("{}/{}", fdir, fname))); + } + + Ok(list) + } + .await; + + match res { + Ok(list) => { + println!("Album loaded has length :{}", list.len()); + q_thread.queue(move |q| { + q.album_queried(FAVS_ALBUM_ID, list); + }); + } + Err(e) => { + println!("Error querying album {}", e.to_string()) + } + } + }); + } + + fn query_recents(&mut self) { + let q_thread = self.qt_thread(); + let con_arc = match &self.connection { + Some(c) => c.clone(), + None => { + println!("NO CONN"); + return; + } + }; + + RUNTIME.spawn(async move { + let res: anyhow::Result = async { + let con = con_arc.lock().await; + let mut list: QStringList = QStringList::default(); + + //recents album + let mut recents_stmt = con.prepare(RECENTS_QUERY)?; + + let recents_iter = recents_stmt.query_map([], |r| { + let fname: String = r.get(0)?; + let fdir: String = r.get(1)?; + Ok((fname, fdir)) + })?; + + for recent_item in recents_iter { + let (fname, fdir) = recent_item?; + list.push(QString::from(format!("{}/{}", fdir, fname))); + } + + Ok(list) + } + .await; + + match res { + Ok(list) => { + println!("Album loaded has length :{}", list.len()); + q_thread.queue(move |q| { + q.album_queried(RECENTS_ALBUM_ID, list); + }); + } + Err(e) => { + println!("Error querying album {}", e.to_string()) + } + } + }); + } } // fn get_album_query_sql(ios_ver : i32, id : i32) { diff --git a/src/service_factory.rs b/src/service_factory.rs index 55f224c..a84b8bf 100644 --- a/src/service_factory.rs +++ b/src/service_factory.rs @@ -1,7 +1,12 @@ -use crate::utils::{empty_qjsvalue, engine_ptr_new_object}; -use crate::{APP_DEVICE_STATE, afc_services::AfcServices, run_sync}; +use crate::device_ctx; +use crate::utils::{empty_qjsvalue, engine_ptr_new_object, vend_app_documents}; + +use crate::{afc_services::AfcServices, run_sync}; +use idevice::afc::AfcClient; use qmetaobject::{QJSValue, prelude::*}; use std::ffi::c_void; +use std::sync::Arc; +use tokio::sync::Mutex; /* we need this because qml side @@ -14,6 +19,8 @@ pub struct ServiceFactory { /* SAFETY: check if engine_ptr.is_null() */ engine_ptr: *mut c_void, create_afc_client: qt_method!(fn(&self, udid: QString, afc2: bool) -> QJSValue), + create_hause_arrest_afc_client: + qt_method!(fn(&self, udid: QString, bundle_id: QString) -> QJSValue), } impl ServiceFactory { @@ -22,6 +29,7 @@ impl ServiceFactory { base: Default::default(), engine_ptr, create_afc_client: Default::default(), + create_hause_arrest_afc_client: Default::default(), } } @@ -37,11 +45,7 @@ impl ServiceFactory { let afc_arc = run_sync({ let udid_str = udid_str.clone(); async move { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid_str.as_str()) - .cloned(); + let maybe_device = device_ctx::get_device_opt(&udid_str).await; let device = match maybe_device { Some(d) => d, None => { @@ -59,13 +63,57 @@ impl ServiceFactory { }); let Some(afc) = afc_arc else { - eprintln!("ServiceFactory: no AFC client available for {udid_str} (afc2={afc2})"); + println!( + "ServiceFactory: no AFC client available for {} (afc2={})", + &udid_str, afc2 + ); return empty_qjsvalue(); }; - let obj = AfcServices::from_afc_client(afc, udid_str); + let obj = AfcServices::from_afc_client(afc, udid_str, None); let obj_ptr = qmetaobject::into_leaked_cpp_ptr(obj); engine_ptr_new_object(engine_ptr, obj_ptr) } + + fn create_hause_arrest_afc_client(&self, udid: QString, bundle_id: QString) -> QJSValue { + let engine_ptr: *mut c_void = self.engine_ptr; + let udid_clone = udid.clone(); + let bundle_id_clone = bundle_id.clone(); + if engine_ptr.is_null() { + eprintln!("ServiceFactory: engine_ptr is null"); + return empty_qjsvalue(); + } + + let afc_res: anyhow::Result = run_sync(async move { + let device = device_ctx::get_device(udid).await?; + + let provider_guard = device.provider.lock().await; + + Ok(vend_app_documents(provider_guard.as_ref(), &bundle_id.to_string()).await?) + }); + + match afc_res { + Ok(afc) => { + let obj = AfcServices::from_afc_client( + Arc::new(Mutex::new(afc)), + udid_clone.to_string(), + Some(bundle_id_clone.to_string()), + ); + let obj_ptr = qmetaobject::into_leaked_cpp_ptr(obj); + + engine_ptr_new_object(engine_ptr, obj_ptr) + } + Err(e) => { + println!( + "Couldn't create hause_arrest_afc for bundle id {} for device {} : Error : {}", + bundle_id_clone, + udid_clone, + e.to_string() + ); + + return empty_qjsvalue(); + } + } + } } diff --git a/src/service_manager.rs b/src/service_manager.rs index e89dcb9..afa915f 100644 --- a/src/service_manager.rs +++ b/src/service_manager.rs @@ -1,7 +1,7 @@ use qmetaobject::prelude::*; use qttypes::{QVariantMap,QStringList}; - -use crate::{APP_DEVICE_STATE, RUNTIME, run_sync, utils,qt_threading::{QtThread,QtThreading},DeviceServices}; +use crate::device_ctx::{DeviceServices,get_device_opt}; +use crate::{RUNTIME, run_sync, utils,qt_threading::{QtThread,QtThreading}}; use macros::QtThreading; use idevice::{afc::opcode::AfcFopenMode, provider}; use idevice::services::core_device_proxy::CoreDeviceProxy; @@ -87,7 +87,7 @@ impl ServiceManager { loop { interval.tick().await; - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let maybe_device = get_device_opt(udid.as_str()).await; let device = match maybe_device { Some(d) => d, diff --git a/src/ui/DeviceContext.qml b/src/ui/DeviceContext.qml index a37c779..e17b032 100644 --- a/src/ui/DeviceContext.qml +++ b/src/ui/DeviceContext.qml @@ -4,24 +4,27 @@ import QtQml.Models 2.15 import QtQuick 2.15 import iDescriptor 1.0 +/* + core is a global obj set from rust side + engine.set_object_property("core".into(), core_obj.pinned()); +*/ QtObject { id: root property ListModel devices: ListModel {} property string currentDeviceUdid : "" - // default info section + // default to info section property int currentSection : 0 property bool showWelcomePage : true - readonly property Core core: Core {} function init() { - root.core.init() + core.init() } // workaround to use connections inside a QtObject property var _connections : Connections { - target: root.core + target: core function onDevice_event(eventType, udid, info) { console.log("Device event:", eventType, udid, JSON.stringify(info)) diff --git a/src/ui/Toolbox.qml b/src/ui/Toolbox.qml index 2d9abe1..a65c3a8 100644 --- a/src/ui/Toolbox.qml +++ b/src/ui/Toolbox.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import QtQuick.Controls.impl +import QtQuick.Dialogs import "." as App // FIXME: handle window creation logic @@ -9,6 +10,11 @@ Item { id: root anchors.fill: parent + MessageDialog { + id: errorDialog + title: "Error" + text: "" + } property string currentDeviceUdid: "" readonly property bool hasDevice: App.DeviceContext.devices && App.DeviceContext.devices.count > 0 @@ -16,7 +22,40 @@ Item { // 0 Airplayer, 1 VirtualLocation, 2 LiveScreen, 3 QueryMobileGestalt, 4 DeveloperDiskImages, // 5 WirelessGalleryImport, 6 iFuse, 7 CableInfo, 8 NetworkDevices, 9 MountDevImage, // 10 Restart, 11 Shutdown, 12 RecoveryMode, 13 EnableWifiConnections - signal toolClicked(int toolId, bool requiresDevice) + // signal toolClicked(int toolId, bool requiresDevice) + function toolClicked(toolId, requiresDevice) { + + switch (toolId) { + case 0: + const gl_plugin_loaded = AirplayImp.load_gst_gl() + if (!gl_plugin_loaded) { + errorDialog.text = "Failed to load gst gl plugin, make sure you have your GPU drivers installed" + errorDialog.open() + return; + } + + const comp = Qt.createComponent("./tools/Airplay.qml") + if (comp.status === Component.Ready) { + const win = comp.createObject(null,{ + }) + if (win !== null) { + win.show() + } else { + console.error("createObject failed:", comp.errorString()) + } + + } else if (comp.status === Component.Error) { + console.error("Component failed to load:", comp.errorString()) + } + break; + + default: + console.log(`No tool for id ${toolId}`) + } + + + } + signal deviceSelectionChanged(string udid) readonly property var mainToolsModel: ([ diff --git a/src/ui/tools/Airplay.qml b/src/ui/tools/Airplay.qml new file mode 100644 index 0000000..df8ba04 --- /dev/null +++ b/src/ui/tools/Airplay.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs +import QtQuick.Window + +import iDescriptor 1.0 +import org.freedesktop.gstreamer.Qt6GLVideoItem 1.0 + +Window { + id: win + width: 900 + height: 600 + visible: true + + Component.onCompleted: { + Qt.callLater(() => { + AirplayImp.init(video) + }) + } + + GstGLQt6VideoItem { + id: video + anchors.fill: parent + objectName: "videoItem" + z: 99 + } + + Rectangle { + color: Qt.rgba(1, 1, 1, 0.7) + border.width: 1 + border.color: "white" + anchors.bottom: video.bottom + anchors.bottomMargin: 15 + anchors.horizontalCenter: parent.horizontalCenter + width : parent.width - 30 + height: parent.height - 30 + radius: 8 + + MouseArea { + id: mousearea + anchors.fill: parent + hoverEnabled: true + onEntered: { + parent.opacity = 1.0 + hidetimer.start() + } + } + + Timer { + id: hidetimer + interval: 5000 + onTriggered: { + parent.opacity = 0.0 + stop() + } + } + } +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs index 064a3aa..615fcba 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,4 @@ -use crate::POSSIBLE_ROOT; -use crate::run_sync; +use crate::{POSSIBLE_ROOT, run_sync}; use cpp::*; use idevice::{ IdeviceError, IdeviceService, @@ -38,6 +37,7 @@ cpp! {{ #include #include #include "src/include/bridge.h" + #include QCoreApplication *globalApp = nullptr; }} @@ -610,3 +610,38 @@ pub fn engine_ptr_new_object(engine_ptr: *mut c_void, obj_ptr: *mut c_void) -> Q } //TODO: implement // pub fn heic_to_qimage() + +pub fn force_load_gst_gl() -> bool { + /* + need to load qml6glsink in order + to make Qt6GLVideoItem available on qml side + */ + cpp!(unsafe [] -> bool as "bool"{ + gst_init(nullptr,nullptr); + // GstElement *pipeline = gst_pipeline_new (NULL); + // GstElement *src = gst_element_factory_make ("videotestsrc", NULL); + // GstElement *glupload = gst_element_factory_make ("glupload", NULL); + GstElement *sink = gst_element_factory_make ("qml6glsink", NULL); + if (!sink) { + return false; + } + + gst_object_unref(sink); + + return true; + }) +} + + +pub fn qvariant_to_ptr(item : QVariant) -> usize { + cpp::cpp!(unsafe [item as "QVariant"] -> usize as "uintptr_t" { + QObject* o = item.value(); + if (!o) return 0; + + // better do this + QQuickItem* q = qobject_cast(o); + if (!q) return 0; + + return reinterpret_cast(q); + }) +} \ No newline at end of file