From 0f1d40de95d9bd2856a664c44e33fcfa8f4a72c5 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Sun, 10 May 2026 17:54:25 +0000 Subject: [PATCH] refactor(core): move to qmetaobject-rs --- .gitignore | 3 +- .vscode/settings.json | 2 +- CMakeLists.txt | 526 ---------- src/rust/Cargo.lock => Cargo.lock | 724 +++++-------- src/rust/Cargo.toml => Cargo.toml | 28 +- build.rs | 331 ++++++ lib/macros/Cargo.toml | 12 + lib/macros/src/lib.rs | 25 + src/afc_services.rs | 683 ++++++++++++ src/afcexplorerwidget.cpp | 853 --------------- src/afcexplorerwidget.h | 140 --- src/airplaywidget.cpp | 741 ------------- src/airplaywidget.h | 167 --- src/appcontext.cpp | 427 -------- src/appcontext.h | 119 --- src/appdownloaddialog.cpp | 127 --- src/appdownloaddialog.h | 59 -- src/appinstalldialog.cpp | 314 ------ src/appinstalldialog.h | 72 -- src/apps.rs | 94 ++ src/appstoremanager.cpp | 266 ----- src/appstoremanager.h | 68 -- src/appswidget.cpp | 814 --------------- src/appswidget.h | 156 --- src/batterywidget.cpp | 156 --- src/batterywidget.h | 58 -- src/{rust/src/thumbnail.cc => bridge.cpp} | 157 ++- src/{rust/src => }/bridge.rs | 4 +- src/cableinfowidget.cpp | 367 ------- src/cableinfowidget.h | 88 -- src/{rust/src/lib.rs => core.rs} | 288 ++---- src/core/services/init_device.cpp | 438 -------- src/core/services/load_heic.cpp | 87 -- src/creddialog.cpp | 128 --- src/creddialog.h | 52 - src/devdiskimagehelper.cpp | 293 ------ src/devdiskimagehelper.h | 79 -- src/devdiskimageswidget.cpp | 733 ------------- src/devdiskimageswidget.h | 101 -- src/devdiskmanager.cpp | 528 ---------- src/devdiskmanager.h | 98 -- src/devicedatabase.cpp | 574 ----------- src/devicedatabase.h | 48 - src/deviceimagewidget.cpp | 341 ------ src/deviceimagewidget.h | 58 -- src/deviceinfowidget.cpp | 413 -------- src/deviceinfowidget.h | 68 -- src/devicelistener.h | 52 - src/devicemanagerwidget.cpp | 383 ------- src/devicemanagerwidget.h | 93 -- src/devicemenuwidget.cpp | 161 --- src/devicemenuwidget.h | 50 - src/devicependingwidget.cpp | 56 - src/devicependingwidget.h | 41 - src/devicesidebarwidget.cpp | 595 ----------- src/devicesidebarwidget.h | 197 ---- src/devicesleepwarningwidget.cpp | 78 -- src/devicesleepwarningwidget.h | 29 - src/devmodewidget.cpp | 86 -- src/devmodewidget.h | 29 - src/diagnosedialog.cpp | 58 -- src/diagnosedialog.h | 46 - src/diagnosewidget.cpp | 913 ---------------- src/diagnosewidget.h | 129 --- src/diskusagebar.cpp | 85 -- src/diskusagebar.h | 53 - src/diskusagewidget.cpp | 441 -------- src/diskusagewidget.h | 103 -- src/exportalbum.cpp | 251 ----- src/exportalbum.h | 85 -- src/fileexplorerwidget.cpp | 259 ----- src/fileexplorerwidget.h | 83 -- src/gallerywidget.cpp | 737 ------------- src/gallerywidget.h | 113 -- src/{rust/src => }/hause_arrest.rs | 0 src/{rust/src => }/heic_to_image.cc | 0 src/howtoconnectdialog.cpp | 131 --- src/howtoconnectdialog.h | 39 - src/httpserver.cpp | 262 ----- src/httpserver.h | 71 -- src/ifusediskunmountbutton.cpp | 32 - src/ifusediskunmountbutton.h | 35 - src/ifusemanager.cpp | 72 -- src/ifusemanager.h | 39 - src/ifusewidget.cpp | 520 ---------- src/ifusewidget.h | 84 -- src/image_cache.rs | 27 + src/{rust/src => }/image_loader.rs | 171 +-- src/image_provider.rs | 98 ++ src/imageloader.cpp | 509 --------- src/imageloader.h | 71 -- src/imagetask.h | 68 -- src/include/bridge.h | 20 + src/infolabel.cpp | 77 -- src/infolabel.h | 63 -- src/installedappswidget.cpp | 699 ------------- src/installedappswidget.h | 180 ---- src/{rust/src => }/io_manager.rs | 0 src/iomanagerclient.cpp | 286 ------ src/iomanagerclient.h | 88 -- src/jailbrokenwidget.cpp | 124 --- src/jailbrokenwidget.h | 64 -- src/keychaindialog.cpp | 157 --- src/keychaindialog.h | 57 - src/live_reload.cpp | 80 ++ src/livescreenwidget.cpp | 180 ---- src/livescreenwidget.h | 78 -- src/loadingspinnerwidget.cpp | 62 -- src/loadingspinnerwidget.h | 47 - src/logindialog.cpp | 182 ---- src/logindialog.h | 51 - src/main.cpp | 147 --- src/main.rs | 258 +++++ src/mediapreviewdialog.cpp | 705 ------------- src/mediapreviewdialog.h | 153 --- src/mediastreamermanager.cpp | 107 -- src/mediastreamermanager.h | 58 -- src/networkdevicestoconnectwidget.cpp | 392 ------- src/networkdevicestoconnectwidget.h | 85 -- src/networkdeviceswidget.cpp | 230 ----- src/networkdeviceswidget.h | 71 -- src/photoimportdialog.cpp | 286 ------ src/photoimportdialog.h | 74 -- src/photomodel.cpp | 343 ------ src/photomodel.h | 116 --- src/privateinfolabel.cpp | 66 -- src/privateinfolabel.h | 49 - src/qballoontip.cpp | 270 ----- src/qballoontip.h | 81 -- src/{rust/src => }/qinput_get_text.cc | 0 src/qmetaobject_rust.hpp | 237 +++++ src/qprocessindicator.cpp | 194 ---- src/qprocessindicator.h | 92 -- src/qquickimageprovider_imp.rs | 213 ++++ src/qrc.rs | 85 ++ src/qt_threading.rs | 55 + src/{rust/src => }/query_sqlite.rs | 195 ++-- src/querymobilegestaltwidget.cpp | 1144 --------------------- src/querymobilegestaltwidget.h | 91 -- src/recoverydeviceinfowidget.cpp | 184 ---- src/recoverydeviceinfowidget.h | 36 - src/releasechangelogdialog.cpp | 118 --- src/releasechangelogdialog.h | 33 - src/responsiveqlabel.cpp | 85 -- src/responsiveqlabel.h | 46 - src/rust/build.rs | 31 - src/rust/include/bridge.h | 13 - src/rust/src/afc.rs | 315 ------ src/rust/src/afc2_services.rs | 839 --------------- src/rust/src/afc_services.rs | 824 --------------- src/rust/src/apps.rs | 107 -- src/rust/src/utils.rs | 188 ---- src/{rust/src => }/screenshot.rs | 0 src/service_factory.rs | 71 ++ src/{rust/src => }/service_manager.rs | 704 ++++--------- src/settingswidget.cpp | 574 ----------- src/settingswidget.h | 96 -- src/sponsorappcard.cpp | 134 --- src/sponsorappcard.h | 37 - src/sponsorwidget.cpp | 56 - src/sponsorwidget.h | 38 - src/statusballoon.cpp | 685 ------------ src/statusballoon.h | 141 --- src/thumbnailprovider.h | 79 -- src/toolboxwidget.cpp | 645 ------------ src/toolboxwidget.h | 148 --- src/{qml => ui}/AlbumContents.qml | 7 +- src/ui/App.qml | 8 + src/{qml => ui}/AppsTab.qml | 27 +- src/{qml => ui}/Device.qml | 7 +- src/ui/DeviceContext.qml | 52 + src/{qml => ui}/DeviceGallery.qml | 13 +- src/{qml => ui}/DeviceImage.qml | 0 src/{qml => ui}/DeviceInfo.qml | 0 src/{qml => ui}/DeviceTab.qml | 35 +- src/ui/FileExplorer.qml | 660 ++++++++++++ src/ui/FilesSection.qml | 339 ++++++ src/{qml => ui}/HowToConnect.qml | 0 src/ui/IconLoader.qml | 94 ++ src/ui/InstalledApps.qml | 0 src/{qml => ui}/LoginDialog.qml | 1 - src/{qml => ui}/Main.qml | 2 +- src/{qml => ui}/PreviewWindow.qml | 0 src/{qml => ui}/SidebarTabButton.qml | 8 +- src/{qml => ui}/TabButton.qml | 0 src/{qml => ui}/Tabs.qml | 8 +- src/ui/Toolbox.qml | 340 ++++++ src/{qml => ui}/Welcome.qml | 0 src/ui/qmldir | 1 + src/{qml => ui}/wIndows/Index.qml | 0 src/{qml => ui}/wIndows/Main.qml | 0 src/utils.rs | 612 +++++++++++ src/virtuallocationwidget.cpp | 482 --------- src/virtuallocationwidget.h | 89 -- src/welcomewidget.cpp | 161 --- src/welcomewidget.h | 54 - src/wirelessgalleryimportwidget.cpp | 200 ---- src/wirelessgalleryimportwidget.h | 67 -- src/zlineedit.cpp | 51 - src/zlineedit.h | 48 - src/zloadingwidget.cpp | 128 --- src/zloadingwidget.h | 72 -- src/ztabwidget.cpp | 383 ------- src/ztabwidget.h | 81 -- 204 files changed, 5191 insertions(+), 32701 deletions(-) delete mode 100644 CMakeLists.txt rename src/rust/Cargo.lock => Cargo.lock (90%) rename src/rust/Cargo.toml => Cargo.toml (53%) create mode 100644 build.rs create mode 100644 lib/macros/Cargo.toml create mode 100644 lib/macros/src/lib.rs create mode 100644 src/afc_services.rs delete mode 100644 src/afcexplorerwidget.cpp delete mode 100644 src/afcexplorerwidget.h delete mode 100644 src/airplaywidget.cpp delete mode 100644 src/airplaywidget.h delete mode 100644 src/appcontext.cpp delete mode 100644 src/appcontext.h delete mode 100644 src/appdownloaddialog.cpp delete mode 100644 src/appdownloaddialog.h delete mode 100644 src/appinstalldialog.cpp delete mode 100644 src/appinstalldialog.h create mode 100644 src/apps.rs delete mode 100644 src/appstoremanager.cpp delete mode 100644 src/appstoremanager.h delete mode 100644 src/appswidget.cpp delete mode 100644 src/appswidget.h delete mode 100644 src/batterywidget.cpp delete mode 100644 src/batterywidget.h rename src/{rust/src/thumbnail.cc => bridge.cpp} (61%) rename src/{rust/src => }/bridge.rs (95%) delete mode 100644 src/cableinfowidget.cpp delete mode 100644 src/cableinfowidget.h rename src/{rust/src/lib.rs => core.rs} (78%) delete mode 100644 src/core/services/init_device.cpp delete mode 100644 src/core/services/load_heic.cpp delete mode 100644 src/creddialog.cpp delete mode 100644 src/creddialog.h delete mode 100644 src/devdiskimagehelper.cpp delete mode 100644 src/devdiskimagehelper.h delete mode 100644 src/devdiskimageswidget.cpp delete mode 100644 src/devdiskimageswidget.h delete mode 100644 src/devdiskmanager.cpp delete mode 100644 src/devdiskmanager.h delete mode 100644 src/devicedatabase.cpp delete mode 100644 src/devicedatabase.h delete mode 100644 src/deviceimagewidget.cpp delete mode 100644 src/deviceimagewidget.h delete mode 100644 src/deviceinfowidget.cpp delete mode 100644 src/deviceinfowidget.h delete mode 100644 src/devicelistener.h delete mode 100644 src/devicemanagerwidget.cpp delete mode 100644 src/devicemanagerwidget.h delete mode 100644 src/devicemenuwidget.cpp delete mode 100644 src/devicemenuwidget.h delete mode 100644 src/devicependingwidget.cpp delete mode 100644 src/devicependingwidget.h delete mode 100644 src/devicesidebarwidget.cpp delete mode 100644 src/devicesidebarwidget.h delete mode 100644 src/devicesleepwarningwidget.cpp delete mode 100644 src/devicesleepwarningwidget.h delete mode 100644 src/devmodewidget.cpp delete mode 100644 src/devmodewidget.h delete mode 100644 src/diagnosedialog.cpp delete mode 100644 src/diagnosedialog.h delete mode 100644 src/diagnosewidget.cpp delete mode 100644 src/diagnosewidget.h delete mode 100644 src/diskusagebar.cpp delete mode 100644 src/diskusagebar.h delete mode 100644 src/diskusagewidget.cpp delete mode 100644 src/diskusagewidget.h delete mode 100644 src/exportalbum.cpp delete mode 100644 src/exportalbum.h delete mode 100644 src/fileexplorerwidget.cpp delete mode 100644 src/fileexplorerwidget.h delete mode 100644 src/gallerywidget.cpp delete mode 100644 src/gallerywidget.h rename src/{rust/src => }/hause_arrest.rs (100%) rename src/{rust/src => }/heic_to_image.cc (100%) delete mode 100644 src/howtoconnectdialog.cpp delete mode 100644 src/howtoconnectdialog.h delete mode 100644 src/httpserver.cpp delete mode 100644 src/httpserver.h delete mode 100644 src/ifusediskunmountbutton.cpp delete mode 100644 src/ifusediskunmountbutton.h delete mode 100644 src/ifusemanager.cpp delete mode 100644 src/ifusemanager.h delete mode 100644 src/ifusewidget.cpp delete mode 100644 src/ifusewidget.h create mode 100644 src/image_cache.rs rename src/{rust/src => }/image_loader.rs (62%) create mode 100644 src/image_provider.rs delete mode 100644 src/imageloader.cpp delete mode 100644 src/imageloader.h delete mode 100644 src/imagetask.h create mode 100644 src/include/bridge.h delete mode 100644 src/infolabel.cpp delete mode 100644 src/infolabel.h delete mode 100644 src/installedappswidget.cpp delete mode 100644 src/installedappswidget.h rename src/{rust/src => }/io_manager.rs (100%) delete mode 100644 src/iomanagerclient.cpp delete mode 100644 src/iomanagerclient.h delete mode 100644 src/jailbrokenwidget.cpp delete mode 100644 src/jailbrokenwidget.h delete mode 100644 src/keychaindialog.cpp delete mode 100644 src/keychaindialog.h create mode 100644 src/live_reload.cpp delete mode 100644 src/livescreenwidget.cpp delete mode 100644 src/livescreenwidget.h delete mode 100644 src/loadingspinnerwidget.cpp delete mode 100644 src/loadingspinnerwidget.h delete mode 100644 src/logindialog.cpp delete mode 100644 src/logindialog.h delete mode 100644 src/main.cpp create mode 100644 src/main.rs delete mode 100644 src/mediapreviewdialog.cpp delete mode 100644 src/mediapreviewdialog.h delete mode 100644 src/mediastreamermanager.cpp delete mode 100644 src/mediastreamermanager.h delete mode 100644 src/networkdevicestoconnectwidget.cpp delete mode 100644 src/networkdevicestoconnectwidget.h delete mode 100644 src/networkdeviceswidget.cpp delete mode 100644 src/networkdeviceswidget.h delete mode 100644 src/photoimportdialog.cpp delete mode 100644 src/photoimportdialog.h delete mode 100644 src/photomodel.cpp delete mode 100644 src/photomodel.h delete mode 100644 src/privateinfolabel.cpp delete mode 100644 src/privateinfolabel.h delete mode 100644 src/qballoontip.cpp delete mode 100644 src/qballoontip.h rename src/{rust/src => }/qinput_get_text.cc (100%) create mode 100644 src/qmetaobject_rust.hpp delete mode 100644 src/qprocessindicator.cpp delete mode 100644 src/qprocessindicator.h create mode 100644 src/qquickimageprovider_imp.rs create mode 100644 src/qrc.rs create mode 100644 src/qt_threading.rs rename src/{rust/src => }/query_sqlite.rs (72%) delete mode 100644 src/querymobilegestaltwidget.cpp delete mode 100644 src/querymobilegestaltwidget.h delete mode 100644 src/recoverydeviceinfowidget.cpp delete mode 100644 src/recoverydeviceinfowidget.h delete mode 100644 src/releasechangelogdialog.cpp delete mode 100644 src/releasechangelogdialog.h delete mode 100644 src/responsiveqlabel.cpp delete mode 100644 src/responsiveqlabel.h delete mode 100644 src/rust/build.rs delete mode 100644 src/rust/include/bridge.h delete mode 100644 src/rust/src/afc.rs delete mode 100644 src/rust/src/afc2_services.rs delete mode 100644 src/rust/src/afc_services.rs delete mode 100644 src/rust/src/apps.rs delete mode 100644 src/rust/src/utils.rs rename src/{rust/src => }/screenshot.rs (100%) create mode 100644 src/service_factory.rs rename src/{rust/src => }/service_manager.rs (59%) delete mode 100644 src/settingswidget.cpp delete mode 100644 src/settingswidget.h delete mode 100644 src/sponsorappcard.cpp delete mode 100644 src/sponsorappcard.h delete mode 100644 src/sponsorwidget.cpp delete mode 100644 src/sponsorwidget.h delete mode 100644 src/statusballoon.cpp delete mode 100644 src/statusballoon.h delete mode 100644 src/thumbnailprovider.h delete mode 100644 src/toolboxwidget.cpp delete mode 100644 src/toolboxwidget.h rename src/{qml => ui}/AlbumContents.qml (97%) create mode 100644 src/ui/App.qml rename src/{qml => ui}/AppsTab.qml (94%) rename src/{qml => ui}/Device.qml (76%) create mode 100644 src/ui/DeviceContext.qml rename src/{qml => ui}/DeviceGallery.qml (96%) rename src/{qml => ui}/DeviceImage.qml (100%) rename src/{qml => ui}/DeviceInfo.qml (100%) rename src/{qml => ui}/DeviceTab.qml (65%) create mode 100644 src/ui/FileExplorer.qml create mode 100644 src/ui/FilesSection.qml rename src/{qml => ui}/HowToConnect.qml (100%) create mode 100644 src/ui/IconLoader.qml create mode 100644 src/ui/InstalledApps.qml rename src/{qml => ui}/LoginDialog.qml (98%) rename src/{qml => ui}/Main.qml (92%) rename src/{qml => ui}/PreviewWindow.qml (100%) rename src/{qml => ui}/SidebarTabButton.qml (95%) rename src/{qml => ui}/TabButton.qml (100%) rename src/{qml => ui}/Tabs.qml (81%) create mode 100644 src/ui/Toolbox.qml rename src/{qml => ui}/Welcome.qml (100%) create mode 100644 src/ui/qmldir rename src/{qml => ui}/wIndows/Index.qml (100%) rename src/{qml => ui}/wIndows/Main.qml (100%) create mode 100644 src/utils.rs delete mode 100644 src/virtuallocationwidget.cpp delete mode 100644 src/virtuallocationwidget.h delete mode 100644 src/welcomewidget.cpp delete mode 100644 src/welcomewidget.h delete mode 100644 src/wirelessgalleryimportwidget.cpp delete mode 100644 src/wirelessgalleryimportwidget.h delete mode 100644 src/zlineedit.cpp delete mode 100644 src/zlineedit.h delete mode 100644 src/zloadingwidget.cpp delete mode 100644 src/zloadingwidget.h delete mode 100644 src/ztabwidget.cpp delete mode 100644 src/ztabwidget.h diff --git a/.gitignore b/.gitignore index d88cd19..634d0b0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ devdiskimgs .qt .qtcreator CMakeFiles -src/rust/target \ No newline at end of file +target +.qmlls.ini \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index c0e47b2..67f2884 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -208,5 +208,5 @@ "qregularexpression": "cpp", "qnetworkaccessmanager": "cpp" }, - "rust-analyzer.linkedProjects": ["src/rust/Cargo.toml"] + "rust-analyzer.linkedProjects": ["Cargo.toml"] } diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 8124e2d..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,526 +0,0 @@ -cmake_minimum_required(VERSION 3.16) -project(iDescriptor VERSION 0.5.0 LANGUAGES C CXX) - -if(WIN32) - set(PKG_CONFIG_EXECUTABLE "C:/msys64/mingw64/bin/pkg-config.exe" CACHE FILEPATH "" FORCE) -endif() - -# Feature options -option(ENABLE_RECOVERY_DEVICE_SUPPORT "Enable recovery device support (requires libirecovery)" ON) -set(PACKAGE_MANAGER_HINT "" CACHE STRING "Name of package manager(s) used to manage future updates (e.g. paru, yay, pamac), only used if PACKAGE_MANAGER_MANAGED is ON)") -option(PACKAGE_MANAGER_MANAGED "Build as package manager managed version (auto updates will be handled by the package manager)" OFF) -option(DEPLOY "Deploy the application (WIN32 only)" ON) - -if (APPLE) -find_program(CARGO_EXECUTABLE cargo REQUIRED PATHS /usr/local/bin) -set(ENV{PATH} "$ENV{HOME}/.cargo/bin:/usr/local/bin:$ENV{PATH}") -else() -find_program(CARGO_EXECUTABLE cargo REQUIRED) -endif() - -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -if (APPLE) - # Target at least macOS 13.0 - set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0") -endif() - -# Platform-specific paths for libraries built from source -if(WIN32) - include_directories("C:/msys64/mingw64/include") - link_directories("C:/msys64/mingw64/lib") - set(PKG_CONFIG_EXECUTABLE "C:/msys64/mingw64/bin/pkg-config.exe") - list(APPEND CMAKE_PREFIX_PATH "C:/lxqt") - set(CUSTOM_LIB_PATH "C:/msys64/mingw64/lib") - set(CUSTOM_INCLUDE_PATH "C:/msys64/mingw64/include") - set(CUSTOM_PKGCONFIG_PATH "C:/msys64/mingw64/lib/pkgconfig") - set(ENV{PKG_CONFIG_PATH} "${CUSTOM_PKGCONFIG_PATH};$ENV{PKG_CONFIG_PATH}") -elseif(APPLE) - set(CUSTOM_LIB_PATH "/usr/local/lib") - # TODO: do we need this on macOS? - # set(CUSTOM_INCLUDE_PATH "/usr/local/include") - set(CUSTOM_PKGCONFIG_PATH "/usr/local/lib/pkgconfig") - set(ENV{PKG_CONFIG_PATH} "${CUSTOM_PKGCONFIG_PATH}:$ENV{PKG_CONFIG_PATH}") -else () - set(CUSTOM_LIB_PATH "/usr/local/lib") - set(CUSTOM_PKGCONFIG_PATH "/usr/local/lib/pkgconfig") - set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:${CUSTOM_PKGCONFIG_PATH}") -endif() - -find_package(PkgConfig REQUIRED) -find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia MultimediaWidgets Network QuickControls2 SerialPort Positioning Location QuickWidgets) -# DBUS -if (UNIX AND NOT APPLE) - find_package(Qt6 REQUIRED COMPONENTS DBus) - pkg_check_modules(DBUS REQUIRED IMPORTED_TARGET dbus-1) -endif() -find_package(SQLite3 REQUIRED) -# Add QTermWidget -# Prefer CMake-native qtermwidget6, fallback to pkg-config if needed -find_package(qtermwidget6 QUIET) - -if(NOT qtermwidget6_FOUND) - message(STATUS "qtermwidget6 not found via CMake, trying pkg-config...") - find_package(PkgConfig REQUIRED) - pkg_check_modules(QTERMWIDGET REQUIRED IMPORTED_TARGET qtermwidget6) - # Create alias so we can use qtermwidget6::qtermwidget6 uniformly - add_library(qtermwidget6 ALIAS PkgConfig::QTERMWIDGET) -else() - message(STATUS "Found qtermwidget6 via CMake") -endif() - -if(WIN32) - # Get the path to the Qt bin directory - get_target_property(QT_BIN_PATH Qt${QT_VERSION_MAJOR}::Core IMPORTED_LOCATION_RELEASE) - if(NOT QT_BIN_PATH) - get_target_property(QT_BIN_PATH Qt${QT_VERSION_MAJOR}::Core IMPORTED_LOCATION_DEBUG) - endif() - if(NOT QT_BIN_PATH) - get_target_property(QT_BIN_PATH Qt${QT_VERSION_MAJOR}::Core IMPORTED_LOCATION) - endif() - get_filename_component(QT_BIN_PATH ${QT_BIN_PATH} DIRECTORY) - message(STATUS "Found Qt bin directory: ${QT_BIN_PATH}") -endif() - -#------------- CXX-QT INTEGRATION ------------- - -# Rust always links against non-debug Windows runtime on *-msvc targets -# Note it is best to set this on the command line to ensure all targets are consistent -# https://github.com/corrosion-rs/corrosion/blob/master/doc/src/common_issues.md#linking-debug-cc-libraries-into-rust-fails-on-windows-msvc-targets -# https://github.com/rust-lang/rust/issues/39016 -if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") -endif() - -set(CXXQT_QTCOMPONENTS Core Gui Qml QuickControls2 QuickTest Widgets Test) -if(NOT BUILD_WASM) - set(CXXQT_QTCOMPONENTS ${CXXQT_QTCOMPONENTS}) -endif() - -if(NOT USE_QT5) - find_package(Qt6 COMPONENTS ${CXXQT_QTCOMPONENTS}) - set(Qt "Qt6") -endif() -if(NOT Qt6_FOUND) - find_package(Qt5 5.15 COMPONENTS ${CXXQT_QTCOMPONENTS} REQUIRED) - set(Qt "Qt5") -endif() - -if(MSVC) - # Qt also needs to link against the non-debug version of the MSVC Runtime libraries. - # Note: The Qt:: targets are ALIAS targets that do not support setting properties directly. - # We therefore need to resolve the target names to either Qt5 or Qt6 directly. - set_property( - TARGET ${Qt}::Core ${Qt}::Gui ${Qt}::Qml ${Qt}::QuickControls2 ${Qt}::QuickTest ${Qt}::Test - PROPERTY MAP_IMPORTED_CONFIG_DEBUG "RELEASE") -endif() - - -find_package(CxxQt QUIET) -if(NOT CxxQt_FOUND) - include(FetchContent) - FetchContent_Declare( - CxxQt - GIT_REPOSITORY https://github.com/kdab/cxx-qt-cmake.git - GIT_TAG 0.8.1 - ) - - FetchContent_MakeAvailable(CxxQt) -endif() - -cxx_qt_import_crate( - MANIFEST_PATH src/rust/Cargo.toml - CRATES idescriptor_rust_codebase - # LOCKED - QT_MODULES Qt::Core Qt::Gui Qt::Qml Qt::QuickControls2 Qt::Widgets -) - -#-------------------------------------------------------------------------------- - -# Add QR code generation library -pkg_check_modules(QRENCODE REQUIRED IMPORTED_TARGET libqrencode) -pkg_check_modules(HEIF REQUIRED IMPORTED_TARGET libheif) -pkg_check_modules(ZIP REQUIRED IMPORTED_TARGET libzip) - -if(WIN32) - pkg_check_modules(LIBARCHIVE REQUIRED IMPORTED_TARGET libarchive) -endif() - -# Add FFmpeg libraries for video thumbnail generation -pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat) -pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec) -pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil) -pkg_check_modules(SWSCALE REQUIRED IMPORTED_TARGET libswscale) - - -# Add libssh for SSH connections -pkg_check_modules(SSH REQUIRED IMPORTED_TARGET libssh) - -# Apple-specific crypto libraries for SSH -if(APPLE) - find_library(SECURITY_FRAMEWORK Security REQUIRED) - find_library(COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED) -endif() - - -pkg_check_modules(PUGIXML REQUIRED IMPORTED_TARGET pugixml) - -if(ENABLE_RECOVERY_DEVICE_SUPPORT) - find_library(IRECOVERY_LIBRARY - NAMES irecovery-1.0 - ${CUSTOM_FIND_LIB_ARGS} - ) - if(IRECOVERY_LIBRARY) - message(STATUS "Building with recovery device support enabled") - else() - message(WARNING "libirecovery not found. Recovery device support will be disabled. This is to be expected if you are installing from Arch AUR.") - set(ENABLE_RECOVERY_DEVICE_SUPPORT OFF) - endif() -else() - message(STATUS "Recovery device support disabled") -endif() - -file(GLOB PROJECT_SOURCES -# src/*.h -# src/*.cpp -src/core/helpers/*.cpp -src/core/services/*.cpp -# src/base/*.cpp -# src/base/*.h -src/networkdeviceprovider.h -src/main.cpp -src/constants.h -# src/thumbnailmodel.h -# src/thumbnailmodel.cpp -src/thumbnailprovider.h -resources.qrc -resources.ui.qrc -) - - -if (NOT ENABLE_RECOVERY_DEVICE_SUPPORT) - list(REMOVE_ITEM PROJECT_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/src/recoverydeviceinfowidget.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/recoverydeviceinfowidget.h - src/recoverydeviceinfowidget.cpp - src/recoverydeviceinfowidget.h - ) -endif() - -if(APPLE) - list(APPEND PROJECT_SOURCES - src/platform/macos/macos.mm - src/core/services/dnssd/dnssd_service.cpp - src/core/services/dnssd/dnssd_service.h - ) -elseif (WIN32) - list(APPEND PROJECT_SOURCES - src/core/services/dnssd/dnssd_service.cpp - src/core/services/dnssd/dnssd_service.h - ) - - file(GLOB WINDOWS_PLATFORM_SOURCES src/platform/windows/*.cpp src/platform/windows/*.h src/platform/windows/widgets/*.cpp src/platform/windows/widgets/*.h) - list(APPEND PROJECT_SOURCES ${WINDOWS_PLATFORM_SOURCES}) - list(APPEND PROJECT_SOURCES - resources.win.qrc) -else() - list(APPEND PROJECT_SOURCES - src/core/services/avahi/avahi_service.cpp - src/core/services/avahi/avahi_service.h - ) -endif() - -add_subdirectory(lib/uxplay) -add_subdirectory(lib/ipatool-go) -add_subdirectory(lib/zupdater) - -if (WIN32) - set(NO_DEPLOY_WIN_IFUSE ON) - add_subdirectory(lib/win-ifuse) -endif() - - - -if (WIN32) - set(app_icon_resource_windows "${CMAKE_CURRENT_SOURCE_DIR}/idescriptor.rc") - qt_add_executable(iDescriptor - MANUAL_FINALIZATION - ${PROJECT_SOURCES} - ${app_icon_resource_windows} -) -elseif (APPLE) - set(MACOSX_BUNDLE_ICON_FILE icon.icns) - set(app_icon_macos "${CMAKE_CURRENT_SOURCE_DIR}/resources/icons/app-icon/icon.icns") - set_source_files_properties(${app_icon_macos} PROPERTIES - MACOSX_PACKAGE_LOCATION "Resources") - - qt_add_executable(iDescriptor - MANUAL_FINALIZATION - ${PROJECT_SOURCES} - ${app_icon_macos} -) -else() - qt_add_executable(iDescriptor - MANUAL_FINALIZATION - ${PROJECT_SOURCES} -) -endif() - -set(RUST_CODEBASE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/rust) - -if(WIN32) - # sqlite3 deps for rust codebase - target_link_libraries(idescriptor_rust_codebase INTERFACE bcrypt userenv ws2_32 advapi32) -endif() - -target_link_libraries(iDescriptor PRIVATE - Qt6::Widgets - Qt6::Multimedia - Qt6::MultimediaWidgets - Qt6::Network - Qt6::Core - Qt6::Quick - Qt6::Location - Qt6::Positioning - Qt6::QuickWidgets - Qt6::Qml - Qt6::QuickControls2 - PkgConfig::SSH - ${SSH_LIBRARY} - PkgConfig::PUGIXML - PkgConfig::QRENCODE - qtermwidget6 - PkgConfig::HEIF - PkgConfig::ZIP - PkgConfig::AVFORMAT - PkgConfig::AVCODEC - PkgConfig::AVUTIL - PkgConfig::SWSCALE - uxplay - ipatool-go - ZUpdater - idescriptor_rust_codebase - SQLite::SQLite3 -) - -if (UNIX AND NOT APPLE) - target_link_libraries(iDescriptor PRIVATE Qt6::DBus PkgConfig::DBUS) -endif() - -if(ENABLE_RECOVERY_DEVICE_SUPPORT) - target_link_libraries(iDescriptor PRIVATE ${IRECOVERY_LIBRARY}) - target_compile_definitions(iDescriptor PRIVATE ENABLE_RECOVERY_DEVICE_SUPPORT) -endif() - -target_include_directories(iDescriptor PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/lib - ${CMAKE_CURRENT_SOURCE_DIR}/lib/zupdater/src -) - - - -target_include_directories(iDescriptor PRIVATE ${IDEVICE_IMPLEMENTATION_INCLUDES}) - -if(APPLE) - find_library(CORE_SERVICES_FRAMEWORK CoreServices REQUIRED) - target_link_libraries(iDescriptor PRIVATE - ${CORE_SERVICES_FRAMEWORK}) - message(STATUS "Using macOS Bonjour framework for network service discovery") -elseif (WIN32) - target_link_libraries(iDescriptor PRIVATE PkgConfig::LIBARCHIVE dwmapi ntdll) - find_path(DNSSD_INCLUDE_DIR dns_sd.h HINTS ${BONJOUR_SDK}/Include ) - # $<$ fixes winres compiler errors - target_include_directories(iDescriptor PRIVATE - $<$:${DNSSD_INCLUDE_DIR}> - ) -else() - pkg_check_modules(AVAHI_CLIENT REQUIRED IMPORTED_TARGET avahi-client) - - target_link_libraries(iDescriptor PRIVATE - PkgConfig::AVAHI_CLIENT - # PkgConfig::AVAHI_COMMON - ) - message(STATUS "Using Avahi for network service discovery") -endif() - - -# Add Apple-specific frameworks for SSH -if(APPLE) - target_link_libraries(iDescriptor PRIVATE - ${SECURITY_FRAMEWORK} - ${COREFOUNDATION_FRAMEWORK} - ) -endif() - -# Add compile definition for source directory -target_compile_definitions(iDescriptor PRIVATE - SOURCE_DIR="${CMAKE_SOURCE_DIR}" -) - -if(PACKAGE_MANAGER_MANAGED) - target_compile_definitions(iDescriptor PRIVATE PACKAGE_MANAGER_MANAGED) - message(STATUS "Building as package manager managed version, updates will be handled by the package manager") - if(PACKAGE_MANAGER_HINT) - message(STATUS "Configured package manager hint: ${PACKAGE_MANAGER_HINT}") - target_compile_definitions(iDescriptor PRIVATE PACKAGE_MANAGER_HINT=\"${PACKAGE_MANAGER_HINT}\") - else() - target_compile_definitions(iDescriptor PRIVATE PACKAGE_MANAGER_HINT="your package manager") - endif() -endif() - - -target_compile_definitions(iDescriptor PRIVATE - APP_VERSION="${PROJECT_VERSION}" -) - -set_target_properties(iDescriptor PROPERTIES - ${BUNDLE_ID_OPTION} - MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} - MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} - MACOSX_BUNDLE TRUE - WIN32_EXECUTABLE TRUE - BUILD_WITH_INSTALL_RPATH TRUE -) - -if (UNIX AND NOT APPLE) - - # Required on Linux to find libirecovery-1.0.so.5 at runtime - if (ENABLE_RECOVERY_DEVICE_SUPPORT) - set_target_properties(iDescriptor PROPERTIES - # Control library search order - system libs first, then /usr/local/lib - INSTALL_RPATH "/usr/lib/x86_64-linux-gnu:/usr/lib:/usr/local/lib:$ORIGIN" - ) - endif() - -# Add install rules for the project -# include(GNUInstallDirs) - -# Install the main executable -install(TARGETS iDescriptor - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} -) - -# Install the .desktop file -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/iDescriptor.desktop - DESTINATION ${CMAKE_INSTALL_DATADIR}/applications -) - -# Install the application icon -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/icons/app-icon/icon.png - RENAME iDescriptor.png - DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/256x256/apps -) -endif() - -include(GNUInstallDirs) - -if (WIN32) -# Set the installation directory to be within the build folder -set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/dist") -endif() -if(QT_VERSION_MAJOR EQUAL 6) - qt_finalize_executable(iDescriptor) -endif() - -# Copy runtime DLLs to build directory after building -if(WIN32 AND DEPLOY) - add_custom_command(TARGET iDescriptor POST_BUILD - COMMAND ${CMAKE_COMMAND} -E echo "Starting Windows deployment..." - COMMAND ${CMAKE_COMMAND} - -DEXECUTABLE_PATH=$ - -DQT_BIN_PATH=${QT_BIN_PATH} - -DMSYS2_BIN_PATH=C:/msys64/mingw64/bin - -DOUTPUT_DIR=$ - -DQML_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/qml - -DPROJECT_SOURCE_DIR=${CMAKE_SOURCE_DIR} - -DWIN_IFUSE=$ - -P ${CMAKE_CURRENT_LIST_DIR}/cmake/win-deploy.cmake - COMMENT "Deploying Windows application with all dependencies" - VERBATIM - ) -endif() - -if(WIN32) - install(CODE " - message(STATUS \"Deploying dependencies to installation directory for packaging...\") - message(STATUS \"CMAKE_INSTALL_PREFIX: \${CMAKE_INSTALL_PREFIX}\") - # copy executable to dist dir - file(MAKE_DIRECTORY \"\${CMAKE_INSTALL_PREFIX}\") - # file(COPY DESTINATION CMAKE_INSTALL_PREFIX) - file(COPY ${CMAKE_CURRENT_BINARY_DIR}/iDescriptor.exe DESTINATION ${CMAKE_INSTALL_PREFIX}) - # Check if file exists before deployment - set(EXECUTABLE_PATH \"\${CMAKE_INSTALL_PREFIX}/iDescriptor.exe\") - message(STATUS \"Looking for executable at: \${EXECUTABLE_PATH}\") - - if(EXISTS \"\${EXECUTABLE_PATH}\") - message(STATUS \"SUCCESS: Executable found at \${EXECUTABLE_PATH}\") - else() - message(STATUS \"ERROR: Executable NOT found at \${EXECUTABLE_PATH}\") - endif() - - execute_process( - COMMAND \"${CMAKE_COMMAND}\" - -DEXECUTABLE_PATH=\${EXECUTABLE_PATH} - -DQT_BIN_PATH=\"${QT_BIN_PATH}\" - -DMSYS2_BIN_PATH=\"C:/msys64/mingw64/bin\" - -DOUTPUT_DIR=\"\${CMAKE_INSTALL_PREFIX}\" - -DQML_SOURCE_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/qml\" - -DPROJECT_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\" - -DWIN_IFUSE=$ - -P \"${CMAKE_CURRENT_LIST_DIR}/cmake/win-deploy.cmake\" - ) - ") -endif() - -# Packaging Configuration (CPack) -set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) -set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) -set(CPACK_WIX_VERSION 4) -set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "iDescriptor Application") -set(CPACK_PACKAGE_VENDOR "iDescriptor") -set(CPACK_PACKAGE_CONTACT "https://github.com/iDescriptor/iDescriptor") - -set(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_BINARY_DIR}/artifacts") - -if(WIN32) - set(CPACK_GENERATOR "WIX;ZIP") - # FIXME: arm64 build support - set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-v${PROJECT_VERSION}-Windows_x86_64") - - set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}") - - set(CPACK_WIX_UPGRADE_GUID "D6C5B4A3-F2E1-D0C9-B8A7-F6E5D4C3B2A1") - set(CPACK_WIX_UI_REF "WixUI_InstallDir") - - set(CPACK_WIX_UI_BANNER "${CMAKE_CURRENT_SOURCE_DIR}/resources/installer/banner.bmp") - set(CPACK_WIX_UI_DIALOG "${CMAKE_CURRENT_SOURCE_DIR}/resources/installer/dialog.bmp") - - set(CPACK_WIX_PROPERTY_ARPCOMMENTS "A free, open-source, and cross-platform iDevice management tool written in C++.") - set(CPACK_WIX_PROPERTY_ARPHELPLINK "${CPACK_PACKAGE_CONTACT}/issues") - set(CPACK_WIX_PROPERTY_ARPURLINFOABOUT "${CPACK_PACKAGE_CONTACT}") - set(CPACK_WIX_PROPERTY_ARPURLUPDATEINFO "${CPACK_PACKAGE_CONTACT}/releases") - - set(CPACK_WIX_INSTALL_SCOPE "perMachine") - set(CPACK_WIX_PROGRAM_MENU_FOLDER "${PROJECT_NAME}") - set(CPACK_PACKAGE_EXECUTABLES "iDescriptor" "iDescriptor") - set(CPACK_CREATE_DESKTOP_LINKS "iDescriptor") - set(CPACK_WIX_PRODUCT_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/icons/app-icon/icon.ico") - set(CPACK_WIX_LICENSE_RTF "${CMAKE_CURRENT_SOURCE_DIR}/resources/installer/LICENSE.rtf") - - set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) - set(CPACK_ARCHIVE_COMPONENT_INSTALL TRUE) - set(CPACK_ARCHIVE_INSTALL_DIRECTORY ".") - - # Tell CPack to use the pre-built dist directory - set(CPACK_INSTALLED_DIRECTORIES "${CMAKE_INSTALL_PREFIX};.") - - # Prevent CPack from running install again - set(CPACK_INSTALL_CMAKE_PROJECTS "") -endif() - -include(CPack) - diff --git a/src/rust/Cargo.lock b/Cargo.lock similarity index 90% rename from src/rust/Cargo.lock rename to Cargo.lock index adbc4c0..471d5f1 100644 --- a/src/rust/Cargo.lock +++ b/Cargo.lock @@ -143,7 +143,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -348,55 +348,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-format" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "696283b40e1a39d208ee614b92e5f6521d16962edeb47c48372585ec92419943" -dependencies = [ - "thiserror 1.0.69", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - [[package]] name = "cmake" version = "0.1.58" @@ -406,27 +357,6 @@ dependencies = [ "cc", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width 0.1.14", -] - -[[package]] -name = "codespan-reporting" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" -dependencies = [ - "serde", - "termcolor", - "unicode-width 0.2.2", -] - [[package]] name = "colorchoice" version = "1.0.5" @@ -459,33 +389,12 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" -[[package]] -name = "console" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" -dependencies = [ - "encode_unicode", - "libc", - "unicode-width 0.2.2", - "windows-sys 0.61.2", -] - [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cookie" version = "0.18.1" @@ -541,6 +450,53 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f6422bf20bc0654eac481a29a03e6d9121ad9f12345a03773b8a76a8701915" +dependencies = [ + "cpp_macros", +] + +[[package]] +name = "cpp_build" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f6fed3200ba0708c2adca5f6ed5ae202edd824bd4cbac7935a85edac9bcddce" +dependencies = [ + "cc", + "cpp_common", + "proc-macro2", + "regex", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "cpp_common" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7280a73ef92e18d27d2ec3005b57fe0043b51d1b506be86b0bf66f588f9857b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "cpp_macros" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc7fd49a5b246251229ea4d4bf94c8d418689a1f9986ef96e00b64992922169" +dependencies = [ + "aho-corasick", + "byteorder", + "cpp_common", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -568,34 +524,12 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossfire" -version = "2.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd901251b9b46c1752c85edfee0aee718c03a85a065f4126d32e5d6d419edf48" -dependencies = [ - "crossbeam-queue", - "crossbeam-utils", - "enum_dispatch", - "futures-core", - "parking_lot", -] - [[package]] name = "crypto-common" version = "0.1.7" @@ -607,6 +541,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -631,152 +575,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "cxx" -version = "1.0.194" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" -dependencies = [ - "cc", - "cxx-build", - "cxxbridge-cmd", - "cxxbridge-flags", - "cxxbridge-macro", - "foldhash 0.2.0", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.194" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" -dependencies = [ - "cc", - "codespan-reporting 0.13.1", - "indexmap", - "proc-macro2", - "quote", - "scratch", - "syn", -] - -[[package]] -name = "cxx-gen" -version = "0.7.194" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "035b6c61a944483e8a4b2ad4fb8b13830d63491bd004943716ad16d85dcc64bc" -dependencies = [ - "codespan-reporting 0.13.1", - "indexmap", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cxx-qt" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdf26e5ee4375a85799d0fe436662e5a8ef099bd0964b84bce8812c35c7daea" -dependencies = [ - "cxx", - "cxx-qt-build", - "cxx-qt-macro", - "qt-build-utils", - "static_assertions", - "thiserror 1.0.69", -] - -[[package]] -name = "cxx-qt-build" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a88c7f8241bfe4dfd19d27397a5845f0f275735a34ee7fb219045da81945e3" -dependencies = [ - "cc", - "codespan-reporting 0.11.1", - "cxx-gen", - "cxx-qt-gen", - "proc-macro2", - "qt-build-utils", - "quote", - "serde", - "serde_json", -] - -[[package]] -name = "cxx-qt-gen" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1917c90b10117890e3794f4a0dc764a24704de40711d688ae128b0704490b8" -dependencies = [ - "clang-format", - "convert_case", - "cxx-gen", - "indoc", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cxx-qt-lib" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b3de184cff59ad29d657021f933380e584d9b70265247f6a96098bef964211" -dependencies = [ - "cxx", - "cxx-qt", - "cxx-qt-build", - "qt-build-utils", -] - -[[package]] -name = "cxx-qt-macro" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230b0d8b20d3d02a85885742be4fabc0382fead4e382dd8c9c9ff1827f221e8c" -dependencies = [ - "cxx-qt-gen", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cxxbridge-cmd" -version = "1.0.194" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" -dependencies = [ - "clap", - "codespan-reporting 0.13.1", - "indexmap", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.194" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.194" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" -dependencies = [ - "indexmap", - "proc-macro2", - "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -821,7 +620,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -841,7 +640,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -885,7 +684,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -928,12 +727,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -943,18 +736,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "env_filter" version = "1.0.1" @@ -1020,13 +801,12 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "2d5b2eef6fafbf69f877e55509ce5b11a760690ac9700a2921be067aa6afaef6" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -1153,7 +933,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1238,9 +1018,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1275,9 +1055,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -1525,29 +1305,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "idescriptor_rust_codebase" -version = "0.1.0" +name = "idescriptor" +version = "0.5.0" dependencies = [ "anyhow", - "cxx", - "cxx-qt", - "cxx-qt-build", - "cxx-qt-lib", + "cc", + "cpp", + "cpp_build", + "cstr", "filetime", "futures", "idevice", "ipatool", + "macros", "once_cell", + "oneshot", + "pkg-config", "plist", "plist-macro", "priority-queue", + "qmetaobject", + "qttypes", "regex", "rusqlite", "serde_json", + "simplelog", "tokio", "tracing", + "url", "urlencoding", "uuid", + "walkdir", + "winres", ] [[package]] @@ -1634,33 +1423,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] -[[package]] -name = "indicatif" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" -dependencies = [ - "console", - "portable-atomic", - "unicode-width 0.2.2", - "unit-prefix", - "web-time", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inout" version = "0.1.4" @@ -1676,16 +1443,13 @@ name = "ipatool" version = "0.1.0" dependencies = [ "bytes", - "clap", "cookie_store", "dirs", "env_logger", "futures-util", - "indicatif", "keyring", "log", "mac_address", - "owo-colors", "plist", "regex", "reqwest", @@ -1704,16 +1468,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1747,20 +1501,22 @@ checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "jktcp" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b9d88c89c8fe802c7e7c2bf32b0fb85ef53187c30a8f130d41578b4362baa7" +checksum = "cfc0141c2b44544a1f62f3fcc0337c00904b8b80b836107517f3206a3de99a08" dependencies = [ - "crossfire", "futures", + "getrandom 0.3.4", "rand 0.9.4", "tokio", "tracing", + "wasm-bindgen-futures", + "wasmtimer", ] [[package]] @@ -1790,7 +1546,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.117", ] [[package]] @@ -1809,7 +1565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1824,9 +1580,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ "cfg-if", "futures-util", @@ -1897,10 +1653,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", "libc", - "plain", - "redox_syscall 0.7.4", ] [[package]] @@ -1914,15 +1667,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" -dependencies = [ - "cc", -] - [[package]] name = "litemap" version = "0.8.2" @@ -1966,6 +1710,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "md-5" version = "0.10.6" @@ -2113,6 +1866,15 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2125,6 +1887,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oneshot" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -2143,12 +1911,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "owo-colors" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" - [[package]] name = "parking" version = "2.2.1" @@ -2173,7 +1935,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -2195,22 +1957,22 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2219,6 +1981,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs1" version = "0.7.5" @@ -2246,12 +2014,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "plist" version = "1.9.0" @@ -2331,7 +2093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -2371,23 +2133,47 @@ dependencies = [ ] [[package]] -name = "qt-build-utils" -version = "0.8.1" +name = "qmetaobject" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d074750fd3baba12fdb47388d591ad9ed043e23864a79d129dcf3a1ef6fc8d9" +checksum = "426a57e85d36f055a0c82cb0a8a261d49ba051ab2a2ef5471835f69d477816cd" dependencies = [ - "anyhow", - "cc", + "cpp", + "cpp_build", + "lazy_static", + "log", + "qmetaobject_impl", + "qttypes", + "semver", +] + +[[package]] +name = "qmetaobject_impl" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc24897c707dcd6963e359e7f2b123857c508f129bed8ac4d3bd575c1a47627" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "qttypes" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7edf5b38c97ad8900ad2a8418ee44b4adceaa866a4a3405e2f1c909871d7ebd" +dependencies = [ + "cpp", + "cpp_build", "semver", - "serde", - "thiserror 1.0.69", ] [[package]] name = "quick-xml" -version = "0.39.2" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] @@ -2553,15 +2339,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" -dependencies = [ - "bitflags", -] - [[package]] name = "redox_users" version = "0.4.6" @@ -2846,12 +2623,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scratch" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" - [[package]] name = "security-framework" version = "2.11.1" @@ -2921,7 +2692,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3019,6 +2790,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "slab" version = "0.4.12" @@ -3075,24 +2857,23 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -3121,7 +2902,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3180,7 +2961,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3191,7 +2972,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3202,7 +2983,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -3268,14 +3051,14 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3296,7 +3079,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3323,6 +3106,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "tower" version = "0.5.3" @@ -3340,20 +3132,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3387,7 +3179,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3417,36 +3209,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - [[package]] name = "universal-hash" version = "0.5.1" @@ -3563,9 +3331,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -3576,9 +3344,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ "js-sys", "wasm-bindgen", @@ -3586,9 +3354,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3596,22 +3364,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -3664,10 +3432,23 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.97" +name = "wasmtimer" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -3744,7 +3525,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3755,7 +3536,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4015,6 +3796,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -4051,7 +3841,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -4067,7 +3857,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -4160,7 +3950,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4181,7 +3971,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4201,7 +3991,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4222,7 +4012,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4255,7 +4045,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/src/rust/Cargo.toml b/Cargo.toml similarity index 53% rename from src/rust/Cargo.toml rename to Cargo.toml index 8ab17de..c3bde2b 100644 --- a/src/rust/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,13 @@ [package] -name = "idescriptor_rust_codebase" -version = "0.1.0" +name = "idescriptor" +version = "0.5.0" edition = "2024" -[lib] -name = "idescriptor_rust_codebase" -crate-type = ["staticlib"] - [dependencies] tokio = { version = "1.50.0", features = ["full"] } futures = "0.3" -idevice = { path = "../../lib/idevice-rs/idevice", features = ["full"] } +idevice = { path = "lib/idevice-rs/idevice", features = ["full"] } uuid = { version = "1.22.0", features = ["v4"] } -cxx = "1.0.95" -cxx-qt = "0.8.1" -cxx-qt-lib = { version = "0.8.1", features = ["qt_full"] } plist = "1.8.0" plist-macro = "0.1.6" tracing = "0.1.44" @@ -27,7 +20,18 @@ filetime = "0.2.27" priority-queue = "2.7.0" anyhow = "1.0.102" ipatool = { path = "/home/uncore/Desktop/proj/ipatool-rs", default-features = false } - +cpp = "0.5.11" +cstr = "0.2.12" +qmetaobject = "0.2.10" +qttypes = { version = "0.2.12", default-features = false, features = ["required", "qtquick", "qtquickcontrols2"]} +simplelog = "0.12.2" +url = "2.5.8" +oneshot = "0.2.1" +macros = { path = "lib/macros"} [build-dependencies] -cxx-qt-build = { version = "0.8.1", features = ["link_qt_object_files"] } \ No newline at end of file +cc = "1.2.61" +cpp_build = "0.5.11" +walkdir = "2.5.0" +winres = "0.1.12" +pkg-config = "0.3.33" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..56d391c --- /dev/null +++ b/build.rs @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2021-2022 Adrian + +use std::env; +use std::fmt::Write; +use std::path::Path; +use std::process::Command; +use walkdir::WalkDir; + +fn compile_qml(dir: &str, qt_include_path: &str, qt_library_path: &str) { + let mut config = cc::Build::new(); + config.include(qt_include_path); + config.include(&format!("{}/QtCore", qt_include_path)); + config.include(&format!("{}/QtQml", qt_include_path)); + + println!("cargo:rerun-if-changed=src/live_reload.cpp"); + + if cfg!(target_os = "macos") { + config.include(format!("{}/QtCore.framework/Headers/", qt_library_path)); + config.include(format!("{}/QtQml.framework/Headers/", qt_library_path)); + } + for f in std::env::var("DEP_QT_COMPILE_FLAGS") + .unwrap() + .split_terminator(';') + { + config.flag(f); + } + + println!("cargo:rerun-if-changed={}", dir); + + let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = Path::new(&out_dir); + let main_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let mut files = Vec::new(); + let mut qrc = "\n\n".to_string(); + WalkDir::new(dir).into_iter().flatten().for_each(|entry| { + let f_name = entry.path().to_string_lossy().replace('\\', "/"); + if f_name.ends_with(".qml") || f_name.ends_with(".js") { + let _ = writeln!(qrc, "{}", f_name); + + let cpp_name = f_name + .replace('/', "_") + .replace(".qml", ".cpp") + .replace(".js", ".cpp"); + let cpp_path = out_dir.join(cpp_name).to_string_lossy().to_string(); + + config.file(&cpp_path); + files.push((f_name, cpp_path)); + } + }); + + let qt_path = std::path::Path::new(qt_library_path).parent().unwrap(); + let compiler_path = if qt_path.join("libexec/qmlcachegen").exists() { + qt_path + .join("libexec/qmlcachegen") + .to_string_lossy() + .to_string() + } else if qt_path.join("../macos/libexec/qmlcachegen").exists() { + qt_path + .join("../macos/libexec/qmlcachegen") + .to_string_lossy() + .to_string() + } else if env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" + && env::var("CARGO_CFG_TARGET_ARCH").unwrap() == "aarch64" + { + qt_path + .join("../msvc2019_64/bin/qmlcachegen") + .to_string_lossy() + .to_string() + } else { + "qmlcachegen".to_string() + }; + + qrc.push_str("\n"); + let qrc_path = Path::new(&main_dir) + .join("ui.qrc") + .to_string_lossy() + .to_string(); + std::fs::write(&qrc_path, qrc).unwrap(); + + for (qml, cpp) in &files { + assert!( + Command::new(&compiler_path) + .args(["--resource", &qrc_path, "-o", cpp, qml]) + .status() + .unwrap() + .success() + ); + } + + let loader_path = out_dir + .join("qmlcache_loader.cpp") + .to_str() + .unwrap() + .to_string(); + assert!( + Command::new(&compiler_path) + .args([ + "--resource-file-mapping", + &qrc_path, + "-o", + &loader_path, + "ui.qrc" + ]) + .status() + .unwrap() + .success() + ); + + config.file(&loader_path); + + std::fs::remove_file(&qrc_path).unwrap(); + + config.cargo_metadata(false).compile("qmlcache"); + println!("cargo:rustc-link-lib=static:+whole-archive=qmlcache"); +} + +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"); + + let mut cc_build = cc::Build::new(); + cc_build + .cpp(true) + .file("src/bridge.cpp") + .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"); + + 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); + println!("cargo:rustc-link-search={}/lib64", ffmpeg_dir); + } else { + // Fallback to pkg-config on Linux + if let Ok(lib) = pkg_config::Config::new() + .atleast_version("58") + .probe("libavformat") + { + for path in lib.include_paths { + cc_build.include(path); + } + } + let _ = pkg_config::Config::new().probe("libavcodec"); + let _ = pkg_config::Config::new().probe("libavutil"); + let _ = pkg_config::Config::new().probe("libswscale"); + } + + cc_build.compile("bridge"); + + for lib in ["avformat", "avcodec", "avutil", "swscale"] { + println!("cargo:rustc-link-lib={}", lib); + } +} + +fn main() { + println!("cargo:rerun-if-changed=src/main.rs"); + + let qt_include_path = env::var("DEP_QT_INCLUDE_PATH").unwrap(); + let qt_library_path = env::var("DEP_QT_LIBRARY_PATH").unwrap(); + let qt_version = env::var("DEP_QT_VERSION").unwrap(); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + + if let Ok(out_dir) = env::var("OUT_DIR") { + println!("cargo::rustc-check-cfg=cfg(compiled_qml)"); + if out_dir.contains("\\deploy\\build\\") + || out_dir.contains("/deploy/build/") + || target_os == "android" + || target_os == "ios" + { + compile_qml("src/ui/", &qt_include_path, &qt_library_path); + println!("cargo:rustc-cfg=compiled_qml"); + } + } + + let mut config = cpp_build::Config::new(); + + for f in env::var("DEP_QT_COMPILE_FLAGS") + .unwrap() + .split_terminator(';') + { + config.flag(f); + } + + let mut public_include = |name| { + if cfg!(target_os = "macos") { + config.include(format!("{}/{}.framework/Headers/", qt_library_path, name)); + } + config.include(format!("{}/{}", qt_include_path, name)); + }; + public_include("QtCore"); + public_include("QtGui"); + public_include("QtQuick"); + public_include("QtQml"); + public_include("QtQuickControls2"); + + let mut private_include = |name| { + if cfg!(target_os = "macos") { + config.include(format!( + "{}/{}.framework/Headers/{}", + qt_library_path, name, qt_version + )); + config.include(format!( + "{}/{}.framework/Headers/{}/{}", + qt_library_path, name, qt_version, name + )); + } + config + .include(format!("{}/{}/{}", qt_include_path, name, qt_version)) + .include(format!( + "{}/{}/{}/{}", + qt_include_path, name, qt_version, name + )); + }; + private_include("QtCore"); + private_include("QtGui"); + private_include("QtQuick"); + private_include("QtQml"); + + match target_os.as_str() { + "android" => { + println!( + "cargo:rustc-link-search={}/lib/arm64-v8a", + std::env::var("FFMPEG_DIR").unwrap() + ); + println!( + "cargo:rustc-link-search={}/lib", + std::env::var("FFMPEG_DIR").unwrap() + ); + config.include(format!("{}/include", std::env::var("FFMPEG_DIR").unwrap())); + } + "macos" | "ios" => { + println!( + "cargo:rustc-link-search={}/lib", + std::env::var("FFMPEG_DIR").unwrap() + ); + println!("cargo:rustc-link-lib=static:+whole-archive=x264"); + println!("cargo:rustc-link-lib=static=x265"); + } + "linux" => { + // println!( + // "cargo:rustc-link-search={}", + // std::env::var("OPENCV_LINK_PATHS").unwrap() + // // ); + // println!( + // "cargo:rustc-link-search={}/lib/{}", + // std::env::var("FFMPEG_DIR").unwrap(), + // std::env::var("FFMPEG_ARCH").unwrap_or("amd64".into()) + // ); + // println!( + // "cargo:rustc-link-search={}/lib", + // std::env::var("FFMPEG_DIR").unwrap() + // ); + // println!("cargo:rustc-link-lib=static:+whole-archive=z"); + // if std::env::var("OPENCV_LINK_PATHS") + // .unwrap_or_default() + // .contains("vcpkg") + // { + // std::env::var("OPENCV_LINK_LIBS") + // .unwrap() + // .split(',') + // .for_each(|lib| { + // println!("cargo:rustc-link-lib=static:+whole-archive={}", lib.trim()) + // }); + // } else { + // std::env::var("OPENCV_LINK_LIBS") + // .unwrap() + // .split(',') + // .for_each(|lib| println!("cargo:rustc-link-lib={}", lib.trim())); + // } + } + "windows" => { + println!("cargo:rustc-link-arg=/EXPORT:NvOptimusEnablement"); + println!("cargo:rustc-link-arg=/EXPORT:AmdPowerXpressRequestHighPerformance"); + println!( + "cargo:rustc-link-search={}", + std::env::var("OPENCV_LINK_PATHS").unwrap() + ); + println!( + "cargo:rustc-link-search={}\\lib\\{}", + std::env::var("FFMPEG_DIR").unwrap(), + std::env::var("FFMPEG_ARCH").unwrap_or("x64".into()) + ); + println!( + "cargo:rustc-link-search={}\\lib", + std::env::var("FFMPEG_DIR").unwrap() + ); + let mut res = winres::WindowsResource::new(); + res.set_icon("resources/app_icon.ico"); + res.set("FileVersion", env!("CARGO_PKG_VERSION")); + res.set("ProductVersion", env!("CARGO_PKG_VERSION")); + res.set("ProductName", "Gyroflow"); + res.set( + "FileDescription", + &format!("Gyroflow v{}", env!("CARGO_PKG_VERSION")), + ); + res.compile().unwrap(); + } + tos => panic!("unknown target os {:?}!", tos), + } + + if let Ok(time) = std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH) + { + println!( + "cargo:rustc-env=BUILD_TIME={}", + (time.as_secs() - 1642516578) / 600 + ); // New version every 10 minutes + } + + // !IMPORTANT + config.include(&qt_include_path).build("src/main.rs"); + + if target_os == "ios" { + let out_dir = env::var("OUT_DIR").unwrap(); + for entry in Path::new(&out_dir).read_dir().unwrap() { + let path = entry.unwrap().path(); + if path.is_file() && path.to_string_lossy().contains("qml_plugins.o") { + println!("cargo:rustc-link-arg=-force_load"); + println!("cargo:rustc-link-arg={}", path.to_string_lossy()); + break; + } + } + } + + compile_bridge(&qt_include_path); +} diff --git a/lib/macros/Cargo.toml b/lib/macros/Cargo.toml new file mode 100644 index 0000000..219725c --- /dev/null +++ b/lib/macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" \ No newline at end of file diff --git a/lib/macros/src/lib.rs b/lib/macros/src/lib.rs new file mode 100644 index 0000000..74833e8 --- /dev/null +++ b/lib/macros/src/lib.rs @@ -0,0 +1,25 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, parse_macro_input}; + +#[proc_macro_derive(QtThreading)] +pub fn derive_qt_threading(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = input.ident; + let generics = input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let expanded = quote! { + impl #impl_generics crate::qt_threading::QtThreading for #name #ty_generics #where_clause { + fn qt_thread(&self) -> crate::qt_threading::QtThread + where + Self: Sized, + { + crate::qt_threading::QtThread::new(self) + } + } + }; + + TokenStream::from(expanded) +} diff --git a/src/afc_services.rs b/src/afc_services.rs new file mode 100644 index 0000000..1d79285 --- /dev/null +++ b/src/afc_services.rs @@ -0,0 +1,683 @@ +use crate::qt_threading::{QtThread, QtThreading}; +use crate::{APP_DEVICE_STATE, RUNTIME, run_sync}; +use idevice::{ + IdeviceService, + afc::{AfcClient, opcode::AfcFopenMode}, +}; +use once_cell::sync::Lazy; +use qmetaobject::prelude::*; +use qttypes::{QStringList, QVariantMap}; +use regex::Regex; +use std::cmp::min; +use std::{any::type_name, sync::Arc}; +use std::{collections::HashMap, net::IpAddr}; +use std::{io::SeekFrom, pin::Pin}; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufWriter}; +use tokio::net::TcpListener; +use tokio::sync::Mutex; +use tokio::sync::oneshot; +#[derive(QObject)] +pub struct AfcServices { + base: qt_base_class!(trait QObject), + afc: Arc>, + udid: String, + // file_to_buffer: qt_method!(fn(self, file_path: QString) -> QByteArray), + // get_file_size: qt_method!(fn(self, path: QString) -> i64), + check_is_dir_and_list: qt_method!(fn(&self, path: QString)), + + check_is_dir_and_list_finished: qt_signal!( + success: bool, + entries: QVariantMap + ), + 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) + } +} + +impl AfcServices { + /* udid is for debugging purposes */ + pub fn from_afc_client(afc_client: Arc>, udid: String) -> Self { + Self { + afc: afc_client, + udid: udid, + base: Default::default(), + // list_dir: Default::default(), + // file_to_buffer: Default::default(), + // is_directory: Default::default(), + // get_file_size: Default::default(), + check_is_dir_and_list: Default::default(), + check_is_dir_and_list_finished: Default::default(), + start_video_stream: Default::default(), + delete_path: Default::default(), + } + } + + // FIXME: resolve symlinks + fn check_is_dir_and_list(&self, path: QString) { + let path_str = path.to_string(); + let afc_arc = self.afc.clone(); + let qt_thread = self.qt_thread(); + RUNTIME.spawn(async move { + let mut map = QVariantMap::default(); + let mut afc = afc_arc.lock().await; + match afc.list_dir(&path_str).await { + Ok(list) => { + for name in list { + // ui already has up/down buttons maybe unnecessary + if name == "." || name == ".." { + continue; + } + let full_path = format!("{}/{}", path_str, name); + let is_dir = match afc.get_file_info(&full_path).await { + Ok(info) => info.st_ifmt == "S_IFDIR", + Err(e) => { + eprintln!("Failed to get file info for {full_path}: {e}"); + false + } + }; + map.insert(QString::from(name), QVariant::from(&is_dir)); + } + } + Err(e) => { + eprintln!("Failed to read directory {path_str}: {e}"); + } + }; + + qt_thread.queue(move |q| { + q.check_is_dir_and_list_finished(true, map); + }); + }); + } + + // fn file_to_buffer(&self, album_path: QString) -> QByteArray { + // let udid = self.get_udid().to_string(); + // let album_path_string = album_path.to_string(); + + // let data: Vec = run_sync(async move { + // let afc_arc = { + // let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + // let device = match maybe_device { + // Some(d) => d, + // None => { + // eprintln!("file_to_buffer: device {udid} not found"); + // return Vec::new(); + // } + // }; + // device.afc.clone() + // }; + + // let mut afc = afc_arc.lock().await; + + // let mut fd = match afc + // .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 get_file_size(self: &Self, path: QString) -> i64 { + // let udid = self.get_udid().to_string(); + // let path_string = path.to_string(); + + // run_sync(async move { + // let afc_arc = { + // let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + // let device = match maybe_device { + // Some(d) => d, + // None => { + // eprintln!("file_to_buffer: device {udid} not found"); + // return -1; + // } + // }; + // device.afc.clone() + // }; + + // let mut afc = afc_arc.lock().await; + + // afc::get_file_size(&mut afc, path_string) + // .await + // .map(|v| v as i64) + // .unwrap_or(-1) + // }) + // } + + // fn get_dirs_item_count(self: &Self, dirs: QList) -> i64 { + // let udid = self.get_udid().to_string(); + + // let mut dir_vec: Vec = Vec::new(); + // for i in 0..dirs.len() { + // if let Some(qdir) = dirs.get(i) { + // dir_vec.push(qdir.to_string()); + // } + // } + + // run_sync(async move { + // let afc_arc = { + // let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + // let device = match maybe_device { + // Some(d) => d, + // None => { + // eprintln!("get_dirs_item_count: device {udid} not found"); + // return -1; + // } + // }; + + // device.afc.clone() + // }; + + // let mut afc = afc_arc.lock().await; + // let mut total: i64 = 0; + + // for dir_str in dir_vec { + // let names = match afc.list_dir(&dir_str).await { + // Ok(list) => list, + // Err(e) => { + // eprintln!("get_dirs_item_count: list_dir({dir_str}) failed: {e}"); + // continue; + // } + // }; + + // let count = names + // .into_iter() + // .filter(|name| name != "." && name != "..") + // .count() as i64; + + // total += count; + // } + + // total + // }) + // } + + fn list_files_flat(&self, dir: QString) -> QStringList { + let dir_str = dir.to_string(); + let afc_arc = self.afc.clone(); + let entries = run_sync(async move { + let mut afc = afc_arc.lock().await; + let names = match afc.list_dir(&dir_str).await { + Ok(list) => list, + Err(e) => { + eprintln!("list_files_flat: list_dir({dir_str}) failed: {e}"); + return Vec::new(); + } + }; + + let mut files = Vec::new(); + for name in names { + if name == "." || name == ".." { + continue; + } + let full_path = format!("{}/{}", dir_str, name); + + match afc.get_file_info(full_path.clone()).await { + Ok(info) => { + if info.st_ifmt != "S_IFDIR" { + files.push(full_path); + } + } + Err(e) => { + eprintln!("list_files_flat: get_file_info({full_path}) failed: {e}"); + continue; + } + } + } + files + }); + + let mut qlist: QStringList = QStringList::default(); + for path in entries { + qlist.push(QString::from(path)); + } + qlist + } + + fn start_video_stream(&self, file_path: QString) -> QString { + let path_str = file_path.to_string(); + let cloned_path = path_str.clone(); + let afc = self.afc.clone(); + let udid = self.udid.clone(); + + eprintln!( + "start_video_stream: request udid={} path={}", + &udid, 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 url_for_insert = url.clone(); + 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 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, cloned_path + ); + return QString::default(); + } + + eprintln!( + "start_video_stream: serving {} for udid={} path={}", + url_clone, udid, 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_clone = afc.clone(); + + tokio::spawn(async move { + Self::handle_http_connection(afc_clone, path_clone, socket).await; + }); + } + } + } + eprintln!("start_video_stream: accept-loop exiting for udid : {} with url :{}", udid, url_clone); + }); + + QString::from(url_clone_for_log) + } + + fn delete_path(&self, path: QString) -> bool { + let udid = self.udid.clone(); + let path_str = path.to_string(); + + run_sync(async move { + let afc_arc = { + let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + + let device = match maybe_device { + Some(d) => d, + None => { + eprintln!("delete_path: device {udid} not found"); + return false; + } + }; + + device.afc.clone() + }; + + let mut afc = afc_arc.lock().await; + + match afc.remove(&path_str).await { + Ok(_) => true, + Err(e) => { + eprintln!("delete_path: delete({path_str}) failed: {e}"); + false + } + } + }) + } + + async fn handle_http_connection( + afc: Arc>, + path: String, + mut socket: tokio::net::TcpStream, + ) { + let mut afc = afc.lock().await; + let mut buf = vec![0u8; 4096]; + let n = match socket.read(&mut buf).await { + Ok(n) if n > 0 => n, + _ => { + eprintln!( + "handle_http_connection: failed to read initial request for {}", + path + ); + return; + } + }; + + let req_str = String::from_utf8_lossy(&buf[..n]).to_string(); + eprintln!( + "handle_http_connection: received request head for {}: {}", + path, + req_str.lines().take(10).collect::>().join("\\n") + ); + let lines: Vec<&str> = req_str.split("\r\n").collect(); + if lines.is_empty() { + eprintln!("handle_http_connection: empty request lines for {}", path); + let _ = socket.shutdown().await; + return; + } + + // request line: "GET /... HTTP/1.1" + let mut method = "GET".to_string(); + if let Some(first) = lines.first() { + let parts: Vec<&str> = first.split_whitespace().collect(); + if parts.len() >= 1 { + method = parts[0].to_string(); + } + } + eprintln!("handle_http_connection: method={} for {}", method, path); + + if method != "GET" && method != "HEAD" { + eprintln!( + "handle_http_connection: unsupported method {} for {}", + method, path + ); + //FIXME:FLUSH? + let _ = socket + .write_all( + b"HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) + .await; + let _ = socket.shutdown().await; + return; + } + + // parse Range header + let mut has_range = false; + let mut range_start: i64 = 0; + let mut range_end: i64 = -1; + for line in &lines[1..] { + if line.is_empty() { + break; + } + if let Some(rest) = line + .strip_prefix("Range: bytes=") + .or_else(|| line.strip_prefix("range: bytes=")) + { + let parts: Vec<&str> = rest.trim().split('-').collect(); + if parts.len() == 2 { + has_range = true; + if let Ok(s) = parts[0].trim().parse::() { + range_start = s; + } + if !parts[1].trim().is_empty() { + if let Ok(e) = parts[1].trim().parse::() { + range_end = e; + } + } + } + } + } + eprintln!( + "handle_http_connection: range parsed has_range={} start={} end={} for {}", + has_range, range_start, range_end, path + ); + + let file_size = { + let info = match afc.get_file_info(path.clone()).await { + Ok(i) => i, + Err(e) => { + eprintln!( + "handle_http_connection: get_file_info({}) failed: {}", + path, e + ); + let _ = socket + .write_all( + b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) + .await; + let _ = socket.shutdown().await; + return; + } + }; + info.size as i64 + }; + + eprintln!( + "handle_http_connection: file_size={} for {}", + file_size, path + ); + if file_size <= 0 { + eprintln!("handle_http_connection: invalid file_size for {}", path); + let _ = socket + .write_all( + b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) + .await; + let _ = socket.shutdown().await; + return; + } + + let mut start = 0_i64; + let mut end = file_size - 1; + + if has_range { + if range_start >= 0 { + start = range_start; + } + if range_end >= 0 && range_end < file_size { + end = range_end; + } + if start < 0 || start >= file_size || start > end { + eprintln!( + "handle_http_connection: range not satisfiable for {} (start={}, end={}, file_size={})", + path, start, end, file_size + ); + let _ = socket + .write_all( + b"HTTP/1.1 416 Range Not Satisfiable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) + .await; + let _ = socket.shutdown().await; + return; + } + } + + let content_len = (end - start + 1).max(0) as u64; + let path_lower = path.to_lowercase(); + let mime_type = if path_lower.ends_with(".mp4") || path_lower.ends_with(".m4v") { + "video/mp4" + } else if path_lower.ends_with(".mov") { + "video/quicktime" + } else if path_lower.ends_with(".avi") { + "video/x-msvideo" + } else if path_lower.ends_with(".mkv") { + "video/x-matroska" + } else { + "application/octet-stream" + }; + + let mut headers = String::new(); + if has_range { + headers.push_str("HTTP/1.1 206 Partial Content\r\n"); + headers.push_str(&format!( + "Content-Range: bytes {}-{}/{}\r\n", + start, end, file_size + )); + } else { + headers.push_str("HTTP/1.1 200 OK\r\n"); + } + + headers.push_str(&format!("Content-Length: {}\r\n", content_len)); + headers.push_str("Accept-Ranges: bytes\r\n"); + headers.push_str(&format!("Content-Type: {}\r\n", mime_type)); + headers.push_str("Connection: close\r\n"); + headers.push_str("Cache-Control: no-cache\r\n\r\n"); + + eprintln!( + "handle_http_connection: sending headers for {}: {}", + path, + headers.lines().next().unwrap_or_default() + ); + if socket.write_all(headers.as_bytes()).await.is_err() { + eprintln!("handle_http_connection: write headers failed for {}", path); + let _ = socket.shutdown().await; + return; + } + if socket.flush().await.is_err() { + eprintln!( + "handle_http_connection: flush failed after headers for {}", + path + ); + let _ = socket.shutdown().await; + return; + } + + if method == "HEAD" { + eprintln!( + "handle_http_connection: HEAD request completed for {}", + path + ); + let _ = socket.shutdown().await; + return; + } + + let mut fd = match afc.open(path.clone(), AfcFopenMode::RdOnly).await { + Ok(f) => f, + Err(e) => { + eprintln!("handle_http_connection: open({}) failed: {}", path, e); + let _ = socket.shutdown().await; + return; + } + }; + + if start > 0 { + if let Err(e) = fd.seek(SeekFrom::Start(start as u64)).await { + eprintln!( + "handle_http_connection: seek({}, {}) failed: {}", + path, start, e + ); + let _ = fd.close().await; + let _ = socket.shutdown().await; + return; + } + } + + eprintln!( + "handle_http_connection: streaming {} bytes ({}-{}) for {}", + content_len, start, end, path + ); + + let mut remaining = content_len; + // let mut chunk = vec![0u8; 64 * 1024]; + let mut writer = BufWriter::with_capacity(256 * 1024, &mut socket); + let mut chunk = vec![0u8; 256 * 1024]; + + while remaining > 0 { + let to_read = min(chunk.len() as u64, remaining) as usize; + let n = match fd.read(&mut chunk[..to_read]).await { + Ok(n) => n, + Err(e) => { + eprintln!("handle_http_connection: read({}) failed: {}", path, e); + break; + } + }; + if n == 0 { + eprintln!("handle_http_connection: EOF while streaming {}", path); + break; + } + if let Err(e) = writer.write_all(&chunk[..n]).await { + eprintln!("handle_http_connection: write({}) failed: {}", path, e); + break; + } + remaining -= n as u64; + } + + writer.flush().await.ok(); + //drop(writer) is explicit so the borrow is released before we touch socket again. + drop(writer); + + let _ = fd.close().await; + let _ = socket.shutdown().await; + eprintln!( + "handle_http_connection: finished/closed connection for {}", + path + ); + } +} diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp deleted file mode 100644 index a275c9b..0000000 --- a/src/afcexplorerwidget.cpp +++ /dev/null @@ -1,853 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "afcexplorerwidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "mediapreviewdialog.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -AfcExplorerWidget::AfcExplorerWidget( - const std::shared_ptr device, bool favEnabled, - std::optional> hause_arrest, bool useAfc2, - QString root, QWidget *parent) - : QWidget(parent), m_device(device), m_favEnabled(favEnabled), - m_hauseArrest(hause_arrest), m_errorMessage("Failed to load directory"), - m_root(root), m_useAfc2(useAfc2) -{ - - QVBoxLayout *rootLayout = new QVBoxLayout(this); - rootLayout->setContentsMargins(0, 0, 0, 0); - - // Setup file explorer - setupFileExplorer(); - - // Main layout - QWidget *contentContainer = new QWidget(); - QHBoxLayout *contentLayout = new QHBoxLayout(contentContainer); - contentLayout->setContentsMargins(0, 0, 0, 0); - - contentLayout->addWidget(m_explorer); - - // Initialize - m_history.push(m_root); - m_currentHistoryIndex = 0; - m_forwardHistory.clear(); - - m_loadingWidget = new ZLoadingWidget(true, this); - rootLayout->addWidget(m_loadingWidget); - m_loadingWidget->setupContentWidget(contentContainer); - - connect(m_loadingWidget, &ZLoadingWidget::retryClicked, this, [this]() { - m_loadingWidget->showLoading(); - QTimer::singleShot(100, this, [this]() { loadPath(m_history.top()); }); - }); - - if (m_useAfc2) { - bool is_available = m_device->afc2_backend->is_available(); - if (!is_available) { - qDebug() - << "[AfcExplorerWidget] AFC2 is not available on this device."; - m_loadingWidget->showError("AFC2 is not available on this device."); - return; - } - } - - if (m_useAfc2) { - connect(m_device->afc2_backend, - &CXX::Afc2Backend::check_is_dir_and_list_finished, this, - &AfcExplorerWidget::onLoadPathFinished); - } else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { - connect(m_hauseArrest.value().get(), - &CXX::HauseArrest::check_is_dir_and_list_finished, this, - &AfcExplorerWidget::onLoadPathFinished); - } else { - connect(m_device->afc_backend, - &CXX::AfcBackend::check_is_dir_and_list_finished, this, - &AfcExplorerWidget::onLoadPathFinished); - } - - loadPath(m_root); - - setupContextMenu(); -} - -void AfcExplorerWidget::goBack() -{ - if (m_history.size() > 1) { - // Move current path to forward history - QString currentPath = m_history.pop(); - m_forwardHistory.push(currentPath); - - QString prevPath = m_history.top(); - loadPath(prevPath); - } -} - -void AfcExplorerWidget::goForward() -{ - if (!m_forwardHistory.isEmpty()) { - QString forwardPath = m_forwardHistory.pop(); - m_history.push(forwardPath); - loadPath(forwardPath); - } -} - -void AfcExplorerWidget::onItemDoubleClicked(QListWidgetItem *item) -{ - QVariant data = item->data(Qt::UserRole); - bool isDir = data.toBool(); - QString name = item->text(); - - // Use breadcrumb to get current path - QString currPath = "/"; - if (!m_history.isEmpty()) - currPath = m_history.top(); - - if (!currPath.endsWith("/")) - currPath += "/"; - QString nextPath = currPath == "/" ? "/" + name : currPath + name; - - if (isDir) { - // Clear forward history when navigating to a new directory - m_forwardHistory.clear(); - m_history.push(nextPath); - loadPath(nextPath); - } else { - const bool isPreviewable = iDescriptor::Utils::isPreviewableFile(name); - if (isPreviewable) { - auto *previewDialog = new MediaPreviewDialog( - m_device, nextPath, m_hauseArrest, m_useAfc2, this); - previewDialog->setAttribute(Qt::WA_DeleteOnClose); - previewDialog->show(); - } else { - openWithDesktopService(item); - } - } -} - -void AfcExplorerWidget::openWithDesktopService(QListWidgetItem *item) -{ - QTemporaryDir *tempDir = new QTemporaryDir(); - if (!tempDir->isValid()) { - QMessageBox::critical(this, "Error", - "Could not create a temporary directory."); - delete tempDir; - return; - } - - exportAndOpenSelectedFile(item, tempDir->path()); -} - -void AfcExplorerWidget::onAddressBarReturnPressed() -{ - QString path = m_addressBar->text().trimmed(); - if (path.isEmpty()) { - path = "/"; - } - - // Normalize the path - if (!path.startsWith("/")) { - path = "/" + path; - } - - // Remove duplicate slashes - path = path.replace(QRegularExpression("/+"), "/"); - - // Clear forward history when navigating to a new path - m_forwardHistory.clear(); - - // Update history and load the path - m_history.push(path); - loadPath(path); -} - -void AfcExplorerWidget::updateNavigationButtons() -{ - // Update button states based on history - if (m_backButton) { - m_backButton->setEnabled(m_history.size() > 1); - } - if (m_forwardButton) { - m_forwardButton->setEnabled(!m_forwardHistory.isEmpty()); - } - if (m_upButton) { - bool canGoUp = !m_history.isEmpty() && m_history.top() != "/"; - m_upButton->setEnabled(canGoUp); - } -} - -void AfcExplorerWidget::updateAddressBar(const QString &path) -{ - // Update the address bar with the current path - m_addressBar->setText(path); -} - -void AfcExplorerWidget::loadPath(const QString &path) -{ - m_loadingWidget->showLoading(); - - if (m_useAfc2) { - bool is_available = m_device->afc2_backend->is_available(); - if (!is_available) { - qDebug() - << "[AfcExplorerWidget] AFC2 is not available on this device."; - m_loadingWidget->showError("AFC2 is not available on this device."); - return; - } - } - - updateAddressBar(path); - updateNavigationButtons(); - - // FIXME: we need a better approach to this - // similar code is repeated in some places - /* use the correct afc client */ - if (m_useAfc2) { - m_device->afc2_backend->check_is_dir_and_list(path); - } else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { - m_hauseArrest.value()->check_is_dir_and_list(path); - } else { - m_device->afc_backend->check_is_dir_and_list(path); - } -} - -void AfcExplorerWidget::onLoadPathFinished(bool success, - const QMap &tree) -{ - m_fileList->clear(); - showFileListState(); - - for (auto it = tree.constBegin(); it != tree.constEnd(); ++it) { - bool is_dir = it.value().toBool(); - - QListWidgetItem *item = new QListWidgetItem(it.key()); - item->setData(Qt::UserRole, is_dir); - if (is_dir) { - QIcon folderIcon = QIcon::fromTheme("folder"); - if (folderIcon.isNull()) { - item->setIcon( - QIcon(":/resources/icons/MaterialSymbolsFolder.png")); - } else { - item->setIcon(folderIcon); - } - } else { - QIcon fileIcon = QIcon::fromTheme("text-x-generic"); - if (fileIcon.isNull()) { - item->setIcon( - QIcon(":/resources/icons/IcBaselineInsertDriveFile.png")); - } else { - item->setIcon(fileIcon); - } - } - m_fileList->addItem(item); - } - - m_loadingWidget->stop(); -} - -void AfcExplorerWidget::setupContextMenu() -{ - m_fileList->setContextMenuPolicy(Qt::CustomContextMenu); - connect(m_fileList, &QListWidget::customContextMenuRequested, this, - &AfcExplorerWidget::onFileListContextMenu); -} - -void AfcExplorerWidget::onFileListContextMenu(const QPoint &pos) -{ - QListWidgetItem *item = m_fileList->itemAt(pos); - if (!item) - return; - - bool isDir = item->data(Qt::UserRole).toBool(); - if (isDir) - return; // TODO: Implement directory export later - Only export files - // for now - - QMenu menu; - QAction *exportAction = menu.addAction("Export"); - QAction *openAction = menu.addAction("Open"); - QAction *openNativeAction = menu.addAction("Open Externally"); - QAction *selectedAction = - menu.exec(m_fileList->viewport()->mapToGlobal(pos)); - if (selectedAction == exportAction) { - QList selectedItems = m_fileList->selectedItems(); - QList filesToExport; - if (selectedItems.isEmpty()) - filesToExport.append(item); // fallback: just the clicked one - else { - for (QListWidgetItem *selItem : selectedItems) { - if (!selItem->data(Qt::UserRole).toBool()) - filesToExport.append(selItem); - } - } - if (filesToExport.isEmpty()) - return; - - handleExport(filesToExport); - - } else if (selectedAction == openAction) { - onItemDoubleClicked(item); - } else if (selectedAction == openNativeAction) { - openWithDesktopService(item); - } -} - -void AfcExplorerWidget::onExportClicked() -{ - QList selectedItems = m_fileList->selectedItems(); - if (selectedItems.isEmpty()) - return; - - // Only files (not directories) - TODO: Implement directory export later - QList filesToExport; - for (QListWidgetItem *item : selectedItems) { - if (!item->data(Qt::UserRole).toBool()) - filesToExport.append(item); - } - if (filesToExport.isEmpty()) - return; - - handleExport(filesToExport); -} - -void AfcExplorerWidget::handleExport(QList filesToExport) -{ - QString dir = - QFileDialog::getExistingDirectory(this, "Select Export Directory"); - if (dir.isEmpty()) - return; - - QList exportItems; - QString currPath = "/"; - if (!m_history.isEmpty()) - currPath = m_history.top(); - if (!currPath.endsWith("/")) - currPath += "/"; - - for (QListWidgetItem *selItem : filesToExport) { - QString fileName = selItem->text(); - QString devicePath = - currPath == "/" ? "/" + fileName : currPath + fileName; - exportItems.append(devicePath); - } - - if (m_useAfc2) { - IOManagerClient::sharedInstance()->startExport( - m_device, exportItems, dir, "Exporting from File Explorer ", - true); - } else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { - IOManagerClient::sharedInstance()->startExport( - m_device, exportItems, dir, - "Exporting from File Explorer (App Container)", - m_hauseArrest.value()->get_bundle_id()); - } else { - IOManagerClient::sharedInstance()->startExport( - m_device, exportItems, dir, "Exporting from File Explorer"); - } -} - -void AfcExplorerWidget::exportAndOpenSelectedFile(QListWidgetItem *item, - const QString &directory) -{ - if (!QDir(directory).exists()) { - QMessageBox::critical(this, "Error", - "Could not access the temporary directory."); - return; - } - - QList exportItems; - QString currPath = "/"; - if (!m_history.isEmpty()) - currPath = m_history.top(); - if (!currPath.endsWith("/")) - currPath += "/"; - - QString fileName = item->text(); - QString devicePath = currPath == "/" ? "/" + fileName : currPath + fileName; - exportItems.append(devicePath); - - QString localPath = QDir(directory).filePath(fileName); - - std::function onExportFinished = [localPath]() { - QDesktopServices::openUrl(QUrl::fromLocalFile(localPath)); - }; - - if (m_useAfc2) { - IOManagerClient::sharedInstance()->startExport( - m_device, exportItems, directory, - "Exporting from File Explorer ", true, onExportFinished); - } else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { - IOManagerClient::sharedInstance()->startExport( - m_device, exportItems, directory, - "Exporting from File Explorer (App Container)", - m_hauseArrest.value()->get_bundle_id(), onExportFinished); - } else { - IOManagerClient::sharedInstance()->startExport( - m_device, exportItems, directory, "Exporting from File Explorer", - onExportFinished); - } -} - -void AfcExplorerWidget::onImportClicked() -{ - QStringList fileNames = QFileDialog::getOpenFileNames(this, "Import Files"); - if (fileNames.isEmpty()) - return; - - QString currPath = "/"; - if (!m_history.isEmpty()) - currPath = m_history.top(); - if (!currPath.endsWith("/")) - currPath += "/"; - - QPointer safeThis(this); - std::function onImportFinished = [this, currPath, safeThis]() { - if (!safeThis || safeThis.isNull()) - return; - QTimer::singleShot(100, this, [this]() { - if (!m_history.isEmpty()) - loadPath(m_history.top()); - }); - }; - if (m_useAfc2) { - IOManagerClient::sharedInstance()->startImport( - m_device, fileNames, currPath, "Importing ", true, - onImportFinished); - } else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) { - IOManagerClient::sharedInstance()->startImport( - m_device, fileNames, currPath, "Importing to App Container", - m_hauseArrest.value()->get_bundle_id(), onImportFinished); - } else { - IOManagerClient::sharedInstance()->startImport( - m_device, fileNames, currPath, "Importing", onImportFinished); - } -} - -void AfcExplorerWidget::setupFileExplorer() -{ - m_explorer = new QWidget(); - QVBoxLayout *explorerLayout = new QVBoxLayout(m_explorer); - explorerLayout->setContentsMargins(0, 0, 0, 0); - m_explorer->setStyleSheet("border : none;"); - - // Export/Import buttons layout - m_exportBtn = - new ZIconWidget(QIcon(":/resources/icons/PhExport.png"), "Export"); - m_importBtn = new ZIconWidget( - QIcon(":/resources/icons/LetsIconsImport.png"), "Import"); - if (m_favEnabled) { - m_addToFavoritesBtn = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsFavorite.png"), - "Add to Favorites"); - } - - // Navigation layout (Address Bar with embedded icons) - m_navWidget = new QWidget(); - m_navWidget->setObjectName("navWidget"); - m_navWidget->setFocusPolicy(Qt::StrongFocus); // Make it focusable - - m_navWidget->setMaximumWidth(500); - m_navWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - - QHBoxLayout *navContainerLayout = new QHBoxLayout(); - navContainerLayout->addStretch(); - navContainerLayout->addWidget(m_navWidget); - navContainerLayout->addStretch(); - - QHBoxLayout *navLayout = new QHBoxLayout(m_navWidget); - navLayout->setContentsMargins(0, 0, 0, 0); - navLayout->setSpacing(0); - - QWidget *explorerLeftSideNavButtons = new QWidget(); - QHBoxLayout *leftNavLayout = new QHBoxLayout(explorerLeftSideNavButtons); - - leftNavLayout->setContentsMargins(0, 0, 0, 0); - leftNavLayout->setSpacing(1); - m_backButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsArrowLeftAlt.png"), "Go Back"); - m_backButton->setEnabled(false); - - m_forwardButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsArrowRightAlt.png"), - "Go Forward"); - m_forwardButton->setEnabled(false); - - m_homeButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsLightHome.png"), "Go Home"); - - m_upButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsArrowUpwardAltRounded.png"), - "Go Up"); - m_upButton->setEnabled(false); - - m_enterButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsLightKeyboardReturn.png"), - "Navigate to path"); - - m_deleteButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsDelete.png"), "Delete"); - - m_addressBar = new QLineEdit(); - m_addressBar->setPlaceholderText("Enter path..."); - m_addressBar->setText("/"); - - // Add widgets to navigation layout - leftNavLayout->addWidget(m_backButton); - leftNavLayout->addWidget(m_forwardButton); - leftNavLayout->addWidget(m_homeButton); - leftNavLayout->addWidget(m_upButton); - navLayout->addWidget(explorerLeftSideNavButtons); - navLayout->addWidget(m_addressBar); - navLayout->addWidget(m_importBtn); - navLayout->addWidget(m_exportBtn); - navLayout->addWidget(m_deleteButton); - if (m_favEnabled) - navLayout->addWidget(m_addToFavoritesBtn); - - navLayout->addWidget(m_enterButton); - - // Add the container layout (which centers navWidget) to the main layout - explorerLayout->addLayout(navContainerLayout); - - // Create stacked widget for content (file list or error state) - m_contentStack = new QStackedWidget(); - - // Create file list widget - m_fileListWidget = new QWidget(); - QVBoxLayout *fileListLayout = new QVBoxLayout(m_fileListWidget); - fileListLayout->setContentsMargins(0, 0, 0, 0); - - // File list - m_fileList = new QListWidget(); - m_fileList->setSelectionMode(QAbstractItemView::ExtendedSelection); -#ifdef WIN32 - m_fileList->setStyleSheet(R"( - QScrollBar:vertical { - border: 6px solid rgba(0, 0, 0, 0); - margin: 14px 0px 14px 0px; - width: 16px; - background-color: transparent; - } - QScrollBar::handle:vertical { - background-color: rgba(0, 0, 0, 110); - border-radius: 2px; - min-height: 25px; - } - )"); -#endif - fileListLayout->addWidget(m_fileList); - - // Create error widget - m_errorWidget = new QWidget(); - QVBoxLayout *errorLayout = new QVBoxLayout(m_errorWidget); - errorLayout->setContentsMargins(20, 20, 20, 20); - - errorLayout->addStretch(); - - m_errorLabel = new QLabel(m_errorMessage); - m_errorLabel->setAlignment(Qt::AlignCenter); - m_errorLabel->setWordWrap(true); - errorLayout->addWidget(m_errorLabel); - - m_retryButton = new QPushButton("Try Again"); - m_retryButton->setMaximumWidth(120); - QHBoxLayout *buttonLayout = new QHBoxLayout(); - buttonLayout->addStretch(); - buttonLayout->addWidget(m_retryButton); - buttonLayout->addStretch(); - errorLayout->addLayout(buttonLayout); - - errorLayout->addStretch(); - - // Add both widgets to the stacked widget - m_contentStack->addWidget(m_fileListWidget); - m_contentStack->addWidget(m_errorWidget); - - // Start with file list view - m_contentStack->setCurrentWidget(m_fileListWidget); - - explorerLayout->addWidget(m_contentStack); - - // Connect buttons and actions - connect(m_backButton, &ZIconWidget::clicked, this, - &AfcExplorerWidget::goBack); - connect(m_forwardButton, &ZIconWidget::clicked, this, - &AfcExplorerWidget::goForward); - connect(m_homeButton, &ZIconWidget::clicked, this, - &AfcExplorerWidget::goHome); - connect(m_upButton, &ZIconWidget::clicked, this, &AfcExplorerWidget::goUp); - connect(m_enterButton, &ZIconWidget::clicked, this, - &AfcExplorerWidget::onAddressBarReturnPressed); - connect(m_addressBar, &QLineEdit::returnPressed, this, - &AfcExplorerWidget::onAddressBarReturnPressed); - connect(m_fileList, &QListWidget::itemDoubleClicked, this, - &AfcExplorerWidget::onItemDoubleClicked); - connect(m_exportBtn, &ZIconWidget::clicked, this, - &AfcExplorerWidget::onExportClicked); - connect(m_importBtn, &ZIconWidget::clicked, this, - &AfcExplorerWidget::onImportClicked); - connect(m_retryButton, &QPushButton::clicked, this, - &AfcExplorerWidget::onRetryClicked); - connect(m_deleteButton, &ZIconWidget::clicked, this, - &AfcExplorerWidget::onDeleteClicked); - connect(m_fileList->selectionModel(), - &QItemSelectionModel::selectionChanged, this, - &AfcExplorerWidget::updateButtonStates); - - if (m_favEnabled) { - connect(m_addToFavoritesBtn, &ZIconWidget::clicked, this, - &AfcExplorerWidget::onAddToFavoritesClicked); - } - - updateNavigationButtons(); - updateButtonStates(); -#ifndef WIN32 - updateNavStyles(); -#endif -} - -void AfcExplorerWidget::onAddToFavoritesClicked() -{ - QString currentPath = "/"; - if (!m_history.isEmpty()) - currentPath = m_history.top(); - - bool ok; - QString alias = QInputDialog::getText( - this, "Add to Favorites", - "Enter alias for this location:", QLineEdit::Normal, "Alias here", &ok); - if (ok && !alias.isEmpty()) { - emit favoritePlaceAdded(alias, currentPath); - } else if (ok && alias.isEmpty()) { - QMessageBox::warning(nullptr, "Invalid Input", "Alias was empty."); - qWarning() << "Cannot save favorite place with empty alias"; - } else if (!ok) { - qWarning() << "Failed to get alias for favorite place"; - } -} - -#ifndef WIN32 -void AfcExplorerWidget::updateNavStyles() -{ - if (!m_navWidget || !m_addressBar) - return; - bool isDark = isDarkMode(); - QColor lightColor = qApp->palette().color(QPalette::Light); - QColor darkColor = qApp->palette().color(QPalette::Dark); - QColor bgColor = isDark ? lightColor : darkColor; - QColor borderColor = qApp->palette().color(QPalette::Mid); - QColor accentColor = qApp->palette().color(QPalette::Highlight); - - QString navStyles = QString("QWidget#navWidget {" - " background-color: %1;" - " border: 1px solid %2;" - " border-radius: 10px;" - "}" - "QWidget#navWidget {" - " outline: 1px solid %3;" - " outline-offset: 1px;" - "}") - .arg(bgColor.name()) - .arg(bgColor.lighter().name()) - .arg(accentColor.name()); - - if (m_navWidget->styleSheet() != navStyles) - m_navWidget->setStyleSheet(navStyles); - - // Update address bar styles to complement the nav widget - QString addressBarStyles = - QString("QLineEdit { background-color: %1; border-radius: 10px; " - "border: 1px solid %2; padding: 2px 4px; color: %3; }" - "QLineEdit:focus {border: 3px solid %4; }") - .arg(isDark ? QColor(Qt::white).name() : QColor(Qt::black).name()) - .arg(borderColor.lighter().name()) - .arg(isDark ? QColor(Qt::black).name() : QColor(Qt::white).name()) - .arg(COLOR_ACCENT_BLUE.name()); - if (m_addressBar->styleSheet() != addressBarStyles) - m_addressBar->setStyleSheet(addressBarStyles); -} -#endif - -void AfcExplorerWidget::updateButtonStates() -{ - QList selectedItems = m_fileList->selectedItems(); - - bool enteriesDoNotContainDirectories = selectedItems.size() > 0; - for (QListWidgetItem *item : selectedItems) { - if (item->data(Qt::UserRole).toBool()) { // a directory - enteriesDoNotContainDirectories = false; - break; - } - } - // TODO: implement directory export and remove - m_exportBtn->setEnabled(enteriesDoNotContainDirectories); - m_deleteButton->setEnabled(enteriesDoNotContainDirectories); -} - -void AfcExplorerWidget::setErrorMessage(const QString &message) -{ - m_errorMessage = message; - if (m_errorLabel) { - m_errorLabel->setText(m_errorMessage); - } -} - -void AfcExplorerWidget::showErrorState() -{ - if (m_contentStack) { - m_contentStack->setCurrentWidget(m_errorWidget); - } -} - -void AfcExplorerWidget::showFileListState() -{ - if (m_contentStack) { - m_contentStack->setCurrentWidget(m_fileListWidget); - } -} - -void AfcExplorerWidget::onRetryClicked() -{ - QString currentPath = "/"; - if (!m_history.isEmpty()) { - currentPath = m_history.top(); - } - loadPath(currentPath); -} - -void AfcExplorerWidget::navigateToPath(const QString &path) -{ - if (path.isEmpty()) { - return; - } - - QString normalizedPath = path; - if (!normalizedPath.startsWith("/")) { - normalizedPath = "/" + normalizedPath; - } - normalizedPath = normalizedPath.replace(QRegularExpression("/+"), "/"); - - m_history.push(normalizedPath); - loadPath(normalizedPath); -} - -void AfcExplorerWidget::goHome() -{ - // Clear forward history when navigating to a new directory - m_forwardHistory.clear(); - m_history.push(m_root); - loadPath(m_root); -} - -void AfcExplorerWidget::goUp() -{ - if (m_history.isEmpty()) { - return; - } - - QString currentPath = m_history.top(); - - // Can't go up from the root directory - if (currentPath == "/") { - return; - } - - // Find the parent directory - int lastSlashIndex = currentPath.lastIndexOf('/'); - QString parentPath = - (lastSlashIndex > 0) ? currentPath.left(lastSlashIndex) : "/"; - - // Going up is a new navigation action, so clear forward history - m_forwardHistory.clear(); - - // Add the new path to history and load it - m_history.push(parentPath); - loadPath(parentPath); -} - -void AfcExplorerWidget::onDeleteClicked() -{ - QList selectedItems = m_fileList->selectedItems(); - if (selectedItems.isEmpty()) - return; - - QString currPath = "/"; - if (!m_history.isEmpty()) - currPath = m_history.top(); - if (!currPath.endsWith("/")) - currPath += "/"; - - QList pathsToDelete; - for (QListWidgetItem *item : selectedItems) { - QString fileName = item->text(); - QString devicePath = - currPath == "/" ? "/" + fileName : currPath + fileName; - pathsToDelete.append(devicePath); - } - - QMessageBox::StandardButton reply = QMessageBox::question( - this, "Confirm Deletion", - QString("Are you sure you want to delete the selected %1 item(s)?") - .arg(pathsToDelete.size()), - QMessageBox::Yes | QMessageBox::No); - - bool success = false; - - for (const QString &path : pathsToDelete) { - if (m_useAfc2) { - success = m_device->afc2_backend->delete_path(path); - } else if (m_hauseArrest.has_value() && - m_hauseArrest.value() != nullptr) { - success = m_hauseArrest.value()->delete_path(path); - } else { - success = m_device->afc_backend->delete_path(path); - } - } - - if (!success) { - QMessageBox::critical(this, "Error", - "Failed to delete one or more items."); - } else { - // Refresh the current directory after deletion - QTimer::singleShot(100, this, [this]() { - if (!m_history.isEmpty()) - loadPath(m_history.top()); - }); - } -} \ No newline at end of file diff --git a/src/afcexplorerwidget.h b/src/afcexplorerwidget.h deleted file mode 100644 index 01fbb0d..0000000 --- a/src/afcexplorerwidget.h +++ /dev/null @@ -1,140 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef AFCEXPLORER_H -#define AFCEXPLORER_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "iomanagerclient.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class ExportManager; - -class AfcExplorerWidget : public QWidget -{ - Q_OBJECT -public: - explicit AfcExplorerWidget(const std::shared_ptr device, - bool favEnabled = false, - std::optional> - hause_arrest = std::nullopt, - bool useAfc2 = false, QString root = "/", - QWidget *parent = nullptr); - void navigateToPath(const QString &path); - void goHome(); -signals: - void favoritePlaceAdded(const QString &alias, const QString &path); - -private slots: - void goBack(); - void goForward(); - void onItemDoubleClicked(QListWidgetItem *item); - void onAddressBarReturnPressed(); - void onFileListContextMenu(const QPoint &pos); - void onExportClicked(); - void onImportClicked(); - void onAddToFavoritesClicked(); - void onRetryClicked(); - -private: - QWidget *m_explorer; - QWidget *m_navWidget; - QStackedWidget *m_contentStack; - QWidget *m_fileListWidget; - QWidget *m_errorWidget; - QLabel *m_errorLabel; - QPushButton *m_retryButton; - ZIconWidget *m_exportBtn; - ZIconWidget *m_importBtn; - ZIconWidget *m_addToFavoritesBtn; - QListWidget *m_fileList; - QStack m_history; - QStack m_forwardHistory; - int m_currentHistoryIndex; - QLineEdit *m_addressBar; - ZIconWidget *m_backButton; - ZIconWidget *m_forwardButton; - ZIconWidget *m_homeButton; - ZIconWidget *m_upButton; - ZIconWidget *m_enterButton; - ZIconWidget *m_deleteButton; - const std::shared_ptr m_device; - bool m_favEnabled; - std::optional> m_hauseArrest; - QString m_errorMessage; - QString m_root; - ZLoadingWidget *m_loadingWidget; - bool m_useAfc2; - - // Export system - ExportManager *m_exportManager; - - void setupFileExplorer(); - void loadPath(const QString &path); - void updateAddressBar(const QString &path); - void updateNavigationButtons(); - void setErrorMessage(const QString &message); - void showErrorState(); - void showFileListState(); - void saveFavoritePlace(const QString &path, const QString &alias); - void openWithDesktopService(QListWidgetItem *item); - void onDeleteClicked(); - - void setupContextMenu(); - void exportAndOpenSelectedFile(QListWidgetItem *item, - const QString &directory); - void updateButtonStates(); - void goUp(); - - void onLoadPathFinished(bool success, - const QMap &entries); - - void handleExport(QList filesToExport); - -#ifndef WIN32 - void updateNavStyles(); - -protected: - void changeEvent(QEvent *event) override - { - if (event->type() == QEvent::PaletteChange) { - updateNavStyles(); - } - QWidget::changeEvent(event); - } -#endif -}; -#endif // AFCEXPLORER_H diff --git a/src/airplaywidget.cpp b/src/airplaywidget.cpp deleted file mode 100644 index 597eb08..0000000 --- a/src/airplaywidget.cpp +++ /dev/null @@ -1,741 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "airplaywidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef Q_OS_LINUX -// V4L2 includes -#include -#include -#include -#include -#include -#include -#endif -#include "settingsmanager.h" - -#include -#include - -#include "diagnosedialog.h" -#ifdef WIN32 -#include "platform/windows/win_common.h" -#endif -#include "toolboxwidget.h" - -AirPlaySettings::AirPlaySettings() - : fps(SettingsManager::sharedInstance()->airplayFps()), - noHold(SettingsManager::sharedInstance()->airplayNoHold()) -#ifdef __linux__ - , - useLegacyPorts(SettingsManager::sharedInstance()->airplayUseLegacyPorts()) -#endif -{ -} - -QStringList AirPlaySettings::toArgs() const -{ - QStringList args; - - // FPS - args << "-fps" << QString::number(fps); - - // Allow new connections to take over - if (noHold) - args << "-nohold"; - -#ifdef __linux__ - // We probably need this only on linux - // https://github.com/iDescriptor/iDescriptor/issues/73 - if (useLegacyPorts) - args << "-p"; -#endif - - return args; -} - -AirPlaySettingsDialog::AirPlaySettingsDialog(QWidget *parent) - : QDialog(parent), m_settings(AirPlaySettings()) -{ - setupUI(); - setWindowTitle("AirPlay Settings"); - resize(300, 300); -} - -void AirPlaySettingsDialog::setupUI() -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Video Settings Group - QGroupBox *videoGroup = new QGroupBox("Video Settings"); - QFormLayout *videoLayout = new QFormLayout(videoGroup); - - // FPS Layout - QVBoxLayout *fpsLayout = new QVBoxLayout(); - m_fpsComboBox = new QComboBox(); - m_fpsComboBox->addItems({"24", "30", "60", "120"}); - m_fpsComboBox->setCurrentText( - QString::number(SettingsManager::sharedInstance()->airplayFps())); - m_fpsComboBox->setToolTip("Set maximum allowed streaming framerate"); - - QLabel *fpsFootnote = - new QLabel("Note: Older devices may not support higher framerates. If " - "you are experiencing issues, set this to 30 FPS or lower."); - fpsFootnote->setWordWrap(true); - fpsFootnote->setStyleSheet("color: #666; font-size: 12px;"); - fpsLayout->addWidget(m_fpsComboBox); - fpsLayout->addWidget(fpsFootnote); - - videoLayout->addRow("Max FPS:", fpsLayout); - - m_noHoldCheckbox = new QCheckBox("Allow New Connections to Take Over"); - m_noHoldCheckbox->setChecked( - SettingsManager::sharedInstance()->airplayNoHold()); - videoLayout->addRow(m_noHoldCheckbox); - -#ifdef __linux__ - m_useLegacyPortsCheckbox = new QCheckBox("Use legacy ports"); - m_useLegacyPortsCheckbox->setChecked( - SettingsManager::sharedInstance()->airplayUseLegacyPorts()); - videoLayout->addRow(m_useLegacyPortsCheckbox); -#endif - - mainLayout->addWidget(videoGroup); - // Buttons - QDialogButtonBox *buttonBox = - new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - mainLayout->addWidget(buttonBox); - connectSignals(); -} - -void AirPlaySettingsDialog::connectSignals() -{ - connect(m_fpsComboBox, &QComboBox::currentTextChanged, this, - [this]() { m_settingsChanged = true; }); - connect(m_noHoldCheckbox, &QCheckBox::toggled, this, - [this]() { m_settingsChanged = true; }); -#ifdef __linux__ - connect(m_useLegacyPortsCheckbox, &QCheckBox::toggled, this, - [this]() { m_settingsChanged = true; }); -#endif -} - -QPair AirPlaySettingsDialog::getSettings() const -{ - AirPlaySettings settings; - settings.fps = m_fpsComboBox->currentText().toInt(); - settings.noHold = m_noHoldCheckbox->isChecked(); -#ifdef __linux__ - settings.useLegacyPorts = m_useLegacyPortsCheckbox->isChecked(); -#endif - return {m_settingsChanged, settings}; -} - -AirPlayWidget::AirPlayWidget(QWidget *parent) - : Tool(parent, false), m_stackedWidget(nullptr), m_tutorialWidget(nullptr), - m_streamingWidget(nullptr), m_loadingIndicator(nullptr), - m_loadingLabel(nullptr), m_tutorialPlayer(nullptr), - m_tutorialVideoWidget(nullptr), m_videoLabel(nullptr), - m_tutorialLayout(nullptr), m_settingsButton(nullptr), -#ifdef __linux__ - m_v4l2Checkbox(nullptr), m_v4l2_fd(-1), m_v4l2_width(0), m_v4l2_height(0), - m_v4l2_enabled(false), -#endif - m_serverThread(nullptr), m_serverRunning(false), m_clientConnected(false) -{ - setupUI(); - setMinimumSize(800, 600); - QTimer::singleShot(0, this, [this]() { - /* HACK: qt ignores resize() calls so let's workaround */ - setMinimumSize(0, 0); - }); - -/* FIXME: this can be handled better, also check for linux */ -#ifdef WIN32 - bool bonjour = IsBonjourServiceInstalled() == SERVICE_AVAILABLE; - if (!bonjour) { - QMessageBox::warning( - this, "Bonjour Service Not Installed", - "Bonjour service is not available on your system."); - - DiagnoseDialog *diagnoseDialog = new DiagnoseDialog(); - diagnoseDialog->show(); - QTimer::singleShot(0, this, &AirPlayWidget::close); - return; - } -#endif - QTimer::singleShot(500, this, &AirPlayWidget::startAirPlayServer); -} - -AirPlayWidget::~AirPlayWidget() -{ - stopAirPlayServer(); -#ifdef Q_OS_LINUX - closeV4L2(); -#endif -} - -void AirPlayWidget::setupUI() -{ - setWindowTitle("AirPlay - iDescriptor"); - QVBoxLayout *mainLayout = new QVBoxLayout(this); - m_stackedWidget = new QStackedWidget(this); - - m_tutorialWidget = new QWidget(); - m_tutorialLayout = new QVBoxLayout(m_tutorialWidget); - m_tutorialLayout->setContentsMargins(0, 0, 0, 0); - m_tutorialLayout->setSpacing(20); - - m_loadingIndicator = new QProcessIndicator(); - m_loadingIndicator->setType(QProcessIndicator::line_rotate); - m_loadingIndicator->setFixedSize(24, 24); - m_loadingIndicator->start(); - - QHBoxLayout *loadingLayout = new QHBoxLayout(); - m_loadingLabel = new QLabel("Starting AirPlay Server..."); - loadingLayout->setContentsMargins(0, 40, 0, 0); - loadingLayout->addStretch(); - loadingLayout->addWidget(m_loadingLabel); - loadingLayout->addSpacing(5); - loadingLayout->addWidget(m_loadingIndicator); - loadingLayout->addStretch(); - - m_tutorialLayout->addLayout(loadingLayout); - m_tutorialLayout->addSpacing(1); - - // Settings button (shown when no client connected) - m_settingsButton = new QPushButton("Settings"); - m_settingsButton->setVisible(false); - connect(m_settingsButton, &QPushButton::clicked, this, - &AirPlayWidget::showSettingsDialog); - QHBoxLayout *settingsLayout = new QHBoxLayout(); - settingsLayout->addStretch(); - settingsLayout->addWidget(m_settingsButton); - settingsLayout->addStretch(); - m_tutorialLayout->addLayout(settingsLayout); - - QTimer::singleShot(100, this, &AirPlayWidget::setupTutorialVideo); - - m_streamingWidget = new QWidget(); - QVBoxLayout *streamingLayout = new QVBoxLayout(m_streamingWidget); - streamingLayout->setContentsMargins(10, 10, 10, 10); - streamingLayout->setSpacing(10); - -#ifdef __linux__ - // Add V4L2 checkbox at the top of streaming view - setupV4L2Checkbox(); - if (m_v4l2Checkbox) { - streamingLayout->addWidget(m_v4l2Checkbox); - } -#endif - - // Video display - m_videoLabel = new QLabel(); - m_videoLabel->setAlignment(Qt::AlignCenter); - m_videoLabel->setScaledContents(false); - streamingLayout->addWidget(m_videoLabel, 1); - - // Add all widgets to stacked widget - m_stackedWidget->addWidget(m_tutorialWidget); - m_stackedWidget->addWidget(m_streamingWidget); - - // Start with tutorial widget - m_stackedWidget->setCurrentWidget(m_tutorialWidget); - mainLayout->addWidget(m_stackedWidget); -#ifdef __linux__ - m_v4l2_enabled = false; // Disable V4L2 by default -#endif -} - -void AirPlayWidget::setupTutorialVideo() -{ - m_tutorialPlayer = new QMediaPlayer(this); - m_tutorialVideoWidget = new QVideoWidget(); - m_tutorialVideoWidget->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - - m_tutorialPlayer->setVideoOutput(m_tutorialVideoWidget); - m_tutorialPlayer->setSource(QUrl("qrc:/resources/airplay-tutorial.mp4")); - m_tutorialVideoWidget->setAspectRatioMode( - Qt::AspectRatioMode::KeepAspectRatioByExpanding); - m_tutorialVideoWidget->setStyleSheet( - "QVideoWidget { background-color: transparent; }"); - // Loop the tutorial video - connect(m_tutorialPlayer, &QMediaPlayer::mediaStatusChanged, this, - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::EndOfMedia) { - m_tutorialPlayer->setPosition(0); - m_tutorialPlayer->play(); - } - }); - - // Auto-play when ready - connect(m_tutorialPlayer, &QMediaPlayer::mediaStatusChanged, this, - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::LoadedMedia) { - m_tutorialPlayer->play(); - } - }); - m_tutorialVideoWidget->setVisible(false); - m_tutorialLayout->addWidget(m_tutorialVideoWidget, 1); -} - -void AirPlayWidget::showTutorialView() -{ - m_stackedWidget->setCurrentWidget(m_tutorialWidget); - if (m_tutorialPlayer) { - m_tutorialPlayer->play(); - m_loadingIndicator->start(); - } -} - -void AirPlayWidget::showStreamingView() -{ - m_loadingIndicator->stop(); - m_stackedWidget->setCurrentWidget(m_streamingWidget); - if (m_tutorialPlayer) { - m_tutorialPlayer->pause(); - } -} - -void AirPlayWidget::showSettingsDialog() -{ - AirPlaySettingsDialog dialog(this); - if (dialog.exec() == QDialog::Accepted) { - QPair result = dialog.getSettings(); - // if not changed, do nothing - if (!result.first) { - return; - } - AirPlaySettings newSettings = result.second; - - // Save settings - SettingsManager::sharedInstance()->setAirplayFps(newSettings.fps); - SettingsManager::sharedInstance()->setAirplayNoHold(newSettings.noHold); -#ifdef __linux__ - SettingsManager::sharedInstance()->setAirplayUseLegacyPorts( - newSettings.useLegacyPorts); -#endif - - QMessageBox::information(this, "Settings Saved", - "AirPlay will be restarted to apply the new " - "settings."); - ToolboxWidget::sharedInstance()->restartAirPlayWidget(); - } -} - -void AirPlayWidget::startAirPlayServer() -{ - if (m_serverRunning) - return; - - m_serverThread = new AirPlayServerThread(this); - connect(m_serverThread, &AirPlayServerThread::statusChanged, this, - &AirPlayWidget::onServerStatusChanged); - connect(m_serverThread, &AirPlayServerThread::videoFrameReady, this, - &AirPlayWidget::updateVideoFrame); - connect(m_serverThread, &AirPlayServerThread::clientConnectionChanged, this, - &AirPlayWidget::onClientConnectionChanged); - connect(m_serverThread, &AirPlayServerThread::errorOccurred, this, - [this](const QString &message) { - QMessageBox::critical(this, "AirPlay Server Error", message); - close(); - }); - - QStringList args = m_settings.toArgs(); - m_serverThread->setArguments(args); - m_serverThread->start(); -} - -void AirPlayWidget::stopAirPlayServer() -{ - if (m_serverThread) { - m_serverThread->quit(); - m_serverThread->deleteLater(); - m_serverThread = nullptr; - } - m_serverRunning = false; -} - -void AirPlayWidget::updateVideoFrame(QByteArray frameData, int width, - int height) -{ - if (frameData.size() != width * height * 3) { - qDebug() << "Invalid frame data size"; - return; - } - -#ifdef __linux__ - // V4L2 output if enabled - if (m_v4l2_enabled) { - writeFrameToV4L2((uint8_t *)frameData.data(), width, height); - // Show message instead of rendering video when V4L2 is active - m_videoLabel->setText("Currently being shared via virtual camera"); - return; - } -#endif - - QImage image((const uchar *)frameData.data(), width, height, - QImage::Format_RGB888); - QPixmap pixmap = QPixmap::fromImage(image); - - // Scale pixmap to fit label while maintaining aspect ratio - QSize labelSize = m_videoLabel->size(); - QPixmap scaledPixmap = - pixmap.scaled(labelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - m_videoLabel->setPixmap(scaledPixmap); -} - -void AirPlayWidget::onServerStatusChanged(bool running) -{ - m_serverRunning = running; - - if (running) { - // Server started successfully, hide loading indicator and show tutorial - // video - m_loadingLabel->setText("Waiting for device connection"); - - // Show tutorial video and instructions - m_tutorialVideoWidget->setVisible(true); - - // Show settings button when server is running but no client connected - m_settingsButton->setVisible(!m_clientConnected); - - // Show tutorial video and instructions - QLabel *instructionLabel = m_tutorialWidget->findChild(); - if (instructionLabel && !instructionLabel->text().contains("Follow")) { - // Find the instruction label (not title or loading label) - QList labels = m_tutorialWidget->findChildren(); - for (QLabel *label : labels) { - if (label->text().contains("Follow")) { - label->setVisible(true); - break; - } - } - } - - if (m_tutorialPlayer) { - m_tutorialPlayer->play(); - } - } -} - -void AirPlayWidget::onClientConnectionChanged(bool connected) -{ - m_clientConnected = connected; - - // Hide settings button when client is connected - m_settingsButton->setVisible(!connected && m_serverRunning); - - if (connected) { - m_loadingLabel->setText("Device connected - receiving stream..."); - - showStreamingView(); - } else { - m_loadingLabel->setText("Waiting for device connection..."); - m_videoLabel->clear(); - showTutorialView(); - } -} -#ifdef __linux__ - -void AirPlayWidget::onV4L2CheckboxToggled(bool enabled) -{ - if (enabled) { - // Check if V4L2 loopback exists - if (!checkV4L2LoopbackExists()) { - // Show message and ask to create V4L2 loopback - QMessageBox::StandardButton reply = QMessageBox::question( - this, "V4L2 Loopback Required", - "Virtual camera device is required for V4L2 output.\n\n" - "This will create a virtual camera that other applications can " - "use " - "to receive the AirPlay stream. The operation requires " - "administrator privileges.\n\n" - "Do you want to create the virtual camera device?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - - if (reply == QMessageBox::Yes) { - if (createV4L2Loopback()) { - m_v4l2_enabled = true; - - } else { - m_v4l2Checkbox->setChecked(false); - m_v4l2_enabled = false; - QMessageBox::warning( - this, "Error", - "Failed to create virtual camera device. Please ensure " - "you have the necessary permissions."); - } - } else { - m_v4l2Checkbox->setChecked(false); - m_v4l2_enabled = false; - } - } else { - m_v4l2_enabled = true; - } - } else { - m_v4l2_enabled = false; - closeV4L2(); - } -} -#endif - -// AirPlayServerThread implementation -AirPlayServerThread::AirPlayServerThread(QObject *parent) - : QThread(parent), m_shouldStop(false) -{ -} - -AirPlayServerThread::~AirPlayServerThread() -{ - uxplay_cleanup(); - wait(); -} - -void AirPlayServerThread::setArguments(const QStringList &args) -{ - QMutexLocker locker(&m_mutex); - - m_argData.clear(); - m_argv.clear(); - - m_argData.append("uxplay"); - - // Add all arguments - for (const QString &arg : args) { - m_argData.append(arg.toUtf8()); - } - - // Build argv array with persistent pointers - for (QByteArray &data : m_argData) { - m_argv.append(data.data()); - } -} - -// Global pointer to current server thread for callbacks -static AirPlayServerThread *g_currentServerThread = nullptr; - -void frame_callback(const unsigned char *data, int width, int height, - int stride, int format) -{ - if (!g_currentServerThread) - return; - QByteArray frameData((const char *)data, width * height * 3); - emit g_currentServerThread->videoFrameReady(frameData, width, height); -} - -void connection_callback(bool connected) -{ - qDebug() << "Connection callback: " - << (connected ? "Connected" : "Disconnected"); - if (!g_currentServerThread) - return; - emit g_currentServerThread->clientConnectionChanged(connected); -} - -void AirPlayServerThread::run() -{ - g_currentServerThread = this; - emit statusChanged(true); - callbacks_t callbacks; - callbacks.frame_callback = frame_callback; - callbacks.connection_callback = connection_callback; - uxplay_callbacks = &callbacks; - - qDebug() << "Starting AirPlay server with arguments:" << m_argv.size(); - for (int i = 0; i < m_argv.size(); ++i) { - qDebug() << " argv[" << i << "] =" << m_argv[i]; - } - - try { - int res = init_uxplay(m_argv.size(), m_argv.data()); - qDebug() << "AirPlay server exited with code: " << res; - if (res != 0) { - emit errorOccurred("AirPlay server exited unexpectedly."); - } - } catch (const std::exception &e) { - qDebug() << "Exception in AirPlay server thread: " << e.what(); - emit errorOccurred( - QString("AirPlay server encountered an error: %1").arg(e.what())); - } - - uxplay_callbacks = nullptr; - g_currentServerThread = nullptr; -} - -#ifdef __linux__ -// V4L2 Implementation -void AirPlayWidget::initV4L2(int width, int height, const char *device) -{ - closeV4L2(); // Close previous device if any - - m_v4l2_fd = open(device, O_WRONLY); - if (m_v4l2_fd < 0) { - qWarning("Failed to open V4L2 device %s: %s", device, strerror(errno)); - return; - } - - struct v4l2_format fmt; - memset(&fmt, 0, sizeof(fmt)); - fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; - fmt.fmt.pix.width = width; - fmt.fmt.pix.height = height; - fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24; - fmt.fmt.pix.field = V4L2_FIELD_NONE; - fmt.fmt.pix.bytesperline = width * 3; - fmt.fmt.pix.sizeimage = (unsigned int)width * height * 3; - - if (ioctl(m_v4l2_fd, VIDIOC_S_FMT, &fmt) < 0) { - qWarning("Failed to set V4L2 format: %s", strerror(errno)); - ::close(m_v4l2_fd); - m_v4l2_fd = -1; - return; - } - - m_v4l2_width = width; - m_v4l2_height = height; - qDebug("V4L2 device %s initialized to %dx%d", device, width, height); -} - -void AirPlayWidget::closeV4L2() -{ - if (m_v4l2_fd >= 0) { - ::close(m_v4l2_fd); - m_v4l2_fd = -1; - } -} - -void AirPlayWidget::writeFrameToV4L2(uint8_t *data, int width, int height) -{ - // Check if V4L2 device needs to be initialized or re-initialized - if (m_v4l2_fd < 0 || m_v4l2_width != width || m_v4l2_height != height) { - initV4L2(width, height, "/dev/video0"); // Use your v4l2loopback device - } - - // Write frame to V4L2 device if it's open - if (m_v4l2_fd >= 0) { - ssize_t bytes_written = - write(m_v4l2_fd, data, (size_t)width * height * 3); - if (bytes_written < 0) { - qWarning("Failed to write frame to V4L2 device: %s", - strerror(errno)); - closeV4L2(); // Close on error to retry initialization - } - } -} - -bool AirPlayWidget::checkV4L2LoopbackExists() -{ - try { - QFileInfo videoDevice("/dev/video0"); - return videoDevice.exists(); - } catch (...) { - qWarning("Exception occurred while checking for V4L2 loopback device"); - return false; - } -} - -bool AirPlayWidget::createV4L2Loopback() -{ - try { - QProcess process; - - // Use pkexec to run modprobe with administrator privileges - QStringList arguments; - arguments << "modprobe" << "v4l2loopback" << "devices=1" - << "video_nr=0" << "card_label=\"iDescriptor Virtual Camera\"" - << "exclusive_caps=1"; - - process.start("pkexec", arguments); - - if (!process.waitForStarted(5000)) { - qWarning("Failed to start pkexec process"); - return false; - } - - if (!process.waitForFinished(10000)) { - qWarning("Timeout waiting for modprobe to complete"); - process.kill(); - return false; - } - - int exitCode = process.exitCode(); - if (exitCode != 0) { - QString errorOutput = process.readAllStandardError(); - qWarning("modprobe failed with exit code %d: %s", exitCode, - errorOutput.toUtf8().constData()); - return false; - } - - // Wait a bit for the device to be created - QThread::msleep(500); - - // Verify the device was created - return checkV4L2LoopbackExists(); - - } catch (...) { - qWarning("Exception occurred while creating V4L2 loopback device"); - return false; - } -} - -void AirPlayWidget::setupV4L2Checkbox() -{ - if (!SettingsManager::sharedInstance()->showV4L2()) - return; - - try { - m_v4l2Checkbox = new QCheckBox("Enable V4L2 Virtual Camera Output"); - m_v4l2Checkbox->setToolTip("Enable output to virtual camera device " - "that other applications can use"); - m_v4l2Checkbox->setChecked(false); - - connect(m_v4l2Checkbox, &QCheckBox::toggled, this, - &AirPlayWidget::onV4L2CheckboxToggled); - - } catch (...) { - qWarning("Exception occurred while setting up V4L2 checkbox"); - } -} -#endif diff --git a/src/airplaywidget.h b/src/airplaywidget.h deleted file mode 100644 index 0f9165e..0000000 --- a/src/airplaywidget.h +++ /dev/null @@ -1,167 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef AIRPLAYWIDGET_H -#define AIRPLAYWIDGET_H - -#include "iDescriptor-ui.h" -#include "qprocessindicator.h" -#include "service.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class AirPlayServerThread : public QThread -{ - Q_OBJECT - -public: - explicit AirPlayServerThread(QObject *parent = nullptr); - ~AirPlayServerThread(); - - // void stopServer(); - void setArguments(const QStringList &args); - -signals: - void statusChanged(bool running); - void videoFrameReady(QByteArray frameData, int width, int height); - void clientConnectionChanged(bool connected); - void errorOccurred(const QString &message); - -protected: - void run() override; - -private: - QMutex m_mutex; - QWaitCondition m_waitCondition; - bool m_shouldStop; - QVector m_argData; - QVector m_argv; -}; - -class AirPlaySettings -{ -public: - explicit AirPlaySettings(); - int fps; - bool noHold; -#ifdef __linux__ - bool useLegacyPorts; -#endif - - QStringList toArgs() const; -}; - -class AirPlaySettingsDialog : public QDialog -{ - Q_OBJECT -public: - explicit AirPlaySettingsDialog(QWidget *parent = nullptr); - QPair getSettings() const; - -private: - void setupUI(); - void connectSignals(); - - QComboBox *m_fpsComboBox; - QCheckBox *m_noHoldCheckbox; -#ifdef __linux__ - QCheckBox *m_useLegacyPortsCheckbox; -#endif - AirPlaySettings m_settings; - bool m_settingsChanged = false; -}; - -class AirPlayWidget : public Tool -{ - Q_OBJECT - -public: - explicit AirPlayWidget(QWidget *parent = nullptr); - ~AirPlayWidget(); - -private slots: - void updateVideoFrame(QByteArray frameData, int width, int height); - void onServerStatusChanged(bool running); - void onClientConnectionChanged(bool connected); - void showSettingsDialog(); -#ifdef __linux__ - void onV4L2CheckboxToggled(bool enabled); -#endif - -private: - void setupUI(); - void setupTutorialVideo(); - void showTutorialView(); - void showStreamingView(); - void startAirPlayServer(); - void stopAirPlayServer(); - -#ifdef __linux__ - void initV4L2(int width, int height, const char *device = "/dev/video0"); - void closeV4L2(); - void writeFrameToV4L2(uint8_t *data, int width, int height); - bool checkV4L2LoopbackExists(); - bool createV4L2Loopback(); - void setupV4L2Checkbox(); -#endif - - // UI Components - QStackedWidget *m_stackedWidget; - QWidget *m_tutorialWidget; - QWidget *m_streamingWidget; - - QProcessIndicator *m_loadingIndicator; - QLabel *m_loadingLabel; - QMediaPlayer *m_tutorialPlayer; - QVideoWidget *m_tutorialVideoWidget; - QLabel *m_videoLabel; - QVBoxLayout *m_tutorialLayout; - QPushButton *m_settingsButton; - -#ifdef __linux__ - QCheckBox *m_v4l2Checkbox; - int m_v4l2_fd; - int m_v4l2_width; - int m_v4l2_height; - bool m_v4l2_enabled = false; -#endif - - AirPlayServerThread *m_serverThread; - bool m_serverRunning; - bool m_clientConnected; - AirPlaySettings m_settings; -}; - -#endif // AIRPLAYWIDGET_H diff --git a/src/appcontext.cpp b/src/appcontext.cpp deleted file mode 100644 index b3753ae..0000000 --- a/src/appcontext.cpp +++ /dev/null @@ -1,427 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "appcontext.h" - -AppContext *AppContext::sharedInstance() -{ - static AppContext instance; - return &instance; -} - -/* - FIXME: waking up from sleep disconnects all devices - and does not reconnect them until the user plugs them - back in, even if they are still connected -*/ -AppContext::AppContext(QObject *parent) : QObject{parent} -{ - cachePairedDevices(); - connect(core, &CXX::Core::device_became_wired, this, - [this](const QString &udid) { - if (auto dev = getDevice(udid)) { - dev->deviceInfo.isWireless = false; - } - - emit deviceBecameWired(udid); - }); -} - -void AppContext::cachePairedDevices() -{ - -#ifndef __APPLE__ - m_pairingFileCache = core->get_pairing_files(); -#else - QMap maybeStalePairingFiles = - SettingsManager::sharedInstance()->getAllIdeviceDefaultPairingFiles(); - - for (const QString &mac : maybeStalePairingFiles.keys()) { - const QString path = maybeStalePairingFiles.value(mac); - qDebug() << "Using pairing file" << path << "for MAC:" << mac - << "cached from settings"; - m_pairingFileCache[mac] = QVariant(path); - } - QMap fresh = core->get_pairing_files(); - for (const QString &mac : fresh.keys()) { - const QString path = fresh.value(mac).toString(); - qDebug() << "Using fresh pairing file" << path << "for MAC:" << mac - << "from backend"; - m_pairingFileCache[mac] = QVariant(path); - } - -#endif -} - -/* addDevice is only called with udid from backend */ -void AppContext::addDevice(iDescriptor::Uniq uniq, - iDescriptor::IdeviceConnectionType conn_type, - AddType addType, QString info, - QString wifiMacAddress, QString ipAddress) -{ - - if (QCoreApplication::closingDown()) { - qDebug() << "Ignoring addDevice during shutdown for" << uniq.get(); - return; - } - - std::shared_ptr existingDevice = nullptr; - existingDevice = getDevice(uniq.get()); - - if (existingDevice) { - uniq.isMac() ? emit deviceAlreadyExistsMAC(uniq) - : emit deviceAlreadyExists(uniq); - // TODO: add a setting for this - - setCurrentDeviceSelection(DeviceSelection(existingDevice->udid), true); - return; - } - - if (addType == AddType::Pairing) { - handlePairing(uniq, true); - return; - } - - if (addType == AddType::FailedToPair) { - // FIXME: no widget is listening for this signal for now - // emit pairingFailed(uniq); - return; - } - - qDebug() << "Device initialized: " << uniq; - - if (m_pendingDevices.contains(uniq)) { - qDebug() << "Removing from pending devices: " << uniq; - m_pendingDevices.removeAll(uniq); - emit devicePairingExpired(uniq); - } - - pugi::xml_document doc; - auto res = doc.load_string(info.toUtf8().constData()); - if (!res) { - core->remove_device(uniq); - QMessageBox::warning(nullptr, "Failed to add device", - "Failed to parse device info XML for device " + - uniq.get() + - ". The device may not function " - "correctly. Please report this issue."); - qWarning() << "Failed to parse device info XML for" << uniq << ":" - << res.description(); - return; - } - - DeviceInfo deviceInfo; - fullDeviceInfo(doc, deviceInfo); - - const iDescriptorDevice device = { - .udid = uniq.get(), - .conn_type = conn_type, - .deviceInfo = deviceInfo, - .ios_version = deviceInfo.parsedDeviceVersion.major, - .service_manager = new CXX::ServiceManager( - uniq.get(), deviceInfo.parsedDeviceVersion.major), - .afc_backend = new CXX::AfcBackend(uniq.get()), - .afc2_backend = new CXX::Afc2Backend(uniq.get())}; - - m_devices[device.udid] = std::make_shared(device); - - if (addType == AddType::Wireless || addType == AddType::UpgradeToWireless || - addType == AddType::Regular) { - qDebug() << "Wireless device added: " << uniq; - - emit deviceAdded(m_devices[device.udid]); - emit deviceChange(); - return; - } - emit devicePaired(m_devices[device.udid]); - emit deviceChange(); - m_pendingDevices.removeAll(uniq); -} - -int AppContext::getConnectedDeviceCount() const -{ -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - return m_devices.size() + m_recoveryDevices.size(); -#else - return m_devices.size(); -#endif -} - -void AppContext::removeDevice(iDescriptor::Uniq uniq, bool ask_backend) -{ - qDebug() << "AppContext::removeDevice device with" - << (uniq.isMac() ? "MAC" : "UDID") << uniq.get(); - - QString q_udid = uniq.get(); - - if (m_pendingDevices.contains(q_udid)) { - m_pendingDevices.removeAll(q_udid); - emit devicePairingExpired(q_udid); - emit deviceChange(); - return; - } else { - qDebug() << "Device with UUID " + q_udid + - " not found in pending devices."; - } - - if (!m_devices.contains(q_udid)) { - qDebug() << "Device with UUID " + q_udid + - " not found in normal devices."; - return; - } - - auto device = m_devices[q_udid]; - m_devices.remove(q_udid); - - qDebug() << "Removed device with UUID " + q_udid << "macAddress:" - << QString::fromStdString(device->deviceInfo.wifiMacAddress) - << "ipAddress:" - << QString::fromStdString(device->deviceInfo.ipAddress) - << "wasWireless:" << device->deviceInfo.isWireless; - - emit deviceRemoved(q_udid, device->deviceInfo.wifiMacAddress, - device->deviceInfo.isWireless); - emit deviceChange(); - - if (ask_backend) { - core->remove_device(q_udid); - } -} - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT -void AppContext::removeRecoveryDevice(uint64_t ecid) -{ - if (!m_recoveryDevices.contains(ecid)) { - qDebug() << "Device with ECID " + QString::number(ecid) + - " not found. Please report this issue."; - return; - } - - qDebug() << "Removing recovery device with ECID:" << ecid; - - iDescriptorRecoveryDevice *deviceInfo = m_recoveryDevices.value(ecid); - m_recoveryDevices.remove(ecid); - - emit recoveryDeviceRemoved(ecid); - // TODO: do we need this ? - // emit deviceChange(); - - std::lock_guard lock(deviceInfo->mutex); - delete deviceInfo; -} -#endif - -std::shared_ptr AppContext::getDevice(const QString &uniq) -{ - auto it = m_devices.find(uniq); - if (it != m_devices.end()) { - return it.value(); - } - return nullptr; -} - -QList> AppContext::getAllDevices() -{ - return m_devices.values(); -} - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT -QList AppContext::getAllRecoveryDevices() -{ - return m_recoveryDevices.values(); -} -#endif - -// Returns whether there are any devices connected (regular or recovery) -bool AppContext::noDevicesConnected() const -{ -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - return (m_devices.isEmpty() && m_recoveryDevices.isEmpty() && - m_pendingDevices.isEmpty()); -#else - return (m_devices.isEmpty() && m_pendingDevices.isEmpty()); -#endif -} - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT -void AppContext::addRecoveryDevice(uint64_t ecid) -{ - auto res = std::make_shared(); - - QFuture future = QtConcurrent::run( - [this, ecid, res]() { init_idescriptor_recovery_device(ecid, *res); }); - QFutureWatcher *watcher = new QFutureWatcher(); - watcher->setFuture(future); - connect(watcher, &QFutureWatcher::finished, this, - [this, ecid, res, watcher]() { - watcher->deleteLater(); - if (!res->success) { - qDebug() - << "Failed to initialize recovery device with ECID: " - << QString::number(ecid); - qDebug() << "Error code: " << res->error; - return; - } - - iDescriptorRecoveryDevice *recoveryDevice = - new iDescriptorRecoveryDevice(); - recoveryDevice->ecid = res->deviceInfo.ecid; - recoveryDevice->mode = res->mode; - recoveryDevice->cpid = res->deviceInfo.cpid; - recoveryDevice->bdid = res->deviceInfo.bdid; - recoveryDevice->displayName = res->displayName; - - m_recoveryDevices[res->deviceInfo.ecid] = recoveryDevice; - emit recoveryDeviceAdded(recoveryDevice); - emit deviceChange(); - }); -} -#endif - -AppContext::~AppContext() -{ - m_devices.clear(); - for (const QString &udid : m_devices.keys()) { - core->remove_device(udid); - } - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - for (auto recoveryDevice : m_recoveryDevices) { - emit recoveryDeviceRemoved(recoveryDevice->ecid); - delete recoveryDevice; - } -#endif -} - -void AppContext::setCurrentDeviceSelection(const DeviceSelection &selection, - bool showConnectedDevices) -{ - if (m_currentSelection.type == selection.type && - m_currentSelection.udid == selection.udid && - m_currentSelection.ecid == selection.ecid && - m_currentSelection.section == selection.section) { - qDebug() << "setCurrentDeviceSelection: No change in selection"; - if (showConnectedDevices) { - MainWindow::sharedInstance()->showConnectedDevicesTab(); - } - return; // No change - } - m_currentSelection = selection; - emit currentDeviceSelectionChanged(m_currentSelection); - - if (showConnectedDevices) { - MainWindow::sharedInstance()->showConnectedDevicesTab(); - } -} - -const DeviceSelection &AppContext::getCurrentDeviceSelection() const -{ - return m_currentSelection; -} - -const iDescriptorDevice * -AppContext::getDeviceByMacAddress(const QString &macAddress) const -{ - for (const auto &device : m_devices) { - if (device->deviceInfo.wifiMacAddress == macAddress.toStdString()) { - return device.get(); - } - } - return nullptr; -} - -void AppContext::cachePairingFile(const QString &uniq, - const QString &pairingFilePath) -{ - m_pairingFileCache.insert(uniq, pairingFilePath); -} -const QString AppContext::getCachedPairingFile(const QString &uniq) const -{ - QString pairingFile; - - if (m_pairingFileCache.contains(uniq)) { - pairingFile = m_pairingFileCache.value(uniq).toString(); - } - return pairingFile; -} - -void AppContext::heartbeatFailed(const QString &macAddress, int tries) -{ - emit deviceHeartbeatFailed(macAddress, tries); -} - -void AppContext::tryToConnectToNetworkDevice(const NetworkDevice &device, - bool forceCache, - bool setSelectionIfExists) -{ - qDebug() << "Trying to connect to network device with MAC:" - << device.macAddress << "IP:" << device.address; - - // force refresh macAddress-pairing file mapping - if (forceCache) { - cachePairedDevices(); - } - - const iDescriptorDevice *existingDevice = nullptr; - existingDevice = getDeviceByMacAddress(device.macAddress); - - if (existingDevice) { - emit deviceAlreadyExistsMAC( - iDescriptor::Uniq(existingDevice->deviceInfo.wifiMacAddress, true)); - // TODO: add a setting for this - if (setSelectionIfExists) { - setCurrentDeviceSelection(DeviceSelection(existingDevice->udid), - true); - } - return; - } - - cachePairedDevices(); - QString pairing_file = getCachedPairingFile(device.macAddress); - if (pairing_file.isEmpty()) { - qDebug() << "No pairing file cached for device with MAC:" - << device.macAddress - << "Emitting noPairingFileForWirelessDevice event"; - emit noPairingFileForWirelessDevice(device.macAddress); - return; - } - core->init_wireless_device(device.address, pairing_file, device.macAddress); - emit initStarted(device.macAddress); -} - -void AppContext::handlePairing(iDescriptor::Uniq uniq, bool timeout) -{ - m_pendingDevices.append(uniq); - emit devicePasswordProtected(uniq); - emit deviceChange(); - if (timeout) { - QTimer::singleShot( - SettingsManager::sharedInstance()->connectionTimeout() * 1000, this, - [this, uniq]() { - if (m_pendingDevices.contains(uniq)) { - qDebug() << "Pairing expired for " - "device UDID:" - << uniq; - m_pendingDevices.removeAll(uniq); - emit devicePairingExpired(uniq); - emit deviceChange(); - } - }); - } -} \ No newline at end of file diff --git a/src/appcontext.h b/src/appcontext.h deleted file mode 100644 index 26535dd..0000000 --- a/src/appcontext.h +++ /dev/null @@ -1,119 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef APPCONTEXT_H -#define APPCONTEXT_H - -#include "devicesidebarwidget.h" -#include "iDescriptor.h" -#include "mainwindow.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include - -class AppContext : public QObject -{ - Q_OBJECT -public: - static AppContext *sharedInstance(); - std::shared_ptr getDevice(const QString &udid); - QList> getAllDevices(); - explicit AppContext(QObject *parent = nullptr); - bool noDevicesConnected() const; - void cachePairingFile(const QString &udid, const QString &pairingFilePath); - const QString getCachedPairingFile(const QString &udid) const; - CXX::Core *core = new CXX::Core(this); - CXX::IOManager *ioManager = new CXX::IOManager(this); - void tryToConnectToNetworkDevice(const NetworkDevice &device, - bool forceCache = true, - bool setSelectionIfExists = true); -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - QList getAllRecoveryDevices(); -#endif - ~AppContext(); - int getConnectedDeviceCount() const; - - void setCurrentDeviceSelection(const DeviceSelection &selection, - bool showConnectedDevices = false); - const DeviceSelection &getCurrentDeviceSelection() const; - const iDescriptorDevice * - getDeviceByMacAddress(const QString &macAddress) const; - -private: - QMap> m_devices; -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - QMap m_recoveryDevices; -#endif - QStringList m_pendingDevices; - DeviceSelection m_currentSelection = DeviceSelection(""); - QMap m_pairingFileCache; - void cachePairedDevices(); - void handlePairing(iDescriptor::Uniq uniq, bool timeout); - -signals: - void deviceAdded(std::shared_ptr device); - void deviceRemoved(const QString &udid, const std::string &macAddress, - bool wasWireless); - void devicePaired(std::shared_ptr device); - void devicePasswordProtected(const QString &udid); - void deviceAlreadyExists(const iDescriptor::Uniq &uniq); - void deviceAlreadyExistsMAC(const iDescriptor::Uniq &uniq); - void deviceBecameWired(const QString &udid); -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - void recoveryDeviceAdded(const iDescriptorRecoveryDevice *deviceInfo); - void recoveryDeviceRemoved(uint64_t ecid); -#endif - void devicePairPending(const QString &udid); - void devicePairingExpired(const QString &udid); - // only fired on wireless devices when we have no pairing file for them - void noPairingFileForWirelessDevice(const QString &macAddress); - void initFailed(const QString &udid); - void initStarted(const QString &udid); - void pairingFailed(const QString &udid); - - /* - Generic change event for any device state change we - need this because many UI elements need to update by - listening for this only you can watch for any event - and using the public members of this class you can - do anything you want - */ - void deviceChange(); - void currentDeviceSelectionChanged(const DeviceSelection &selection); - void deviceHeartbeatFailed(const QString &macAddress, int tries); -public slots: - void removeDevice(iDescriptor::Uniq uniq, bool ask_backend = false); - void addDevice(iDescriptor::Uniq udid, - iDescriptor::IdeviceConnectionType connType, AddType addType, - QString info = QString(), QString wifiMacAddress = QString(), - QString ipAddress = QString()); - void heartbeatFailed(const QString &macAddress, int tries); - // void heartbeatThreadExited(const QString &macAddress); -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - void addRecoveryDevice(uint64_t ecid); - void removeRecoveryDevice(uint64_t ecid); -#endif -}; - -#endif // APPCONTEXT_H diff --git a/src/appdownloaddialog.cpp b/src/appdownloaddialog.cpp deleted file mode 100644 index fc0b5e6..0000000 --- a/src/appdownloaddialog.cpp +++ /dev/null @@ -1,127 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "appdownloaddialog.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "libipatool-go.h" -#include -#include -#include -#include -#include -#include -#include -#include - -AppDownloadDialog::AppDownloadDialog(const QString &appName, - const QString &bundleId, - const QString &description, - QWidget *parent) - : AppDownloadBaseDialog(appName, bundleId, parent), - m_outputDir( - QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)), - m_bundleId(bundleId) -{ - setWindowTitle("Download " + appName + " IPA"); - setModal(true); - setFixedWidth(500); - setContentsMargins(0, 0, 0, 0); - - m_loadingWidget = new ZLoadingWidget(false, this); - layout()->addWidget(m_loadingWidget); - QVBoxLayout *contentLayout = new QVBoxLayout(); - contentLayout->setContentsMargins(0, 0, 0, 0); - - // m_bgLabel = new QLabel(); - // m_bgLabel->setScaledContents(true); - // m_bgLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); - // contentLayout->addWidget(m_bgLabel); - - QHBoxLayout *cardLayout = new QHBoxLayout(); - cardLayout->setContentsMargins(20, 20, 20, 20); - contentLayout->addLayout(cardLayout); - - m_appIcon = new IDLoadingIconLabel(); - - cardLayout->addWidget(m_appIcon); - cardLayout->addSpacing(5); - QVBoxLayout *textLayout = new QVBoxLayout(); - - QLabel *nameLabel = new QLabel(appName); - nameLabel->setStyleSheet("font-size: 18px; font-weight: bold;"); - nameLabel->setWordWrap(true); - textLayout->addWidget(nameLabel); - - QLabel *bundleIdLabel = new QLabel(bundleId); - bundleIdLabel->setStyleSheet("font-size: 12px; color: #666;"); - bundleIdLabel->setWordWrap(true); - textLayout->addWidget(bundleIdLabel); - - textLayout->addSpacing(5); - - QLabel *descLabel = new QLabel(description); - descLabel->setWordWrap(true); - descLabel->setStyleSheet("font-size: 14px; color: #666;"); - textLayout->addWidget(descLabel); - - cardLayout->addLayout(textLayout); - QPointer safeThis = this; - fetchAppIconFromApple( - m_manager, bundleId, - [safeThis](const QPixmap &pixmap, const QJsonObject &appInfo) { - if (auto dialog = safeThis.data()) { - dialog->m_appIcon->setLoadedPixmap(pixmap); - dialog->m_loadingWidget->stop(true); - } - }); - - m_dirPickerLabel = new ZDirPickerLabel("Save to:"); - - contentLayout->addWidget(m_dirPickerLabel); - - QHBoxLayout *buttonLayout = new QHBoxLayout(); - - m_actionButton = new QPushButton("Download IPA"); - m_actionButton->setDefault(true); - connect(m_actionButton, &QPushButton::clicked, this, - &AppDownloadDialog::onDownloadClicked); - buttonLayout->addWidget(m_actionButton); - - QPushButton *cancelButton = new QPushButton("Cancel"); - connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); - buttonLayout->addWidget(cancelButton); - - contentLayout->addLayout(buttonLayout); - - m_loadingWidget->setupContentWidget(contentLayout); -} - -void AppDownloadDialog::onDownloadClicked() -{ - m_dirPickerLabel->disableDirSelection(); - m_actionButton->setEnabled(false); - - int buttonIndex = m_layout->indexOf(m_actionButton); - layout()->removeWidget(m_actionButton); - m_actionButton->deleteLater(); - qDebug() << "Starting download to" << m_outputDir; - qDebug() << "Bundle ID:" << m_bundleId; - startDownloadProcess(m_bundleId, m_outputDir, buttonIndex, true, true); -} diff --git a/src/appdownloaddialog.h b/src/appdownloaddialog.h deleted file mode 100644 index 36a3401..0000000 --- a/src/appdownloaddialog.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef APPDOWNLOADDIALOG_H -#define APPDOWNLOADDIALOG_H - -#include "base/appdownload.h" -#include "iDescriptor-ui.h" -#include "zloadingwidget.h" -#include -#include -#include - -class AppDownloadDialog : public AppDownloadBaseDialog -{ - Q_OBJECT -public: - explicit AppDownloadDialog(const QString &appName, const QString &bundleId, - const QString &description, - QWidget *parent = nullptr); - - // protected: - // void resizeEvent(QResizeEvent *event) override - // { - // QWidget::resizeEvent(event); - // m_bgLabel->setFixedSize(event->size()); - // } - -private slots: - void onDownloadClicked(); - -private: - QString m_outputDir; - QPushButton *m_dirButton; - ZLabel *m_dirLabel; - QString m_bundleId; - QLabel *m_bgLabel; - IDLoadingIconLabel *m_appIcon; - ZLoadingWidget *m_loadingWidget; - ZDirPickerLabel *m_dirPickerLabel; -}; - -#endif // APPDOWNLOADDIALOG_H diff --git a/src/appinstalldialog.cpp b/src/appinstalldialog.cpp deleted file mode 100644 index cc1820b..0000000 --- a/src/appinstalldialog.cpp +++ /dev/null @@ -1,314 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "appinstalldialog.h" -#include -#include -extern "C" { -char *IpaToolGetDownloadedFilePath(const char *bundleId); -} -static QString ipaPathFromIpatool(const QString &bundleId) -{ - QByteArray utf8 = bundleId.toUtf8(); - char *cPath = IpaToolGetDownloadedFilePath(utf8.constData()); - if (!cPath) { - return {}; - } - QString path = QString::fromUtf8(cPath); - std::free(cPath); - return path; -} - -AppInstallDialog::AppInstallDialog(const QString &appName, - const QString &description, - const QString &bundleId, QWidget *parent) - : AppDownloadBaseDialog(appName, bundleId, parent), m_bundleId(bundleId), - m_statusLabel(nullptr), m_installWatcher(nullptr) -{ - setWindowTitle("Install " + appName + " - iDescriptor"); - setModal(true); - setFixedWidth(500); - setContentsMargins(0, 0, 0, 0); - - QVBoxLayout *baseLayout = qobject_cast(this->layout()); - m_loadingWidget = new ZLoadingWidget(false, this); - baseLayout->addWidget(m_loadingWidget); - - QVBoxLayout *contentLayout = new QVBoxLayout(); - contentLayout->setContentsMargins(0, 0, 0, 0); - - // App info section - QHBoxLayout *appInfoLayout = new QHBoxLayout(); - m_iconLabel = new IDLoadingIconLabel(); - QPointer safeThis = this; - - ::fetchAppIconFromApple( - m_manager, bundleId, - [safeThis](const QPixmap &pixmap, const QJsonObject &appInfo) { - if (safeThis && safeThis.data()) { - safeThis->m_iconLabel->setLoadedPixmap(pixmap); - } - if (safeThis && safeThis->m_loadingWidget) { - safeThis->m_loadingWidget->stop(true); - } - }); - - appInfoLayout->addWidget(m_iconLabel); - - QVBoxLayout *detailsLayout = new QVBoxLayout(); - QLabel *nameLabel = new QLabel(appName); - { - QFont f = nameLabel->font(); - f.setPointSize(20); - f.setBold(true); - nameLabel->setFont(f); - } - detailsLayout->addWidget(nameLabel); - - QLabel *descLabel = new QLabel(description); - descLabel->setWordWrap(true); - { - QFont f = descLabel->font(); - f.setPointSize(14); - descLabel->setFont(f); - } - detailsLayout->addWidget(descLabel); - - appInfoLayout->addLayout(detailsLayout); - appInfoLayout->addStretch(); - contentLayout->addLayout(appInfoLayout); - - QLabel *deviceLabel = new QLabel("Choose Device:"); - { - QFont f = deviceLabel->font(); - f.setPointSize(16); - f.setBold(true); - deviceLabel->setFont(f); - } - contentLayout->addWidget(deviceLabel); - - m_deviceCombo = new QComboBox(); - contentLayout->addWidget(m_deviceCombo); - - m_statusLabel = new QLabel("Ready to install"); - { - QFont f = m_statusLabel->font(); - f.setPointSize(14); - m_statusLabel->setFont(f); - } - m_statusLabel->setAlignment(Qt::AlignCenter); - contentLayout->addWidget(m_statusLabel); - - contentLayout->addStretch(); - - m_actionButton = new QPushButton("Install"); - m_actionButton->setFixedHeight(40); - - connect(m_actionButton, &QPushButton::clicked, this, - &AppInstallDialog::onInstallClicked); - contentLayout->addWidget(m_actionButton); - - m_cancelButton = new QPushButton("Cancel"); - m_cancelButton->setFixedHeight(40); - connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); - contentLayout->addWidget(m_cancelButton); - - m_loadingWidget->setupContentWidget(contentLayout); - - connect(AppContext::sharedInstance(), &AppContext::deviceChange, this, - &AppInstallDialog::updateDeviceList); - - updateDeviceList(); -} - -void AppInstallDialog::updateDeviceList() -{ - m_deviceCombo->clear(); - auto devices = AppContext::sharedInstance()->getAllDevices(); - if (devices.empty()) { - m_deviceCombo->addItem("No devices connected"); - m_deviceCombo->setEnabled(false); - m_actionButton->setDefault(false); - m_actionButton->setEnabled(false); - m_statusLabel->setText("No devices connected"); - setLabelTextColor(m_statusLabel, Qt::red); - } else { - m_deviceCombo->setEnabled(true); - for (const auto &device : devices) { - QString deviceName = - QString::fromStdString(device->deviceInfo.productType); - m_deviceCombo->addItem(deviceName + " / " + device->udid.left(8) + - "...", - device->udid); - } - m_actionButton->setDefault(true); - m_actionButton->setEnabled(true); - m_statusLabel->setText("Ready to install"); - resetLabelTextColor(m_statusLabel); - } -} - -void AppInstallDialog::performInstallation(const QString &ipaPath, - const QString &ipaName, - const QString &deviceUdid) -{ - m_statusLabel->setText("Installing app..."); - // cannot cancel from this point - m_cancelButton->setEnabled(false); - resetLabelTextColor(m_statusLabel); - - std::shared_ptr device = - AppContext::sharedInstance()->getDevice(deviceUdid); - - QPointer safeThis = this; - // install_ipa_init - connect(device->service_manager, &CXX::ServiceManager::install_ipa_init, - this, [safeThis, this](bool started, const QString &state) { - if (!safeThis || !safeThis.data()) { - return; - } - - if (!started) { - QString msg = - state.isEmpty() - ? QStringLiteral("Failed to start installation") - : state; - m_loadingWidget->showError(msg); - return; - } - - if (!state.isEmpty()) { - m_statusLabel->setText(state); - } else { - m_statusLabel->setText("Installing app..."); - } - resetLabelTextColor(m_statusLabel); - }); - - // install_ipa_progress - connect(device->service_manager, &CXX::ServiceManager::install_ipa_progress, - this, [this, safeThis](double progress, const QString &state) { - if (!safeThis || !safeThis.data()) { - return; - } - if (!state.isEmpty()) { - m_statusLabel->setText(state); - } - m_progressBar->setValue(static_cast(progress * 100)); - - // treat >= 100% as completion - if (progress >= 1.0) { - m_cancelButton->setEnabled(true); - m_statusLabel->setText( - "Installation completed successfully!"); - setLabelTextColor(m_statusLabel, Qt::darkGreen); - QMessageBox::information(this, "Success", - "App installed successfully!"); - accept(); - } - }); - device->service_manager->install_ipa(ipaPath); -} - -void AppInstallDialog::onInstallClicked() -{ - if (m_deviceCombo->count() == 0) { - QMessageBox::warning(this, "No Device", - "Please connect a device first."); - return; - } - - m_deviceCombo->setEnabled(false); - m_actionButton->setEnabled(false); - m_statusLabel->setText("Downloading app..."); - resetLabelTextColor(m_statusLabel); - - QString selectedDevice = m_deviceCombo->currentData().toString(); - - int buttonIndex = m_layout->indexOf(m_actionButton); - layout()->removeWidget(m_actionButton); - m_actionButton->deleteLater(); - m_actionButton = nullptr; - - if (m_tempDir) { - delete m_tempDir; - m_tempDir = nullptr; - } - // Create a new temporary directory for each installation - m_tempDir = new QTemporaryDir(); - if (!m_tempDir->isValid()) { - m_statusLabel->setText("Failed to create temporary directory"); - setLabelTextColor(m_statusLabel, Qt::red); - QMessageBox::critical( - this, "Error", - "Could not create temporary directory for download."); - return; - } - - startDownloadProcess(m_bundleId, m_tempDir->path(), buttonIndex, false, - false); - - connect( - this, &AppDownloadBaseDialog::downloadFinished, this, - [this, selectedDevice](bool success) { - if (success) { - qDebug() << "Download finished, starting installation..."; - - QString ipaFile = ipaPathFromIpatool(m_bundleId); - if (ipaFile.isEmpty()) { - m_statusLabel->setText("Download failed - IPA not found"); - setLabelTextColor(m_statusLabel, Qt::red); - QMessageBox::critical( - this, "Error", - QString( - "Downloaded IPA path not reported by libipatool")); - return; - } - - QFileInfo fi(ipaFile); - qDebug() << "IPA:" << ipaFile; - performInstallation(ipaFile, fi.fileName(), selectedDevice); - } - }); -} - -void AppInstallDialog::reject() -{ - // FIMXE: is this ok ? - // if cancel button is already disabled, it means we're in the middle of - // installation and cannot cancel - if (!m_cancelButton->isEnabled()) { - return; - } - - if (m_statusLabel) { - m_statusLabel->setText("Installation cancelled"); - setLabelTextColor(m_statusLabel, Qt::red); - } - - AppDownloadBaseDialog::reject(); -} - -AppInstallDialog::~AppInstallDialog() -{ - if (m_tempDir) { - delete m_tempDir; - m_tempDir = nullptr; - } -} \ No newline at end of file diff --git a/src/appinstalldialog.h b/src/appinstalldialog.h deleted file mode 100644 index a1b515d..0000000 --- a/src/appinstalldialog.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef APPINSTALLDIALOG_H -#define APPINSTALLDIALOG_H - -#include "appcontext.h" -#include "base/appdownload.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class AppInstallDialog : public AppDownloadBaseDialog -{ - Q_OBJECT -public: - explicit AppInstallDialog(const QString &appName, - const QString &description, - const QString &bundleId, - QWidget *parent = nullptr); - ~AppInstallDialog(); - -protected: - void reject() override; - -private slots: - void onInstallClicked(); - -private: - QComboBox *m_deviceCombo; - QString m_bundleId; - QLabel *m_statusLabel; - QFutureWatcher *m_installWatcher; - IDLoadingIconLabel *m_iconLabel = nullptr; - QTemporaryDir *m_tempDir = nullptr; - ZLoadingWidget *m_loadingWidget = nullptr; - QPushButton *m_cancelButton = nullptr; - void updateDeviceList(); - void performInstallation(const QString &ipaPath, const QString &ipaName, - const QString &deviceUdid); -}; - -#endif // APPINSTALLDIALOG_H diff --git a/src/apps.rs b/src/apps.rs new file mode 100644 index 0000000..0385fbd --- /dev/null +++ b/src/apps.rs @@ -0,0 +1,94 @@ +use crate::{RUNTIME, qt_threading::QtThreading, qvariantmap_insert}; +use ipatool::Account; +use ipatool::IpaTool; +use macros::QtThreading; +use qmetaobject::prelude::*; +use qttypes::{QStringList, QVariantMap}; +use std::pin::Pin; + +#[derive(QObject, Default, QtThreading)] +pub struct Apps { + base: qt_base_class!(trait QObject), + state: qt_property!(QVariantMap; NOTIFY state_changed), + state_changed: qt_signal!(), + + init: qt_method!(fn(&mut self)), + sign_in: qt_method!(fn(&mut self, email: QString, password: QString)), +} + +impl Apps { + pub fn new_with_state() -> Self { + let mut state = QVariantMap::default(); + qvariantmap_insert!(state, "init", false); + qvariantmap_insert!(state, "error", QString::default()); + qvariantmap_insert!(state, "email", QString::default()); + + let mut def = Self::default(); + def.state = state; + def + } + + fn init(&mut self) { + let q_thread = self.qt_thread(); + RUNTIME.spawn(async move { + let res: anyhow::Result> = async { + let tool = IpaTool::new_default().await?; + Ok(tool.account_info().await?) + } + .await; + + match res { + Ok(maybe_acc) => { + let acc = maybe_acc.unwrap_or_default(); + println!("email :{}", acc.email); + + let mut state = QVariantMap::default(); + qvariantmap_insert!(state, "init", true); + qvariantmap_insert!(state, "error", QString::default()); + qvariantmap_insert!(state, "email", QString::from(acc.email)); + + q_thread.queue(|t| { + t.state = state; + t.state_changed(); + }) + } + Err(err) => { + let mut state = QVariantMap::default(); + qvariantmap_insert!(state, "init", true); + qvariantmap_insert!(state, "error", QString::from(format!("{}", err))); + qvariantmap_insert!(state, "email", QString::default()); + + q_thread.queue(|t| { + t.state = state; + t.state_changed(); + }); + } + } + }); + } + + fn sign_in(&mut self, email: QString, password: QString) { + // FIXME: implement + // RUNTIME.spawn(async move { + // let res: anyhow::Result<()> = async { + // let tool = IpaTool::new_default().await?; + + // let auth_code_cb: Box ipatool::Result + Send + Sync> = + // Box::new(|| { + // }); + // tool.login( + // &email.to_string(), + // &password.to_string(), + // Some(auth_code_cb), + // None, + // ) + // .await; + + // Ok(()) + // } + // .await; + + // Ok(()) + // }); + } +} diff --git a/src/appstoremanager.cpp b/src/appstoremanager.cpp deleted file mode 100644 index 76e3132..0000000 --- a/src/appstoremanager.cpp +++ /dev/null @@ -1,266 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "appstoremanager.h" -#include "libipatool-go.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// 2FA callback for login -static char *getAuthCodeCallback() -{ - static QByteArray buffer; - QString code; - QMetaObject::invokeMethod( - qApp, - [&]() { - bool ok; - code = QInputDialog::getText( - nullptr, "Two-Factor Authentication", - "Enter the 2FA code:", QLineEdit::Normal, QString(), &ok); - }, - Qt::BlockingQueuedConnection); - - if (code.isEmpty()) { - return nullptr; - } - buffer = code.toUtf8(); - return buffer.data(); -} - -AppStoreManager *AppStoreManager::sharedInstance() -{ - static AppStoreManager instance; - return instance.m_initialized ? &instance : nullptr; -} - -AppStoreManager::AppStoreManager(QObject *parent) - : QObject(parent), m_initialized(false) -{ - m_initialized = initialize(); -} - -bool AppStoreManager::initialize() -{ - bool useUnsecureBackend = - SettingsManager::sharedInstance()->useUnsecureBackend(); - - QString backends; - - if (useUnsecureBackend) { - backends = "file"; - } else { -#ifdef __APPLE__ - backends = "keychain,file"; -#elif defined(WIN32) - backends = "wincred,file"; -#else - backends = "secret-service,file"; -#endif - } - - int result = IpaToolInitialize(backends.toUtf8().data()); - if (result != 0) { - qDebug() << "IpaToolInitialize failed with error code:" << result; - return false; - } - qDebug() << "IpaToolInitialize succeeded"; - return true; -} - -QJsonObject AppStoreManager::getAccountInfo() -{ - if (!m_initialized) { - return QJsonObject(); - } - - char *accountInfoCStr = IpaToolGetAccountInfo(); - if (!accountInfoCStr) { - return QJsonObject(); - } - - QString jsonAccountInfo(accountInfoCStr); - free(accountInfoCStr); - - QJsonParseError parseError; - QJsonDocument doc = - QJsonDocument::fromJson(jsonAccountInfo.toUtf8(), &parseError); - - if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { - qDebug() << "JSON parse error:" << parseError.errorString(); - return QJsonObject(); - } - - return doc.object(); -} - -void AppStoreManager::loginWithCallback( - const QString &email, const QString &password, - std::function callback) -{ - if (!m_initialized) { - callback(false, QJsonObject()); - return; - } - - QFuture future = QtConcurrent::run([email, password]() { - return IpaToolLoginWithCallback(email.toUtf8().data(), - password.toUtf8().data(), - getAuthCodeCallback); - }); - - QFutureWatcher *watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, - [this, watcher, callback]() { - int result = watcher->result(); - watcher->deleteLater(); - - bool success = (result == 0); - QJsonObject accountInfo; - - if (success) { - accountInfo = getAccountInfo(); - emit loginSuccessful(accountInfo); - } - - callback(success, accountInfo); - }); - watcher->setFuture(future); -} - -void AppStoreManager::revokeCredentials() -{ - if (!m_initialized) { - return; - } - - IpaToolRevokeCredentials(); - // todo: should we ? - // could be problematic if user logs in using ipatool - // emit loggedOut(getAccountInfo()); - emit loggedOut(QJsonObject()); -} - -void AppStoreManager::searchApps( - const QString &searchTerm, int limit, - std::function callback) -{ - if (!m_initialized) { - callback(false, QString()); - return; - } - - QFuture future = - QtConcurrent::run([searchTerm, limit]() -> QString { - char *resultsCStr = - IpaToolSearch(searchTerm.toUtf8().data(), limit); - if (!resultsCStr) { - return QString(); - } - QString results(resultsCStr); - free(resultsCStr); - return results; - }); - - QFutureWatcher *watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, - [watcher, callback]() { - QString results = watcher->result(); - watcher->deleteLater(); - callback(!results.isEmpty(), results); - }); - watcher->setFuture(future); -} - -void AppStoreManager::downloadApp( - const QString &bundleId, const QString &outputDir, - const QString &externalVersionId, bool acquireLicense, - std::function callback, - std::function progressCallback) -{ - if (!m_initialized) { - callback(-1); - return; - } - - // Create a wrapper for the progress callback - void *progressUserData = nullptr; - void (*cProgressCallback)(long long, long long, void *) = nullptr; - - if (progressCallback) { - // Store the callback in a way that can be accessed from C - auto *callbackPtr = - new std::function(progressCallback); - progressUserData = callbackPtr; - - cProgressCallback = [](long long current, long long total, - void *userData) { - auto *cb = static_cast *>( - userData); - QMetaObject::invokeMethod( - qApp, [cb, current, total]() { (*cb)(current, total); }, - Qt::QueuedConnection); - }; - } - - QFuture future = QtConcurrent::run( - [bundleId, outputDir, externalVersionId, acquireLicense, - cProgressCallback, progressUserData]() { - int result = IpaToolDownloadApp( - bundleId.toUtf8().data(), outputDir.toUtf8().data(), - externalVersionId.toUtf8().data(), acquireLicense, - cProgressCallback, progressUserData); - return result; - }); - - QFutureWatcher *watcher = new QFutureWatcher(this); - connect( - watcher, &QFutureWatcher::finished, this, - [watcher, callback, progressUserData]() { - int result = watcher->result(); - watcher->deleteLater(); - - // Clean up progress callback if it was allocated - if (progressUserData) { - delete static_cast *>( - progressUserData); - } - - callback(result); - }); - watcher->setFuture(future); -} - -void AppStoreManager::cancelDownload(const QString &bundleId) -{ - if (!m_initialized) { - return; - } - qDebug() << "[AppStoreManager::cancelDownload] : Cancelling download for" - << bundleId; - IpaToolCancelDownload(bundleId.toUtf8().data()); -} \ No newline at end of file diff --git a/src/appstoremanager.h b/src/appstoremanager.h deleted file mode 100644 index 595a3b8..0000000 --- a/src/appstoremanager.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef APPSTOREMANAGER_H -#define APPSTOREMANAGER_H - -#include -#include -#include - -class AppStoreManager : public QObject -{ - Q_OBJECT - -public: - static AppStoreManager *sharedInstance(); - - // Account management - QJsonObject getAccountInfo(); - void loginWithCallback( - const QString &email, const QString &password, - std::function - callback); - void revokeCredentials(); - - // App operations - void searchApps( - const QString &searchTerm, int limit, - std::function callback); - void - downloadApp(const QString &bundleId, const QString &outputPath, - const QString &externalVersionId, bool acquireLicense, - std::function completionCallback, - std::function progressCallback); - void cancelDownload(const QString &bundleId); - -signals: - void loginSuccessful(const QJsonObject &accountInfo); - void loggedOut(const QJsonObject &accountInfo); - -private: - AppStoreManager(QObject *parent = nullptr); - ~AppStoreManager() = default; - - AppStoreManager(const AppStoreManager &) = delete; - AppStoreManager &operator=(const AppStoreManager &) = delete; - - bool m_initialized; - bool initialize(); -}; - -#endif // APPSTOREMANAGER_H \ No newline at end of file diff --git a/src/appswidget.cpp b/src/appswidget.cpp deleted file mode 100644 index 55bc5b7..0000000 --- a/src/appswidget.cpp +++ /dev/null @@ -1,814 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "appswidget.h" -#include "appcontext.h" -#include "appdownloaddialog.h" -#include "appinstalldialog.h" -#include "appstoremanager.h" -#include "creddialog.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "keychaindialog.h" -#include "logindialog.h" -#include "mainwindow.h" -#include "settingsmanager.h" -#include "sponsorwidget.h" -#include "zlineedit.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// FIXME: we dont watch for login and logout events because there is no such api -AppsWidget *AppsWidget::sharedInstance() -{ - static AppsWidget *instance = new AppsWidget(); - return instance; -} - -AppsWidget::AppsWidget(QWidget *parent) : QWidget(parent), m_isLoggedIn(false) -{ - m_debounceTimer = new QTimer(this); - setupUI(); -} - -void AppsWidget::setupUI() -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - // Header with login - m_networkManager = new QNetworkAccessManager(this); - - QWidget *headerWidget = new QWidget(); - headerWidget->setFixedHeight(60); - // headerWidget->setStyleSheet( - // "border-bottom: 1px solid #363d32; border-radius: 0px;"); - - QHBoxLayout *headerLayout = new QHBoxLayout(headerWidget); - headerLayout->setContentsMargins(20, 10, 20, 10); - - // Create status label first - m_statusLabel = new QLabel("Not signed in"); - m_statusLabel->setStyleSheet("margin-right: 20px; border: none;"); - - m_loginButton = new QPushButton(); - m_searchEdit = new QLineEdit(); - m_searchEdit->setMaximumWidth(200); - - // --- Status and Login Button --- - m_manager = AppStoreManager::sharedInstance(); - - m_statusLabel->setStyleSheet("font-size: 14px; color: #666;"); - - mainLayout->addWidget(headerWidget); - - m_searchIcon = ZIcon(":/resources/icons/MdiLightMagnify.png"); - m_searchAction = m_searchEdit->addAction( - m_searchIcon.getThemedPixmap(QSize(16, 16), palette()), - QLineEdit::TrailingPosition); - m_searchAction->setToolTip("Search"); - connect(m_searchAction, &QAction::triggered, this, - &AppsWidget::performSearch); - - headerLayout->addWidget(m_searchEdit); - headerLayout->addStretch(); - headerLayout->addWidget(m_statusLabel); - headerLayout->addWidget(m_loginButton); - - // Stacked widget for different pages - m_stackedWidget = new QStackedWidget(); - setupDefaultAppsPage(); - setupLoadingPage(); - setupErrorPage(); - - mainLayout->addWidget(m_stackedWidget); - - // Show default apps initially - showLoading("Loading apps..."); - // Connections - connect(m_loginButton, &QPushButton::clicked, this, - &AppsWidget::onLoginClicked); - connect(m_searchEdit, &QLineEdit::textChanged, this, - &AppsWidget::onSearchTextChanged); - m_debounceTimer->setSingleShot(true); - connect(m_debounceTimer, &QTimer::timeout, this, - &AppsWidget::performSearch); - connect(m_manager, &AppStoreManager::loginSuccessful, this, - &AppsWidget::onAppStoreInitialized); - connect(m_manager, &AppStoreManager::loggedOut, this, - &AppsWidget::onAppStoreInitialized); -} - -void AppsWidget::init() -{ - QUrl sponsorsUrl(SPONSORS_JSON_URL); - QNetworkRequest request(sponsorsUrl); - QNetworkReply *reply = m_networkManager->get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - try { - if (reply->error() == QNetworkReply::NoError) { - QByteArray responseData = reply->readAll(); - QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); - if (jsonDoc.isNull() || !jsonDoc.isObject()) { - qDebug() << "Failed to parse sponsors JSON"; - showDefaultApps(); - reply->deleteLater(); - return; - } - - QJsonObject rootObj = jsonDoc.object(); - QJsonObject versioned = getVersionedConfig(rootObj); - - if (versioned.isEmpty()) { - qDebug() << "No sponsor configuration found for version" - << APP_VERSION << "or default."; - showDefaultApps(); - reply->deleteLater(); - return; - } - - QJsonObject sponsorObj = versioned["sponsors"].toObject(); - QJsonObject platinumObj = sponsorObj["platinum"].toObject(); - QJsonObject goldObj = sponsorObj["gold"].toObject(); - QJsonObject silverObj = sponsorObj["silver"].toObject(); - QJsonObject bronzeObj = sponsorObj["bronze"].toObject(); - - m_platinumSponsors = platinumObj["members"].toArray(); - m_goldSponsors = goldObj["members"].toArray(); - m_silverSponsors = silverObj["members"].toArray(); - m_bronzeSponsors = bronzeObj["members"].toArray(); - - if (!m_platinumSponsors.isEmpty()) { - qDebug() << "Platinum Sponsors found"; - } - } - qDebug() << "Sponsors fetch completed"; - reply->deleteLater(); - QTimer::singleShot(0, this, &AppsWidget::handleInit); - } catch (...) { - qDebug() << "Exception occurred while processing sponsors"; - reply->deleteLater(); - QTimer::singleShot(0, this, &AppsWidget::handleInit); - } - }); -} - -void AppsWidget::handleInit() -{ - if (!m_manager) { - qDebug() << "AppStoreManager failed to initialize"; - m_statusLabel->setText("Failed to initialize"); - m_loginButton->setText("Failed to initialize"); - m_loginButton->setEnabled(false); -#ifndef WIN32 - m_loginButton->setStyleSheet( - "background-color: #ccc; color: #666; " - "border: " - "none; border-radius: " - "4px; padding: 8px 16px; font-size: 14px;"); -#endif - return; - } - /* - FIXME: ipatoolinitialze still uses the secure backends - even if the user rejects it, the moment he/she tries to sign in - prompt(keychain or secret-service whatever the backend is) will be seen - again - */ - if (!SettingsManager::sharedInstance()->useUnsecureBackend() && - SettingsManager::sharedInstance()->showKeychainDialog()) { -#ifdef __APPLE__ - KeychainDialog dialog(this); - if (dialog.exec() == QDialog::Rejected) { - // pass empty QJsonObject to skip signing in - onAppStoreInitialized(QJsonObject()); - showDefaultApps(); - return; - } -// windows doesn't show any keychain dialog -#elif __linux__ - CredDialog dialog(this); - if (dialog.exec() == QDialog::Rejected) { - // pass empty QJsonObject to skip signing in - onAppStoreInitialized(QJsonObject()); - showDefaultApps(); - return; - } -#endif - } - onAppStoreInitialized(m_manager->getAccountInfo()); - showDefaultApps(); -} - -void AppsWidget::onAppStoreInitialized(const QJsonObject &accountInfo) -{ - if (accountInfo.contains("success") && - accountInfo.value("success").toBool()) { - if (accountInfo.contains("email")) { - QString email = accountInfo.value("email").toString(); - m_statusLabel->setText("Signed in as " + email); - m_isLoggedIn = true; - m_searchEdit->setDisabled(false); - } else { - m_statusLabel->setText("Not signed in"); - m_searchEdit->setDisabled(true); - } - } else { - m_searchEdit->setDisabled(true); - m_statusLabel->setText("Not signed in"); - } - - m_loginButton->setText(m_isLoggedIn ? "Sign Out" : "Sign In"); -#ifndef WIN32 - m_loginButton->setStyleSheet( - "background-color: #007AFF; color: white; border: none; " - "border-radius: " - "4px; padding: 8px 16px; font-size: 14px;"); -#endif - m_searchEdit->setPlaceholderText(m_isLoggedIn ? "Search for apps..." - : "Sign in to search"); -} - -void AppsWidget::setupDefaultAppsPage() -{ - m_defaultAppsPage = new QWidget(); - - // Scroll area for apps - m_scrollArea = new QScrollArea(); - m_scrollArea->setWidgetResizable(true); - m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - m_scrollArea->viewport()->setStyleSheet("background: transparent;"); - - m_contentWidget = new QWidget(); - QGridLayout *gridLayout = new QGridLayout(m_contentWidget); - gridLayout->setContentsMargins(20, 20, 20, 20); - gridLayout->setSpacing(20); - - m_scrollArea->setWidget(m_contentWidget); - - QVBoxLayout *pageLayout = new QVBoxLayout(m_defaultAppsPage); - pageLayout->setContentsMargins(0, 0, 0, 0); - pageLayout->addWidget(m_scrollArea); - - m_stackedWidget->addWidget(m_defaultAppsPage); -} - -void AppsWidget::setupLoadingPage() -{ - m_loadingPage = new QWidget(); - - QVBoxLayout *loadingLayout = new QVBoxLayout(m_loadingPage); - loadingLayout->setAlignment(Qt::AlignCenter); - - m_loadingIndicator = new QProcessIndicator(); - m_loadingIndicator->setType(QProcessIndicator::line_rotate); - m_loadingIndicator->setFixedSize(64, 32); - - m_loadingLabel = new QLabel("Loading..."); - m_loadingLabel->setAlignment(Qt::AlignCenter); - m_loadingLabel->setStyleSheet( - "font-size: 16px; color: #666; margin-top: 20px;"); - - loadingLayout->addWidget(m_loadingIndicator, 0, Qt::AlignCenter); - loadingLayout->addWidget(m_loadingLabel, 0, Qt::AlignCenter); - - m_stackedWidget->addWidget(m_loadingPage); -} - -void AppsWidget::setupErrorPage() -{ - m_errorPage = new QWidget(); - - QVBoxLayout *errorLayout = new QVBoxLayout(m_errorPage); - errorLayout->setAlignment(Qt::AlignCenter); - - m_errorLabel = new QLabel("Error occurred"); - m_errorLabel->setAlignment(Qt::AlignCenter); - m_errorLabel->setWordWrap(true); - m_errorLabel->setStyleSheet("font-size: 16px; color: #666;"); - - errorLayout->addWidget(m_errorLabel, 0, Qt::AlignCenter); - - m_stackedWidget->addWidget(m_errorPage); -} - -void AppsWidget::showDefaultApps() -{ - clearAppGrid(); - populateDefaultApps(); - m_stackedWidget->setCurrentWidget(m_defaultAppsPage); -} - -void AppsWidget::showLoading(const QString &message) -{ - m_loadingLabel->setText(message); - m_loadingIndicator->start(); - m_stackedWidget->setCurrentWidget(m_loadingPage); -} - -void AppsWidget::showError(const QString &message) -{ - m_loadingIndicator->stop(); - m_errorLabel->setText(message); - m_stackedWidget->setCurrentWidget(m_errorPage); -} - -void AppsWidget::populateDefaultApps() -{ - QGridLayout *gridLayout = - qobject_cast(m_contentWidget->layout()); - if (!gridLayout) - return; - - int row = 0; - int col = 0; - const int maxCols = 3; - - auto advanceGridPos = [&]() { - col++; - if (col >= maxCols) { - col = 0; - row++; - } - }; - - for (const QJsonValue &sponsorValue : m_platinumSponsors) { - QJsonObject sponsorObj = sponsorValue.toObject(); - QString name = sponsorObj.value("name").toString(); - QString bundleId = sponsorObj.value("bundleId").toString(); - QString logoUrl = sponsorObj.value("logo").toString(); - QString description = sponsorObj.value("description").toString(); - QString url = sponsorObj.value("url").toString(); - bool useBundleIdForIcon = - sponsorObj.value("useBundleIdForIcon").toBool(true); - createAppCard(name, bundleId, description, logoUrl, url, gridLayout, - row, col, useBundleIdForIcon, - SponsorType(SponsorType::Platinum)); - advanceGridPos(); - } - - for (const QJsonValue &sponsorValue : m_goldSponsors) { - QJsonObject sponsorObj = sponsorValue.toObject(); - QString name = sponsorObj.value("name").toString(); - QString bundleId = sponsorObj.value("bundleId").toString(); - QString description = sponsorObj.value("description").toString(); - QString logoUrl = sponsorObj.value("logo").toString(); - QString url = sponsorObj.value("url").toString(); - bool useBundleIdForIcon = - sponsorObj.value("useBundleIdForIcon").toBool(true); - createAppCard(name, bundleId, description, logoUrl, url, gridLayout, - row, col, useBundleIdForIcon, - SponsorType(SponsorType::Gold)); - advanceGridPos(); - } - - for (const QJsonValue &sponsorValue : m_silverSponsors) { - QJsonObject sponsorObj = sponsorValue.toObject(); - QString name = sponsorObj.value("name").toString(); - QString bundleId = sponsorObj.value("bundleId").toString(); - QString description = sponsorObj.value("description").toString(); - QString url = sponsorObj.value("url").toString(); - QString logoUrl = sponsorObj.value("logo").toString(); - bool useBundleIdForIcon = - sponsorObj.value("useBundleIdForIcon").toBool(true); - - createAppCard(name, bundleId, description, logoUrl, url, gridLayout, - row, col, useBundleIdForIcon, - SponsorType(SponsorType::Silver)); - advanceGridPos(); - } - - for (const QJsonValue &sponsorValue : m_bronzeSponsors) { - QJsonObject sponsorObj = sponsorValue.toObject(); - QString name = sponsorObj.value("name").toString(); - QString bundleId = sponsorObj.value("bundleId").toString(); - QString description = sponsorObj.value("description").toString(); - QString url = sponsorObj.value("url").toString(); - QString logoUrl = sponsorObj.value("logo").toString(); - - bool useBundleIdForIcon = - sponsorObj.value("useBundleIdForIcon").toBool(true); - createAppCard(name, bundleId, description, logoUrl, url, gridLayout, - row, col, useBundleIdForIcon, - SponsorType(SponsorType::Bronze)); - advanceGridPos(); - } - - if (m_platinumSponsors.empty() && m_goldSponsors.empty()) { - createSponsorCard(gridLayout, row, col); - advanceGridPos(); - } - - createAppCard("Instagram", "com.burbn.instagram", - "Photo & Video sharing social network", "", "", gridLayout, - row, col); - advanceGridPos(); - createAppCard("Spotify", "com.spotify.client", - "Music streaming and podcast platform", "", "", gridLayout, - row, col); - advanceGridPos(); - createAppCard("YouTube", "com.google.ios.youtube", - "Video sharing and streaming platform", "", "", gridLayout, - row, col); - advanceGridPos(); - createAppCard("X", "com.atebits.Tweetie2", "Social media and microblogging", - "", "", gridLayout, row, col); - advanceGridPos(); - createAppCard("TikTok", "com.zhiliaoapp.musically", - "Short-form video hosting service", "", "", gridLayout, row, - col); - advanceGridPos(); - createAppCard("Twitch", "tv.twitch", "Live streaming platform", "", "", - gridLayout, row, col); - advanceGridPos(); - createAppCard("Telegram", "ph.telegra.Telegraph", - "Cloud-based instant messaging", "", "", gridLayout, row, - col); - advanceGridPos(); - createAppCard("Reddit", "com.reddit.Reddit", - "Social news aggregation platform", "", "", gridLayout, row, - col); - advanceGridPos(); - - gridLayout->setRowStretch(gridLayout->rowCount(), 1); -} - -void AppsWidget::clearAppGrid() -{ - QGridLayout *gridLayout = - qobject_cast(m_contentWidget->layout()); - if (!gridLayout) - return; - - QLayoutItem *item; - while ((item = gridLayout->takeAt(0)) != nullptr) { - if (item->widget()) { - item->widget()->deleteLater(); - } - delete item; - } -} - -void AppsWidget::createSponsorCard(QGridLayout *gridLayout, int row, int col) -{ - if (!gridLayout) - return; - - ClickableWidget *sponsorCard = new ClickableWidget(); - sponsorCard->setStyleSheet("border: 1px solid #ddd; border-radius: 8px;"); - sponsorCard->setCursor(Qt::PointingHandCursor); - connect(sponsorCard, &ClickableWidget::clicked, this, [this]() { - auto sWidget = new SponsorWidget(); - sWidget->setAttribute(Qt::WA_DeleteOnClose); - sWidget->show(); - }); - QVBoxLayout *sponsorLayout = new QVBoxLayout(sponsorCard); - sponsorLayout->setContentsMargins(12, 12, 12, 12); - sponsorLayout->setSpacing(8); - - QLabel *sponsorLabel = new QLabel("Become a Sponsor!"); - sponsorLabel->setAlignment(Qt::AlignCenter); - sponsorLabel->setStyleSheet("font-size: 14px; font-weight: bold;"); - sponsorLayout->addWidget(sponsorLabel); - - gridLayout->addWidget(sponsorCard, row, col); -} - -void AppsWidget::createAppCard( - const QString &name, const QString &bundleId, const QString &description, - const QString &logoUrl, const QString &websiteUrl, QGridLayout *gridLayout, - int row, int col, bool useBundleIdForIcon, const SponsorType &sponsorType) -{ - QWidget *cardWidget = new QWidget(); - - QHBoxLayout *cardLayout = new QHBoxLayout(cardWidget); - cardLayout->setContentsMargins(15, 15, 15, 15); - cardLayout->setSpacing(10); - - // App icon - IDLoadingIconLabel *iconLabel = new IDLoadingIconLabel(); - QPointer safeIconLabel = iconLabel; - iconLabel->setAlignment(Qt::AlignCenter); - cardLayout->addWidget(iconLabel); - - if (!logoUrl.isEmpty() && !useBundleIdForIcon) { - QUrl url(logoUrl); - QNetworkRequest request(url); - QNetworkReply *reply = m_networkManager->get(request); - connect(reply, &QNetworkReply::finished, this, - [reply, safeIconLabel]() { - if (safeIconLabel) { - if (reply->error() == QNetworkReply::NoError) { - QByteArray data = reply->readAll(); - safeIconLabel->setLoadedPixmap(data); - } else { - safeIconLabel->setLoadFailed(); - } - } - reply->deleteLater(); - }); - connect(iconLabel, &QObject::destroyed, reply, &QNetworkReply::abort); - } else if (!bundleId.isEmpty()) { - fetchAppIconFromApple( - m_networkManager, bundleId, - [safeIconLabel](const QPixmap &pixmap, const QJsonObject &appInfo) { - Q_UNUSED(appInfo); - if (!safeIconLabel) - return; - - if (!pixmap.isNull()) { - safeIconLabel->setLoadedPixmap(pixmap); - } else { - safeIconLabel->setLoadFailed(); - } - }); - } - - // Vertical layout for name and description - QVBoxLayout *textLayout = new QVBoxLayout(); - - // App name with sponsor indicator - QHBoxLayout *nameLayout = new QHBoxLayout(); - QLabel *nameLabel = new QLabel(name); - // nameLabel->font().setSize(16); - nameLabel->setStyleSheet("font-size: 16px;"); - nameLabel->setWordWrap(true); - nameLayout->addWidget(nameLabel); - - // Add sponsor type indicator - if (!sponsorType.isEmpty()) { - QLabel *sponsorLabel = new QLabel(sponsorType.name); - sponsorLabel->setStyleSheet(QString("font-size: 10px; " - "font-weight: bold; " - "color: #333; " - "background-color: %2; " - "border-radius: 4px; " - "padding: 2px 6px; " - "margin-left: 8px;") - .arg(sponsorType.color)); - sponsorLabel->setFixedHeight(16); - sponsorLabel->setAlignment(Qt::AlignCenter); - nameLayout->addWidget(sponsorLabel); - } - - nameLayout->addStretch(); - textLayout->addLayout(nameLayout); - - // App description - QLabel *descLabel = new QLabel(description); - descLabel->setStyleSheet("font-size: 12px; color: #666;"); - descLabel->setAlignment(Qt::AlignLeft); - descLabel->setWordWrap(true); - textLayout->addWidget(descLabel); - - cardLayout->addLayout(textLayout); - - QVBoxLayout *buttonsLayout = new QVBoxLayout(); - - // Install button placeholder - if (!bundleId.isEmpty()) { - ZLabel *installLabel = new ZLabel("Install"); - installLabel->setAlignment(Qt::AlignCenter); - installLabel->setStyleSheet( - QString("font-size: 12px; color: %1; font-weight: " - "bold; background-color: transparent;") - .arg(COLOR_ACCENT_BLUE.name())); - installLabel->setCursor(Qt::PointingHandCursor); - installLabel->setFixedHeight(30); - - buttonsLayout->addStretch(); - buttonsLayout->addWidget(installLabel); - connect(installLabel, &ZLabel::clicked, this, - [this, name, bundleId, description]() { - onAppCardClicked(name, bundleId, description); - }); - } - if (websiteUrl.isEmpty()) { - ZLabel *downloadIpaLabel = new ZLabel("Download IPA"); - downloadIpaLabel->setAlignment(Qt::AlignCenter); - downloadIpaLabel->setStyleSheet("font-size: 12px; font-weight: " - "bold; background-color: transparent;"); - downloadIpaLabel->setCursor(Qt::PointingHandCursor); - - connect( - downloadIpaLabel, &ZLabel::clicked, this, - [this, name, bundleId]() { onDownloadIpaClicked(name, bundleId); }); - - buttonsLayout->addWidget(downloadIpaLabel); - } else { - ZLabel *websiteLabel = new ZLabel("Website"); - websiteLabel->setStyleSheet("font-size: 12px; font-weight: " - "bold; background-color: transparent;"); - websiteLabel->setAlignment(Qt::AlignCenter); - websiteLabel->setCursor(Qt::PointingHandCursor); - - connect(websiteLabel, &ZLabel::clicked, this, [this, websiteUrl]() { - QDesktopServices::openUrl(QUrl(websiteUrl)); - }); - buttonsLayout->addWidget(websiteLabel); - } - - buttonsLayout->addStretch(); - - cardLayout->addLayout(buttonsLayout); - gridLayout->addWidget(cardWidget, row, col); -} -void AppsWidget::onDownloadIpaClicked(const QString &name, - const QString &bundleId) -{ - if (!m_isLoggedIn) { - QMessageBox::information(this, "Sign In Required", - "Please sign in to download IPA files."); - return; - } - QString description = "Download the IPA file for " + name; - AppDownloadDialog dialog(name, bundleId, description, this); - dialog.exec(); -} - -void AppsWidget::onLoginClicked() -{ - if (m_isLoggedIn) { - AppStoreManager *manager = AppStoreManager::sharedInstance(); - if (manager) { - manager->revokeCredentials(); - } - m_isLoggedIn = false; - m_loginButton->setText("Sign In"); - m_statusLabel->setText("Not signed in"); - m_searchEdit->setPlaceholderText("Sign in to search"); - return; - } - - LoginDialog dialog(this); - if (dialog.exec() == QDialog::Accepted) { - // Login was successful, update UI - AppStoreManager *manager = AppStoreManager::sharedInstance(); - if (manager) { - QJsonObject accountInfo = manager->getAccountInfo(); - if (accountInfo.contains("success") && - accountInfo.value("success").toBool()) { - if (accountInfo.contains("email")) { - QString email = accountInfo.value("email").toString(); - m_statusLabel->setText("Signed in as " + email); - m_isLoggedIn = true; - m_loginButton->setText("Sign Out"); - m_searchEdit->setPlaceholderText("Search for apps..."); - } - } - } - } -} - -void AppsWidget::onAppCardClicked(const QString &appName, - const QString &bundleId, - const QString &description) -{ - if (!m_isLoggedIn) { - QMessageBox::information(this, "Sign In Required", - "Please sign in to install apps."); - return; - } - - AppInstallDialog dialog(appName, description, bundleId, this); - dialog.exec(); -} - -void AppsWidget::onSearchTextChanged() { m_debounceTimer->start(300); } - -void AppsWidget::performSearch() -{ - QString searchTerm = m_searchEdit->text().trimmed(); - if (searchTerm.isEmpty()) { - showDefaultApps(); - return; - } - - showLoading(QString("Searching for \"%1\"...").arg(searchTerm)); - - AppStoreManager *manager = AppStoreManager::sharedInstance(); - if (!manager) { - showError("Failed to initialize App Store manager."); - return; - } - - manager->searchApps(searchTerm, 20, - [this](bool success, const QString &results) { - onSearchFinished(success, results); - }); -} - -void AppsWidget::onSearchFinished(bool success, const QString &results) -{ - // FIXME: cancel fetch instead of just ignoring results - QString searchTerm = m_searchEdit->text().trimmed(); - if (searchTerm.isEmpty()) { - showDefaultApps(); - return; - } - - if (!success || results.isEmpty()) { - showError("No apps found or search failed."); - return; - } - - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(results.toUtf8(), &parseError); - - if (parseError.error != QJsonParseError::NoError) { - qDebug() << "JSON parse error:" << parseError.errorString() - << " on output: " << results; - showError("Failed to parse search results."); - return; - } - - qDebug() << "Search results:" << doc; - QJsonObject rootObj = doc.object(); - if (!rootObj.value("success").toBool()) { - QString errorMessage = - rootObj.value("error").toString("Unknown search error."); - showError(QString("Search error: %1").arg(errorMessage)); - return; - } - - QJsonArray resultsArray = rootObj.value("results").toArray(); - if (resultsArray.isEmpty()) { - showError("No apps found."); - return; - } - - clearAppGrid(); - QGridLayout *gridLayout = - qobject_cast(m_contentWidget->layout()); - if (!gridLayout) - return; - - int row = 0; - int col = 0; - const int maxCols = 3; - - for (const QJsonValue &appValue : resultsArray) { - QJsonObject appObj = appValue.toObject(); - QString name = appObj.value("trackName").toString(); - QString bundleId = appObj.value("bundleId").toString(); - QString description = "Version: " + appObj.value("version").toString(); - - createAppCard(name, bundleId, description, "", "", gridLayout, row, - col); - - col++; - if (col >= maxCols) { - col = 0; - row++; - } - } - gridLayout->setRowStretch(gridLayout->rowCount(), 1); - m_stackedWidget->setCurrentWidget(m_defaultAppsPage); -} diff --git a/src/appswidget.h b/src/appswidget.h deleted file mode 100644 index 95b5e80..0000000 --- a/src/appswidget.h +++ /dev/null @@ -1,156 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef APPSWIDGET_H -#define APPSWIDGET_H - -#include "appstoremanager.h" -#include "iDescriptor-ui.h" -#include "qprocessindicator.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct SponsorType { - enum Level { None, Bronze, Silver, Gold, Platinum }; - - Level level; - QString name; - QString color; - - SponsorType(Level l = None) : level(l) - { - switch (l) { - case Platinum: - name = "PLATINUM"; - color = "#E5E4E2"; // Platinum silver-white - break; - case Gold: - name = "GOLD"; - color = "#FFD700"; // Gold - break; - case Silver: - name = "SILVER"; - color = "#C0C0C0"; // Silver - break; - case Bronze: - name = "BRONZE"; - color = "#CD7F32"; // Bronze - break; - default: - name = ""; - color = ""; - break; - } - } - - bool isEmpty() const { return level == None; } -}; - -class AppsWidget : public QWidget -{ - Q_OBJECT -public: - explicit AppsWidget(QWidget *parent = nullptr); - static AppsWidget *sharedInstance(); - void onAppCardClicked(const QString &appName, const QString &bundleId, - const QString &description); - void init(); -private slots: - void onLoginClicked(); - void onDownloadIpaClicked(const QString &name, const QString &bundleId); - void onSearchTextChanged(); - void performSearch(); - void onSearchFinished(bool success, const QString &results); - void onAppStoreInitialized(const QJsonObject &accountInfo); - -private: - void setupUI(); - void createAppCard(const QString &name, const QString &bundleId, - const QString &description, const QString &logoUrl, - const QString &websiteUrl, QGridLayout *gridLayout, - int row, int col, bool useBundleIdForIcon = true, - const SponsorType &sponsorType = SponsorType()); - void setupDefaultAppsPage(); - void setupLoadingPage(); - void setupErrorPage(); - void showDefaultApps(); - void showLoading(const QString &message = "Loading..."); - void showError(const QString &message); - void clearAppGrid(); - void populateDefaultApps(); - void createSponsorCard(QGridLayout *gridLayout, int row, int col); - void handleInit(); - QStackedWidget *m_stackedWidget; - QWidget *m_defaultAppsPage; - QWidget *m_loadingPage; - QWidget *m_errorPage; - QProcessIndicator *m_loadingIndicator; - QLabel *m_loadingLabel; - QLabel *m_errorLabel; - QScrollArea *m_scrollArea; - QWidget *m_contentWidget; - QPushButton *m_loginButton; - QLabel *m_statusLabel; - bool m_isLoggedIn; - AppStoreManager *m_manager; - QNetworkAccessManager *m_networkManager = nullptr; - - // Search - QLineEdit *m_searchEdit; - QTimer *m_debounceTimer; - QAction *m_searchAction; - - // Sponsors - QJsonArray m_platinumSponsors; - QJsonArray m_goldSponsors; - QJsonArray m_silverSponsors; - QJsonArray m_bronzeSponsors; - ZIcon m_searchIcon; - -protected: - void changeEvent(QEvent *event) override - { - if (event->type() == QEvent::PaletteChange) { - if (m_searchAction && !m_searchIcon.isNull()) { - m_searchAction->setIcon( - m_searchIcon.getThemedPixmap(QSize(16, 16), palette())); - } - } - QWidget::changeEvent(event); - } -}; - -#endif // APPSWIDGET_H diff --git a/src/batterywidget.cpp b/src/batterywidget.cpp deleted file mode 100644 index b659a4a..0000000 --- a/src/batterywidget.cpp +++ /dev/null @@ -1,156 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -// https://github.com/p-dobrzynski-dev/QtCustomWidgets/blob/master/batterywidget.cpp -#include "batterywidget.h" - -#include -#include -#include -#include -#include -#include - -BatteryWidget::BatteryWidget(float value, bool isCharging, QWidget *parent) - : QWidget(parent), m_value(value), m_isCharging(isCharging) -{ - setMinimumSize(30, 30); - setMaximumSize(40, 40); -} - -void BatteryWidget::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::PaletteChange) { - update(); - } - QWidget::changeEvent(event); -} - -void BatteryWidget::resizeEvent(QResizeEvent *) -{ - widgetFrame = this->rect(); - - widgetFrame.setSize(QSize(widgetFrame.width(), widgetFrame.height() / 2)); - widgetFrame.moveTop(widgetFrame.center().y()); - - float scaleValue = 0.95; - QSizeF mainBatteryFrameSize = - QSizeF(widgetFrame.width() * scaleValue, widgetFrame.height()); - mainBatteryFrame.setSize(mainBatteryFrameSize); - - mainBatteryFrame.moveTopLeft(widgetFrame.topLeft()); - - QSizeF tipBatteryFrameSize = - QSizeF(widgetFrame.width() / 3, widgetFrame.height() / 2); - tipBatteryFrame.setSize(tipBatteryFrameSize); - - QPointF tipBatteryFramePoint = - QPointF(widgetFrame.topRight().x() - tipBatteryFrameSize.width(), - widgetFrame.topRight().y() + tipBatteryFrameSize.height() / 2); - tipBatteryFrame.moveTopLeft(tipBatteryFramePoint); - - float batteryLevelOffset = mainBatteryFrame.height() / 20; - QSizeF batteryLevelFrameSize = - QSizeF(mainBatteryFrame.width() - 2 * batteryLevelOffset, - mainBatteryFrame.height() - 2 * batteryLevelOffset); - batteryLevelFrame.setSize(batteryLevelFrameSize); - - QPointF batteryFramePoint = - QPointF(mainBatteryFrame.topLeft().x() + batteryLevelOffset, - mainBatteryFrame.topLeft().y() + batteryLevelOffset); - batteryLevelFrame.moveTopLeft(batteryFramePoint); -} - -void BatteryWidget::setValue(float newValue) -{ - m_value = newValue; - this->update(); -} - -float BatteryWidget::getValue() const { return m_value; } - -void BatteryWidget::setChargingState(bool state) -{ - m_isCharging = state; - this->update(); - this->repaint(); -} - -void BatteryWidget::updateContext(bool isCharging, float newValue) -{ - m_isCharging = isCharging; - m_value = newValue; - this->update(); - this->repaint(); -} - -bool BatteryWidget::getChargingState() { return m_isCharging; } - -void BatteryWidget::paintEvent(QPaintEvent *) -{ - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing, true); - QPen pen = QPen(Qt::red); - QBrush brush = QBrush(Qt::white); - - painter.setPen(pen); - - float widgetCorner = widgetFrame.height() / 15; - - QPainterPath FramePath; - FramePath.setFillRule(Qt::WindingFill); - FramePath.addRoundedRect(mainBatteryFrame, widgetCorner, widgetCorner); - FramePath.addRoundedRect(tipBatteryFrame, widgetCorner, widgetCorner); - FramePath = FramePath.simplified(); - - pen.setColor(Qt::gray); - pen.setWidth(widgetFrame.width() / 75); - painter.setPen(pen); - painter.drawPath(FramePath); - - painter.setBrush(QBrush(QColor("#44bd32"))); - painter.setPen(Qt::NoPen); - QRectF batteryLevelRect = QRectF(); - QSize batterySizeRect = - QSize(batteryLevelFrame.width() * m_value / m_maxValue, - batteryLevelFrame.height()); - batteryLevelRect.setSize(batterySizeRect); - batteryLevelRect.moveTo(batteryLevelFrame.topLeft()); - painter.drawRoundedRect(batteryLevelRect, widgetCorner, widgetCorner); - - pen.setColor(palette().color(QPalette::Text)); - painter.setPen(pen); -#ifndef WIN32 - QFont textFont = QFont(); - textFont.setWeight(QFont::Bold); -#else - QFont textFont = QFont("Segoe UI Variable Small", 12); -#endif - textFont.setPixelSize(widgetFrame.height() / 1.65); - painter.setFont(textFont); - QFontMetrics fm(textFont); - QString percentageLevelString = QString("%1%").arg(m_value); - float textWidth = fm.horizontalAdvance(percentageLevelString); - float textHeight = fm.height(); - - QPointF textPosition = QPointF(widgetFrame.center().x() - textWidth / 2, - widgetFrame.center().y() + textHeight / 3); - painter.drawText(textPosition, percentageLevelString); -} \ No newline at end of file diff --git a/src/batterywidget.h b/src/batterywidget.h deleted file mode 100644 index e2bc277..0000000 --- a/src/batterywidget.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef BATTERYWIDGET_H -#define BATTERYWIDGET_H - -#include -#include - -class BatteryWidget : public QWidget -{ - Q_OBJECT -public: - BatteryWidget(float value, bool isCharging, QWidget *parent); - bool getChargingState(); - void setChargingState(bool state); - void updateContext(bool isCharging, float newValue); - - // New methods for value management - void setValue(float newValue); - float getValue() const; - -protected: - void changeEvent(QEvent *event) override; - -private: - QRectF widgetFrame; - QRectF mainBatteryFrame; - QRectF tipBatteryFrame; - QRectF batteryLevelFrame; - - bool m_isCharging = false; - - float m_value = 0.0f; - float m_minValue = 0.0f; - float m_maxValue = 100.0f; - - void resizeEvent(QResizeEvent *event) override; - void paintEvent(QPaintEvent *event) override; -}; - -#endif // BATTERYWIDGET_H diff --git a/src/rust/src/thumbnail.cc b/src/bridge.cpp similarity index 61% rename from src/rust/src/thumbnail.cc rename to src/bridge.cpp index 9b0ba0f..69c66e9 100644 --- a/src/rust/src/thumbnail.cc +++ b/src/bridge.cpp @@ -1,6 +1,9 @@ -#include "bridge.h" -#include "rust/cxx.h" +#include "include/bridge.h" #include +#include +#include +#include + extern "C" { #include #include @@ -9,78 +12,71 @@ extern "C" { #include } -#include "idescriptor_rust_codebase/src/bridge.cxxqt.h" +/* this is declared in Rust with #[no_mangle]*/ +extern "C" void afc_reader_read_at(const void *reader_ptr, int64_t offset, + int32_t size, uint8_t *out_buf, + int32_t *out_len); -QImage generate_thumbnail_with_reader(const AfcReader &reader, - int32_t file_size, int32_t requested_w, - int32_t requested_h) +QImage generate_thumbnail_with_reader_ffi(const void *reader_ptr, + int32_t file_size, + int32_t requested_w, + int32_t requested_h) { - QImage result; - - AVFormatContext *formatCtx = avformat_alloc_context(); - if (!formatCtx) { - // qWarning() << "Failed to allocate format context"; - return result; - } - struct StreamContext { - const AfcReader *reader; + const void *readerPtr; int32_t fileSize; int currentPos; }; - auto *streamCtx = new StreamContext{&reader, file_size, 0}; + auto *streamCtx = new StreamContext{reader_ptr, file_size, 0}; auto readPacket = [](void *opaque, uint8_t *buf, int bufSize) -> int { auto *ctx = static_cast(opaque); - - if (ctx->currentPos >= ctx->fileSize) { + if (ctx->currentPos >= ctx->fileSize) return AVERROR_EOF; - } int toRead = std::min(bufSize, ctx->fileSize - ctx->currentPos); - auto data = ctx->reader->read_at(ctx->currentPos, toRead); + int32_t got = 0; + afc_reader_read_at(ctx->readerPtr, ctx->currentPos, toRead, buf, &got); - if (data.empty()) { + if (got <= 0) return (toRead == 0) ? AVERROR_EOF : AVERROR(EIO); - } - const int n = std::min(data.size(), toRead); - memcpy(buf, data.data(), n); - ctx->currentPos += n; - return n; + ctx->currentPos += got; + return got; }; auto seekPacket = [](void *opaque, int64_t offset, int whence) -> int64_t { auto *ctx = static_cast(opaque); - - if (whence == AVSEEK_SIZE) { + if (whence == AVSEEK_SIZE) return ctx->fileSize; - } int newPos = 0; switch (whence) { case SEEK_SET: - newPos = offset; + newPos = (int)offset; break; case SEEK_CUR: - newPos = ctx->currentPos + offset; + newPos = ctx->currentPos + (int)offset; break; case SEEK_END: - newPos = ctx->fileSize + offset; + newPos = ctx->fileSize + (int)offset; break; default: return -1; } - - if (newPos < 0 || newPos > ctx->fileSize) { + if (newPos < 0 || newPos > ctx->fileSize) return -1; - } - ctx->currentPos = newPos; return newPos; }; + AVFormatContext *formatCtx = avformat_alloc_context(); + if (!formatCtx) { + delete streamCtx; + return {}; + } + const int avioBufferSize = 32768; unsigned char *avioBuffer = static_cast(av_malloc(avioBufferSize)); @@ -103,30 +99,26 @@ QImage generate_thumbnail_with_reader(const AfcReader &reader, formatCtx->pb = avioCtx; formatCtx->flags |= AVFMT_FLAG_CUSTOM_IO; - // Open input if (avformat_open_input(&formatCtx, nullptr, nullptr, nullptr) < 0) { - // qWarning() << "Failed to open video format"; av_free(avioCtx->buffer); avio_context_free(&avioCtx); - avformat_free_context(formatCtx); + delete streamCtx; return {}; } - // Find stream info if (avformat_find_stream_info(formatCtx, nullptr) < 0) { - // qWarning() << "Failed to find stream info"; avformat_close_input(&formatCtx); av_free(avioCtx->buffer); avio_context_free(&avioCtx); + delete streamCtx; return {}; } - // Find video stream int videoStreamIndex = -1; const AVCodec *codec = nullptr; AVCodecParameters *codecParams = nullptr; - for (unsigned int i = 0; i < formatCtx->nb_streams; i++) { + for (unsigned i = 0; i < formatCtx->nb_streams; i++) { if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { videoStreamIndex = i; codecParams = formatCtx->streams[i]->codecpar; @@ -136,43 +128,29 @@ QImage generate_thumbnail_with_reader(const AfcReader &reader, } if (videoStreamIndex == -1 || !codec) { - // qWarning() << "No video stream found"; avformat_close_input(&formatCtx); av_free(avioCtx->buffer); avio_context_free(&avioCtx); + delete streamCtx; return {}; } - // Allocate codec context AVCodecContext *codecCtx = avcodec_alloc_context3(codec); - if (!codecCtx) { + if (!codecCtx || avcodec_parameters_to_context(codecCtx, codecParams) < 0 || + avcodec_open2(codecCtx, codec, nullptr) < 0) { + if (codecCtx) + avcodec_free_context(&codecCtx); avformat_close_input(&formatCtx); av_free(avioCtx->buffer); avio_context_free(&avioCtx); + delete streamCtx; return {}; } - if (avcodec_parameters_to_context(codecCtx, codecParams) < 0) { - avcodec_free_context(&codecCtx); - avformat_close_input(&formatCtx); - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - return {}; - } - - // Open codec - if (avcodec_open2(codecCtx, codec, nullptr) < 0) { - avcodec_free_context(&codecCtx); - avformat_close_input(&formatCtx); - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - return {}; - } - - // Allocate frame AVFrame *frame = av_frame_alloc(); AVPacket *packet = av_packet_alloc(); + // first video frame ───────────────────────────────────────────── if (!frame || !packet) { if (frame) av_frame_free(&frame); @@ -182,26 +160,26 @@ QImage generate_thumbnail_with_reader(const AfcReader &reader, avformat_close_input(&formatCtx); av_free(avioCtx->buffer); avio_context_free(&avioCtx); + delete streamCtx; return {}; } - // Read frames until we get a valid one bool frameDecoded = false; while (av_read_frame(formatCtx, packet) >= 0) { if (packet->stream_index == videoStreamIndex) { - if (avcodec_send_packet(codecCtx, packet) >= 0) { - if (avcodec_receive_frame(codecCtx, frame) >= 0) { - frameDecoded = true; - av_packet_unref(packet); - break; - } + if (avcodec_send_packet(codecCtx, packet) >= 0 && + avcodec_receive_frame(codecCtx, frame) >= 0) { + frameDecoded = true; + av_packet_unref(packet); + break; } } av_packet_unref(packet); } + QImage result; + if (frameDecoded) { - // Get rotation from display matrix double rotation = 0.0; if (AVFrameSideData *sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX)) { @@ -209,7 +187,6 @@ QImage generate_thumbnail_with_reader(const AfcReader &reader, -av_display_rotation_get(reinterpret_cast(sd->data)); } - // Convert frame to RGB24 SwsContext *swsCtx = sws_getContext(frame->width, frame->height, static_cast(frame->format), @@ -228,39 +205,28 @@ QImage generate_thumbnail_with_reader(const AfcReader &reader, frame->height, rgbFrame->data, rgbFrame->linesize); - // Convert to QImage QImage img(rgbFrame->data[0], rgbFrame->width, rgbFrame->height, rgbFrame->linesize[0], QImage::Format_RGB888); - // Create a deep copy since AVFrame will be freed - QImage imgCopy = img.copy(); + // deep copy since rgbFrame will be freed + result = img.copy(); - // Apply rotation if (rotation != 0.0) { - QTransform transform; - transform.rotate(rotation); - imgCopy = imgCopy.transformed(transform); + QTransform t; + t.rotate(rotation); + result = result.transformed(t); } - result = imgCopy; - // FIXME: scale - // Scale to requested size - /* - TODO: scaling might become optional - if we ever needed the raw frame, - might need to abstract the main logic to get the - frame and handle scaling separately - */ - // result = - // imgCopy.scaled(requestedSize, - // Qt::KeepAspectRatio, - // Qt::SmoothTransformation); + // scale + if (requested_w > 0 && requested_h > 0) { + result = result.scaled(requested_w, requested_h, + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + } } - av_frame_free(&rgbFrame); } - sws_freeContext(swsCtx); } } @@ -270,6 +236,7 @@ QImage generate_thumbnail_with_reader(const AfcReader &reader, av_packet_free(&packet); avcodec_free_context(&codecCtx); avformat_close_input(&formatCtx); + delete streamCtx; return result; } \ No newline at end of file diff --git a/src/rust/src/bridge.rs b/src/bridge.rs similarity index 95% rename from src/rust/src/bridge.rs rename to src/bridge.rs index fea5eaf..b280ac4 100644 --- a/src/rust/src/bridge.rs +++ b/src/bridge.rs @@ -79,8 +79,6 @@ pub mod bridge { include!("bridge.h"); include!("cxx-qt-lib/qimage.h"); - include!("cxx-qt-lib/qbytearray.h"); - // include!("cxx-qt-lib/qmap.h"); include!("cxx-qt-lib/qstring.h"); type QImage = cxx_qt_lib::QImage; @@ -96,6 +94,6 @@ pub mod bridge { fn heic_to_image(data: &[u8]) -> QImage; - fn qinput_get_text(ok: bool) -> QString; + // fn qinput_get_text(ok: bool) -> QString; } } diff --git a/src/cableinfowidget.cpp b/src/cableinfowidget.cpp deleted file mode 100644 index ff80a29..0000000 --- a/src/cableinfowidget.cpp +++ /dev/null @@ -1,367 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "cableinfowidget.h" -#include "appcontext.h" -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include "platform/windows/win_common.h" -#endif - -CableInfoWidget::CableInfoWidget( - const std::shared_ptr device, QWidget *parent) - : Tool(parent), m_device(device) -{ - setupUI(); - setAttribute(Qt::WA_DeleteOnClose); - resize(600, 400); - - connect(m_device->service_manager, - &CXX::ServiceManager::cable_info_retrieved, this, - [this](const QString &response) { - m_response = response; - analyzeCableInfo(); - updateUI(); - }); - - connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, - [this](const QString &udid) { - if (m_device->udid == udid) { - this->close(); - } - }); - QTimer::singleShot(200, this, &CableInfoWidget::initCableInfo); -} - -void CableInfoWidget::setupUI() -{ - setWindowTitle("Cable Information - iDescriptor"); - - QVBoxLayout *rootLayout = new QVBoxLayout(this); - rootLayout->setContentsMargins(0, 0, 0, 0); - - QWidget *contentContainer = new QWidget(this); - m_mainLayout = new QVBoxLayout(contentContainer); - m_mainLayout->setSpacing(20); - m_mainLayout->setContentsMargins(20, 20, 20, 20); - - // Header section - QHBoxLayout *headerLayout = new QHBoxLayout(); - - m_statusLabel = new QLabel("Analyzing cable..."); - m_statusLabel->setStyleSheet("QLabel { " - "font-size: 18px; " - "}"); - m_descriptionLabel = - new QLabel("Please wait while we analyze the connected cable."); - m_descriptionLabel->setStyleSheet("font-size: 9px;"); - - QPushButton *redoButton = new QPushButton("Re-analyze"); - connect(redoButton, &QPushButton::clicked, this, [this]() { - m_loadingWidget->showLoading(); - QTimer::singleShot(200, this, &CableInfoWidget::initCableInfo); - }); - headerLayout->addWidget(m_statusLabel); - headerLayout->addStretch(); - headerLayout->addWidget(redoButton); - - m_mainLayout->addLayout(headerLayout); - - m_infoWidget = new QGroupBox("Cable Information"); - - m_infoLayout = new QGridLayout(m_infoWidget); - m_infoLayout->setSpacing(12); - m_infoLayout->setColumnStretch(1, 1); - - m_mainLayout->addWidget(m_descriptionLabel); - m_mainLayout->addWidget(m_infoWidget); - m_mainLayout->addStretch(); - - m_loadingWidget = new ZLoadingWidget(true, this); - rootLayout->addWidget(m_loadingWidget); - m_loadingWidget->setupContentWidget(contentContainer); - - connect(m_loadingWidget, &ZLoadingWidget::retryClicked, this, [this]() { - m_loadingWidget->showLoading(); - QTimer::singleShot(200, this, &CableInfoWidget::initCableInfo); - }); -} - -void CableInfoWidget::initCableInfo() -{ - if (!m_device) { - m_loadingWidget->showError("Something went wrong (no device ?)"); - return; - } - - m_statusLabel->setText("Analyzing cable..."); - m_device->service_manager->get_cable_info(); -} - -void CableInfoWidget::analyzeCableInfo() -{ - // FIXME: genuine check is not perfect, still need more research - // The 'return;' statement here prevents the entire function from executing. - // It should be removed for the parsing logic to run. - // return; - - qDebug() << "Analyzing cable info..."; - - m_cableInfo = CableInfo(); - - // Original logic: `if (!m_response.isEmpty()) { ... showError ... return; - // }` This meant if the response was NOT empty, it showed an error and - // returned. The correct logic is to show an error if the response IS empty. - if (m_response.isEmpty()) { - m_loadingWidget->showError("No cable information retrieved."); - return; - } - - pugi::xml_document doc; - auto res = doc.load_string(m_response.toUtf8().constData()); - if (!res) { - m_loadingWidget->showError("Failed to parse cable information."); - return; - } - XmlPlistDict ioreg(doc.child("plist").child("dict")); - - if (!ioreg.valid()) { - m_loadingWidget->showError( - "Failed to find plist dictionary in response."); - return; - } - m_cableInfo.isOldDevice = !ioreg["ConnectionActive"].valid(); - m_cableInfo.isConnected = ioreg["ConnectionActive"].getBool(); - - // Check if genuine (Apple manufacturer and valid model info) - m_cableInfo.manufacturer = QString::fromStdString( - ioreg["IOAccessoryAccessoryManufacturer"].getString()); - m_cableInfo.modelNumber = QString::fromStdString( - ioreg["IOAccessoryAccessoryModelNumber"].getString()); - m_cableInfo.accessoryName = - QString::fromStdString(ioreg["IOAccessoryAccessoryName"].getString()); - m_cableInfo.serialNumber = QString::fromStdString( - ioreg["IOAccessoryAccessorySerialNumber"].getString()); - m_cableInfo.interfaceModuleSerial = QString::fromStdString( - ioreg["IOAccessoryInterfaceModuleSerialNumber"].getString()); - - // Check if Type-C (based on accessory name or TriStar class) - m_cableInfo.triStarClass = - QString::fromStdString(ioreg["TriStarICClass"].getString()); - m_cableInfo.isTypeC = - (m_cableInfo.accessoryName.contains("USB-C", Qt::CaseInsensitive) || - m_cableInfo.triStarClass.contains("1612")); // CBTL1612 is Type-C - - // Determine if genuine based on manufacturer and presence of detailed info - bool preGenuineCheck = - (m_cableInfo.manufacturer.contains("Apple", Qt::CaseInsensitive) && - !m_cableInfo.modelNumber.isEmpty() && - !m_cableInfo.accessoryName.isEmpty()); - - // Further checks for Type-C cables - // if report says it's Type-C, it must match the actual connection type - if (m_cableInfo.isTypeC) { - bool actuallyTypeC = - m_device->deviceInfo.batteryInfo.usbConnectionType == - BatteryInfo::ConnectionType::USB_TYPEC; - if (!actuallyTypeC) { - // most likely a fake cable with faked info - m_cableInfo.isFakeInfo = true; - } - m_cableInfo.isGenuine = actuallyTypeC && preGenuineCheck; - } else { - m_cableInfo.isGenuine = preGenuineCheck; - } - - // Power information - m_cableInfo.currentLimit = ioreg["IOAccessoryUSBCurrentLimit"].getUInt(); - m_cableInfo.chargingVoltage = - ioreg["IOAccessoryUSBChargingVoltage"].getUInt(); - - // Connection type - QString connectString = QString::fromStdString( - ioreg["IOAccessoryUSBConnectString"].getString()); - int connectType = - static_cast(ioreg["IOAccessoryUSBConnectType"].getUInt()); - m_cableInfo.connectionType = - QString("%1 (Type %2)").arg(connectString).arg(connectType); - - // Supported and active transports - // In XML plists, an array is typically represented by an tag, - // and its elements by , , etc. tags. - - // Handle "TransportsSupported" - XmlPlistDict supportedTransportsWrapper = ioreg["TransportsSupported"]; - // Assuming XmlPlistDict has a 'node()' method to access its underlying - // pugi::xml_node - pugi::xml_node supportedTransportsNode = - supportedTransportsWrapper.getNode(); - - if (supportedTransportsNode && - supportedTransportsNode.name() == - "array") { // Check if it's a valid node and if its tag name is - // "array" - for (pugi::xml_node transportChild : - supportedTransportsNode.children()) { - if (transportChild.name() == - "string") { // Assume each item in the array is a node - m_cableInfo.supportedTransports.append( - QString::fromStdString(transportChild.text().as_string())); - } - } - } - - // Handle "TransportsActive" similarly - XmlPlistDict activeTransportsWrapper = ioreg["TransportsActive"]; - pugi::xml_node activeTransportsNode = activeTransportsWrapper.getNode(); - - if (activeTransportsNode && activeTransportsNode.name() == "array") { - for (pugi::xml_node transportChild : activeTransportsNode.children()) { - if (transportChild.name() == "string") { - m_cableInfo.activeTransports.append( - QString::fromStdString(transportChild.text().as_string())); - } - } - } -} - -void CableInfoWidget::updateUI() -{ - // Clear existing info - QLayoutItem *item; - while ((item = m_infoLayout->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } - - // old devices don't report ConnectionActive - if (!m_cableInfo.isConnected && !m_cableInfo.isOldDevice) { - m_loadingWidget->showError( - QString("%1 does not seem to be connected to any cable.") - .arg(QString::fromStdString(m_device->deviceInfo.productType))); - return; - } - - // Update status and icon based on cable type - QString statusText; - QString statusStyle; - - m_descriptionLabel->setText("Please note that this check may not be " - "absolute guarantee of authenticity."); - if (m_cableInfo.isGenuine) { - // todo: type-c to type-c - statusText = QString("Genuine %1") - .arg(m_cableInfo.isTypeC ? "USB-C to Lightning Cable" - : "Lightning Cable"); - statusStyle = - "QLabel { color: #28a745; font-size: 18px; font-weight: bold; }"; - } else { - statusText = "Third-party Cable"; - statusStyle = - "QLabel { color: #dc3545; font-size: 18px; font-weight: bold; }"; - - if (m_cableInfo.isFakeInfo) { - m_descriptionLabel->setText("The cable reports false information. " - "It is most likely a fake cable."); - } - } - - m_statusLabel->setText(statusText); - m_statusLabel->setStyleSheet(statusStyle); - - int row = 0; - - // Basic information - if (!m_cableInfo.accessoryName.isEmpty()) { - createInfoRow(m_infoLayout, row++, "Name:", m_cableInfo.accessoryName); - } - - if (!m_cableInfo.manufacturer.isEmpty()) { - createInfoRow(m_infoLayout, row++, - "Manufacturer:", m_cableInfo.manufacturer); - } - - if (!m_cableInfo.modelNumber.isEmpty()) { - createInfoRow(m_infoLayout, row++, "Model:", m_cableInfo.modelNumber); - } - - if (!m_cableInfo.serialNumber.isEmpty()) { - createInfoRow(m_infoLayout, row++, - "Serial Number:", m_cableInfo.serialNumber); - } - - if (!m_cableInfo.interfaceModuleSerial.isEmpty()) { - createInfoRow(m_infoLayout, row++, - "Interface Module:", m_cableInfo.interfaceModuleSerial); - } - - // Technical information - createInfoRow(m_infoLayout, row++, "Cable Type:", - m_cableInfo.isTypeC ? "USB-C to Lightning" - : "Lightning to USB-A"); - - if (m_cableInfo.currentLimit > 0) { - createInfoRow(m_infoLayout, row++, "Current Limit:", - QString("%1 mA").arg(m_cableInfo.currentLimit)); - } - - if (m_cableInfo.chargingVoltage > 0) { - createInfoRow(m_infoLayout, row++, "Charging Voltage:", - QString("%1 mV").arg(m_cableInfo.chargingVoltage)); - } - - if (!m_cableInfo.connectionType.isEmpty()) { - createInfoRow(m_infoLayout, row++, - "Connection:", m_cableInfo.connectionType); - } - - if (!m_cableInfo.triStarClass.isEmpty()) { - createInfoRow(m_infoLayout, row++, - "Controller:", m_cableInfo.triStarClass); - } - - // Transport information - if (!m_cableInfo.activeTransports.isEmpty()) { - createInfoRow(m_infoLayout, row++, "Active Transports:", - m_cableInfo.activeTransports.join(", ")); - } - - if (!m_cableInfo.supportedTransports.isEmpty()) { - createInfoRow(m_infoLayout, row++, "Supported Transports:", - m_cableInfo.supportedTransports.join(", ")); - } - m_loadingWidget->stop(true); -} - -void CableInfoWidget::createInfoRow(QGridLayout *layout, int row, - const QString &label, const QString &value) -{ - QLabel *labelWidget = new QLabel(label); - - QLabel *valueWidget = new QLabel(value); - valueWidget->setWordWrap(true); - - layout->addWidget(labelWidget, row, 0, Qt::AlignTop); - layout->addWidget(valueWidget, row, 1, Qt::AlignTop); -} \ No newline at end of file diff --git a/src/cableinfowidget.h b/src/cableinfowidget.h deleted file mode 100644 index 73441b7..0000000 --- a/src/cableinfowidget.h +++ /dev/null @@ -1,88 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef CABLEINFOWIDGET_H -#define CABLEINFOWIDGET_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include - -class CableInfoWidget : public Tool -{ - Q_OBJECT - -public: - explicit CableInfoWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - -private slots: - void initCableInfo(); - -private: - void setupUI(); - void analyzeCableInfo(); - void updateUI(); - void createInfoRow(QGridLayout *layout, int row, const QString &label, - const QString &value); - - // Cable information structure - struct CableInfo { - bool isConnected = false; - bool isGenuine = false; - bool isTypeC = false; - QString manufacturer; - QString modelNumber; - QString accessoryName; - QString serialNumber; - QString interfaceModuleSerial; - uint64_t currentLimit = 0; - uint64_t chargingVoltage = 0; - QString connectionType; - QString triStarClass; - QStringList supportedTransports; - QStringList activeTransports; - bool isFakeInfo = false; - bool isOldDevice = - false; // devices that don't report ConnectionActive are likely old - }; - - // UI components - QVBoxLayout *m_mainLayout; - QLabel *m_statusLabel; - QLabel *m_descriptionLabel; - QGroupBox *m_infoWidget; - QGridLayout *m_infoLayout; - ZLoadingWidget *m_loadingWidget; - - // Data - const std::shared_ptr m_device; - CableInfo m_cableInfo; - QString m_response; -}; - -#endif // CABLEINFOWIDGET_H \ No newline at end of file diff --git a/src/rust/src/lib.rs b/src/core.rs similarity index 78% rename from src/rust/src/lib.rs rename to src/core.rs index 8c99a06..c367c2f 100644 --- a/src/rust/src/lib.rs +++ b/src/core.rs @@ -1,4 +1,5 @@ use futures::StreamExt; +use idevice::afc::opcode::AfcFopenMode; use idevice::{ IdeviceError, IdeviceService, afc::AfcClient, @@ -9,7 +10,8 @@ use idevice::{ provider::TcpProvider, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection, UsbmuxdListenEvent}, }; -use idevice::afc::opcode::AfcFopenMode; +use qmetaobject::{qt_base_class, qt_method}; +use qttypes::QVariantMap; use std::{any::type_name, sync::Arc}; use std::{collections::HashMap, net::IpAddr}; @@ -23,129 +25,51 @@ use tokio::sync::oneshot; use tokio::task::JoinHandle; use core::pin::Pin; -use cxx_qt::Threading; -use cxx_qt_lib::{QMap, QMapPair_QString_QVariant, QString, QVariant}; -use crate::qobject::Core; use once_cell::sync::Lazy; use plist::{Dictionary, Value}; -mod afc; -mod afc2_services; -mod afc_services; -mod hause_arrest; -mod io_manager; -mod screenshot; -mod service_manager; -mod utils; -mod query_sqlite; -mod image_loader; -mod bridge; -mod apps; +use qmetaobject::prelude::*; +use crate::{ + APP_DEVICE_STATE, APP_LABEL, DeviceServices, EV_CONNECTED, EV_DISCONNECTED, EV_FAIL, + EV_PAIRING_PENDING, POSSIBLE_ROOT, RUNTIME, + qt_threading::{QtThread, QtThreading}, + utils, +}; -const POSSIBLE_ROOT: &str = "../../../../"; -const APP_LABEL: &str = "iDescriptor"; -const EV_CONNECTED: u32 = 1; -const EV_DISCONNECTED: u32 = 2; -const EV_PAIRING_PENDING: u32 = 3; -const EV_FAIL: u32 = 4; +#[derive(Default, QObject)] +pub struct Core { + base: qt_base_class!(trait QObject), -#[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>, + init: qt_method!(fn(&mut self)), + + 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)), + // 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), + no_pairing_file: qt_signal!(mac_address : QString), + sleepy_time_detected: qt_signal!(), + device_became_wired: qt_signal!(udid: QString), + // listen : fn (&mut self), } -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() - .build() - .unwrap() -}); - -pub fn run_sync(fut: F) -> R -where - F: Future + Send + 'static, - R: Send + 'static, -{ - let (tx, rx) = mpsc::sync_channel(1); - - RUNTIME.spawn(async move { - let res = fut.await; - let _ = tx.send(res); - }); - - rx.recv().expect("Tokio runtime worker panicked") -} - -#[cxx_qt::bridge] -mod qobject { - unsafe extern "C++" { - include!("cxx-qt-lib/qlist.h"); - include!("cxx-qt-lib/qmap.h"); - include!("cxx-qt-lib/qstring.h"); - - type QString = cxx_qt_lib::QString; - type QMap_QString_QVariant = cxx_qt_lib::QMap; +impl QtThreading for Core { + fn qt_thread(&self) -> crate::qt_threading::QtThread + where + Self: Sized, + { + QtThread::new(self) } - - extern "RustQt" { - #[qobject] - #[qml_element] - type Core = super::RCore; - - #[qinvokable] - fn init(self: Pin<&mut Core>); - - #[qinvokable] - fn init_wireless_device( - self: Pin<&mut Core>, - ip: &QString, - pairing_file: &QString, - mac_address: &QString, - ); - - #[qinvokable] - fn get_pairing_files(self: Pin<&mut Core>) -> QMap_QString_QVariant; - - #[qinvokable] - fn remove_device(self: Pin<&mut Core>, udid: &QString); - - #[qsignal] - fn device_event(self: Pin<&mut Core>, event_type: u32, udid: &QString, info: &QMap_QString_QVariant); - - #[qsignal] - fn init_failed(self: Pin<&mut Core>, mac_address: &QString); - - #[qsignal] - fn no_pairing_file(self: Pin<&mut Core>, mac_address: &QString); - - #[qsignal] - fn sleepy_time_detected(self: Pin<&mut Core>); - - #[qsignal] - fn device_became_wired(self: Pin<&mut Core>, udid: &QString); - } - impl cxx_qt::Threading for Core {} } -#[derive(Default)] -pub struct RCore; - -impl qobject::Core { - fn init(self: Pin<&mut Self>) { +impl Core { + fn init(&mut self) { self.listen(); } - fn listen(self: Pin<&mut Self>) { + fn listen(&mut self) { let qt_t = self.qt_thread(); thread::spawn(move || { @@ -213,11 +137,10 @@ impl qobject::Core { .queue(move |core_qobj| { core_qobj.device_event( EV_DISCONNECTED, - &QString::from(udid), - &QMap::::default(), + QString::from(udid), + QVariantMap::default(), ); - }) - .ok(); + }); } } Err(e) => { @@ -238,17 +161,12 @@ impl qobject::Core { }); } - fn init_wireless_device( - self: Pin<&mut Self>, - ip: &QString, - pairing_file: &QString, - mac_address: &QString, - ) { + fn init_wireless_device(&mut self, ip: QString, pairing_file: QString, mac_address: QString) { eprintln!( "init_wireless_device: MAC: {} IP: {} PairingFile: {}", mac_address, ip, pairing_file ); - let qt_t = self.qt_thread(); + let qt_thread = self.qt_thread(); let ip_owned = ip.to_string(); let pairing_path = pairing_file.to_string(); let mac_address_owned = mac_address.to_string(); @@ -256,12 +174,10 @@ impl qobject::Core { let pairing_file = match PairingFile::read_from_file(&pairing_path) { Ok(pf) => pf, Err(e) => { - let qt_thread = qt_t.clone(); qt_thread .queue(move |core_qobj| { - core_qobj.no_pairing_file(&QString::from(mac_address_owned)); - }) - .ok(); + core_qobj.no_pairing_file(QString::from(mac_address_owned)); + }); eprintln!("Failed to read pairing file: {e}"); return; } @@ -284,8 +200,9 @@ impl qobject::Core { }; + let qt_t_clone = qt_thread.clone(); let result = tokio::select! { - res = init_idescriptor_device(t, qt_t.clone()) => res, + res = init_idescriptor_device(t, qt_t_clone) => res, /* timeout */ _ = tokio::time::sleep(tokio::time::Duration::from_secs(20)) => { eprintln!("Timeout collecting device info for wireless device mac address: {mac_address_owned}"); @@ -296,33 +213,28 @@ impl qobject::Core { match result { Ok((udid, info)) => { // emit event with info - let qt_thread = qt_t.clone(); - qt_thread .queue(move |core_qobj| { core_qobj.device_event( EV_CONNECTED, - &QString::from(udid), - &info, + QString::from(udid), + info, ); - }) - .ok(); + }); } Err(e) => { eprintln!("Failed to initialize wireless device mac address: {mac_address_owned} {e:?}"); - let qt_thread = qt_t.clone(); qt_thread .queue(move |core_qobj| { - core_qobj.init_failed(&QString::from(mac_address_owned)); - }) - .ok(); + core_qobj.init_failed(QString::from(mac_address_owned)); + }); } } }); } - fn get_pairing_files(self: Pin<&mut Self>) -> QMap { - let mut map = QMap::::default(); + fn get_pairing_files(&mut self) -> QVariantMap { + let mut map = QVariantMap::default(); #[cfg(not(target_os = "macos"))] { @@ -423,20 +335,15 @@ fn is_pairing_related_error(e: &IdeviceError) -> bool { ) } -async fn handle_pairing( - qt_thread: cxx_qt::CxxQtThread, - udid: String, -) -> Result<(), IdeviceError> { +async fn handle_pairing(qt_thread: QtThread, udid: String) -> Result<(), IdeviceError> { let udid_for_event = udid.clone(); - qt_thread - .queue(move |core_qobj| { - core_qobj.device_event( - EV_PAIRING_PENDING, - &QString::from(udid_for_event), - &QMap::::default(), - ); - }) - .ok(); + qt_thread.queue(move |core_qobj| { + core_qobj.device_event( + EV_PAIRING_PENDING, + QString::from(udid_for_event), + QVariantMap::default(), + ); + }); let mut uc2 = UsbmuxdConnection::default().await?; @@ -495,19 +402,17 @@ async fn handle_pairing( } fn emit_pairing_failed( - qt_thread: cxx_qt::CxxQtThread, + qt_thread: QtThread, udid: String, // FIXME: we may want to use this in the future _reason: &str, ) { - qt_thread - .queue(move |core_qobj| { - core_qobj.device_event(EV_FAIL, &QString::from(udid), &QMap::::default()); - }) - .ok(); + qt_thread.queue(move |core_qobj| { + core_qobj.device_event(EV_FAIL, QString::from(udid), QVariantMap::default()); + }); } -async fn emit_connected(qt_thread: cxx_qt::CxxQtThread, udid: String) { +async fn emit_connected(qt_thread: QtThread, udid: String) { // one init retry after successful pairing let mut retried_after_pair = false; @@ -524,17 +429,19 @@ async fn emit_connected(qt_thread: cxx_qt::CxxQtThread, udid: String) { let provider = dev.to_provider(UsbmuxdAddr::default(), APP_LABEL); - match init_idescriptor_device(provider, qt_thread.clone()).await { + let qt_t_clone = qt_thread.clone(); + + match init_idescriptor_device(provider, qt_t_clone).await { Ok((udid_for_event, info_for_event)) => { - qt_thread - .queue(move |core_qobj| { - core_qobj.device_event( - EV_CONNECTED, - &QString::from(udid_for_event), - &info_for_event, - ); - }) - .ok(); + println!("Emitting connected"); + + qt_thread.queue(move |core_qobj| { + core_qobj.device_event( + EV_CONNECTED, + QString::from(udid_for_event), + info_for_event, + ); + }); return; } Err(e) if is_pairing_related_error(&e) && !retried_after_pair => { @@ -573,8 +480,8 @@ async fn init_idescriptor_device< T: idevice::provider::IdeviceProvider + Send + Sync + Clone + 'static, >( provider: T, - qt_thread: cxx_qt::CxxQtThread, -) -> Result<(String, QMap), IdeviceError> { + qt_thread: QtThread, +) -> Result<(String, QVariantMap), IdeviceError> { let provider_name = type_name::(); let is_wireless = provider_name == "idevice::provider::TcpProvider"; @@ -651,7 +558,6 @@ async fn init_idescriptor_device< ) .await?; - eprintln!("init_idescriptor_device: Storing device services."); let device_services = DeviceServices { afc: Arc::new(Mutex::new(afc_client)), @@ -671,11 +577,9 @@ async fn init_idescriptor_device< task.abort(); } let udid_for_signal = udid.clone(); - qt_thread - .queue(move |core_qobj| { - core_qobj.device_became_wired(&QString::from(udid_for_signal)); - }) - .ok(); + qt_thread.queue(move |core_qobj| { + core_qobj.device_became_wired(QString::from(udid_for_signal)); + }); } } @@ -683,7 +587,7 @@ async fn init_idescriptor_device< match hb { Some(hb_client) => { eprintln!("init_idescriptor_device: Spawning heartbeat task."); - match spawn_heartbeat_task(hb_client, qt_thread.clone(), udid.clone()).await { + match spawn_heartbeat_task(hb_client, qt_thread, udid.clone()).await { Ok(task) => { let mut state = APP_DEVICE_STATE.lock().await; if let Some(svc) = state.get_mut(&udid) { @@ -716,8 +620,8 @@ async fn collect_info( mut lc: &mut LockdownClient, mut diag_relay: &mut DiagnosticsRelayClient, is_wireless: bool, -) -> Result, IdeviceError> { - let mut info = QMap::::default(); +) -> Result { + let mut info = QVariantMap::default(); eprintln!("init_idescriptor_device: Attempting to get default values from Lockdown."); @@ -808,7 +712,7 @@ async fn collect_info( } async fn spawn_heartbeat_task( mut hb_client: heartbeat::HeartbeatClient, - qt_thread: cxx_qt::CxxQtThread, + qt_thread: QtThread, udid: String, ) -> Result>, ()> { let udid_for_hb = udid.clone(); @@ -831,11 +735,9 @@ async fn spawn_heartbeat_task( match e { IdeviceError::Heartbeat(idevice::HeartbeatError::SleepyTime) => { println!("heartbeat: Sleepy time"); - qt_thread - .queue(move |core_qobj| { - core_qobj.sleepy_time_detected(); - }) - .ok(); + qt_thread.queue(move |core_qobj| { + core_qobj.sleepy_time_detected(); + }); } _ => {} }; @@ -848,8 +750,8 @@ async fn spawn_heartbeat_task( let _ = qt_thread.queue(move |core_qobj| { core_qobj.device_event( EV_DISCONNECTED, - &QString::from(udid_for_event), - &QMap::::default(), + QString::from(udid_for_event), + QVariantMap::default(), ); }); break; @@ -865,11 +767,9 @@ async fn spawn_heartbeat_task( match e { IdeviceError::Heartbeat(idevice::HeartbeatError::SleepyTime) => { println!("heartbeat: Sleepy time"); - qt_thread - .queue(move |core_qobj| { - core_qobj.sleepy_time_detected(); - }) - .ok(); + qt_thread.queue(move |core_qobj| { + core_qobj.sleepy_time_detected(); + }); } _ => {} }; @@ -881,8 +781,8 @@ async fn spawn_heartbeat_task( let _ = qt_thread.queue(move |core_qobj| { core_qobj.device_event( EV_DISCONNECTED, - &QString::from(udid_for_event), - &QMap::::default(), + QString::from(udid_for_event), + QVariantMap::default(), ); }); break; diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp deleted file mode 100644 index 6cb52af..0000000 --- a/src/core/services/init_device.cpp +++ /dev/null @@ -1,438 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "../../appcontext.h" -#include "../../devicedatabase.h" -#include "../../iDescriptor.h" -#include - -#ifdef _WIN32 -#include "../../platform/windows/win_common.h" -#else -#include -#include -#include -#include -#endif - -#include -#include -#include - -std::string safeGetXML(const char *key, pugi::xml_node dict) -{ - for (pugi::xml_node child = dict.first_child(); child; - child = child.next_sibling()) { - if (strcmp(child.name(), "key") == 0 && - strcmp(child.text().as_string(), key) == 0) { - pugi::xml_node value = child.next_sibling(); - if (value) { - // Handle different XML element types - if (strcmp(value.name(), "true") == 0) { - return "true"; - } else if (strcmp(value.name(), "false") == 0) { - return "false"; - } else if (strcmp(value.name(), "integer") == 0) { - return value.text().as_string(); - } else if (strcmp(value.name(), "string") == 0) { - return value.text().as_string(); - } else if (strcmp(value.name(), "real") == 0) { - return value.text().as_string(); - } else { - // For any other type, try to get the text content - return value.text().as_string(); - } - } - } - } - return ""; -} - -void parseOldDeviceBattery(XmlPlistDict &ioreg, DeviceInfo &d) -{ - d.batteryInfo.isCharging = ioreg["IsCharging"].getBool(); - - d.batteryInfo.fullyCharged = ioreg["FullyCharged"].getBool(); - - uint64_t appleRawCurrentCapacity = - ioreg["AppleRawCurrentCapacity"].getUInt(); - uint64_t appleRawMaxCapacity = ioreg["AppleRawMaxCapacity"].getUInt(); - - uint64_t oldCurrrentBatteryLevel = - (appleRawCurrentCapacity && appleRawMaxCapacity) - ? (appleRawCurrentCapacity * 100 / appleRawMaxCapacity) - : 0; - - d.batteryInfo.currentBatteryLevel = oldCurrrentBatteryLevel; - - // adaptor details - d.batteryInfo.usbConnectionType = - ioreg["AdapterDetails"]["Description"].getString() == "usb type-c" - ? BatteryInfo::ConnectionType::USB_TYPEC - : BatteryInfo::ConnectionType::USB; - d.batteryInfo.adapterVoltage = 0; - - // watt - d.batteryInfo.watts = ioreg["AdapterDetails"]["Watts"].getUInt(); -} - -void parseOldDevice(XmlPlistDict &ioreg, DeviceInfo &d) -{ - uint64_t cycleCount = ioreg["CycleCount"].getUInt(); - - // skipping on very old devices for now - std::string batterySerialNumber = ""; - uint64_t designCapacity = ioreg["DesignCapacity"].getUInt(); - - uint64_t maxCapacity = ioreg["MaxCapacity"].getUInt(); - - qDebug() << "Design capacity: " << designCapacity; - qDebug() << "Max capacity: " << maxCapacity; - - // Compat - int healthPercent = - (designCapacity != 0) ? (maxCapacity * 100) / designCapacity : 0; - healthPercent = std::min(healthPercent, 100); - d.batteryInfo.health = QString::number(qBound(0, healthPercent, 100)) + "%"; - d.batteryInfo.cycleCount = cycleCount; - d.batteryInfo.serialNumber = !batterySerialNumber.empty() - ? batterySerialNumber - : "Error retrieving serial number"; - - parseOldDeviceBattery(ioreg, d); -} - -void parseDeviceBattery(XmlPlistDict &ioreg, DeviceInfo &d) -{ - d.batteryInfo.isCharging = ioreg["IsCharging"].getBool(); - - d.batteryInfo.fullyCharged = ioreg["FullyCharged"].getBool(); - - /* data is sometimes not accurate here so we need to calculate */ - // d.batteryInfo.currentBatteryLevel = - // ioreg["BatteryData"]["StateOfCharge"].getUInt(); - - uint64_t appleRawCurrentCapacity = - ioreg["AppleRawCurrentCapacity"].getUInt(); - uint64_t appleRawMaxCapacity = ioreg["AppleRawMaxCapacity"].getUInt(); - - uint64_t currentBatteryLevel = - (appleRawCurrentCapacity && appleRawMaxCapacity) - ? (appleRawCurrentCapacity * 100 / appleRawMaxCapacity) - : 0; - - d.batteryInfo.currentBatteryLevel = currentBatteryLevel; - - d.batteryInfo.usbConnectionType = - ioreg["AdapterDetails"]["Description"].getString() == "usb type-c" - ? BatteryInfo::ConnectionType::USB_TYPEC - : BatteryInfo::ConnectionType::USB; - - // adaptor details - d.batteryInfo.adapterVoltage = - ioreg["AppleRawAdapterDetails"][0]["AdapterVoltage"].getUInt(); - - d.batteryInfo.watts = ioreg["AppleRawAdapterDetails"][0]["Watts"].getUInt(); -} - -void fullDeviceInfo(const pugi::xml_document &doc, DeviceInfo &d) -{ - pugi::xml_node dict = doc.child("plist").child("dict"); - auto safeGet = [&](const char *key) -> std::string { - for (pugi::xml_node child = dict.first_child(); child; - child = child.next_sibling()) { - if (strcmp(child.name(), "key") == 0 && - strcmp(child.text().as_string(), key) == 0) { - pugi::xml_node value = child.next_sibling(); - if (value) - return value.text().as_string(); - } - } - return ""; - }; - - auto safeGetBool = [&](const char *key) -> bool { - for (pugi::xml_node child = dict.first_child(); child; - child = child.next_sibling()) { - if (strcmp(child.name(), "key") == 0 && - strcmp(child.text().as_string(), key) == 0) { - pugi::xml_node value = child.next_sibling(); - if (value && strcmp(value.name(), "true") == 0) - return true; - else - return false; - } - } - return false; - }; - - d.deviceName = safeGet("DeviceName"); - d.deviceClass = safeGet("DeviceClass"); - d.deviceColor = safeGet("DeviceColor"); - d.modelNumber = safeGet("ModelNumber"); - d.cpuArchitecture = safeGet("CPUArchitecture"); - d.buildVersion = safeGet("BuildVersion"); - d.hardwareModel = safeGet("HardwareModel"); - d.hardwarePlatform = safeGet("HardwarePlatform"); - d.ethernetAddress = safeGet("EthernetAddress"); - d.bluetoothAddress = safeGet("BluetoothAddress"); - d.firmwareVersion = safeGet("FirmwareVersion"); - d.productVersion = safeGet("ProductVersion"); - d.wifiMacAddress = safeGet("WiFiAddress"); - d.UniqueDeviceID = safeGet("UniqueDeviceID"); - - QString q_version = QString::fromStdString(d.productVersion); - QStringList parts = q_version.split('.'); - - unsigned int major = (parts.length() > 0) ? parts[0].toInt() : 0; - unsigned int minor = (parts.length() > 1) ? parts[1].toInt() : 0; - unsigned int patch = (parts.length() > 2) ? parts[2].toInt() : 0; - - d.parsedDeviceVersion = - DeviceVersion{.major = major, .minor = minor, .patch = patch}; - - /*DiskInfo*/ - try { - auto safeParseU64 = [&](const char *key) -> uint64_t { - std::string s = safeGet(key); - if (s.empty()) - return 0; - try { - return std::stoull(s); - } catch (...) { - qDebug() << "Failed to parse key to uint64_t:" << key - << "value:" << QString::fromStdString(s); - return 0; - } - }; - d.diskInfo.totalDiskCapacity = safeParseU64("TotalDiskCapacity"); - d.diskInfo.totalDataCapacity = safeParseU64("TotalDataCapacity"); - d.diskInfo.totalSystemCapacity = safeParseU64("TotalSystemCapacity"); - /* - For some reason this is way inaccrutate for iOS 17 and up - */ - d.diskInfo.totalDataAvailable = safeParseU64("TotalDataAvailable"); - - try { - /* - Example : this data seems to be the most accurate - */ - //"Model: iPhone12,8" - // "FSTotalBytes: 63966400512" - // "FSFreeBytes: 2867101696" - // "FSBlockSize: 4096" - XmlPlistDict root(dict); - pugi::xml_node afc_info_node = root["AFC_INFO"].getNode(); - if (afc_info_node) { - // FIXME: could be handled better - auto safeGetAfcString = [&](const char *key) -> std::string { - for (pugi::xml_node child = afc_info_node.first_child(); - child; child = child.next_sibling()) { - if (strcmp(child.name(), "key") == 0 && - strcmp(child.text().as_string(), key) == 0) { - pugi::xml_node value = child.next_sibling(); - if (value) { - return value.text().as_string(); - } - } - } - return ""; - }; - - // Lambda to parse uint64_t values from the afc_info_node - auto safeParseAfcU64 = [&](const char *key) -> uint64_t { - std::string s = safeGetAfcString(key); - if (s.empty()) - return 0; - try { - return std::stoull(s); - } catch (...) { - qDebug() - << "Failed to parse AFC_INFO key to uint64_t:" - << key << "value:" << QString::fromStdString(s); - return 0; - } - }; - - d.diskInfo.totalDataAvailable = safeParseAfcU64("FreeBytes"); - } - } catch (const std::exception &e) { - qDebug() << "Error parsing disk info: " << e.what(); - } - } catch (const std::exception &e) { - qDebug() << e.what(); - /*It's ok if any of those fails*/ - } - - std::string _activationState = safeGet("ActivationState"); - - /* older devices dont have fusing status lets default to ProductionSOC - for - * now*/ - // std::string fStatus = safeGet("FusingStatus"); - // d.productionDevice = std::stoi(fStatus.empty() ? "0" : fStatus) == 3; - - d.productionDevice = safeGetBool("ProductionSOC"); - if (_activationState == "Activated") { - d.activationState = DeviceInfo::ActivationState::Activated; - // IOS 6 - } else if (_activationState == "WildcardActivated") { - d.activationState = - DeviceInfo::ActivationState::Activated; // Treat as activated - } else if (_activationState == "FactoryActivated") { - d.activationState = DeviceInfo::ActivationState::FactoryActivated; - } else if (_activationState == "Unactivated") { - d.activationState = DeviceInfo::ActivationState::Unactivated; - } else { - d.activationState = - DeviceInfo::ActivationState::Unactivated; // Default value - } - std::string regionInfo = safeGet("RegionInfo"); - d.regionRaw = regionInfo; - d.region = DeviceDatabase::parseRegionInfo(regionInfo); - std::string rawProductType = safeGet("ProductType"); - const DeviceDatabaseInfo *info = - DeviceDatabase::findByIdentifier(rawProductType); - d.productType = - info ? info->displayName ? info->displayName : info->marketingName - : "Unknown Device"; - d.marketingName = info ? info->marketingName : "Unknown Device"; - d.rawProductType = rawProductType; - d.jailbroken = safeGetBool("Jailbroken"); - d.is_iPhone = safeGet("DeviceClass") == "iPhone"; - d.serialNumber = safeGet("SerialNumber"); - d.mobileEquipmentIdentifier = safeGet("MobileEquipmentIdentifier"); - d.isWireless = safeGet("ConnectionType") == "Wireless"; - - /*BatteryInfo*/ - XmlPlistDict ioreg = - XmlPlistDict(doc.child("plist").child("dict"))["DIAG_INFO"]; - - if (!ioreg.valid()) { - qDebug() << "Failed to get diagnostics plist."; - return; - } - - try { - // old devices do not have "BatteryData" - d.oldDevice = ioreg["BatteryData"].valid() ? false : true; - if (d.oldDevice) { - parseOldDevice(ioreg, d); - return; - } - - bool newerThaniPhone8 = - iDescriptor::Utils::isProductTypeNewer(rawProductType, "iPhone8,1"); - - uint64_t cycleCount = ioreg["BatteryData"]["CycleCount"].getUInt(); - - // Battery serial number - std::string batterySerialNumber = - ioreg["BatteryData"]["BatterySerialNumber"].getString(); - - uint64_t designCapacity = - ioreg["BatteryData"]["DesignCapacity"].getUInt(); - - uint64_t maxCapacity = - d.is_iPhone ? newerThaniPhone8 - ? ioreg["AppleRawMaxCapacity"].getUInt() - : ioreg["BatteryData"]["MaxCapacity"].getUInt() - : ioreg["BatteryData"]["MaxCapacity"].getUInt(); - - qDebug() << "Design capacity: " << designCapacity; - qDebug() << "Max capacity: " << maxCapacity; - - // seems to be to the most accurate way to get health - d.batteryInfo.health = - QString::number( - qBound(0, (maxCapacity * 100) / designCapacity, 100)) + - "%"; - d.batteryInfo.cycleCount = cycleCount; - d.batteryInfo.serialNumber = !batterySerialNumber.empty() - ? batterySerialNumber - : "Error retrieving serial number"; - qDebug() << "Cycle count: " << cycleCount; - parseDeviceBattery(ioreg, d); - - return; - } catch (const std::exception &e) { - qDebug() << "Error occurred: " << e.what(); - return; - } -} - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT -void init_idescriptor_recovery_device( - uint64_t ecid, iDescriptorInitDeviceResultRecovery &result) -{ - qDebug() << "Initializing iDescriptor recovery device with ECID: " << ecid; - result = {}; - - irecv_client_t client = nullptr; - const irecv_device_info *deviceInfo = nullptr; - irecv_device_t device = nullptr; - const DeviceDatabaseInfo *info = nullptr; - - irecv_error_t ret = irecv_open_with_ecid_and_attempts( - &client, ecid, RECOVERY_CLIENT_CONNECTION_TRIES); - - if (ret != IRECV_E_SUCCESS) { - qDebug() << "Failed to open recovery client with ECID:" << ecid - << "Error:" << ret; - result.error = ret; - goto cleanup; - } - - ret = irecv_get_mode(client, (int *)&result.mode); - if (ret != IRECV_E_SUCCESS) { - qDebug() << "Failed to get recovery mode. Error:" << ret; - result.error = ret; - goto cleanup; - } - - deviceInfo = irecv_get_device_info(client); - if (!deviceInfo) { - qDebug() << "Failed to get device info from recovery client"; - result.error = IRECV_E_UNKNOWN_ERROR; - goto cleanup; - } - - if (irecv_devices_get_device_by_client(client, &device) == - IRECV_E_SUCCESS && - device && device->hardware_model) { - qDebug() << "Recovery device hardware_model: " - << device->hardware_model; - info = - DeviceDatabase::findByHwModel(std::string(device->hardware_model)); - } else { - qDebug() << "Could not resolve hardware_model from client."; - } - - result.displayName = - info ? (info->displayName ? info->displayName : info->marketingName) - : "Unknown Device"; - result.deviceInfo = *deviceInfo; - result.success = true; - -cleanup: - if (client) { - irecv_close(client); - } -} -#endif // ENABLE_RECOVERY_DEVICE_SUPPORT diff --git a/src/core/services/load_heic.cpp b/src/core/services/load_heic.cpp deleted file mode 100644 index 8561bf4..0000000 --- a/src/core/services/load_heic.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "../../iDescriptor.h" -#include -#include -#include -#include -#include - -QImage load_heic(const QByteArray &imageData) -{ - heif_context *ctx = heif_context_alloc(); - if (!ctx) { - qWarning() << "Failed to allocate heif_context"; - return QImage(); - } - - heif_error err = heif_context_read_from_memory(ctx, imageData.constData(), - imageData.size(), nullptr); - if (err.code != heif_error_Ok) { - qWarning() << "Failed to read HEIC from memory:" << err.message; - heif_context_free(ctx); - return QImage(); - } - - heif_image_handle *handle; - err = heif_context_get_primary_image_handle(ctx, &handle); - if (err.code != heif_error_Ok) { - qWarning() << "Failed to get primary image handle:" << err.message; - heif_context_free(ctx); - return QImage(); - } - - heif_image *img; - err = heif_decode_image(handle, &img, heif_colorspace_RGB, - heif_chroma_interleaved_RGB, nullptr); - if (err.code != heif_error_Ok) { - qWarning() << "Failed to decode HEIC image:" << err.message; - heif_image_handle_release(handle); - heif_context_free(ctx); - return QImage(); - } - - int width = heif_image_get_width(img, heif_channel_interleaved); - int height = heif_image_get_height(img, heif_channel_interleaved); - int stride; - /* - FIXME: use heif_image_get_plane_readonly2 in future, on ubuntu 24 it's not - available yet - */ - const uint8_t *data = - heif_image_get_plane_readonly(img, heif_channel_interleaved, &stride); - - if (!data) { - qWarning() << "Failed to get image plane data"; - heif_image_release(img); - heif_image_handle_release(handle); - heif_context_free(ctx); - return QImage(); - } - - QImage qimg(data, width, height, stride, QImage::Format_RGB888); - QImage copy = - qimg.copy(); // Deep copy since the original data will be freed - heif_image_release(img); - heif_image_handle_release(handle); - heif_context_free(ctx); - - return copy; -} diff --git a/src/creddialog.cpp b/src/creddialog.cpp deleted file mode 100644 index 89d2475..0000000 --- a/src/creddialog.cpp +++ /dev/null @@ -1,128 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "creddialog.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include - -CredDialog::CredDialog(QWidget *parent) - : QDialog(parent), m_mainLayout(nullptr), m_okButton(nullptr), - m_titleLabel(nullptr), m_descriptionLabel(nullptr), - m_dontShowAgainCheckbox(nullptr) -{ - setupUI(); -} - -CredDialog::~CredDialog() {} - -void CredDialog::setupUI() -{ -#ifdef WIN32 - setWindowTitle("Windows Credential Manager Access Required"); -#else - setWindowTitle("Secret Service Access Required"); -#endif - setModal(true); - setMinimumSize(500, 250); - resize(600, 300); - - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(20, 20, 20, 20); - m_mainLayout->setSpacing(15); - - // Title label -#ifdef WIN32 - m_titleLabel = new QLabel("Windows Credential Manager Access Required"); -#else - m_titleLabel = new QLabel("Secret Service Access Required"); -#endif - m_titleLabel->setAlignment(Qt::AlignCenter); - m_titleLabel->setStyleSheet( - "font-size: 18px; font-weight: bold; margin-bottom: 10px;"); - m_mainLayout->addWidget(m_titleLabel); - - // Description label -#ifdef WIN32 - QString description = - "In order to sign in to App Store we use the Windows Credential " - "Manager " - "to safely store and retrieve your credentials. You may be prompted to " - "allow access to the credential manager. " - "This is a security feature to protect your Apple ID credentials. You " - "can disable this in Settings."; -#else - QString description = - "In order to sign in to App Store we use the Secret Service " - "(gnome-keyring " - "or similar) to safely store and retrieve your credentials. You may be " - "prompted to unlock your keyring or allow access. " - "This is a security feature to protect your Apple ID credentials. You " - "can disable this in Settings."; -#endif - - m_descriptionLabel = new QLabel(description); - m_descriptionLabel->setAlignment(Qt::AlignCenter); - m_descriptionLabel->setWordWrap(true); - m_descriptionLabel->setStyleSheet("font-size: 14px; margin: 10px;"); - m_mainLayout->addWidget(m_descriptionLabel); - - m_mainLayout->addStretch(); - - m_dontShowAgainCheckbox = new QCheckBox("Don't show this again"); - m_mainLayout->addWidget(m_dontShowAgainCheckbox, 0, Qt::AlignCenter); - - QHBoxLayout *buttonsLayout = new QHBoxLayout(); - m_skipSigningInButton = new QPushButton("Skip For Now"); - m_skipSigningInButton->setFixedHeight(40); - - m_okButton = new QPushButton("OK, I understand"); - m_okButton->setDefault(true); - m_okButton->setFixedHeight(40); - - buttonsLayout->addWidget(m_skipSigningInButton); - buttonsLayout->addWidget(m_okButton); - - m_mainLayout->addLayout(buttonsLayout, Qt::AlignCenter); - - connect(m_okButton, &QPushButton::clicked, this, &CredDialog::onOkClicked); - connect(m_skipSigningInButton, &QPushButton::clicked, this, - &CredDialog::onSkipSigningInClicked); -} - -void CredDialog::onOkClicked() -{ - if (m_dontShowAgainCheckbox && m_dontShowAgainCheckbox->isChecked()) { - SettingsManager::sharedInstance()->setShowKeychainDialog(false); - } - - accept(); -} - -void CredDialog::onSkipSigningInClicked() -{ - if (m_dontShowAgainCheckbox && m_dontShowAgainCheckbox->isChecked()) { - SettingsManager::sharedInstance()->setShowKeychainDialog(false); - } - - reject(); -} diff --git a/src/creddialog.h b/src/creddialog.h deleted file mode 100644 index 6d8319d..0000000 --- a/src/creddialog.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef CRED_DIALOG_H -#define CRED_DIALOG_H - -#include -#include -#include -#include -#include - -class CredDialog : public QDialog -{ - Q_OBJECT - -public: - explicit CredDialog(QWidget *parent = nullptr); - ~CredDialog(); - -private slots: - void onOkClicked(); - void onSkipSigningInClicked(); - -private: - void setupUI(); - - QVBoxLayout *m_mainLayout; - QPushButton *m_okButton; - QPushButton *m_skipSigningInButton; - QLabel *m_titleLabel; - QLabel *m_descriptionLabel; - QCheckBox *m_dontShowAgainCheckbox; -}; - -#endif // CRED_DIALOG_H diff --git a/src/devdiskimagehelper.cpp b/src/devdiskimagehelper.cpp deleted file mode 100644 index 960cb9d..0000000 --- a/src/devdiskimagehelper.cpp +++ /dev/null @@ -1,293 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "devdiskimagehelper.h" -#include "appcontext.h" -#include "devdiskmanager.h" -#include "settingsmanager.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include - -DevDiskImageHelper::DevDiskImageHelper( - const std::shared_ptr device, QWidget *parent) - : QDialog(parent), m_device(device) -{ - setAttribute(Qt::WA_DeleteOnClose); -#ifdef WIN32 - setupWinWindow(this); -#endif - setWindowTitle("Developer Disk Image - iDescriptor"); - setupUI(); - - connect(this, &QDialog::accepted, this, - [this]() { emit mountingCompleted(true); }); - connect(this, &QDialog::rejected, this, - [this]() { emit mountingCompleted(false); }); -} - -void DevDiskImageHelper::setupUI() -{ - auto *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(20, 20, 20, 20); - mainLayout->setSpacing(15); - - m_loadingWidget = new ZLoadingWidget(true, this); - connect(m_loadingWidget, &ZLoadingWidget::retryClicked, this, - &DevDiskImageHelper::onRetryButtonClicked); - - auto *contentLayout = new QHBoxLayout(); - contentLayout->addStretch(); - - m_statusLabel = new QLabel("Please wait..."); - m_statusLabel->setWordWrap(true); - m_statusLabel->setAlignment(Qt::AlignCenter); - contentLayout->addWidget(m_statusLabel); - contentLayout->addStretch(); - - m_loadingWidget->setupContentWidget(contentLayout); - mainLayout->addWidget(m_loadingWidget); - - setMinimumWidth(400); - setMinimumHeight(200); - setModal(true); - show(); -} - -/* try to mount a specific version */ -void DevDiskImageHelper::mountVersion(const QString &version) -{ - m_loadingWidget->stop(); - m_statusLabel->setText("Please wait..."); - m_version = version; - unsigned int deviceMajorVersion = - m_device->deviceInfo.parsedDeviceVersion.major; - - connect( - m_device->service_manager, - &CXX::ServiceManager::mounted_image_retrieved, this, - [this, deviceMajorVersion, version](bool success, bool locked, - QByteArray signature, - u_int64_t sig_length) { - if (!success) { - if (locked) { - qDebug() << "Failed to retrieve mounted image signature: " - "device is locked."; - m_loadingWidget->showError( - "The device appears to be locked. Please unlock the " - "device and try again."); - return; - } - qDebug() << "Failed to retrieve mounted image signature."; - m_loadingWidget->showError( - "Failed to retrieve mounted image signature."); - return; - } - - if (!signature.isEmpty() || sig_length > 0) { - m_loadingWidget->showError( - "A developer disk image already mounted. " - "Please restart the device and try again."); - } else { - const QString downloadPath = - SettingsManager::sharedInstance()->devdiskimgpath(); - const bool isDownloaded = - DevDiskManager::sharedInstance()->isImageDownloaded( - version, downloadPath); - - qDebug() << "isDownloaded:" << isDownloaded; - if (!isDownloaded) { - m_loadingWidget->showError( - "The developer disk image for iOS " + version + - " is not downloaded. Please download it first."); - } else { - handleMounting(version); - } - } - }, - Qt::SingleShotConnection); - m_device->service_manager->get_mounted_image(); -} - -/* mount a compatible version */ -void DevDiskImageHelper::start() -{ - m_loadingWidget->stop(); - m_statusLabel->setText("Please wait..."); - - unsigned int deviceMajorVersion = - m_device->deviceInfo.parsedDeviceVersion.major; - - // FIXME:we dont have developer disk images for ios 6 and below - if (deviceMajorVersion > 5) { - checkAndMount(); - } else { - m_loadingWidget->showError( - "Developer disk image is not available for iOS version " + - QString::number(deviceMajorVersion) + - ". Please use a device with iOS 6 or above."); - return; - } -} - -void DevDiskImageHelper::checkAndMount() -{ - unsigned int deviceMajorVersion = - m_device->deviceInfo.parsedDeviceVersion.major; - - connect( - m_device->service_manager, - &CXX::ServiceManager::mounted_image_retrieved, this, - [this, deviceMajorVersion](bool success, bool locked, - QByteArray signature, u_int64_t sig_length) { - qDebug() << "[ DevDiskImageHelper::checkAndMount] qobject::connect " - "of mounted_image_retrieved consumed"; - if (!success) { - if (locked) { - qDebug() << "Failed to retrieve mounted image signature: " - "device is locked."; - m_loadingWidget->showError( - "The device appears to be locked. Please unlock the " - "device and try again."); - return; - } - qDebug() << "Failed to retrieve mounted image info."; - m_loadingWidget->showError( - "Failed to retrieve mounted image info."); - return; - } - - if (!signature.isEmpty() || sig_length > 0) { - qDebug() - << "Developer disk image already mounted with signature:" - << "length:" << sig_length << "signature:" << signature; - finishWithSuccess(); - } else { - const QString version = - DevDiskManager::sharedInstance()->downloadCompatibleImage( - m_device, [this](bool success, const QString &version) { - if (success) { - handleMounting(version); - } else { - m_loadingWidget->showError( - "Failed to download compatible image."); - } - }); - m_version = version; - qDebug() << "Is there a compatible image ?" - << !version.isEmpty(); - if (version.isEmpty()) { - // FIXME: we need to disable the retry button in this case - m_loadingWidget->showError( - "There is no compatible developer disk " - "image available for " + - QString::number(deviceMajorVersion) + "."); - } else { - m_statusLabel->setText( - QString("Downloading compatible developer disk " - "image for iOS %1..") - .arg(deviceMajorVersion)); - } - } - }, - Qt::SingleShotConnection); - m_device->service_manager->get_mounted_image(); -} -// todo called twice -// finishWithSuccess called with wait = false -void DevDiskImageHelper::handleMounting(const QString &version) -{ - m_statusLabel->setText("Mounting..."); - auto paths = DevDiskManager::sharedInstance()->getPathsForVersion(version); - qDebug() << "Mounting image with paths:" << paths.first << paths.second; - - connect( - m_device->service_manager, &CXX::ServiceManager::dev_image_mounted, - this, - [this](bool success, bool isLocked) { - qDebug() << "[devdiskimagehelper] : Developer disk image " - "mount result:" - << success; - if (success) { - qDebug() << "[devdiskimagehelper] : Developer disk image " - "mounted successfully."; - finishWithSuccess(true); - } else { - if (isLocked) { - qDebug() << "[devdiskimagehelper] : Failed to mount " - "developer disk image: device is locked."; - m_loadingWidget->showError( - "Failed to mount developer disk image.\n" - "The device appears to be locked. Please unlock the " - "device and try again."); - return; - } else { - - qDebug() - << "[devdiskimagehelper] : Failed to mount developer " - "disk image."; - m_loadingWidget->showError( - "Failed to mount developer disk image.\n" - "Please ensure the device is unlocked and " - "using a genuine " - "cable."); - } - } - }, - Qt::SingleShotConnection); - - m_device->service_manager->mount_dev_image(paths.first, paths.second); -} - -void DevDiskImageHelper::onRetryButtonClicked() -{ - m_loadingWidget->showLoading(); - - QTimer::singleShot(200, this, [this]() { - if (!m_version.isEmpty()) { - qDebug() << "Retrying mount for version:" << m_version; - mountVersion(m_version); - } else { - start(); - } - }); -} - -/* - waiting is sometimes required because services - may not become available - as soon as the img is mounted -*/ -void DevDiskImageHelper::finishWithSuccess(bool wait) -{ - qDebug() << "finishWithSuccess called with wait =" << wait; - auto handler = [this]() { - if (m_loadingWidget) { - m_loadingWidget->stop(false); - } - accept(); - }; - if (wait) { - return QTimer::singleShot(3000, handler); - } - handler(); -} diff --git a/src/devdiskimagehelper.h b/src/devdiskimagehelper.h deleted file mode 100644 index 7d1a23f..0000000 --- a/src/devdiskimagehelper.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVDISKIMAGEHELPER_H -#define DEVDISKIMAGEHELPER_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "settingsmanager.h" -#include -#include -#include -#include - -class ZLoadingWidget; - -class DevDiskImageHelper : public QDialog -{ - Q_OBJECT -public: - explicit DevDiskImageHelper(const std::shared_ptr device, - QWidget *parent = nullptr); - - // Start the mounting process - void start(); - void mountVersion(const QString &version); - - static bool - canMountForDevice(const std::shared_ptr device) - { - /* - iOS 17 and later are not supported - even though there are some images called "Personalized Disk Images" - but we dont support them for now - */ - return device->ios_version < 17; - } -signals: - void mountingCompleted(bool success); - void downloadStarted(); - void downloadCompleted(bool success); - -private slots: - void checkAndMount(); - void onRetryButtonClicked(); - -private: - void setupUI(); - void finishWithSuccess(bool wait = false); - void handleMounting(const QString &version); - - const std::shared_ptr m_device; - - QLabel *m_statusLabel; - ZLoadingWidget *m_loadingWidget; - - QString m_downloadingVersion; - - // set when called with mountVersion - QString m_version = QString(); -}; - -#endif // DEVDISKIMAGEHELPER_H diff --git a/src/devdiskimageswidget.cpp b/src/devdiskimageswidget.cpp deleted file mode 100644 index c68148b..0000000 --- a/src/devdiskimageswidget.cpp +++ /dev/null @@ -1,733 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "devdiskimageswidget.h" -#include "appcontext.h" -#include "devdiskmanager.h" -#include "iDescriptor.h" -#include "qprocessindicator.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -DevDiskImagesWidget::DevDiskImagesWidget(const QString &deviceUdid, - QWidget *parent) - : Tool(parent), m_currentDeviceUdid(deviceUdid) -{ - setMinimumSize(400, 400); - resize(800, 600); - setupUi(); - connect(DevDiskManager::sharedInstance(), &DevDiskManager::imageListFetched, - this, &DevDiskImagesWidget::onImageListFetched); - - updateDeviceList(); - connect(AppContext::sharedInstance(), &AppContext::deviceAdded, this, - &DevDiskImagesWidget::updateDeviceList); - connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, - &DevDiskImagesWidget::updateDeviceList); - connect(m_deviceComboBox, - QOverload::of(&QComboBox::currentIndexChanged), this, - &DevDiskImagesWidget::onDeviceSelectionChanged); - - connect(m_imageListWidget, &QListWidget::itemClicked, this, - [this](QListWidgetItem *item) { - m_mountButton->setEnabled(item != nullptr); - }); - - connect(m_imageListWidget, &QListWidget::itemSelectionChanged, this, - &DevDiskImagesWidget::onItemSelectionChanged); -} - -void DevDiskImagesWidget::setupUi() -{ - setWindowTitle("Developer Disk Images - iDescriptor"); - auto *layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - - auto *mountLayout = new QHBoxLayout(); - mountLayout->addWidget(new QLabel("Device:")); - m_deviceComboBox = new QComboBox(this); - mountLayout->addWidget(m_deviceComboBox); - m_mountButton = new QPushButton("Mount", this); - m_mountButton->setEnabled(false); - m_check_mountedButton = new QPushButton("Check Mounted", this); - connect(m_mountButton, &QPushButton::clicked, this, - &DevDiskImagesWidget::onMountButtonClicked); - connect(m_check_mountedButton, &QPushButton::clicked, this, - &DevDiskImagesWidget::checkMountedImage); - mountLayout->setContentsMargins(10, 10, 10, 10); - mountLayout->addWidget(m_mountButton); - mountLayout->addWidget(m_check_mountedButton); - layout->addLayout(mountLayout); - - m_stackedWidget = new QStackedWidget(this); - layout->addWidget(m_stackedWidget); - - // Create loading page with process indicator - auto *loadingPage = new QWidget(); - auto *loadingLayout = new QVBoxLayout(loadingPage); - loadingLayout->addStretch(); - - auto *indicatorLayout = new QHBoxLayout(); - indicatorLayout->addStretch(); - m_processIndicator = new QProcessIndicator(loadingPage); - m_processIndicator->setFixedSize(40, 40); - m_processIndicator->setType(QProcessIndicator::line_rotate); - indicatorLayout->addWidget(m_processIndicator); - indicatorLayout->addStretch(); - loadingLayout->addLayout(indicatorLayout); - - m_statusLabel = new QLabel("Fetching image list..."); - m_statusLabel->setAlignment(Qt::AlignCenter); - m_statusLabel->setStyleSheet("QLabel { color: #666; margin-top: 10px; }"); - loadingLayout->addWidget(m_statusLabel); - loadingLayout->addStretch(); - - m_stackedWidget->addWidget(loadingPage); - - m_imageListWidget = new QListWidget(this); - m_imageListWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_imageListWidget->setStyleSheet( - "QListWidget { background: transparent; border: none; }"); - - m_stackedWidget->addWidget(m_imageListWidget); - - m_processIndicator->start(); - m_stackedWidget->setCurrentIndex(0); // Show loading page - // TODO: we may force to refetch most up to date image list - QTimer::singleShot(500, this, [this]() { - displayImages(); - m_stackedWidget->setCurrentWidget(m_imageListWidget); - }); -} - -void DevDiskImagesWidget::fetchImages() -{ - m_processIndicator->start(); - m_stackedWidget->setCurrentIndex(0); // Show loading page - m_statusLabel->setText("Fetching image list..."); - // DevDiskManager::sharedInstance()->fetchImageList(); -} - -void DevDiskImagesWidget::onImageListFetched(bool success, - const QString &errorMessage) -{ - m_processIndicator->stop(); - - if (!success) { - qDebug() << "Error fetching image list:" << errorMessage; - m_statusLabel->setText( - QString("Error fetching image list: %1").arg(errorMessage)); - // Keep showing the loading page with error message - return; - } - - qDebug() << "Image list fetched successfully"; - displayImages(); - m_stackedWidget->setCurrentWidget(m_imageListWidget); -} - -void DevDiskImagesWidget::onDeviceSelectionChanged(int index) -{ - if (index < 0 || - index >= AppContext::sharedInstance()->getAllDevices().size()) - return; - - auto device = AppContext::sharedInstance()->getAllDevices()[index]; - if (device == nullptr) - return; - - m_currentDeviceUdid = device->udid; - displayImages(); -} - -void DevDiskImagesWidget::displayImages() -{ - qDebug() << "Displaying images for device"; - m_imageListWidget->clear(); - - // Look up device by UDID - std::shared_ptr currentDevice = nullptr; - if (!m_currentDeviceUdid.isEmpty()) { - currentDevice = - AppContext::sharedInstance()->getDevice(m_currentDeviceUdid); - } - bool hasConnectedDevice = (currentDevice != nullptr); - - int major = hasConnectedDevice - ? currentDevice->deviceInfo.parsedDeviceVersion.major - : 0; - int minor = hasConnectedDevice - ? currentDevice->deviceInfo.parsedDeviceVersion.minor - : 0; - - QString path = SettingsManager::sharedInstance()->mkDevDiskImgPath(); - QList allImages = - DevDiskManager::sharedInstance()->parseImageList( - path, major, minor, m_mounted_sig.c_str(), m_mounted_sig_len); - - qDebug() << "Total images:" << allImages.size(); - - int itemIndex = 0; - - // Create UI items - auto createVersionItem = [&](const ImageInfo &info) { - bool isCompatible = - (info.compatibility == ImageCompatibility::Compatible || - info.compatibility == ImageCompatibility::MaybeCompatible); - auto *itemWidget = new QWidget(); - itemWidget->setObjectName("itemWidget"); - auto *itemLayout = new QHBoxLayout(itemWidget); - - // TODO: maybe create a custom widget for this, if we ever need this - // elsewhere ? - QColor baseColor = QApplication::palette().color(QPalette::Window); - QColor bgColor = - itemIndex % 2 == 0 ? baseColor.lighter(110) : baseColor; - itemWidget->setStyleSheet( - QString("QWidget#itemWidget { background-color: %1; }") - .arg(bgColor.name())); - itemIndex++; - - auto *versionLabel = new QLabel(info.version); - if (isCompatible) { - if (info.compatibility == ImageCompatibility::Compatible) { - versionLabel->setStyleSheet( - "QLabel { font-weight: bold; color: #2E7D32; }"); - } else if (info.compatibility == - ImageCompatibility::MaybeCompatible) { - versionLabel->setStyleSheet( - "QLabel { font-weight: bold; color: #F57C00; }"); - } - } - itemLayout->addWidget(versionLabel); - - // Add status labels - if (hasConnectedDevice) { - if (isCompatible) { - if (info.isMounted) { - auto *mountedLabel = new QLabel("Mounted"); - mountedLabel->setStyleSheet( - "QLabel { color: #1565C0; font-weight: bold; }"); - itemLayout->addWidget(mountedLabel); - } else if (info.compatibility == - ImageCompatibility::MaybeCompatible) { - auto *maybeLabel = new QLabel("Maybe compatible"); - maybeLabel->setStyleSheet("QLabel { color: #F57C00; " - "margin-left: 10px; font-weight: " - "bold; }"); - itemLayout->addWidget(maybeLabel); - } - } else { - auto *incompatLabel = new QLabel("Not compatible"); - incompatLabel->setStyleSheet( - "QLabel { color: #D32F2F; margin-left: 10px; font-weight: " - "bold; }"); - itemLayout->addWidget(incompatLabel); - } - } - - itemLayout->addStretch(); - - auto *progressBar = new QProgressBar(); - progressBar->setVisible(false); - itemLayout->addWidget(progressBar); - - auto *downloadButton = - new QPushButton(info.isDownloaded ? "Re-download" : "Download"); - downloadButton->setDefault(true); - downloadButton->setProperty("version", info.version); - connect(downloadButton, &QPushButton::clicked, this, - &DevDiskImagesWidget::onDownloadButtonClicked); - itemLayout->addWidget(downloadButton); - - auto *listItem = new QListWidgetItem(m_imageListWidget); - listItem->setSizeHint(itemWidget->sizeHint()); - m_imageListWidget->addItem(listItem); - m_imageListWidget->setItemWidget(listItem, itemWidget); - }; - - bool hasCompatibleImages = false; - bool hasOtherImages = false; - bool separatorAdded = false; - - // Add all images, inserting separator when transitioning from compatible to - // not compatible - for (const auto &info : allImages) { - bool isCompatible = - (info.compatibility == ImageCompatibility::Compatible || - info.compatibility == ImageCompatibility::MaybeCompatible); - - if (isCompatible) { - hasCompatibleImages = true; - } else { - hasOtherImages = true; - // Add separator before first non-compatible image if we have - // compatible ones - if (hasCompatibleImages && !separatorAdded) { - auto *separatorItem = new QListWidgetItem(m_imageListWidget); - auto *separatorWidget = new QWidget(); - auto *separatorLayout = new QHBoxLayout(separatorWidget); - auto *separatorLabel = new QLabel("Other versions"); - separatorLabel->setStyleSheet( - "QLabel { font-weight: bold; color: #757575; margin: 10px " - "0;}"); - separatorLayout->addWidget(separatorLabel); - separatorItem->setSizeHint(separatorWidget->sizeHint()); - m_imageListWidget->addItem(separatorItem); - m_imageListWidget->setItemWidget(separatorItem, - separatorWidget); - separatorAdded = true; - } - } - - createVersionItem(info); - } - - // Show device info if available - if (hasConnectedDevice) { - QString deviceVersion = QString("%1.%2").arg(major).arg(minor); - m_statusLabel->setText( - QString("Connected device: iOS %1 - Compatible images shown at top") - .arg(deviceVersion)); - } -} - -void DevDiskImagesWidget::onDownloadButtonClicked() -{ - auto *button = qobject_cast(sender()); - if (!button) - return; - - QString version = button->property("version").toString(); - - QString versionPath = - QDir(SettingsManager::sharedInstance()->devdiskimgpath()) - .filePath(version); - if (QDir(versionPath).exists()) { - auto reply = QMessageBox::question( - this, "Confirm Overwrite", - QString( - "Directory '%1' already exists. Do you want to overwrite it?") - .arg(version), - QMessageBox::Yes | QMessageBox::No); - if (reply == QMessageBox::No) { - return; - } - } - - startDownload(version); -} - -void DevDiskImagesWidget::startDownload(const QString &version) -{ - // Find the button and progress bar for this version - QPushButton *downloadButton = nullptr; - QProgressBar *progressBar = nullptr; - for (int i = 0; i < m_imageListWidget->count(); ++i) { - auto *item = m_imageListWidget->item(i); - auto *widget = m_imageListWidget->itemWidget(item); - auto *button = widget->findChild(); - if (button && button->property("version") == version) { - downloadButton = button; - progressBar = widget->findChild(); - break; - } - } - - if (!downloadButton || !progressBar) - return; - - downloadButton->setEnabled(false); - progressBar->setVisible(true); - progressBar->setValue(0); - - QString targetDir = - QDir(SettingsManager::sharedInstance()->devdiskimgpath()) - .filePath(version); - if (!QDir().mkpath(targetDir)) { - QMessageBox::critical( - this, "Error", - QString("Could not create directory: %1").arg(targetDir)); - downloadButton->setEnabled(true); - progressBar->setVisible(false); - return; - } - // todo is this safe ? - auto *downloadItem = new DownloadItem(); - downloadItem->version = version; - downloadItem->progressBar = progressBar; - downloadItem->downloadButton = downloadButton; - - auto replies = DevDiskManager::sharedInstance()->downloadImage(version); - downloadItem->dmgReply = replies.first; - downloadItem->sigReply = replies.second; - - if (!downloadItem->dmgReply || !downloadItem->sigReply) { - delete downloadItem; - downloadButton->setEnabled(true); - progressBar->setVisible(false); - return; - } - - connect(downloadItem->dmgReply, &QNetworkReply::downloadProgress, this, - &DevDiskImagesWidget::onDownloadProgress); - connect(downloadItem->dmgReply, &QNetworkReply::finished, this, - &DevDiskImagesWidget::onFileDownloadFinished); - connect(downloadItem->sigReply, &QNetworkReply::downloadProgress, this, - &DevDiskImagesWidget::onDownloadProgress); - connect(downloadItem->sigReply, &QNetworkReply::finished, this, - &DevDiskImagesWidget::onFileDownloadFinished); - - m_activeDownloads[downloadItem->dmgReply] = downloadItem; - m_activeDownloads[downloadItem->sigReply] = downloadItem; -} - -void DevDiskImagesWidget::onDownloadProgress(qint64 bytesReceived, - qint64 bytesTotal) -{ - auto *reply = qobject_cast(sender()); - if (!reply || !m_activeDownloads.contains(reply)) - return; - - auto *item = m_activeDownloads[reply]; - - if (reply->property("totalSizeAdded").isNull() && bytesTotal > 0) { - item->totalSize += bytesTotal; - reply->setProperty("totalSizeAdded", true); - } - - if (reply == item->dmgReply) { - item->dmgReceived = bytesReceived; - } else if (reply == item->sigReply) { - item->sigReceived = bytesReceived; - } - - item->totalReceived = item->dmgReceived + item->sigReceived; - - if (item->totalSize > 0) { - item->progressBar->setValue((item->totalReceived * 100) / - item->totalSize); - } -} - -// TODO: file saving should be in manager -void DevDiskImagesWidget::onFileDownloadFinished() -{ - auto *reply = qobject_cast(sender()); - if (!reply || !m_activeDownloads.contains(reply)) - return; - - auto *item = m_activeDownloads[reply]; - m_activeDownloads.remove(reply); - - if (reply->error() != QNetworkReply::NoError) { - QMessageBox::critical(this, "Download Error", - QString("Failed to download %1: %2") - .arg(reply->url().path()) - .arg(reply->errorString())); - - if (reply == item->dmgReply && item->sigReply) - item->sigReply->abort(); - if (reply == item->sigReply && item->dmgReply) - item->dmgReply->abort(); - - item->downloadButton->setEnabled(true); - item->downloadButton->setText("Retry"); - item->progressBar->setVisible(false); - - if (m_activeDownloads.key(item) == nullptr) { - delete item; - } - reply->deleteLater(); - return; - } - - QString path = QUrl::fromPercentEncoding(reply->url().path().toUtf8()); - QFileInfo fileInfo(path); - QString filename = fileInfo.fileName(); - QString targetPath = - QDir(QDir(SettingsManager::sharedInstance()->devdiskimgpath()) - .filePath(item->version)) - .filePath(filename); - - QFile file(targetPath); - if (!file.open(QIODevice::WriteOnly)) { - QMessageBox::critical( - this, "File Error", - QString("Could not save file: %1").arg(targetPath)); - } else { - file.write(reply->readAll()); - file.close(); - } - - reply->deleteLater(); - - if (m_activeDownloads.key(item) == nullptr) { // Both files downloaded - item->downloadButton->setText("Downloaded"); - item->downloadButton->setEnabled(false); - item->progressBar->setValue(100); - item->progressBar->setVisible(false); - delete item; - } -} - -void DevDiskImagesWidget::updateDeviceList() -{ - auto devices = AppContext::sharedInstance()->getAllDevices(); - - if (devices.isEmpty()) { - m_currentDeviceUdid.clear(); - m_check_mountedButton->setEnabled(false); - m_deviceComboBox->setEnabled(false); - } else { - m_deviceComboBox->setEnabled(true); - m_check_mountedButton->setEnabled(true); - } - - QString currentUdid = ""; - if (m_deviceComboBox->count() > 0 && - m_deviceComboBox->currentIndex() >= 0) { - currentUdid = m_deviceComboBox->currentData().toString(); - } else if (!m_currentDeviceUdid.isEmpty()) { - currentUdid = m_currentDeviceUdid; - } - - m_deviceComboBox->clear(); - - int newIndex = -1; - for (int i = 0; i < devices.size(); ++i) { - std::shared_ptr device = devices.at(i); - m_deviceComboBox->addItem( - QString("%1 / (%2)") - .arg(QString::fromStdString(device->deviceInfo.deviceName)) - .arg(QString::fromStdString(device->deviceInfo.productType)), - device->udid); - if (device->udid == currentUdid) { - newIndex = i; - } - } - - if (newIndex != -1) { - m_deviceComboBox->setCurrentIndex(newIndex); - } - displayImages(); -} - -void DevDiskImagesWidget::onMountButtonClicked() -{ - qDebug() << "Current index:" << m_deviceComboBox->currentIndex(); - if (m_deviceComboBox->currentIndex() < 0) { - QMessageBox::warning(this, "No Device", - "Please select a device to mount the image on."); - return; - } - - auto *currentItem = m_imageListWidget->currentItem(); - if (!currentItem) { - QMessageBox::warning(this, "No Image Selected", - "Please select a disk image to mount."); - return; - } - - auto *widget = m_imageListWidget->itemWidget(currentItem); - auto *button = widget->findChild(); - if (!button) - return; - - QString version = button->property("version").toString(); - - this->mountImage(version); -} - -void DevDiskImagesWidget::mountImage(const QString &version) -{ - QString udid = m_deviceComboBox->currentData().toString(); - m_deviceComboBox->setEnabled(false); - - if (udid.isEmpty()) { - QMessageBox::warning(this, "No Device", "Please select a device."); - return; - } - std::shared_ptr device = - AppContext::sharedInstance()->getDevice(udid); - - if (!device) { - QMessageBox::warning(this, "Device Not Found", - "The selected device could not be found."); - return; - } - - if (device->ios_version >= 17) { - QMessageBox::warning(this, "Unsupported iOS Version", - "Mounting developer disk images is not supported " - "on iOS 17 and later."); - m_deviceComboBox->setEnabled(true); - return; - } - auto *helper = new DevDiskImageHelper(device, this); - connect(helper, &DevDiskImageHelper::finished, this, [this, helper]() { - m_deviceComboBox->setEnabled(true); - displayImages(); // Refresh - }); - helper->mountVersion(version); -} - -void DevDiskImagesWidget::closeEvent(QCloseEvent *event) -{ - if (!m_activeDownloads.isEmpty()) { - auto reply = QMessageBox::question( - this, "Downloads in Progress", - QString( - "There are %1 download(s) in progress. Do you really want to " - "close and cancel all downloads?") - .arg(m_activeDownloads.size()), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (reply == QMessageBox::No) { - event->ignore(); - return; - } - - // Cancel all active downloads - for (auto it = m_activeDownloads.begin(); it != m_activeDownloads.end(); - ++it) { - QNetworkReply *reply = it.key(); - if (reply) { - reply->abort(); - } - } - } - - event->accept(); -} - -void DevDiskImagesWidget::checkMountedImage() -{ - QString udid = m_deviceComboBox->currentData().toString(); - m_deviceComboBox->setEnabled(false); - - if (udid.isEmpty()) { - QMessageBox::warning(this, "No Device", "Please select a device."); - return; - } - qDebug() << "Checking mounted image for device UDID:" << udid; - std::shared_ptr device = - AppContext::sharedInstance()->getDevice(udid); - - if (!device) { - QMessageBox::warning(this, "Device Not Found", - "The selected device could not be found."); - return; - } - - if (device->ios_version >= 17) { - QMessageBox::warning(this, "Unsupported iOS Version", - "Checking mounted developer disk images is not " - "supported on iOS 17 and later."); - return; - } - - connect( - device->service_manager, &CXX::ServiceManager::mounted_image_retrieved, - this, - [this](bool success, bool locked, QByteArray signature, - std::uint64_t sig_length) { - m_deviceComboBox->setEnabled(true); - if (!success) { - if (locked) { - QMessageBox::warning( - this, "Device Locked", - "The device appears to be locked. Please unlock the " - "device and try again."); - return; - } - QMessageBox::critical( - this, "Error", - "Failed to retrieve mounted image information."); - return; - } - - if (signature.isEmpty() || sig_length == 0) { - QMessageBox::information( - this, "No Image Mounted", - "There is currently no image mounted on the device."); - m_mounted_sig.clear(); - m_mounted_sig_len = 0; - displayImages(); // Refresh - return; - } - - QMessageBox::information(this, "Image Mounted", - "A developer disk image is currently " - "mounted on the device."); - m_mounted_sig = - std::string(reinterpret_cast(signature.data()), - signature.size()); - m_mounted_sig_len = sig_length; - displayImages(); // Refresh - }, - Qt::SingleShotConnection); - device->service_manager->get_mounted_image(); -} - -void DevDiskImagesWidget::onItemSelectionChanged() -{ - QColor baseColor = QApplication::palette().color(QPalette::Window); - QColor highlightColor = QApplication::palette().color(QPalette::Highlight); - - for (int row = 0; row < m_imageListWidget->count(); ++row) { - auto *item = m_imageListWidget->item(row); - auto *w = m_imageListWidget->itemWidget(item); - if (!w) - continue; - - QColor bgColor = (row % 2 == 0) ? baseColor.lighter(110) : baseColor; - - if (item->isSelected()) { - bgColor = highlightColor; - } - - w->setStyleSheet( - QStringLiteral("QWidget#itemWidget { background-color: %1; }") - .arg(bgColor.name())); - } -} \ No newline at end of file diff --git a/src/devdiskimageswidget.h b/src/devdiskimageswidget.h deleted file mode 100644 index f02cc89..0000000 --- a/src/devdiskimageswidget.h +++ /dev/null @@ -1,101 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVDISKIMAGESWIDGET_H -#define DEVDISKIMAGESWIDGET_H - -#include "devdiskimagehelper.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "qprocessindicator.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class DevDiskImagesWidget : public Tool -{ - Q_OBJECT -public: - explicit DevDiskImagesWidget(const QString &deviceUdid, - QWidget *parent = nullptr); - -private slots: - void fetchImages(); - void onDownloadButtonClicked(); - void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); - void onFileDownloadFinished(); - void updateDeviceList(); - void onMountButtonClicked(); - void onImageListFetched(bool success, - const QString &errorMessage = QString()); - void onItemSelectionChanged(); - -private: - void setupUi(); - void displayImages(); - void startDownload(const QString &version); - void mountImage(const QString &version); - void onDeviceSelectionChanged(int index); - void closeEvent(QCloseEvent *event) override; - void checkMountedImage(); - - struct DownloadItem { - QNetworkReply *dmgReply = nullptr; - QNetworkReply *sigReply = nullptr; - QProgressBar *progressBar = nullptr; - QPushButton *downloadButton = nullptr; - QString version; - qint64 totalSize = 0; - qint64 totalReceived = 0; - qint64 dmgReceived = 0; - qint64 sigReceived = 0; - }; - - std::string m_mounted_sig = ""; - uint64_t m_mounted_sig_len = 0; - - QStackedWidget *m_stackedWidget; - QListWidget *m_imageListWidget; - QLabel *m_statusLabel; - QLabel *m_initialStatusLabel; - QWidget *m_errorWidget; - QComboBox *m_deviceComboBox; - QPushButton *m_mountButton; - QPushButton *m_check_mountedButton; - QProcessIndicator *m_processIndicator; - - QString m_currentDeviceUdid; - QStringList m_compatibleVersions; - QStringList m_otherVersions; - - QMap> - m_availableImages; // version -> {dmg_path, sig_path} - QMap m_activeDownloads; -}; - -#endif // DEVDISKIMAGESWIDGET_H diff --git a/src/devdiskmanager.cpp b/src/devdiskmanager.cpp deleted file mode 100644 index 850cee6..0000000 --- a/src/devdiskmanager.cpp +++ /dev/null @@ -1,528 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "devdiskmanager.h" -#include "iDescriptor.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -DevDiskManager *DevDiskManager::sharedInstance() -{ - static DevDiskManager instance; - return &instance; -} - -DevDiskManager::DevDiskManager(QObject *parent) : QObject{parent} -{ - m_networkManager = new QNetworkAccessManager(this); - populateImageList(); -} - -/* - * if we have DeveloperDiskImages.json in docs read from there if not populate - * with the file in resources and then try to update it - */ -void DevDiskManager::populateImageList() -{ - QString localPath = QDir(SettingsManager::sharedInstance()->homePath()) - .filePath("DeveloperDiskImages.json"); - qDebug() << "Looking for DeveloperDiskImages.json at" << localPath; - QFile localFile(localPath); - - if (localFile.exists() && localFile.open(QIODevice::ReadOnly)) { - m_imageListJsonData = localFile.readAll(); - localFile.close(); - qDebug() << "Loaded DeveloperDiskImages.json from local cache."; - } else { - QFile qrcFile(":/DeveloperDiskImages.json"); - if (qrcFile.open(QIODevice::ReadOnly)) { - m_imageListJsonData = qrcFile.readAll(); - qrcFile.close(); - qDebug() << "Loaded DeveloperDiskImages.json from QRC resources."; - } else { - qWarning() - << "Could not open DeveloperDiskImages.json from QRC. " - "Image list will be empty until network fetch succeeds."; - } - } - QUrl url(DEVELOPER_DISK_IMAGE_JSON_URL); - QNetworkRequest request(url); - auto *reply = m_networkManager->get(request); - - connect(reply, &QNetworkReply::finished, this, [this, localPath, reply]() { - if (reply->error() == QNetworkReply::NoError) { - // FIXME: better have this in settings - QDir().mkdir(QDir::homePath() + "/.idescriptor"); - m_imageListJsonData = reply->readAll(); - QFile file(localPath); - if (file.open(QIODevice::WriteOnly)) { - file.write(m_imageListJsonData); - file.close(); - } - } - reply->deleteLater(); - }); -} - -QMap> DevDiskManager::parseDiskDir() -{ - QJsonDocument doc = QJsonDocument::fromJson(m_imageListJsonData); - if (!doc.isObject()) { - qWarning() << "parseDiskDir: Invalid JSON response from image list API"; - return {}; - } - - QMap> - imageFiles; // version -> {type -> url} - - QJsonObject root = getVersionedConfig(doc.object()); - if (root.isEmpty()) { - qWarning() << "parseDiskDir: No valid versioned config found in image " - "list JSON"; - return {}; - } - for (auto it = root.constBegin(); it != root.constEnd(); ++it) { - const QString version = it.key(); - const QJsonObject versionData = it.value().toObject(); - - // Skip special entries - if (version == "Fallback") { - continue; - } - - QMap versionFiles; - - // Handle Image URLs - if (versionData.contains("Image")) { - QJsonArray imageArray = versionData["Image"].toArray(); - if (!imageArray.isEmpty()) { - versionFiles["DeveloperDiskImage.dmg"] = - imageArray[0].toString(); - } - } - - // Handle Signature URLs - if (versionData.contains("Signature")) { - QJsonArray sigArray = versionData["Signature"].toArray(); - if (!sigArray.isEmpty()) { - versionFiles["DeveloperDiskImage.dmg.signature"] = - sigArray[0].toString(); - } - } - - // Only add versions that have at least an image file - if (!versionFiles.isEmpty() && - versionFiles.contains("DeveloperDiskImage.dmg")) { - imageFiles[version] = versionFiles; - } - } - - return imageFiles; -} - -QList DevDiskManager::parseImageList(QString path, - int deviceMajorVersion, - int deviceMinorVersion, - const char *mounted_sig, - uint64_t mounted_sig_len) -{ - m_availableImages.clear(); - - QMap> imageFiles = parseDiskDir(); - QList sortedResult = - getImagesSorted(imageFiles, path, deviceMajorVersion, - deviceMinorVersion, mounted_sig, mounted_sig_len); - - return sortedResult; -} - -QList DevDiskManager::getImagesSorted( - QMap> imageFiles, QString path, - int deviceMajorVersion, int deviceMinorVersion, const char *mounted_sig, - uint64_t mounted_sig_len) -{ - QList allImages; - // FIXME: i guess we could do better here but works for now - bool hasConnectedDevice = (deviceMajorVersion > 0); - - for (auto it = imageFiles.constBegin(); it != imageFiles.constEnd(); ++it) { - if (it.value().contains("DeveloperDiskImage.dmg") && - it.value().contains("DeveloperDiskImage.dmg.signature")) { - QString version = it.key(); - - ImageInfo info; - info.version = version; - info.dmgPath = it.value()["DeveloperDiskImage.dmg"]; - info.sigPath = it.value()["DeveloperDiskImage.dmg.signature"]; - info.isDownloaded = isImageDownloaded(version, path); - - // Determine compatibility - if (hasConnectedDevice) { - QStringList versionParts = version.split('.'); - if (versionParts.size() >= 1) { - bool ma_ok; - bool mi_ok; - int imageMajorVersion = versionParts[0].toInt(&ma_ok); - int imageMinorVersion = (versionParts.size() >= 2) - ? versionParts[1].toInt(&mi_ok) - : 0; - - if (ma_ok && mi_ok) { - - // FIXME: this seems to work only for older iphones - // so commented out but in the future , it may be - // enabled - if (imageMajorVersion == deviceMajorVersion) { - if (imageMinorVersion == deviceMinorVersion) { - // Exact match - info.compatibility = - ImageCompatibility::Compatible; - } else { - // Major matches but minor doesn't - info.compatibility = - ImageCompatibility::MaybeCompatible; - } - } else { - info.compatibility = - ImageCompatibility::NotCompatible; - } - } - } - } - - // Check if mounted - /* - in my testing some ios versions do accept older minor versions - as well for example an iPhone 5s with iOS 12.5 accepts 12.4 but - newer iPhones are more strict, so lets just check where it's - compatible or not - */ - // if (info.isCompatible && info.isDownloaded && mounted_sig) - if (info.isDownloaded && mounted_sig) { - QString sigLocalPath = - QDir( - QDir( - SettingsManager::sharedInstance()->devdiskimgpath()) - .filePath(version)) - .filePath("DeveloperDiskImage.dmg.signature"); - info.isMounted = - compareSignatures(sigLocalPath.toUtf8().constData(), - mounted_sig, mounted_sig_len); - } - - m_availableImages[version] = info; - allImages.append(info); - } - } - - // Sort images: Compatible first, then MaybeCompatible, then NotCompatible - // Within each group, sort by version (newest first) - auto versionSort = [](const ImageInfo &a, const ImageInfo &b) { - // First sort by compatibility - if (a.compatibility != b.compatibility) { - return a.compatibility < - b.compatibility; // Compatible(0) < MaybeCompatible(1) < - // NotCompatible(2) - } - - // Then sort by version (newest first) - QStringList aParts = a.version.split('.'); - QStringList bParts = b.version.split('.'); - - for (int i = 0; i < qMax(aParts.size(), bParts.size()); ++i) { - int aNum = (i < aParts.size()) ? aParts[i].toInt() : 0; - int bNum = (i < bParts.size()) ? bParts[i].toInt() : 0; - - if (aNum != bNum) { - return aNum > bNum; // Descending order (newest first) - } - } - return false; - }; - - std::sort(allImages.begin(), allImages.end(), versionSort); - - return allImages; -} - -QList DevDiskManager::getAllImages() const -{ - return m_availableImages.values(); -} - -QPair -DevDiskManager::downloadImage(const QString &version) -{ - qDebug() << "Request to download image version:" << version; - if (!m_availableImages.contains(version)) { - qDebug() << "Image not found:" << version; - emit imageDownloadFinished(version, false, "Image version not found."); - return {nullptr, nullptr}; - } - - QString targetDir = - QDir(SettingsManager::sharedInstance()->devdiskimgpath()) - .filePath(version); - if (!QDir().mkpath(targetDir)) { - qDebug() << "Could not create directory:" << targetDir; - emit imageDownloadFinished( - version, false, - QString("Could not create directory: %1").arg(targetDir)); - return {nullptr, nullptr}; - } - - const ImageInfo &info = m_availableImages[version]; - - QUrl dmgUrl(info.dmgPath); - QNetworkRequest dmgRequest(dmgUrl); - QNetworkReply *dmgReply = m_networkManager->get(dmgRequest); - - QUrl sigUrl(info.sigPath); - QNetworkRequest sigRequest(sigUrl); - QNetworkReply *sigReply = m_networkManager->get(sigRequest); - - return {dmgReply, sigReply}; -} - -bool DevDiskManager::isImageDownloaded(const QString &version, - const QString &downloadPath) const -{ - QString versionPath = QDir(downloadPath).filePath(version); - QString dmgPath = QDir(versionPath).filePath("DeveloperDiskImage.dmg"); - QString sigPath = - QDir(versionPath).filePath("DeveloperDiskImage.dmg.signature"); - - return QFile::exists(dmgPath) && QFile::exists(sigPath); -} - -QString DevDiskManager::downloadCompatibleImage( - const std::shared_ptr device, - std::function callback) -{ - QString path = SettingsManager::sharedInstance()->mkDevDiskImgPath(); - unsigned int deviceMajorVersion = - device->deviceInfo.parsedDeviceVersion.major; - unsigned int deviceMinorVersion = - device->deviceInfo.parsedDeviceVersion.minor; - qDebug() << "Device version:" << deviceMajorVersion << "." - << deviceMinorVersion; - QList images = - parseImageList(path, deviceMajorVersion, deviceMinorVersion, "", 0); - - if (images.isEmpty()) { - qDebug() << "No images found for device version:" << deviceMajorVersion - << "." << deviceMinorVersion; - callback(false, ""); - return ""; - } - - for (const ImageInfo &info : images) { - if (info.compatibility != ImageCompatibility::Compatible && - info.compatibility != ImageCompatibility::MaybeCompatible) { - continue; - } - if (info.isDownloaded) { - callback(true, info.version); - return info.version; - } - } - - // If none are downloaded, download the newest compatible one - for (const ImageInfo &info : images) { - if (info.compatibility == ImageCompatibility::Compatible || - info.compatibility == ImageCompatibility::MaybeCompatible) { - const QString versionToDownload = info.version; - connect( - this, &DevDiskManager::imageDownloadFinished, this, - [this, versionToDownload, - callback](const QString &finishedVersion, bool success, - const QString &errorMessage) { - if (finishedVersion == versionToDownload) { - callback(success, versionToDownload); - } - }, - Qt::SingleShotConnection); - - qDebug() - << "No compatible image found locally. Downloading version:" - << versionToDownload; - - QPair replies = - downloadImage(versionToDownload); - auto *downloadItem = new DownloadItem(); - downloadItem->version = versionToDownload; - downloadItem->downloadPath = path; - downloadItem->dmgReply = replies.first; - downloadItem->sigReply = replies.second; - - connect(downloadItem->dmgReply, &QNetworkReply::downloadProgress, - this, &DevDiskManager::onDownloadProgress); - connect(downloadItem->dmgReply, &QNetworkReply::finished, this, - &DevDiskManager::onFileDownloadFinished); - connect(downloadItem->sigReply, &QNetworkReply::downloadProgress, - this, &DevDiskManager::onDownloadProgress); - connect(downloadItem->sigReply, &QNetworkReply::finished, this, - &DevDiskManager::onFileDownloadFinished); - - m_activeDownloads[downloadItem->dmgReply] = downloadItem; - m_activeDownloads[downloadItem->sigReply] = downloadItem; - return versionToDownload; - } - } - - qDebug() << "No compatible image found to mount on device:" << device->udid; - - return ""; -} - -std::pair -DevDiskManager::getPathsForVersion(const QString &version) -{ - const QString downloadPath = - SettingsManager::sharedInstance()->devdiskimgpath(); - QString versionPath = QDir(downloadPath).filePath(version); - QString dmgPath = QDir(versionPath).filePath("DeveloperDiskImage.dmg"); - QString sigPath = - QDir(versionPath).filePath("DeveloperDiskImage.dmg.signature"); - - return {dmgPath, sigPath}; -} - -void DevDiskManager::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - auto *reply = qobject_cast(sender()); - if (!reply || !m_activeDownloads.contains(reply)) - return; - - auto *item = m_activeDownloads[reply]; - - if (reply->property("totalSizeAdded").isNull() && bytesTotal > 0) { - item->totalSize += bytesTotal; - reply->setProperty("totalSizeAdded", true); - } - - if (reply == item->dmgReply) { - item->dmgReceived = bytesReceived; - } else if (reply == item->sigReply) { - item->sigReceived = bytesReceived; - } - - qint64 totalReceived = item->dmgReceived + item->sigReceived; - - if (item->totalSize > 0) { - emit imageDownloadProgress(item->version, - (totalReceived * 100) / item->totalSize); - } -} - -void DevDiskManager::onFileDownloadFinished() -{ - auto *reply = qobject_cast(sender()); - if (!reply || !m_activeDownloads.contains(reply)) - return; - - auto *item = m_activeDownloads[reply]; - m_activeDownloads.remove(reply); - - if (reply->error() != QNetworkReply::NoError) { - emit imageDownloadFinished(item->version, false, reply->errorString()); - - if (reply == item->dmgReply && item->sigReply) - item->sigReply->abort(); - if (reply == item->sigReply && item->dmgReply) - item->dmgReply->abort(); - - if (m_activeDownloads.key(item) == nullptr) { - delete item; - } - reply->deleteLater(); - return; - } - - QString path = QUrl::fromPercentEncoding(reply->url().path().toUtf8()); - QFileInfo fileInfo(path); - QString filename = fileInfo.fileName(); - QString targetPath = QDir(QDir(item->downloadPath).filePath(item->version)) - .filePath(filename); - - QFile file(targetPath); - qDebug() << "Saving downloaded file to:" << targetPath; - if (!file.open(QIODevice::WriteOnly)) { - emit imageDownloadFinished( - item->version, false, - QString("Could not save file: %1").arg(targetPath)); - } else { - file.write(reply->readAll()); - file.close(); - } - - reply->deleteLater(); - - if (m_activeDownloads.key(item) == nullptr) { // Both files finished - emit imageDownloadFinished(item->version, true); - delete item; - } -} - -bool DevDiskManager::compareSignatures(const char *signature_file_path, - const char *mounted_sig, - uint64_t mounted_sig_len) -{ - FILE *f_sig = fopen(signature_file_path, "rb"); - if (!f_sig) { - qDebug() << "ERROR: Could not open signature file:" - << signature_file_path; - return false; - } - - fseek(f_sig, 0, SEEK_END); - long local_sig_len = ftell(f_sig); - fseek(f_sig, 0, SEEK_SET); - - char *local_sig = (char *)malloc(local_sig_len); - if (!local_sig) { - fclose(f_sig); - return false; - } - - fread(local_sig, 1, local_sig_len, f_sig); - fclose(f_sig); - - bool matches = false; - if ((mounted_sig_len == (uint64_t)local_sig_len) && - (memcmp(mounted_sig, local_sig, mounted_sig_len) == 0)) { - qDebug() << "Signatures match!"; - matches = true; - } else { - qDebug() << "Signatures DO NOT match!"; - } - - free(local_sig); - return matches; -} \ No newline at end of file diff --git a/src/devdiskmanager.h b/src/devdiskmanager.h deleted file mode 100644 index 28481f3..0000000 --- a/src/devdiskmanager.h +++ /dev/null @@ -1,98 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVDISKMANAGER_H -#define DEVDISKMANAGER_H - -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include - -class DevDiskManager : public QObject -{ - Q_OBJECT -public: - explicit DevDiskManager(QObject *parent = nullptr); - static DevDiskManager *sharedInstance(); - - QList parseImageList(QString path, int deviceMajorVersion, - int deviceMinorVersion, - const char *mounted_sig, - uint64_t mounted_sig_len); - QList getAllImages() const; - - // Download management - QPair - downloadImage(const QString &version); - bool isImageDownloaded(const QString &version, - const QString &downloadPath) const; - - // Mount operations - std::pair getPathsForVersion(const QString &version); - - // Signature comparison - bool compareSignatures(const char *signature_file_path, - const char *mounted_sig, uint64_t mounted_sig_len); - - QByteArray getImageListData() const { return m_imageListJsonData; } - QString downloadCompatibleImage( - const std::shared_ptr device, - std::function callback); - -signals: - void imageListFetched(bool success, - const QString &errorMessage = QString()); - void imageDownloadProgress(const QString &version, int percentage); - void imageDownloadFinished(const QString &version, bool success, - const QString &errorMessage = QString()); - -private slots: - void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); - void onFileDownloadFinished(); - -private: - struct DownloadItem { - QString version; - QString downloadPath; - QNetworkReply *dmgReply = nullptr; - QNetworkReply *sigReply = nullptr; - qint64 dmgReceived = 0; - qint64 sigReceived = 0; - qint64 totalSize = 0; - }; - - QNetworkAccessManager *m_networkManager; - QByteArray m_imageListJsonData; - QMap m_availableImages; - QMap m_activeDownloads; - - QMap> parseDiskDir(); - QList - getImagesSorted(QMap> imageFiles, - QString path, int deviceMajorVersion, - int deviceMinorVersion, const char *mounted_sig, - uint64_t mounted_sig_len); - void populateImageList(); -}; - -#endif // DEVDISKMANAGER_H diff --git a/src/devicedatabase.cpp b/src/devicedatabase.cpp deleted file mode 100644 index 5b5767f..0000000 --- a/src/devicedatabase.cpp +++ /dev/null @@ -1,574 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "devicedatabase.h" -#include - -// https://github.com/libimobiledevice/libirecovery/blob/master/src/libirecovery.c -const DeviceDatabaseInfo DeviceDatabase::m_devices[] = { - /* iPhone */ - {"iPhone1,1", "m68ap", 0x00, 0x8900, "iPhone 2G", "iPhone 2G"}, - {"iPhone1,2", "n82ap", 0x04, 0x8900, "iPhone 3G", "iPhone 3G"}, - {"iPhone2,1", "n88ap", 0x00, 0x8920, "iPhone 3Gs", "iPhone 3Gs"}, - {"iPhone3,1", "n90ap", 0x00, 0x8930, "iPhone 4 (GSM)", "iPhone 4"}, - {"iPhone3,2", "n90bap", 0x04, 0x8930, "iPhone 4 (GSM) R2 2012", "iPhone 4"}, - {"iPhone3,3", "n92ap", 0x06, 0x8930, "iPhone 4 (CDMA)", "iPhone 4"}, - {"iPhone4,1", "n94ap", 0x08, 0x8940, "iPhone 4s", "iPhone 4s"}, - {"iPhone5,1", "n41ap", 0x00, 0x8950, "iPhone 5 (GSM)", "iPhone 5"}, - {"iPhone5,2", "n42ap", 0x02, 0x8950, "iPhone 5 (Global)", "iPhone 5"}, - {"iPhone5,3", "n48ap", 0x0a, 0x8950, "iPhone 5c (GSM)", "iPhone 5c"}, - {"iPhone5,4", "n49ap", 0x0e, 0x8950, "iPhone 5c (Global)", "iPhone 5c"}, - {"iPhone6,1", "n51ap", 0x00, 0x8960, "iPhone 5s (GSM)", "iPhone 5s"}, - {"iPhone6,2", "n53ap", 0x02, 0x8960, "iPhone 5s (Global)", "iPhone 5s"}, - {"iPhone7,1", "n56ap", 0x04, 0x7000, "iPhone 6 Plus", "iPhone 6 Plus"}, - {"iPhone7,2", "n61ap", 0x06, 0x7000, "iPhone 6", "iPhone 6"}, - {"iPhone8,1", "n71ap", 0x04, 0x8000, "iPhone 6s", "iPhone 6s"}, - {"iPhone8,1", "n71map", 0x04, 0x8003, "iPhone 6s", "iPhone 6s"}, - {"iPhone8,2", "n66ap", 0x06, 0x8000, "iPhone 6s Plus", "iPhone 6s Plus"}, - {"iPhone8,2", "n66map", 0x06, 0x8003, "iPhone 6s Plus", "iPhone 6s Plus"}, - {"iPhone8,4", "n69ap", 0x02, 0x8003, "iPhone SE (1st gen)", "iPhone SE"}, - {"iPhone8,4", "n69uap", 0x02, 0x8000, "iPhone SE (1st gen)", "iPhone SE"}, - {"iPhone9,1", "d10ap", 0x08, 0x8010, "iPhone 7 (Global)", "iPhone 7"}, - {"iPhone9,2", "d11ap", 0x0a, 0x8010, "iPhone 7 Plus (Global)", - "iPhone 7 Plus"}, - {"iPhone9,3", "d101ap", 0x0c, 0x8010, "iPhone 7 (GSM)", "iPhone 7"}, - {"iPhone9,4", "d111ap", 0x0e, 0x8010, "iPhone 7 Plus (GSM)", - "iPhone 7 Plus"}, - {"iPhone10,1", "d20ap", 0x02, 0x8015, "iPhone 8 (Global)", "iPhone 8"}, - {"iPhone10,2", "d21ap", 0x04, 0x8015, "iPhone 8 Plus (Global)", - "iPhone 8 Plus"}, - {"iPhone10,3", "d22ap", 0x06, 0x8015, "iPhone X (Global)", "iPhone X"}, - {"iPhone10,4", "d201ap", 0x0a, 0x8015, "iPhone 8 (GSM)", "iPhone 8"}, - {"iPhone10,5", "d211ap", 0x0c, 0x8015, "iPhone 8 Plus (GSM)", - "iPhone 8 Plus"}, - {"iPhone10,6", "d221ap", 0x0e, 0x8015, "iPhone X (GSM)", "iPhone X"}, - {"iPhone11,2", "d321ap", 0x0e, 0x8020, "iPhone XS", "iPhone XS"}, - {"iPhone11,4", "d331ap", 0x0a, 0x8020, "iPhone XS Max (China)", - "iPhone XS Max"}, - {"iPhone11,6", "d331pap", 0x1a, 0x8020, "iPhone XS Max", "iPhone XS Max"}, - {"iPhone11,8", "n841ap", 0x0c, 0x8020, "iPhone XR", "iPhone XR"}, - {"iPhone12,1", "n104ap", 0x04, 0x8030, "iPhone 11", "iPhone 11"}, - {"iPhone12,3", "d421ap", 0x06, 0x8030, "iPhone 11 Pro", "iPhone 11 Pro"}, - {"iPhone12,5", "d431ap", 0x02, 0x8030, "iPhone 11 Pro Max", - "iPhone 11 Pro Max"}, - {"iPhone12,8", "d79ap", 0x10, 0x8030, "iPhone SE (2nd gen)", - "iPhone SE 2nd gen"}, - {"iPhone13,1", "d52gap", 0x0A, 0x8101, "iPhone 12 mini", "iPhone 12 mini"}, - {"iPhone13,2", "d53gap", 0x0C, 0x8101, "iPhone 12", "iPhone 12"}, - {"iPhone13,3", "d53pap", 0x0E, 0x8101, "iPhone 12 Pro", "iPhone 12 Pro"}, - {"iPhone13,4", "d54pap", 0x08, 0x8101, "iPhone 12 Pro Max", - "iPhone 12 Pro Max"}, - {"iPhone14,2", "d63ap", 0x0C, 0x8110, "iPhone 13 Pro", "iPhone 13 Pro"}, - {"iPhone14,3", "d64ap", 0x0E, 0x8110, "iPhone 13 Pro Max", - "iPhone 13 Pro Max"}, - {"iPhone14,4", "d16ap", 0x08, 0x8110, "iPhone 13 mini", "iPhone 13 mini"}, - {"iPhone14,5", "d17ap", 0x0A, 0x8110, "iPhone 13", "iPhone 13"}, - {"iPhone14,6", "d49ap", 0x10, 0x8110, "iPhone SE (3rd gen)", - "iPhone SE 3rd gen"}, - {"iPhone14,7", "d27ap", 0x18, 0x8110, "iPhone 14", "iPhone 14"}, - {"iPhone14,8", "d28ap", 0x1A, 0x8110, "iPhone 14 Plus", "iPhone 14 Plus"}, - {"iPhone15,2", "d73ap", 0x0C, 0x8120, "iPhone 14 Pro", "iPhone 14 Pro"}, - {"iPhone15,3", "d74ap", 0x0E, 0x8120, "iPhone 14 Pro Max", - "iPhone 14 Pro Max"}, - {"iPhone15,4", "d37ap", 0x08, 0x8120, "iPhone 15", "iPhone 15"}, - {"iPhone15,5", "d38ap", 0x0A, 0x8120, "iPhone 15 Plus", "iPhone 15 Plus"}, - {"iPhone16,1", "d83ap", 0x04, 0x8130, "iPhone 15 Pro", "iPhone 15 Pro"}, - {"iPhone16,2", "d84ap", 0x06, 0x8130, "iPhone 15 Pro Max", - "iPhone 15 Pro Max"}, - {"iPhone17,1", "d93ap", 0x0C, 0x8140, "iPhone 16 Pro", "iPhone 16 Pro"}, - {"iPhone17,2", "d94ap", 0x0E, 0x8140, "iPhone 16 Pro Max", - "iPhone 16 Pro Max"}, - {"iPhone17,3", "d47ap", 0x08, 0x8140, "iPhone 16", "iPhone 16"}, - {"iPhone17,4", "d48ap", 0x0A, 0x8140, "iPhone 16 Plus", "iPhone 16 Plus"}, - {"iPhone17,5", "v59ap", 0x04, 0x8140, "iPhone 16e", "iPhone 16e"}, - {"iPhone18,1", "v53ap", 0x0C, 0x8150, "iPhone 17 Pro", "iPhone 17 Pro"}, - {"iPhone18,2", "v54ap", 0x0E, 0x8150, "iPhone 17 Pro Max", - "iPhone 17 Pro Max"}, - {"iPhone18,3", "v57ap", 0x08, 0x8150, "iPhone 17", "iPhone 17"}, - {"iPhone18,4", "d23ap", 0x0A, 0x8150, "iPhone Air", "iPhone Air"}, - /* iPod */ - {"iPod1,1", "n45ap", 0x02, 0x8900, "iPod Touch (1st gen)", - "iPod Touch 1st gen"}, - {"iPod2,1", "n72ap", 0x00, 0x8720, "iPod Touch (2nd gen)", - "iPod Touch 2nd gen"}, - {"iPod3,1", "n18ap", 0x02, 0x8922, "iPod Touch (3rd gen)", - "iPod Touch 3rd gen"}, - {"iPod4,1", "n81ap", 0x08, 0x8930, "iPod Touch (4th gen)", - "iPod Touch 4th gen"}, - {"iPod5,1", "n78ap", 0x00, 0x8942, "iPod Touch (5th gen)", - "iPod Touch 5th gen"}, - {"iPod7,1", "n102ap", 0x10, 0x7000, "iPod Touch (6th gen)", - "iPod Touch 6th gen"}, - {"iPod9,1", "n112ap", 0x16, 0x8010, "iPod Touch (7th gen)", - "iPod Touch 7th gen"}, - /* iPad */ - {"iPad1,1", "k48ap", 0x02, 0x8930, "iPad"}, - {"iPad2,1", "k93ap", 0x04, 0x8940, "iPad 2 (WiFi)", "iPad 2"}, - {"iPad2,2", "k94ap", 0x06, 0x8940, "iPad 2 (GSM)", "iPad 2"}, - {"iPad2,3", "k95ap", 0x02, 0x8940, "iPad 2 (CDMA)", "iPad 2"}, - {"iPad2,4", "k93aap", 0x06, 0x8942, "iPad 2 (WiFi) R2 2012", "iPad 2"}, - {"iPad2,5", "p105ap", 0x0a, 0x8942, "iPad mini (WiFi)", "iPad mini"}, - {"iPad2,6", "p106ap", 0x0c, 0x8942, "iPad mini (GSM)", "iPad mini"}, - {"iPad2,7", "p107ap", 0x0e, 0x8942, "iPad mini (Global)", "iPad mini"}, - {"iPad3,1", "j1ap", 0x00, 0x8945, "iPad (3rd gen, WiFi)", "iPad (3rd gen)"}, - {"iPad3,2", "j2ap", 0x02, 0x8945, "iPad (3rd gen, CDMA)", "iPad (3rd gen)"}, - {"iPad3,3", "j2aap", 0x04, 0x8945, "iPad (3rd gen, GSM)", "iPad (3rd gen)"}, - {"iPad3,4", "p101ap", 0x00, 0x8955, "iPad (4th gen, WiFi)", - "iPad (4th gen)"}, - {"iPad3,5", "p102ap", 0x02, 0x8955, "iPad (4th gen, GSM)", - "iPad (4th gen)"}, - {"iPad3,6", "p103ap", 0x04, 0x8955, "iPad (4th gen, Global)", - "iPad (4th gen)"}, - {"iPad4,1", "j71ap", 0x10, 0x8960, "iPad Air (WiFi)", "iPad Air"}, - {"iPad4,2", "j72ap", 0x12, 0x8960, "iPad Air (Cellular)", "iPad Air"}, - {"iPad4,3", "j73ap", 0x14, 0x8960, "iPad Air (China)", "iPad Air"}, - {"iPad4,4", "j85ap", 0x0a, 0x8960, "iPad mini 2 (WiFi)", "iPad mini 2"}, - {"iPad4,5", "j86ap", 0x0c, 0x8960, "iPad mini 2 (Cellular)", "iPad mini 2"}, - {"iPad4,6", "j87ap", 0x0e, 0x8960, "iPad mini 2 (China)", "iPad mini 2"}, - {"iPad4,7", "j85map", 0x32, 0x8960, "iPad mini 3 (WiFi)", "iPad mini 3"}, - {"iPad4,8", "j86map", 0x34, 0x8960, "iPad mini 3 (Cellular)", - "iPad mini 3"}, - {"iPad4,9", "j87map", 0x36, 0x8960, "iPad mini 3 (China)", "iPad mini 3"}, - {"iPad5,1", "j96ap", 0x08, 0x7000, "iPad mini 4 (WiFi)", "iPad mini 4"}, - {"iPad5,2", "j97ap", 0x0A, 0x7000, "iPad mini 4 (Cellular)", "iPad mini 4"}, - {"iPad5,3", "j81ap", 0x06, 0x7001, "iPad Air 2 (WiFi)", "iPad Air 2"}, - {"iPad5,4", "j82ap", 0x02, 0x7001, "iPad Air 2 (Cellular)", "iPad Air 2"}, - {"iPad6,3", "j127ap", 0x08, 0x8001, "iPad Pro 9.7-inch (WiFi)", - "iPad Pro 9.7-inch"}, - {"iPad6,4", "j128ap", 0x0a, 0x8001, "iPad Pro 9.7-inch (Cellular)", - "iPad Pro 9.7-inch"}, - {"iPad6,7", "j98aap", 0x10, 0x8001, "iPad Pro 12.9-inch (1st gen, WiFi)", - "iPad Pro 12.9-inch (1st gen)"}, - {"iPad6,8", "j99aap", 0x12, 0x8001, - "iPad Pro 12.9-inch (1st gen, Cellular)", "iPad Pro 12.9-inch (1st gen)"}, - {"iPad6,11", "j71sap", 0x10, 0x8000, "iPad (5th gen, WiFi)", - "iPad (5th gen)"}, - {"iPad6,11", "j71tap", 0x10, 0x8003, "iPad (5th gen, WiFi)", - "iPad (5th gen)"}, - {"iPad6,12", "j72sap", 0x12, 0x8000, "iPad (5th gen, Cellular)", - "iPad (5th gen)"}, - {"iPad6,12", "j72tap", 0x12, 0x8003, "iPad (5th gen, Cellular)", - "iPad (5th gen)"}, - {"iPad7,1", "j120ap", 0x0C, 0x8011, "iPad Pro 12.9-inch (2nd gen, WiFi)", - "iPad Pro 12.9-inch (2nd gen)"}, - {"iPad7,2", "j121ap", 0x0E, 0x8011, - "iPad Pro 12.9-inch (2nd gen, Cellular)", "iPad Pro 12.9-inch (2nd gen)"}, - {"iPad7,3", "j207ap", 0x04, 0x8011, "iPad Pro 10.5-inch (WiFi)", - "iPad Pro 10.5-inch"}, - {"iPad7,4", "j208ap", 0x06, 0x8011, "iPad Pro 10.5-inch (Cellular)", - "iPad Pro 10.5-inch"}, - {"iPad7,5", "j71bap", 0x18, 0x8010, "iPad (6th gen, WiFi)", - "iPad (6th gen)"}, - {"iPad7,6", "j72bap", 0x1A, 0x8010, "iPad (6th gen, Cellular)", - "iPad (6th gen)"}, - {"iPad7,11", "j171ap", 0x1C, 0x8010, "iPad (7th gen, WiFi)", - "iPad (7th gen)"}, - {"iPad7,12", "j172ap", 0x1E, 0x8010, "iPad (7th gen, Cellular)", - "iPad (7th gen)"}, - {"iPad8,1", "j317ap", 0x0C, 0x8027, "iPad Pro 11-inch (1st gen, WiFi)", - "iPad Pro 11-inch (1st gen)"}, - {"iPad8,2", "j317xap", 0x1C, 0x8027, - "iPad Pro 11-inch (1st gen, WiFi, 1TB)"}, - {"iPad8,3", "j318ap", 0x0E, 0x8027, "iPad Pro 11-inch (1st gen, Cellular)"}, - {"iPad8,4", "j318xap", 0x1E, 0x8027, - "iPad Pro 11-inch (1st gen, Cellular, 1TB)"}, - {"iPad8,5", "j320ap", 0x08, 0x8027, "iPad Pro 12.9-inch (3rd gen, WiFi)"}, - {"iPad8,6", "j320xap", 0x18, 0x8027, - "iPad Pro 12.9-inch (3rd gen, WiFi, 1TB)"}, - {"iPad8,7", "j321ap", 0x0A, 0x8027, - "iPad Pro 12.9-inch (3rd gen, Cellular)"}, - {"iPad8,8", "j321xap", 0x1A, 0x8027, - "iPad Pro 12.9-inch (3rd gen, Cellular, 1TB)"}, - {"iPad8,9", "j417ap", 0x3C, 0x8027, "iPad Pro 11-inch (2nd gen, WiFi)"}, - {"iPad8,10", "j418ap", 0x3E, 0x8027, - "iPad Pro 11-inch (2nd gen, Cellular)"}, - {"iPad8,11", "j420ap", 0x38, 0x8027, "iPad Pro 12.9-inch (4th gen, WiFi)"}, - {"iPad8,12", "j421ap", 0x3A, 0x8027, - "iPad Pro 12.9-inch (4th gen, Cellular)"}, - {"iPad11,1", "j210ap", 0x14, 0x8020, "iPad mini (5th gen, WiFi)"}, - {"iPad11,2", "j211ap", 0x16, 0x8020, "iPad mini (5th gen, Cellular)"}, - {"iPad11,3", "j217ap", 0x1C, 0x8020, "iPad Air (3rd gen, WiFi)"}, - {"iPad11,4", "j218ap", 0x1E, 0x8020, "iPad Air (3rd gen, Cellular)"}, - {"iPad11,6", "j171aap", 0x24, 0x8020, "iPad (8th gen, WiFi)"}, - {"iPad11,7", "j172aap", 0x26, 0x8020, "iPad (8th gen, Cellular)"}, - {"iPad12,1", "j181ap", 0x18, 0x8030, "iPad (9th gen, WiFi)"}, - {"iPad12,2", "j182ap", 0x1A, 0x8030, "iPad (9th gen, Cellular)"}, - {"iPad13,1", "j307ap", 0x04, 0x8101, "iPad Air (4th gen, WiFi)"}, - {"iPad13,2", "j308ap", 0x06, 0x8101, "iPad Air (4th gen, Cellular)"}, - {"iPad13,4", "j517ap", 0x08, 0x8103, "iPad Pro 11-inch (3rd gen, WiFi)"}, - {"iPad13,5", "j517xap", 0x0A, 0x8103, - "iPad Pro 11-inch (3rd gen, WiFi, 2TB)"}, - {"iPad13,6", "j518ap", 0x0C, 0x8103, - "iPad Pro 11-inch (3rd gen, Cellular)"}, - {"iPad13,7", "j518xap", 0x0E, 0x8103, - "iPad Pro 11-inch (3rd gen, Cellular, 2TB)"}, - {"iPad13,8", "j522ap", 0x18, 0x8103, "iPad Pro 12.9-inch (5th gen, WiFi)"}, - {"iPad13,9", "j522xap", 0x1A, 0x8103, - "iPad Pro 12.9-inch (5th gen, WiFi, 2TB)"}, - {"iPad13,10", "j523ap", 0x1C, 0x8103, - "iPad Pro 12.9-inch (5th gen, Cellular)"}, - {"iPad13,11", "j523xap", 0x1E, 0x8103, - "iPad Pro 12.9-inch (5th gen, Cellular, 2TB)"}, - {"iPad13,16", "j407ap", 0x10, 0x8103, "iPad Air (5th gen, WiFi)"}, - {"iPad13,17", "j408ap", 0x12, 0x8103, "iPad Air (5th gen, Cellular)"}, - {"iPad13,18", "j271ap", 0x14, 0x8101, "iPad (10th gen, WiFi)"}, - {"iPad13,19", "j272ap", 0x16, 0x8101, "iPad (10th gen, Cellular)"}, - {"iPad14,1", "j310ap", 0x04, 0x8110, "iPad mini (6th gen, WiFi)"}, - {"iPad14,2", "j311ap", 0x06, 0x8110, "iPad mini (6th gen, Cellular)"}, - {"iPad14,3", "j617ap", 0x08, 0x8112, "iPad Pro 11-inch (4th gen, WiFi)"}, - {"iPad14,4", "j618ap", 0x0A, 0x8112, - "iPad Pro 11-inch (4th gen, Cellular)"}, - {"iPad14,5", "j620ap", 0x0C, 0x8112, "iPad Pro 12.9-inch (6th gen, WiFi)"}, - {"iPad14,6", "j621ap", 0x0E, 0x8112, - "iPad Pro 12.9-inch (6th gen, Cellular)"}, - {"iPad14,8", "j507ap", 0x10, 0x8112, "iPad Air 11-inch (M2, WiFi)"}, - {"iPad14,9", "j508ap", 0x12, 0x8112, "iPad Air 11-inch (M2, Cellular)"}, - {"iPad14,10", "j537ap", 0x14, 0x8112, "iPad Air 13-inch (M2, WiFi)"}, - {"iPad14,11", "j538ap", 0x16, 0x8112, "iPad Air 13-inch (M2, Cellular)"}, - {"iPad15,3", "j607ap", 0x08, 0x8122, "iPad Air 11-inch (M3, WiFi)"}, - {"iPad15,4", "j608ap", 0x0A, 0x8122, "iPad Air 11-inch (M3, Cellular)"}, - {"iPad15,5", "j637ap", 0x0C, 0x8122, "iPad Air 13-inch (M3, WiFi)"}, - {"iPad15,6", "j638ap", 0x0E, 0x8122, "iPad Air 13-inch (M3, Cellular)"}, - {"iPad15,7", "j481ap", 0x10, 0x8120, "iPad (A16, WiFi)"}, - {"iPad15,8", "j482ap", 0x12, 0x8120, "iPad (A16, Cellular)"}, - {"iPad16,1", "j410ap", 0x08, 0x8130, "iPad mini (A17 Pro, WiFi)"}, - {"iPad16,2", "j411ap", 0x0A, 0x8130, "iPad mini (A17 Pro, Cellular)"}, - {"iPad16,3", "j717ap", 0x08, 0x8132, "iPad Pro 11-inch (M4, WiFi)"}, - {"iPad16,4", "j718ap", 0x0A, 0x8132, "iPad Pro 11-inch (M4, Cellular)"}, - {"iPad16,5", "j720ap", 0x0C, 0x8132, "iPad Pro 13-inch (M4, WiFi)"}, - {"iPad16,6", "j721ap", 0x0E, 0x8132, "iPad Pro 13-inch (M4, Cellular)"}, - /* Apple TV */ - {"AppleTV2,1", "k66ap", 0x10, 0x8930, "Apple TV 2"}, - {"AppleTV3,1", "j33ap", 0x08, 0x8942, "Apple TV 3"}, - {"AppleTV3,2", "j33iap", 0x00, 0x8947, "Apple TV 3 (2013)"}, - {"AppleTV5,3", "j42dap", 0x34, 0x7000, "Apple TV 4"}, - {"AppleTV6,2", "j105aap", 0x02, 0x8011, "Apple TV 4K"}, - {"AppleTV11,1", "j305ap", 0x08, 0x8020, "Apple TV 4K (2nd gen)"}, - {"AppleTV14,1", "j255ap", 0x02, 0x8110, "Apple TV 4K (3rd gen)"}, - /* HomePod */ - {"AudioAccessory1,1", "b238aap", 0x38, 0x7000, "HomePod (1st gen)"}, - {"AudioAccessory1,2", "b238ap", 0x1A, 0x7000, "HomePod (1st gen)"}, - {"AudioAccessory5,1", "b520ap", 0x22, 0x8006, "HomePod mini"}, - {"AudioAccessory6,1", "b620ap", 0x18, 0x8301, "HomePod (2nd gen)"}, - /* Apple Watch */ - {"Watch1,1", "n27aap", 0x02, 0x7002, "Apple Watch 38mm (1st gen)"}, - {"Watch1,2", "n28aap", 0x04, 0x7002, "Apple Watch 42mm (1st gen)"}, - {"Watch2,6", "n27dap", 0x02, 0x8002, "Apple Watch Series 1 (38mm)"}, - {"Watch2,7", "n28dap", 0x04, 0x8002, "Apple Watch Series 1 (42mm)"}, - {"Watch2,3", "n74ap", 0x0C, 0x8002, "Apple Watch Series 2 (38mm)"}, - {"Watch2,4", "n75ap", 0x0E, 0x8002, "Apple Watch Series 2 (42mm)"}, - {"Watch3,1", "n111sap", 0x1C, 0x8004, - "Apple Watch Series 3 (38mm Cellular)"}, - {"Watch3,2", "n111bap", 0x1E, 0x8004, - "Apple Watch Series 3 (42mm Cellular)"}, - {"Watch3,3", "n121sap", 0x18, 0x8004, "Apple Watch Series 3 (38mm)"}, - {"Watch3,4", "n121bap", 0x1A, 0x8004, "Apple Watch Series 3 (42mm)"}, - {"Watch4,1", "n131sap", 0x08, 0x8006, "Apple Watch Series 4 (40mm)"}, - {"Watch4,2", "n131bap", 0x0A, 0x8006, "Apple Watch Series 4 (44mm)"}, - {"Watch4,3", "n141sap", 0x0C, 0x8006, - "Apple Watch Series 4 (40mm Cellular)"}, - {"Watch4,4", "n141bap", 0x0E, 0x8006, - "Apple Watch Series 4 (44mm Cellular)"}, - {"Watch5,1", "n144sap", 0x10, 0x8006, "Apple Watch Series 5 (40mm)"}, - {"Watch5,2", "n144bap", 0x12, 0x8006, "Apple Watch Series 5 (44mm)"}, - {"Watch5,3", "n146sap", 0x14, 0x8006, - "Apple Watch Series 5 (40mm Cellular)"}, - {"Watch5,4", "n146bap", 0x16, 0x8006, - "Apple Watch Series 5 (44mm Cellular)"}, - {"Watch5,9", "n140sap", 0x28, 0x8006, "Apple Watch SE (40mm)"}, - {"Watch5,10", "n140bap", 0x2A, 0x8006, "Apple Watch SE (44mm)"}, - {"Watch5,11", "n142sap", 0x2C, 0x8006, "Apple Watch SE (40mm Cellular)"}, - {"Watch5,12", "n142bap", 0x2E, 0x8006, "Apple Watch SE (44mm Cellular)"}, - {"Watch6,1", "n157sap", 0x08, 0x8301, "Apple Watch Series 6 (40mm)"}, - {"Watch6,2", "n157bap", 0x0A, 0x8301, "Apple Watch Series 6 (44mm)"}, - {"Watch6,3", "n158sap", 0x0C, 0x8301, - "Apple Watch Series 6 (40mm Cellular)"}, - {"Watch6,4", "n158bap", 0x0E, 0x8301, - "Apple Watch Series 6 (44mm Cellular)"}, - {"Watch6,6", "n187sap", 0x10, 0x8301, "Apple Watch Series 7 (41mm)"}, - {"Watch6,7", "n187bap", 0x12, 0x8301, "Apple Watch Series 7 (45mm)"}, - {"Watch6,8", "n188sap", 0x14, 0x8301, - "Apple Watch Series 7 (41mm Cellular)"}, - {"Watch6,9", "n188bap", 0x16, 0x8301, - "Apple Watch Series 7 (45mm Cellular)"}, - {"Watch6,10", "n143sap", 0x28, 0x8301, "Apple Watch SE 2 (40mm)"}, - {"Watch6,11", "n143bap", 0x2A, 0x8301, "Apple Watch SE 2 (44mm)"}, - {"Watch6,12", "n149sap", 0x2C, 0x8301, "Apple Watch SE 2 (40mm Cellular)"}, - {"Watch6,13", "n149bap", 0x2E, 0x8301, "Apple Watch SE 2 (44mm Cellular)"}, - {"Watch6,14", "n197sap", 0x30, 0x8301, "Apple Watch Series 8 (41mm)"}, - {"Watch6,15", "n197bap", 0x32, 0x8301, "Apple Watch Series 8 (45mm)"}, - {"Watch6,16", "n198sap", 0x34, 0x8301, - "Apple Watch Series 8 (41mm Cellular)"}, - {"Watch6,17", "n198bap", 0x36, 0x8301, - "Apple Watch Series 8 (45mm Cellular)"}, - {"Watch6,18", "n199ap", 0x26, 0x8301, "Apple Watch Ultra"}, - {"Watch7,1", "n207sap", 0x08, 0x8310, "Apple Watch Series 9 (41mm)"}, - {"Watch7,2", "n207bap", 0x0A, 0x8310, "Apple Watch Series 9 (45mm)"}, - {"Watch7,3", "n208sap", 0x0C, 0x8310, - "Apple Watch Series 9 (41mm Cellular)"}, - {"Watch7,4", "n208bap", 0x0E, 0x8310, - "Apple Watch Series 9 (45mm Cellular)"}, - {"Watch7,5", "n210ap", 0x02, 0x8310, "Apple Watch Ultra 2"}, - {"Watch7,8", "n217sap", 0x10, 0x8310, "Apple Watch Series 10 (42mm)"}, - {"Watch7,9", "n217bap", 0x12, 0x8310, "Apple Watch Series 10 (46mm)"}, - {"Watch7,10", "n218sap", 0x14, 0x8310, - "Apple Watch Series 10 (42mm Cellular)"}, - {"Watch7,11", "n218bap", 0x16, 0x8310, - "Apple Watch Series 10 (46mm Cellular)"}, - {"Watch7,12", "n230ap", 0x22, 0x8310, "Apple Watch Ultra 3"}, - {"Watch7,13", "n243sap", 0x28, 0x8310, "Apple Watch SE 3 (40mm)"}, - {"Watch7,14", "n243bap", 0x2A, 0x8310, "Apple Watch SE 3 (44mm)"}, - {"Watch7,15", "n244sap", 0x2C, 0x8310, "Apple Watch SE 3 (40mm Cellular)"}, - {"Watch7,16", "n244bap", 0x2E, 0x8310, "Apple Watch SE 3 (44mm Cellular)"}, - {"Watch7,17", "n227sap", 0x18, 0x8310, "Apple Watch Series 11 (42mm)"}, - {"Watch7,18", "n227bap", 0x1A, 0x8310, "Apple Watch Series 11 (46mm)"}, - {"Watch7,19", "n228sap", 0x1C, 0x8310, - "Apple Watch Series 11 (42mm Cellular)"}, - {"Watch7,20", "n228bap", 0x1E, 0x8310, - "Apple Watch Series 11 (46mm Cellular)"}, - /* Apple Silicon Macs */ - {"ADP3,2", "j273aap", 0x42, 0x8027, "Developer Transition Kit (2020)"}, - {"Macmini9,1", "j274ap", 0x22, 0x8103, "Mac mini (M1, 2020)"}, - {"MacBookPro17,1", "j293ap", 0x24, 0x8103, - "MacBook Pro (M1, 13-inch, 2020)"}, - {"MacBookPro18,1", "j316sap", 0x0A, 0x6000, - "MacBook Pro (M1 Pro, 16-inch, 2021)"}, - {"MacBookPro18,2", "j316cap", 0x0A, 0x6001, - "MacBook Pro (M1 Max, 16-inch, 2021)"}, - {"MacBookPro18,3", "j314sap", 0x08, 0x6000, - "MacBook Pro (M1 Pro, 14-inch, 2021)"}, - {"MacBookPro18,4", "j314cap", 0x08, 0x6001, - "MacBook Pro (M1 Max, 14-inch, 2021)"}, - {"MacBookAir10,1", "j313ap", 0x26, 0x8103, "MacBook Air (M1, 2020)"}, - {"iMac21,1", "j456ap", 0x28, 0x8103, "iMac 24-inch (M1, Two Ports, 2021)"}, - {"iMac21,2", "j457ap", 0x2A, 0x8103, "iMac 24-inch (M1, Four Ports, 2021)"}, - {"Mac13,1", "j375cap", 0x04, 0x6001, "Mac Studio (M1 Max, 2022)"}, - {"Mac13,2", "j375dap", 0x0C, 0x6002, "Mac Studio (M1 Ultra, 2022)"}, - {"Mac14,2", "j413ap", 0x28, 0x8112, "MacBook Air (M2, 2022)"}, - {"Mac14,7", "j493ap", 0x2A, 0x8112, "MacBook Pro (M2, 13-inch, 2022)"}, - {"Mac14,3", "j473ap", 0x24, 0x8112, "Mac mini (M2, 2023)"}, - {"Mac14,5", "j414cap", 0x04, 0x6021, "MacBook Pro (14-inch, M2 Max, 2023)"}, - {"Mac14,6", "j416cap", 0x06, 0x6021, "MacBook Pro (16-inch, M2 Max, 2023)"}, - {"Mac14,8", "j180dap", 0x08, 0x6022, "Mac Pro (2023)"}, - {"Mac14,9", "j414sap", 0x04, 0x6020, "MacBook Pro (14-inch, M2 Pro, 2023)"}, - {"Mac14,10", "j416sap", 0x06, 0x6020, - "MacBook Pro (16-inch, M2 Pro, 2023)"}, - {"Mac14,12", "j474sap", 0x02, 0x6020, "Mac mini (M2 Pro, 2023)"}, - {"Mac14,13", "j475cap", 0x0A, 0x6021, "Mac Studio (M2 Max, 2023)"}, - {"Mac14,14", "j475dap", 0x0A, 0x6022, "Mac Studio (M2 Ultra, 2023)"}, - {"Mac14,15", "j415ap", 0x2E, 0x8112, "MacBook Air (M2, 15-inch, 2023)"}, - {"Mac15,3", "j504ap", 0x22, 0x8122, "MacBook Pro (14-inch, M3, Nov 2023)"}, - {"Mac15,4", "j433ap", 0x28, 0x8122, "iMac 24-inch (M3, Two Ports, 2023)"}, - {"Mac15,5", "j434ap", 0x2A, 0x8122, "iMac 24-inch (M3, Four Ports, 2023)"}, - {"Mac15,6", "j514sap", 0x04, 0x6030, - "MacBook Pro (14-inch, M3 Pro, Nov 2023)"}, - {"Mac15,7", "j516sap", 0x06, 0x6030, - "MacBook Pro (16-inch, M3 Pro, Nov 2023)"}, - {"Mac15,8", "j514cap", 0x44, 0x6031, - "MacBook Pro (14-inch, M3 Max, Nov 2023)"}, - {"Mac15,9", "j516cap", 0x46, 0x6031, - "MacBook Pro (16-inch, M3 Max, Nov 2023)"}, - {"Mac15,10", "j514map", 0x44, 0x6034, - "MacBook Pro (14-inch, M3 Max, Nov 2023)"}, - {"Mac15,11", "j516map", 0x46, 0x6034, - "MacBook Pro (16-inch, M3 Max, Nov 2023)"}, - {"Mac15,12", "j613ap", 0x30, 0x8122, "MacBook Air (13-inch, M3, 2024)"}, - {"Mac15,13", "j615ap", 0x32, 0x8122, "MacBook Air (15-inch, M3, 2024)"}, - {"Mac15,14", "j575dap", 0x44, 0x6032, "Mac Studio (M3 Ultra, 2025)"}, - {"Mac16,1", "j604ap", 0x22, 0x8132, "MacBook Pro (14-inch, M4, Nov 2024)"}, - {"Mac16,2", "j623ap", 0x24, 0x8132, "iMac 24-inch (M4, Two Ports, 2024)"}, - {"Mac16,3", "j624ap", 0x26, 0x8132, "iMac 24-inch (M4, Four Ports, 2024)"}, - {"Mac16,5", "j616cap", 0x06, 0x6041, - "MacBook Pro (16-inch, M4 Max, Nov 2024)"}, - {"Mac16,6", "j614cap", 0x04, 0x6041, - "MacBook Pro (14-inch, M4 Max, Nov 2024)"}, - {"Mac16,7", "j616sap", 0x06, 0x6040, - "MacBook Pro (16-inch, M4 Pro, Nov 2024)"}, - {"Mac16,8", "j614sap", 0x04, 0x6040, - "MacBook Pro (14-inch, M4 Pro, Nov 2024)"}, - {"Mac16,9", "j575cap", 0x02, 0x6041, "Mac Studio (M4 Max, 2025)"}, - {"Mac16,10", "j773gap", 0x2A, 0x8132, "Mac mini (M4, 2024)"}, - {"Mac16,11", "j773sap", 0x02, 0x6040, "Mac mini (M4 Pro, 2024)"}, - {"Mac16,12", "j713ap", 0x2C, 0x8132, "MacBook Air (13-inch, M4, 2025)"}, - {"Mac16,13", "j715ap", 0x2E, 0x8132, "MacBook Air (15-inch, M4, 2025)"}, - /* Apple Silicon VMs (supported by Virtualization.framework on macOS 12) */ - {"VirtualMac2,1", "vma2macosap", 0x20, 0xFE00, "Apple Virtual Machine 1"}, - /* Apple T2 Coprocessor */ - {"iBridge2,1", "j137ap", 0x0A, 0x8012, "Apple T2 iMacPro1,1 (j137)"}, - {"iBridge2,3", "j680ap", 0x0B, 0x8012, "Apple T2 MacBookPro15,1 (j680)"}, - {"iBridge2,4", "j132ap", 0x0C, 0x8012, "Apple T2 MacBookPro15,2 (j132)"}, - {"iBridge2,5", "j174ap", 0x0E, 0x8012, "Apple T2 Macmini8,1 (j174)"}, - {"iBridge2,6", "j160ap", 0x0F, 0x8012, "Apple T2 MacPro7,1 (j160)"}, - {"iBridge2,7", "j780ap", 0x07, 0x8012, "Apple T2 MacBookPro15,3 (j780)"}, - {"iBridge2,8", "j140kap", 0x17, 0x8012, "Apple T2 MacBookAir8,1 (j140k)"}, - {"iBridge2,10", "j213ap", 0x18, 0x8012, "Apple T2 MacBookPro15,4 (j213)"}, - {"iBridge2,12", "j140aap", 0x37, 0x8012, "Apple T2 MacBookAir8,2 (j140a)"}, - {"iBridge2,14", "j152fap", 0x3A, 0x8012, "Apple T2 MacBookPro16,1 (j152f)"}, - {"iBridge2,15", "j230kap", 0x3F, 0x8012, "Apple T2 MacBookAir9,1 (j230k)"}, - {"iBridge2,16", "j214kap", 0x3E, 0x8012, "Apple T2 MacBookPro16,2 (j214k)"}, - {"iBridge2,19", "j185ap", 0x22, 0x8012, "Apple T2 iMac20,1 (j185)"}, - {"iBridge2,20", "j185fap", 0x23, 0x8012, "Apple T2 iMac20,2 (j185f)"}, - {"iBridge2,21", "j223ap", 0x3B, 0x8012, "Apple T2 MacBookPro16,3 (j223)"}, - {"iBridge2,22", "j215ap", 0x38, 0x8012, "Apple T2 MacBookPro16,4 (j215)"}, - /* Apple Displays */ - {"AppleDisplay2,1", "j327ap", 0x22, 0x8030, "Studio Display"}, - /* Apple Vision Pro */ - {"RealityDevice14,1", "n301ap", 0x42, 0x8112, "Apple Vision Pro"}, - {NULL, NULL, -1, -1, NULL}}; - -const DeviceDatabaseInfo * -DeviceDatabase::findByIdentifier(const std::string &identifier) -{ - for (const auto &device : m_devices) { - if (device.modelIdentifier == nullptr) { - break; - } - if (identifier == device.modelIdentifier) { - return &device; - } - } - return nullptr; -} - -const DeviceDatabaseInfo * -DeviceDatabase::findByHwModel(const std::string &hwModel) -{ - for (const auto &device : m_devices) { - if (device.boardId == nullptr) { - break; - } - if (hwModel == device.boardId) { - return &device; - } - } - return nullptr; -} - -std::string DeviceDatabase::parseRegionInfo(const std::string &code) -{ - // North America - if (code == "LL/A") - return "United States, Canada"; - if (code == "LL") - return "United States, Canada"; - - // Latin America - if (code == "LA/A") - return "Latin America"; - if (code == "BR/A" || code == "BZ/A") - return "Brazil"; - if (code == "CL/A") - return "Chile"; - if (code == "CO/A") - return "Colombia"; - if (code == "MX/A") - return "Mexico"; - if (code == "AR/A") - return "Argentina"; - - // Asia Pacific - if (code == "J/A") - return "Japan"; - if (code == "KH/A") - return "Thailand, Cambodia"; - if (code == "MY/A") - return "Malaysia"; - if (code == "ZP/A") - return "Hong Kong, Macau"; - if (code == "CH/A") - return "China"; - if (code == "TA/A") - return "Taiwan"; - if (code == "KR/A") - return "Korea"; - if (code == "SG/A") - return "Singapore"; - if (code == "IN/A") - return "India"; - if (code == "TH/A") - return "Thailand"; - if (code == "VN/A") - return "Vietnam"; - if (code == "ID/A") - return "Indonesia"; - if (code == "PH/A") - return "Philippines"; - if (code == "NZ/A") - return "New Zealand"; - if (code == "AU/A" || code == "X/A") - return "Australia"; - - // Europe - if (code == "ZA/A") - return "South Africa"; - if (code == "AB/A") - return "Egypt, Jordan, Saudi Arabia, UAE"; - if (code == "AE/A") - return "United Arab Emirates"; - if (code == "B/A") - return "United Kingdom, Ireland"; - if (code == "FB/A") - return "France, Luxembourg"; - if (code == "FD/A") - return "Austria, Liechtenstein, Switzerland"; - if (code == "GR/A") - return "Greece"; - if (code == "HN/A") - return "India"; - if (code == "IP/A") - return "Italy"; - if (code == "KN/A") - return "Denmark, Norway"; - if (code == "KS/A") - return "Finland, Sweden"; - if (code == "LZ/A") - return "Paraguay, Uruguay"; - if (code == "MG/A") - return "Hungary"; - if (code == "PO/A") - return "Poland"; - if (code == "PP/A") - return "Philippines"; - if (code == "RO/A") - return "Romania"; - if (code == "RS/A") - return "Russia"; - if (code == "SL/A") - return "Slovakia"; - if (code == "SO/A") - return "South Africa"; - if (code == "T/A") - return "Italy"; - if (code == "TU/A") - return "Turkey"; - if (code == "Y/A") - return "Spain"; - if (code == "ZD/A") - return "Germany, Luxembourg"; - - // Middle East - if (code == "HB/A") - return "Israel"; - - // Canada - if (code == "C/A") - return "Canada (English, French)"; - - return code; -} \ No newline at end of file diff --git a/src/devicedatabase.h b/src/devicedatabase.h deleted file mode 100644 index 3d0ab50..0000000 --- a/src/devicedatabase.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVICEDATABASE_H -#define DEVICEDATABASE_H - -#include - -struct DeviceDatabaseInfo { - const char *modelIdentifier; - const char *boardId; - int boardNumber; - int chipId; - const char *marketingName; - const char *displayName; -}; - -class DeviceDatabase -{ -public: - DeviceDatabase() = delete; - - static const DeviceDatabaseInfo * - findByIdentifier(const std::string &identifier); - static const DeviceDatabaseInfo *findByHwModel(const std::string &hwModel); - static std::string parseRegionInfo(const std::string &code); - -private: - static const DeviceDatabaseInfo m_devices[]; -}; - -#endif // DEVICEDATABASE_H diff --git a/src/deviceimagewidget.cpp b/src/deviceimagewidget.cpp deleted file mode 100644 index d958bb8..0000000 --- a/src/deviceimagewidget.cpp +++ /dev/null @@ -1,341 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "deviceimagewidget.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include - -DeviceImageWidget::DeviceImageWidget( - const std::shared_ptr device, QWidget *parent) - : QWidget(parent), m_device(device) -{ - QVBoxLayout *layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - m_imageLabel = new ResponsiveQLabel(this); - m_imageLabel->setMinimumWidth(200); - m_imageLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - m_imageLabel->setStyleSheet("background: transparent; border: none;"); - - m_mockupName = getMockupNameFromDisplayName( - QString::fromStdString(m_device->deviceInfo.productType)); - layout->addWidget(m_imageLabel); - - setupDeviceImage(); - m_timeUpdateTimer = new QTimer(this); - connect(m_timeUpdateTimer, &QTimer::timeout, this, - &DeviceImageWidget::updateTime); - m_timeUpdateTimer->start(60000); // Update every minute - - updateTime(); -} - -DeviceImageWidget::~DeviceImageWidget() -{ - if (m_timeUpdateTimer) { - m_timeUpdateTimer->stop(); - } -} - -void DeviceImageWidget::setupDeviceImage() -{ - m_mockupPath = getDeviceMockupPath(); - m_wallpaperPath = getWallpaperPath(); - - qDebug() << "Using mockup:" << m_mockupPath; - qDebug() << "Using wallpaper:" << m_wallpaperPath; -} - -QString DeviceImageWidget::getDeviceMockupPath() const -{ - if (m_mockupName == "iPad") { - return QString(":/resources/ipad-mockups/ipad.png"); - } - if (m_mockupName == "unknown") { - return QString(":/resources/ipad-mockups/ipad.png"); - } - - return QString(":/resources/iphone-mockups/iphone-%1.png") - .arg(m_mockupName); -} - -QString DeviceImageWidget::getWallpaperPath() const -{ - int iosVersion = getIosVersionFromDevice(); - qDebug() << "Detected iOS version:" << iosVersion; - // Map iOS version to available wallpapers - QString wallpaperVersion; - - if (iosVersion <= 4) { - wallpaperVersion = "ios4"; - } else if (iosVersion == 5) { - wallpaperVersion = "ios5"; - } else if (iosVersion == 6) { - wallpaperVersion = "ios6"; - } else if (iosVersion == 7) { - wallpaperVersion = "ios7"; - } else if (iosVersion == 8) { - wallpaperVersion = "ios8"; - } else if (iosVersion == 9) { - wallpaperVersion = "ios9"; - } else if (iosVersion == 10) { - wallpaperVersion = "ios10"; - } else if (iosVersion == 11) { - wallpaperVersion = "ios11"; - } else if (iosVersion == 12) { - wallpaperVersion = "ios12"; - } else if (iosVersion == 13) { - wallpaperVersion = "ios13"; - } else if (iosVersion == 14) { - wallpaperVersion = "ios14"; - } else if (iosVersion == 15) { - wallpaperVersion = "ios15"; - } else if (iosVersion == 16) { - wallpaperVersion = "ios16"; - } else if (iosVersion == 17) { - wallpaperVersion = "ios17"; - } else if (iosVersion == 18) { - wallpaperVersion = "ios18"; - } else if (iosVersion == 26) { - wallpaperVersion = "ios26"; - } else { - // For future versions, use the latest available wallpaper - wallpaperVersion = "ios26"; - } - return QString(":/resources/ios-wallpapers/iphone-%1.png") - .arg(wallpaperVersion); -} - -QString DeviceImageWidget::getMockupNameFromDisplayName( - const QString &displayName) const -{ - if (displayName.contains("iPhone 16", Qt::CaseInsensitive) || - displayName.contains("iPhone 17", Qt::CaseInsensitive)) { - return "16"; - } else if (displayName.contains("iPhone 15", Qt::CaseInsensitive) || - displayName.contains("iPhone 14", Qt::CaseInsensitive)) { - return "15"; - } else if (displayName.contains("iPhone X", Qt::CaseInsensitive) || - displayName.contains("iPhone 11", Qt::CaseInsensitive) || - displayName.contains("iPhone 12", Qt::CaseInsensitive) || - displayName.contains("iPhone 13", Qt::CaseInsensitive)) { - return "x"; - } else if (displayName.contains("iPhone 6", Qt::CaseInsensitive) || - displayName.contains("iPhone 7", Qt::CaseInsensitive) || - displayName.contains("iPhone 8", Qt::CaseInsensitive) || - displayName.contains("iPhone SE 2nd", Qt::CaseInsensitive) || - displayName.contains("iPhone SE 3rd", Qt::CaseInsensitive)) { - return "6"; - } else if (displayName.contains("iPhone 5", Qt::CaseInsensitive) || - displayName.contains("iPhone SE", Qt::CaseInsensitive)) { - return "5"; - } else if (displayName.contains("iPhone 4", Qt::CaseInsensitive)) { - return "4"; - } else if (displayName.contains("iPhone 3", Qt::CaseInsensitive)) { - return "3"; - } else if (displayName.contains("iPad", Qt::CaseInsensitive)) { - return "iPad"; - } else { - return "unknown"; - } -} - -int DeviceImageWidget::getIosVersionFromDevice() const -{ - unsigned int version = m_device->deviceInfo.parsedDeviceVersion.major; - - if (version > 0) { - return version; - } - - return 0; -} - -/* - this method is only here to calculate the screen area - so that wallpaper perfectly fits to the screen size - it's costy so if you want to add a new mockup run - through this method qDebug the result and add it to createCompositeImage - example : screenRect = QRect(152, 79, 195, 296); -*/ -QRect DeviceImageWidget::findScreenArea(const QPixmap &mockup) const -{ - QImage image = mockup.toImage().convertToFormat(QImage::Format_ARGB32); - if (image.isNull()) { - return QRect(); - } - - int width = image.width(); - int height = image.height(); - int centerX = width / 2; - int centerY = height / 2; - - if (qAlpha(image.pixel(centerX, centerY)) != 0) { - qWarning() << "Cannot find screen area: center pixel is not " - "transparent. Falling back to default."; - return QRect(width * 0.1, height * 0.1, width * 0.8, height * 0.8); - } - - int left = centerX; - int right = centerX; - int top = centerY; - int bottom = centerY; - - // Scan left from center - while (left > 0 && qAlpha(image.pixel(left, centerY)) == 0) { - left--; - } - - // Scan right from center - while (right < width - 1 && qAlpha(image.pixel(right, centerY)) == 0) { - right++; - } - - // Scan up from center - while (top > 0 && qAlpha(image.pixel(centerX, top)) == 0) { - top--; - } - - // Scan down from center - while (bottom < height - 1 && qAlpha(image.pixel(centerX, bottom)) == 0) { - bottom++; - } - - return QRect(left + 1, top + 1, right - left - 2, bottom - top - 2); -} - -QPixmap DeviceImageWidget::createCompositeImage() const -{ - QPixmap mockup(m_mockupPath); - QPixmap wallpaper(m_wallpaperPath); - - if (mockup.isNull()) { - qWarning() << "Failed to load mockup:" << m_mockupPath; - return QPixmap(":/resources/iphone.png"); // Fallback - } - - if (wallpaper.isNull()) { - qWarning() << "Failed to load wallpaper:" << m_wallpaperPath; - return mockup; // Return just the mockup - } - - // Create composite with mockup dimensions - QPixmap composite(mockup.size()); - composite.fill(Qt::transparent); - - QPainter painter(&composite); - painter.setRenderHint(QPainter::Antialiasing); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - - QRect screenRect; - bool useRoundedCorners = false; - int cornerRadius = 35; - bool isUnknown = (m_mockupName == "unknown"); - - if (m_mockupName == "3") { - screenRect = QRect(145, 72, 209, 310); - } else if (m_mockupName == "4") { - screenRect = QRect(414, 181, 380, 548); - } else if (m_mockupName == "5") { - screenRect = QRect(27, 106, 304, 537); - } else if (m_mockupName == "6") { - screenRect = QRect(68, 348, 1279, 2270); - } else if (m_mockupName == "x") { - screenRect = QRect(245, 200, 2389, 5303); - useRoundedCorners = true; - } else if (m_mockupName == "15") { - screenRect = QRect(15, 10, 337, 730); - useRoundedCorners = true; - } else if (m_mockupName == "16") { - screenRect = QRect(17, 16, 333, 730); - useRoundedCorners = true; - } else if (m_mockupName == "iPad") { - screenRect = QRect(30, 30, 480, 690); - } else if (m_mockupName == "unknown") { - screenRect = QRect(33, 36, 471, 680); - } else { - screenRect = QRect(mockup.width() * 0.12, mockup.height() * 0.08, - mockup.width() * 0.76, mockup.height() * 0.84); - } - - QPixmap scaledWallpaper = wallpaper.scaled( - screenRect.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - - // Create a clipping path with rounded corners - if (useRoundedCorners) { - QPainterPath clipPath; - clipPath.addRoundedRect(screenRect, cornerRadius, cornerRadius); - painter.setClipPath(clipPath); - } - - painter.drawPixmap(screenRect, scaledWallpaper); - - if (useRoundedCorners) { - painter.setClipping(false); - } - - painter.drawPixmap(0, 0, mockup); - - // Draw question mark for unknown devices - if (isUnknown) { - QFont questionFont; - questionFont.setFamily("SF Pro Display, Helvetica, Arial"); - int questionSize = screenRect.width() / 3; - questionFont.setPointSize(questionSize); - questionFont.setWeight(QFont::Bold); - painter.setFont(questionFont); - - // Question mark shadow - painter.setPen(QColor(0, 0, 0, 150)); - painter.drawText(screenRect.adjusted(3, 3, 3, 3), Qt::AlignCenter, "?"); - - // Question mark main - painter.setPen(QColor(255, 255, 255, 255)); - painter.drawText(screenRect, Qt::AlignCenter, "?"); - } - - QString currentTime = QDateTime::currentDateTime().toString("hh:mm"); - - QFont timeFont; -#ifndef WIN32 - timeFont.setFamily("SF Pro Display, Helvetica, Arial"); -#else - timeFont.setFamily("Segoe UI"); -#endif - int fontSize = screenRect.width() / 5; - timeFont.setPointSize(fontSize); - timeFont.setWeight(QFont::Light); - painter.setFont(timeFont); - - painter.setPen(QColor(255, 255, 255, 255)); - painter.drawText(screenRect, Qt::AlignCenter, currentTime); - - painter.end(); - return composite; -} - -void DeviceImageWidget::updateTime() -{ - QPixmap composite = createCompositeImage(); - m_imageLabel->setPixmap(composite); -} \ No newline at end of file diff --git a/src/deviceimagewidget.h b/src/deviceimagewidget.h deleted file mode 100644 index 4955e3f..0000000 --- a/src/deviceimagewidget.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVICEIMAGEWIDGET_H -#define DEVICEIMAGEWIDGET_H - -#include "iDescriptor.h" -#include "responsiveqlabel.h" -#include -#include - -class DeviceImageWidget : public QWidget -{ - Q_OBJECT - -public: - explicit DeviceImageWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - ~DeviceImageWidget(); - -private slots: - void updateTime(); - -private: - QString m_mockupName; - void setupDeviceImage(); - QString getDeviceMockupPath() const; - QString getWallpaperPath() const; - QString getMockupNameFromDisplayName(const QString &displayName) const; - int getIosVersionFromDevice() const; - QPixmap createCompositeImage() const; - QRect findScreenArea(const QPixmap &mockup) const; - - const std::shared_ptr m_device; - ResponsiveQLabel *m_imageLabel; - QTimer *m_timeUpdateTimer; - - QString m_mockupPath; - QString m_wallpaperPath; -}; - -#endif // DEVICEIMAGEWIDGET_H \ No newline at end of file diff --git a/src/deviceinfowidget.cpp b/src/deviceinfowidget.cpp deleted file mode 100644 index 5577cd4..0000000 --- a/src/deviceinfowidget.cpp +++ /dev/null @@ -1,413 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "deviceinfowidget.h" -#include "batterywidget.h" -#include "diskusagewidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "infolabel.h" -#include "privateinfolabel.h" -#include "toolboxwidget.h" - -DeviceInfoWidget::DeviceInfoWidget( - const std::shared_ptr device, QWidget *parent) - : QWidget(parent), m_device(device) -{ - // Main layout with horizontal orientation - QHBoxLayout *mainLayout = new QHBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 10, 0); - mainLayout->setSpacing(1); - - // Left side container for image and actions - QWidget *leftContainer = new QWidget(); - // leftContainer->setStyleSheet("margin-left: 100px"); - QVBoxLayout *leftLayout = new QVBoxLayout(leftContainer); - leftLayout->setContentsMargins(0, 0, 0, 0); - leftLayout->setSpacing(1); - - // Create responsive device image widget - m_deviceImageWidget = new DeviceImageWidget(device, this); - - // Actions group box - QWidget *actionsWidget = new QWidget(); - actionsWidget->setObjectName("actionsWidget"); - actionsWidget->setFixedHeight(40); - actionsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - actionsWidget->setStyleSheet( - "QWidget#actionsWidget { background: transparent; border: none; }"); - QHBoxLayout *actionsLayout = new QHBoxLayout(actionsWidget); - actionsLayout->setContentsMargins(1, 1, 1, 1); - actionsLayout->setSpacing(10); - - ZIconWidget *shutdownBtn = new ZIconWidget( - QIcon(":/resources/icons/IcOutlinePowerSettingsNew.png"), "Shutdown", - 1.0, this); - shutdownBtn->setIconSize(QSize(20, 20)); - connect(shutdownBtn, &ZIconWidget::clicked, this, - [device]() { ToolboxWidget::shutdownDevice(device); }); - - ZIconWidget *restartBtn = - new ZIconWidget(QIcon(":/resources/icons/IcTwotoneRestartAlt.png"), - "Restart", 1.0, this); - restartBtn->setIconSize(QSize(20, 20)); - connect(restartBtn, &ZIconWidget::clicked, this, - [device]() { ToolboxWidget::restartDevice(device); }); - - ZIconWidget *recoveryBtn = - new ZIconWidget(QIcon(":/resources/icons/HugeiconsWrench01.png"), - "Recovery", 1.0, this); - recoveryBtn->setIconSize(QSize(20, 20)); - connect(recoveryBtn, &ZIconWidget::clicked, this, - [device]() { ToolboxWidget::enterRecoveryMode(device); }); - - actionsLayout->addWidget(shutdownBtn); - actionsLayout->addWidget(restartBtn); - actionsLayout->addWidget(recoveryBtn); - - leftLayout->addStretch(); - leftLayout->addWidget(m_deviceImageWidget); - leftLayout->addWidget(actionsWidget, 0, Qt::AlignCenter); - leftLayout->addStretch(); - - // Add stretches around leftContainer to center it horizontally - mainLayout->addStretch(); - mainLayout->addWidget(leftContainer); - mainLayout->addStretch(); - - // Right side: Info Table - QWidget *infoContainer = new QWidget(); - infoContainer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - - QVBoxLayout *infoLayout = new QVBoxLayout(infoContainer); - - // Header - QGroupBox *headerWidget = new QGroupBox(); - QHBoxLayout *headerLayout = new QHBoxLayout(headerWidget); - headerLayout->setContentsMargins(5, 5, 5, 5); - headerLayout->setSpacing(15); - - QLabel *devProductType = - new QLabel(QString::fromStdString(device->deviceInfo.productType)); - devProductType->setToolTip( - QString::fromStdString(device->deviceInfo.marketingName)); -#ifndef WIN32 - devProductType->setStyleSheet("font-size: 1rem; font-weight: bold;"); -#else - devProductType->setStyleSheet( - mergeStyles(devProductType, "font-size: 18px; font-weight: 500;")); -#endif - - QLabel *diskCapacityLabel = new QLabel( - QString::number(device->deviceInfo.diskInfo.totalDiskCapacity / - (1000 * 1000 * 1000)) + - " GB"); - - diskCapacityLabel->setSizePolicy(QSizePolicy::Maximum, - QSizePolicy::Preferred); - diskCapacityLabel->setAttribute(Qt::WA_StyledBackground, true); - diskCapacityLabel->setStyleSheet(QString("background-color: %1;" - "padding: 2px 4px;" - "color : white;" - "border-radius: 13px;") - .arg(COLOR_ACCENT_BLUE.name())); - - m_chargingStatusLabel = - new QLabel(device->deviceInfo.batteryInfo.isCharging ? "Charging" - : "Not Charging"); - // Create the layout without a parent widget - QHBoxLayout *chargingLayout = new QHBoxLayout(); - chargingLayout->setContentsMargins(0, 0, 0, 0); - chargingLayout->setSpacing(5); - - // Create icon label - m_lightningIconLabel = - new ZIconLabel(QIcon(":/resources/icons/MdiLightningBolt.png"), - " Charging", 1.0, this); - - m_batteryWidget = new BatteryWidget( - qBound(1, device->deviceInfo.batteryInfo.currentBatteryLevel, 100), - device->deviceInfo.batteryInfo.isCharging, this); - - // Add the widgets to the new layout - chargingLayout->addWidget(m_chargingStatusLabel); - chargingLayout->addWidget(m_lightningIconLabel); - chargingLayout->addWidget(m_batteryWidget); - - m_chargingWattsWithCableTypeLabel = new QLabel(); - - updateChargingStatus(); - - headerLayout->addWidget(devProductType); - headerLayout->addWidget(diskCapacityLabel); - headerLayout->addStretch(); // Push items to the left - headerLayout->addLayout(chargingLayout); - headerLayout->addWidget(m_chargingWattsWithCableTypeLabel); - - infoLayout->addWidget(headerWidget); - // add spacer - infoLayout->addSpacerItem( - new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding)); - // Add maximum stretch between header and grid - infoLayout->addStretch(); - - QGroupBox *gridContainer = new QGroupBox("Device Information"); - gridContainer->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Preferred); - QGridLayout *gridLayout = new QGridLayout(); // Set layout on gridWidget - gridLayout->setSpacing(8); - gridLayout->setColumnStretch(1, 1); // Allow value column to stretch - gridLayout->setColumnStretch( - 3, 1); // Allow value column for right side to stretch - gridLayout->setContentsMargins(17, 17, 17, 17); - gridContainer->setLayout(gridLayout); - QList> infoItems; - - auto createValueLabel = [](const QString &text) { - return new InfoLabel(text); - }; - - infoItems.append({"iOS Version:", createValueLabel(QString::fromStdString( - device->deviceInfo.productVersion))}); - infoItems.append({"Device Name:", createValueLabel(QString::fromStdString( - device->deviceInfo.deviceName))}); - - // Activation state label with color and tooltip - QLabel *activationLabel = new QLabel; - QString stateText; - QString tooltipText; - QColor color; - - switch (device->deviceInfo.activationState) { - case DeviceInfo::ActivationState::Activated: - stateText = "Activated"; - color = COLOR_GREEN; - tooltipText = "Device is activated and ready for use."; - break; - case DeviceInfo::ActivationState::FactoryActivated: - stateText = "Factory Activated"; - color = COLOR_ORANGE; - tooltipText = "Activation is most likely bypassed."; - break; - default: - stateText = "Unactivated"; - color = COLOR_RED; - tooltipText = "Device is not activated and requires setup."; - break; - } - - activationLabel->setText(stateText); - activationLabel->setStyleSheet("color: " + color.name() + ";"); - activationLabel->setToolTip(tooltipText); - infoItems.append({"Activation State:", activationLabel}); - - infoItems.append({"Device Class:", createValueLabel(QString::fromStdString( - device->deviceInfo.deviceClass))}); - infoItems.append( - {"Jailbroken:", createValueLabel(QString::fromStdString( - device->deviceInfo.jailbroken ? "Yes" : "No"))}); - infoItems.append({"Model Number:", createValueLabel(QString::fromStdString( - device->deviceInfo.modelNumber))}); - infoItems.append( - {"CPU Architecture:", createValueLabel(QString::fromStdString( - device->deviceInfo.cpuArchitecture))}); - infoItems.append({"Build Version:", createValueLabel(QString::fromStdString( - device->deviceInfo.buildVersion))}); - infoItems.append( - {"Hardware Model:", createValueLabel(QString::fromStdString( - device->deviceInfo.hardwareModel))}); - infoItems.append({"Region:", createValueLabel(QString::fromStdString( - device->deviceInfo.region))}); - infoItems.append( - {"Hardware Platform:", createValueLabel(QString::fromStdString( - device->deviceInfo.hardwarePlatform))}); - // infoItems.append( - // {"Battery Cycle:", createValueLabel(QString::number( - // m_device->deviceInfo.batteryInfo.cycleCount))}); - infoItems.append( - {"Firmware Version:", createValueLabel(QString::fromStdString( - device->deviceInfo.firmwareVersion))}); - - // FIXME: Battery Info - QWidget *batteryWidget = new QWidget(); - QHBoxLayout *batteryLayout = new QHBoxLayout(batteryWidget); - batteryLayout->setContentsMargins(0, 0, 0, 0); - batteryLayout->setSpacing(5); - batteryLayout->addWidget(new QLabel(device->deviceInfo.batteryInfo.health)); - QPushButton *moreButton = new QPushButton("More"); - moreButton->setStyleSheet(mergeStyles( - moreButton, - "QPushButton { height : 20px; min-height: 20px; padding: 2px " - "8px; font-size: 10px; }")); - connect(moreButton, &QPushButton::clicked, this, - &DeviceInfoWidget::onBatteryMoreClicked); - batteryLayout->addWidget(moreButton); - batteryLayout->addStretch(); - infoItems.append({"Battery Health:", batteryWidget}); - - infoItems.append( - {"Production Device:", - createValueLabel(QString::fromStdString( - device->deviceInfo.productionDevice ? "Yes" : "No"))}); - - // Serial Number with privacy - if (!device->deviceInfo.serialNumber.empty()) { - infoItems.append( - {"Serial Number:", - new PrivateInfoLabel( - QString::fromStdString(device->deviceInfo.serialNumber), - this)}); - } - - // IMEI with privacy (Mobile Equipment Identifier) - if (!device->deviceInfo.mobileEquipmentIdentifier.empty()) { - infoItems.append( - {"IMEI:", new PrivateInfoLabel( - QString::fromStdString( - device->deviceInfo.mobileEquipmentIdentifier), - this)}); - } - - // Distribute items into the grid - int numRows = (infoItems.size() + 1) / 2; - for (int i = 0; i < numRows; ++i) { - // Left column item - QLabel *keyLabelLeft = new QLabel(infoItems[i].first); -#ifndef WIN32 - keyLabelLeft->setStyleSheet("font-weight: bold;"); -#else - keyLabelLeft->setStyleSheet("font-size: 15px; font-weight: 500;"); -#endif - gridLayout->addWidget(keyLabelLeft, i, 0); - gridLayout->addWidget(infoItems[i].second, i, 1); - - // Right column item - int rightIndex = i + numRows; - if (rightIndex < infoItems.size()) { - QLabel *keyLabelRight = new QLabel(infoItems[rightIndex].first); -#ifndef WIN32 - keyLabelRight->setStyleSheet("font-weight: bold;"); -#else - keyLabelRight->setStyleSheet("font-size: 15px; font-weight: 500;"); -#endif - gridLayout->addWidget(keyLabelRight, i, 2); - gridLayout->addWidget(infoItems[rightIndex].second, i, 3); - } - } - - infoLayout->addWidget(gridContainer); - infoLayout->addStretch(); // Pushes footer to the bottom - - // Footer - QLabel *footerLabel = new QLabel("UDID: " + device->udid); - footerLabel->setToolTip("Unique Device Identifier"); - footerLabel->setStyleSheet( - "font-size: 10px; color: #666; margin-top: 5px; "); - footerLabel->setWordWrap(true); - infoLayout->addWidget(footerLabel); - - QVBoxLayout *rightSideLayout = new QVBoxLayout(); - rightSideLayout->setSpacing(10); - rightSideLayout->addStretch(); - - rightSideLayout->addWidget(infoContainer); - rightSideLayout->addWidget(new DiskUsageWidget(device, this)); - - rightSideLayout->addStretch(); - - mainLayout->addLayout(rightSideLayout); - mainLayout->addStretch(); - - connect(m_device->service_manager, - &CXX::ServiceManager::battery_info_updated, this, - &DeviceInfoWidget::updateBatteryInfo); -} - -DeviceInfoWidget::~DeviceInfoWidget() {} - -void DeviceInfoWidget::onBatteryMoreClicked() -{ - QMessageBox msgBox; - msgBox.setWindowTitle("Battery Details"); - QString details = - "Battery Cycle Count: " + - QString::number(m_device->deviceInfo.batteryInfo.cycleCount) + "\n" + - "Battery Serial Number: " + - QString::fromStdString(m_device->deviceInfo.batteryInfo.serialNumber); - msgBox.setText(details); - msgBox.exec(); -} - -void DeviceInfoWidget::updateBatteryInfo(const QString &diagnostics) -{ - /*DATA*/ - DeviceInfo &d = const_cast(m_device->deviceInfo); - pugi::xml_document doc; - auto res = doc.load_string(diagnostics.toUtf8().constData()); - if (!res) { - qDebug() << "Failed to parse battery info XML:"; - return; - } - auto xml_doc = XmlPlistDict(doc.child("plist").child("dict")); - if (d.oldDevice) - parseOldDeviceBattery(xml_doc, d); - else - parseDeviceBattery(xml_doc, d); - /*UI*/ - updateChargingStatus(); -} - -void DeviceInfoWidget::updateChargingStatus() -{ - auto &d = m_device->deviceInfo; - int watts = d.batteryInfo.watts; - - auto setValues = [this, d]() { - m_chargingWattsWithCableTypeLabel->setText( - QString::number(d.batteryInfo.watts) + "W" + "/" + - (d.batteryInfo.usbConnectionType == BatteryInfo::ConnectionType::USB - ? "USB" - : "USB-C")); - }; - - // watts can be 0 device is not charging - if (watts) { - setValues(); - } else if (d.isWireless) { - m_chargingWattsWithCableTypeLabel->setText("Wireless"); - } else { - setValues(); - } - - m_batteryWidget->updateContext( - d.batteryInfo.isCharging, - qBound(1, d.batteryInfo.currentBatteryLevel, 100)); - - if (m_device->deviceInfo.batteryInfo.isCharging) { - m_chargingStatusLabel->setText("Charging"); - m_chargingStatusLabel->setStyleSheet( - QString("color: %1;").arg(COLOR_GREEN.name())); - m_lightningIconLabel->show(); - - } else { - m_chargingStatusLabel->setText("Not Charging"); - m_chargingStatusLabel->setStyleSheet("margin-right: 5px;"); - m_lightningIconLabel->hide(); - } -} diff --git a/src/deviceinfowidget.h b/src/deviceinfowidget.h deleted file mode 100644 index 0851683..0000000 --- a/src/deviceinfowidget.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVICEINFOWIDGET_H -#define DEVICEINFOWIDGET_H -#include "batterywidget.h" -#include "deviceimagewidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class DeviceInfoWidget : public QWidget -{ - Q_OBJECT -public: - explicit DeviceInfoWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - ~DeviceInfoWidget(); - -private slots: - void onBatteryMoreClicked(); - -private: - const std::shared_ptr m_device; - void updateBatteryInfo(const QString &diagnostics); - void updateChargingStatus(); - QLabel *m_chargingStatusLabel; - QLabel *m_chargingWattsWithCableTypeLabel; - BatteryWidget *m_batteryWidget; - ZIconLabel *m_lightningIconLabel; - - DeviceImageWidget *m_deviceImageWidget; -}; - -#endif // DEVICEINFOWIDGET_H diff --git a/src/devicelistener.h b/src/devicelistener.h deleted file mode 100644 index 78d57c9..0000000 --- a/src/devicelistener.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -// #ifndef DEVICELISTENER_H -// #define DEVICELISTENER_H -// #include -// #include -// #include -// #include -// #include - -// class DeviceListener : public QObject -// { -// Q_OBJECT - -// public: -// DeviceListener(); -// ~DeviceListener(); - -// private slots: -// void clientConnected(); -// void clientDisconnected(); - -// private: -// void setupVirtualKeyboard(); - -// QLowEnergyAdvertisingData m_advertisingData; -// QLowEnergyServiceData m_hidServiceData; - -// QLowEnergyController *m_leController = nullptr; -// QLowEnergyService *m_service = nullptr; -// QLowEnergyCharacteristic -// m_inputReportChar; // Keep a reference to the characteristic -// }; - -// #endif // DEVICELISTENER_H diff --git a/src/devicemanagerwidget.cpp b/src/devicemanagerwidget.cpp deleted file mode 100644 index f2a8391..0000000 --- a/src/devicemanagerwidget.cpp +++ /dev/null @@ -1,383 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "devicemanagerwidget.h" - -DeviceManagerWidget::DeviceManagerWidget(QWidget *parent) - : QWidget(parent), m_currentDeviceUuid("") -{ - setupUI(); - - connect( - AppContext::sharedInstance(), &AppContext::deviceAdded, this, - [this](const std::shared_ptr device) { - addDevice(device); - - SettingsManager::sharedInstance()->doIfEnabled( - SettingsManager::Setting::AutoRaiseWindow, []() { - if (MainWindow *mainWindow = MainWindow::sharedInstance()) { - mainWindow->raiseDeviceTab(); - } - }); - - SettingsManager::sharedInstance()->doIfEnabled( - SettingsManager::Setting::SwitchToNewDevice, [this, device]() { - AppContext::sharedInstance()->setCurrentDeviceSelection( - DeviceSelection(device->udid)); - }); - - updateUI(); - }); - - connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, - [this](const QString &uuid) { - removeDevice(uuid); - auto devices = AppContext::sharedInstance()->getAllDevices(); - if (!devices.isEmpty()) - AppContext::sharedInstance()->setCurrentDeviceSelection( - DeviceSelection(devices.first()->udid)); - updateUI(); - }); - - connect(AppContext::sharedInstance(), &AppContext::devicePairPending, this, - [this](const QString &udid) { - addPendingDevice(udid, false); - updateUI(); - }); - - connect(AppContext::sharedInstance(), &AppContext::devicePasswordProtected, - this, [this](const QString &udid) { - addPendingDevice(udid, true); - updateUI(); - }); - - connect(AppContext::sharedInstance(), &AppContext::devicePaired, this, - [this](const std::shared_ptr device) { - addPairedDevice(device); - SettingsManager::sharedInstance()->doIfEnabled( - SettingsManager::Setting::SwitchToNewDevice, - [this, device]() { - AppContext::sharedInstance()->setCurrentDeviceSelection( - DeviceSelection(device->udid)); - }); - - updateUI(); - }); - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - connect(AppContext::sharedInstance(), &AppContext::recoveryDeviceAdded, - this, [this](const iDescriptorRecoveryDevice *recoveryDeviceInfo) { - addRecoveryDevice(recoveryDeviceInfo); - updateUI(); - }); - - connect(AppContext::sharedInstance(), &AppContext::recoveryDeviceRemoved, - this, [this](uint64_t ecid) { - removeRecoveryDevice(ecid); - updateUI(); - }); -#endif - - connect(AppContext::sharedInstance(), &AppContext::devicePairingExpired, - this, [this](const QString &udid) { - removePendingDevice(udid); - updateUI(); - }); - onDeviceSelectionChanged( - AppContext::sharedInstance()->getCurrentDeviceSelection()); -} - -void DeviceManagerWidget::setupUI() -{ - m_mainLayout = new QHBoxLayout(this); - m_mainLayout->setContentsMargins(0, 0, 0, 0); - m_mainLayout->setSpacing(0); - - m_noDevicesLabel = new QLabel("This is where devices will appear", this); - m_noDevicesLabel->setFont(QFont("", 20, QFont::Bold)); - m_noDevicesLabel->setAlignment(Qt::AlignCenter); - m_noDevicesLabel->setWordWrap(true); - - // Create sidebar - m_sidebar = new DeviceSidebarWidget(); - - // Create stacked widget for device content - m_stackedWidget = new QStackedWidget(); - - // Add to layout - m_mainLayout->addWidget(m_sidebar); - m_mainLayout->addWidget(m_stackedWidget); - - // Connect signals - connect(AppContext::sharedInstance(), - &AppContext::currentDeviceSelectionChanged, this, - &DeviceManagerWidget::onDeviceSelectionChanged); -} - -void DeviceManagerWidget::addDevice( - const std::shared_ptr device) -{ - if (m_deviceWidgets.contains(device->udid)) { - qWarning() << "Device already exists:" << device->udid; - return; - } - qDebug() << "Connect ::deviceAdded Adding:" << device->udid; - DeviceMenuWidget *deviceWidget = new DeviceMenuWidget(device, this); - - QString tabTitle = QString::fromStdString(device->deviceInfo.productType); - - m_stackedWidget->addWidget(deviceWidget); - m_deviceWidgets[device->udid] = std::pair{ - deviceWidget, m_sidebar->addDevice(tabTitle, device->udid, - device->deviceInfo.isWireless)}; -} - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT -void DeviceManagerWidget::addRecoveryDevice( - const iDescriptorRecoveryDevice *device) -{ - try { - // Create device info widget - RecoveryDeviceInfoWidget *recoveryDeviceInfoWidget = - new RecoveryDeviceInfoWidget(device); - m_recoveryDeviceWidgets.insert( - device->ecid, - std::pair{recoveryDeviceInfoWidget, - m_sidebar->addRecoveryDevice(device->ecid)}); - m_stackedWidget->addWidget(recoveryDeviceInfoWidget); - - } catch (...) { - qDebug() << "Error initializing recovery device"; - } -} - -void DeviceManagerWidget::removeRecoveryDevice(uint64_t ecid) -{ - qDebug() << "Removing recovery device with ECID:" << ecid; - if (!m_recoveryDeviceWidgets.contains(ecid)) { - qDebug() << "Recovery device with ECID" + QString::number(ecid) + - " not found. Please report this issue."; - return; - } - - RecoveryDeviceInfoWidget *deviceWidget = - m_recoveryDeviceWidgets[ecid].first; - RecoveryDeviceSidebarItem *sidebarItem = - m_recoveryDeviceWidgets[ecid].second; - - if (deviceWidget != nullptr && sidebarItem != nullptr) { - qDebug() << "Recovery device exists removing:" << QString::number(ecid); - - m_recoveryDeviceWidgets.remove(ecid); - m_stackedWidget->removeWidget(deviceWidget); - m_sidebar->removeRecoveryDevice(ecid); - deviceWidget->deleteLater(); - - emit updateNoDevicesConnected(); - } -} -#endif - -void DeviceManagerWidget::addPendingDevice(const QString &uniq, bool locked) -{ - qDebug() << "Adding pending device:" << uniq; - if (m_pendingDeviceWidgets.contains(uniq) && !locked) { - qDebug() << "Pending device already exists, moving to next state:" - << uniq; - m_pendingDeviceWidgets[uniq].first->next(); - return; - } else if (m_pendingDeviceWidgets.contains(uniq) && locked) { - // Already exists and still locked, do nothing - qDebug() - << "Pending device already exists and is locked, doing nothing:" - << uniq; - return; - } - - qDebug() << "Created pending widget for:" << uniq << "Locked:" << locked; - DevicePendingWidget *pendingWidget = new DevicePendingWidget(locked, this); - m_stackedWidget->addWidget(pendingWidget); - m_pendingDeviceWidgets[uniq] = - std::pair{pendingWidget, m_sidebar->addPendingDevice(uniq)}; -} - -void DeviceManagerWidget::removePendingDevice(const QString &udid) -{ - qDebug() << "Removing pending device:" << udid; - if (!m_pendingDeviceWidgets.contains(udid)) { - qDebug() << "Pending device not found:" << udid; - return; - } - DevicePendingWidget *deviceWidget = m_pendingDeviceWidgets[udid].first; - DevicePendingSidebarItem *sidebarItem = m_pendingDeviceWidgets[udid].second; - - if (deviceWidget != nullptr && sidebarItem != nullptr) { - qDebug() << "Pending device exists removing:" << udid; - m_pendingDeviceWidgets.remove(udid); - m_stackedWidget->removeWidget(deviceWidget); - m_sidebar->removePendingDevice(udid); - deviceWidget->deleteLater(); - } -} - -void DeviceManagerWidget::addPairedDevice( - const std::shared_ptr device) -{ - qDebug() << "Device paired:" << device->udid; - - // Check if pending device exists - if (m_pendingDeviceWidgets.contains(device->udid)) { - std::pair &pair = - m_pendingDeviceWidgets[device->udid]; - - // Remove from sidebar if it exists - if (pair.second) { - qDebug() << "Removing pending device from sidebar:" << device->udid; - m_sidebar->removePendingDevice(device->udid); - } - - // Clean up widget if it exists - if (pair.first) { - qDebug() << "Removing pending device widget:" << device->udid; - m_stackedWidget->removeWidget(pair.first); - pair.first->deleteLater(); - } - - m_pendingDeviceWidgets.remove(device->udid); - } - - addDevice(device); -} - -void DeviceManagerWidget::removeDevice(const QString &uuid) -{ - - qDebug() << "Removing:" << uuid; - DeviceMenuWidget *deviceWidget = m_deviceWidgets[uuid].first; - DeviceSidebarItem *sidebarItem = m_deviceWidgets[uuid].second; - - if (deviceWidget != nullptr && sidebarItem != nullptr) { - qDebug() << "Device exists removing:" << uuid; - // FIXME: cleanups - m_deviceWidgets.remove(uuid); - m_stackedWidget->removeWidget(deviceWidget); - m_sidebar->removeDevice(uuid); - deviceWidget->deleteLater(); - } -} - -void DeviceManagerWidget::setCurrentDevice(const QString &uuid) -{ - qDebug() << "Setting current device to:" << uuid; - if (m_currentDeviceUuid == uuid) - return; - - if (!m_deviceWidgets.contains(uuid)) { - qWarning() << "Device UUID not found:" << uuid; - return; - } - - m_currentDeviceUuid = uuid; - - QWidget *widget = m_deviceWidgets[uuid].first; - m_stackedWidget->setCurrentWidget(widget); -} - -QString DeviceManagerWidget::getCurrentDevice() const -{ - return m_currentDeviceUuid; -} - -void DeviceManagerWidget::onDeviceSelectionChanged( - const DeviceSelection &selection) -{ - // Update sidebar selection - m_sidebar->setCurrentSelection(selection); - - switch (selection.type) { - case DeviceSelection::Normal: - if (m_deviceWidgets.contains(selection.udid)) { - if (m_currentDeviceUuid != selection.udid) { - setCurrentDevice(selection.udid); - } - - // Handle navigation section - QWidget *tabWidget = m_deviceWidgets[selection.udid].first; - DeviceMenuWidget *deviceMenuWidget = - qobject_cast(tabWidget); - qDebug() << "Switching to tab:" << selection.section - << deviceMenuWidget; - if (deviceMenuWidget && !selection.section.isEmpty()) { - deviceMenuWidget->switchToTab(selection.section); - } - } - break; - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - case DeviceSelection::Recovery: - if (m_recoveryDeviceWidgets.contains(selection.ecid)) { - QWidget *tabWidget = m_recoveryDeviceWidgets[selection.ecid].first; - if (tabWidget) { - m_stackedWidget->setCurrentWidget(tabWidget); - // Clear current device since we're viewing recovery device - m_currentDeviceUuid = ""; - } - } - break; -#endif - - case DeviceSelection::Pending: - if (m_pendingDeviceWidgets.contains(selection.udid)) { - QWidget *tabWidget = m_pendingDeviceWidgets[selection.udid].first; - if (tabWidget) { - m_stackedWidget->setCurrentWidget(tabWidget); - // Clear current device since we're viewing pending device - m_currentDeviceUuid = ""; - } - } - break; - } -} - -void DeviceManagerWidget::updateUI() -{ - emit updateNoDevicesConnected(); - m_noDevicesLabel->setVisible( - AppContext::sharedInstance()->noDevicesConnected()); -} - -void DeviceManagerWidget::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - - if (!m_noDevicesLabel) - return; - - const int margin = 10; - int maxWidth = qMax(0, width() - 2 * margin); - m_noDevicesLabel->setMaximumWidth(maxWidth); - m_noDevicesLabel->adjustSize(); - - int x = (width() - m_noDevicesLabel->width()) / 2; - int y = (height() - m_noDevicesLabel->height()) / 2; - x = qMax(margin, x); - y = qMax(margin, y); - - m_noDevicesLabel->move(x, y); -} diff --git a/src/devicemanagerwidget.h b/src/devicemanagerwidget.h deleted file mode 100644 index 04c66c9..0000000 --- a/src/devicemanagerwidget.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVICEMANAGERWIDGET_H -#define DEVICEMANAGERWIDGET_H - -class DeviceMenuWidget; - -#include "devicemenuwidget.h" -#include "devicependingwidget.h" -#include "devicesidebarwidget.h" -#include "iDescriptor.h" -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT -#include "recoverydeviceinfowidget.h" -#endif -#include "appcontext.h" -#include "mainwindow.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include - -class DeviceManagerWidget : public QWidget -{ - Q_OBJECT - -public: - explicit DeviceManagerWidget(QWidget *parent = nullptr); - - void setCurrentDevice(const QString &uuid); - QString getCurrentDevice() const; - -signals: - void updateNoDevicesConnected(); - -private slots: - void onDeviceSelectionChanged(const DeviceSelection &selection); - -protected: - void resizeEvent(QResizeEvent *event) override; - -private: - void setupUI(); - void updateUI(); - void addDevice(const std::shared_ptr device); - void removeDevice(const QString &uuid); -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - void addRecoveryDevice(const iDescriptorRecoveryDevice *device); - void removeRecoveryDevice(uint64_t ecid); -#endif - void addPendingDevice(const QString &udid, bool locked); - void addPairedDevice(const std::shared_ptr device); - void removePendingDevice(const QString &udid); - - QHBoxLayout *m_mainLayout; - DeviceSidebarWidget *m_sidebar; - QStackedWidget *m_stackedWidget; - - QMap> - m_deviceWidgets; // Map to store devices by UDID - - QMap> - m_pendingDeviceWidgets; // Map to store devices by UDID - -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - QMap> - m_recoveryDeviceWidgets; // Map to store recovery devices by ECID -#endif - - QString m_currentDeviceUuid; - QLabel *m_noDevicesLabel = nullptr; -}; - -#endif // DEVICEMANAGERWIDGET_H \ No newline at end of file diff --git a/src/devicemenuwidget.cpp b/src/devicemenuwidget.cpp deleted file mode 100644 index f7db36c..0000000 --- a/src/devicemenuwidget.cpp +++ /dev/null @@ -1,161 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "devicemenuwidget.h" -#include "cableinfowidget.h" -#include "devdiskimageswidget.h" -#include "iDescriptor.h" -#include "livescreenwidget.h" -#include "qprocessindicator.h" -#include "querymobilegestaltwidget.h" -#include "virtuallocationwidget.h" -#include -#include -#include - -DeviceMenuWidget::DeviceMenuWidget( - const std::shared_ptr device, QWidget *parent) - : QWidget{parent}, m_device(device) -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - setContentsMargins(0, 0, 0, 0); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - stackedWidget = new QStackedWidget(this); - mainLayout->addWidget(stackedWidget); - - QProcessIndicator *loadingIndicator = new QProcessIndicator(); - loadingIndicator->setType(QProcessIndicator::line_rotate); - loadingIndicator->setFixedSize(64, 32); - - QWidget *loadingWidget = new QWidget(); - QVBoxLayout *loadingLayout = new QVBoxLayout(loadingWidget); - loadingLayout->setAlignment(Qt::AlignCenter); - loadingLayout->addWidget(loadingIndicator, 0, Qt::AlignCenter); - loadingIndicator->start(); - stackedWidget->addWidget(loadingWidget); - stackedWidget->setCurrentIndex(0); - - QTimer::singleShot(100, this, &DeviceMenuWidget::init); -} - -void DeviceMenuWidget::init() -{ - - // Create and add widgets to the stacked widget - m_deviceInfoWidget = new DeviceInfoWidget(m_device, this); - m_installedAppsWidget = new InstalledAppsWidget(m_device, this); - m_galleryWidget = new GalleryWidget(m_device, this); - m_fileExplorerWidget = new FileExplorerWidget(m_device, this); - - // Set minimum heights - m_galleryWidget->setMinimumHeight(300); - m_fileExplorerWidget->setMinimumHeight(300); - - stackedWidget->addWidget(m_deviceInfoWidget); // Index 0 - Info - stackedWidget->addWidget(m_installedAppsWidget); // Index 1 - Apps - stackedWidget->addWidget(m_galleryWidget); // Index 2 - Gallery - stackedWidget->addWidget(m_fileExplorerWidget); // Index 3 - Files - - // Set default to Info tab - stackedWidget->setCurrentWidget(m_deviceInfoWidget); - - // Connect to current changed signal for lazy loading - connect(stackedWidget, &QStackedWidget::currentChanged, this, - [this](int index) { - if (stackedWidget->widget(index) == - m_galleryWidget) { // Gallery tab - m_galleryWidget->load(); - } else if (stackedWidget->widget(index) == - m_fileExplorerWidget) { // Files tab - QTimer::singleShot( - 200, this, [this]() { m_fileExplorerWidget->init(); }); - } else if (stackedWidget->widget(index) == - m_installedAppsWidget) { // Apps tab - m_installedAppsWidget->init(); - } - }); - - QWidget *loadingWidget = stackedWidget->widget(0); - stackedWidget->removeWidget(loadingWidget); - loadingWidget->deleteLater(); - - // ═══════════════════════════════════════════════════════════════════════ - // Enable wireless connections for iOS 14+ devices - // (maybe supported on 13 too, untested) - // ═══════════════════════════════════════════════════════════════════════ - if (m_device->deviceInfo.parsedDeviceVersion.major >= 14) { - auto sm = SettingsManager::sharedInstance(); - - // Don't enable if - // auto-enable wifi connections is disabled - // we've already seen this device - // device is already wireless - if (!sm->autoEnableWifiConnections() || - sm->hasSeenDevice(m_device->udid) || - m_device->deviceInfo.isWireless) - return; - - connect( - m_device->service_manager, - &CXX::ServiceManager::enable_wifi_connections_result, this, - [this, sm](bool success) { - if (success) { - QMessageBox::information( - this, "Wireless connections enabled", - "You can now connect to this device wirelessly."); - } else { - QMessageBox::warning( - this, "Failed to enable wireless connections", - "Could not enable wireless connections for this " - "device."); - } - // FIXME: this could be a problem if - // depend on this value elsewhere, but it should be fine for - // now since it's only used for showing the warning about - // auto-enabling wifi connections - sm->setHasSeenDevice(m_device->udid, true); - }, - Qt::SingleShotConnection); - - m_device->service_manager->enable_wifi_connections(); - } -} - -void DeviceMenuWidget::switchToTab(const QString &tabName) -{ - if (tabName == "Info") { - stackedWidget->setCurrentWidget(m_deviceInfoWidget); - } else if (tabName == "Apps") { - stackedWidget->setCurrentWidget(m_installedAppsWidget); - } else if (tabName == "Gallery") { - qDebug() << "Switching to Gallery tab"; - stackedWidget->setCurrentWidget(m_galleryWidget); - } else if (tabName == "Files") { - stackedWidget->setCurrentWidget(m_fileExplorerWidget); - } else { - qDebug() << "Tab not found:" << tabName; - } -} - -DeviceMenuWidget::~DeviceMenuWidget() -{ - qDebug() << "DeviceMenuWidget destructor called"; -} diff --git a/src/devicemenuwidget.h b/src/devicemenuwidget.h deleted file mode 100644 index 65c6c6d..0000000 --- a/src/devicemenuwidget.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVICEMENUWIDGET_H -#define DEVICEMENUWIDGET_H -#include "deviceinfowidget.h" -#include "fileexplorerwidget.h" -#include "gallerywidget.h" -#include "iDescriptor.h" -#include "installedappswidget.h" -#include -#include - -class DeviceMenuWidget : public QWidget -{ - Q_OBJECT -public: - explicit DeviceMenuWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - void switchToTab(const QString &tabName); - void init(); - ~DeviceMenuWidget(); - -private: - QStackedWidget *stackedWidget; - const std::shared_ptr m_device; - DeviceInfoWidget *m_deviceInfoWidget; - InstalledAppsWidget *m_installedAppsWidget; - GalleryWidget *m_galleryWidget; - FileExplorerWidget *m_fileExplorerWidget; -signals: -}; - -#endif // DEVICEMENUWIDGET_H diff --git a/src/devicependingwidget.cpp b/src/devicependingwidget.cpp deleted file mode 100644 index 8386a5e..0000000 --- a/src/devicependingwidget.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "devicependingwidget.h" -#include "responsiveqlabel.h" -#include -#include - -DevicePendingWidget::DevicePendingWidget(bool locked, QWidget *parent) - : QWidget{parent}, m_label{nullptr}, m_imageLabel{nullptr}, m_locked{locked} -{ - QVBoxLayout *layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(5); - layout->addStretch(); - m_label = new QLabel( - m_locked ? "Please unlock the screen and click on trust on the popup" - : "Please click on trust on the popup", - this); - m_label->setWordWrap(true); - m_label->setAlignment(Qt::AlignCenter); - QFont font = m_label->font(); - font.setPointSize(18); - m_label->setFont(font); - layout->addWidget(m_label); - - m_imageLabel = new ResponsiveQLabel(this); - m_imageLabel->setPixmap(QPixmap(":/resources/trust.png")); - m_imageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - m_imageLabel->setMinimumSize(400, 200); - m_imageLabel->setScaledContents(true); - layout->addWidget(m_imageLabel, 1, Qt::AlignHCenter); - layout->addStretch(); - setLayout(layout); -} - -void DevicePendingWidget::next() -{ - m_label->setText("Please click on trust on the popup"); -} \ No newline at end of file diff --git a/src/devicependingwidget.h b/src/devicependingwidget.h deleted file mode 100644 index 06d5e46..0000000 --- a/src/devicependingwidget.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVICEPENDINGWIDGET_H -#define DEVICEPENDINGWIDGET_H - -#include -#include - -class ResponsiveQLabel; - -class DevicePendingWidget : public QWidget -{ - Q_OBJECT -public: - explicit DevicePendingWidget(bool locked, QWidget *parent); - void next(); -signals: -private: - QLabel *m_label; - ResponsiveQLabel *m_imageLabel; - bool m_locked; -}; - -#endif // DEVICEPENDINGWIDGET_H diff --git a/src/devicesidebarwidget.cpp b/src/devicesidebarwidget.cpp deleted file mode 100644 index ecd83a1..0000000 --- a/src/devicesidebarwidget.cpp +++ /dev/null @@ -1,595 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "devicesidebarwidget.h" -#include "appcontext.h" -#include "iDescriptor-ui.h" -#include "loadingspinnerwidget.h" -#include "qprocessindicator.h" -#include -#include - -// DeviceSidebarItem Implementation -DeviceSidebarItem::DeviceSidebarItem(const QString &deviceName, - const QString &uuid, bool isWireless, - QWidget *parent) - : QFrame(parent), m_deviceName(deviceName), m_uuid(uuid), m_selected(false), - m_wireless(isWireless), m_collapsed(false) -{ - setupUI(); - setFrameStyle(QFrame::StyledPanel); - setLineWidth(1); - updateToggleButton(); - setObjectName("DeviceSidebarItem"); -} - -void DeviceSidebarItem::setupUI() -{ - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(5, 5, 5, 5); - m_mainLayout->setSpacing(5); - - // Header section (always visible) - m_headerWidget = new ClickableWidget(); - QVBoxLayout *headerLayout = new QVBoxLayout(m_headerWidget); - headerLayout->setContentsMargins(0, 0, 0, 0); - headerLayout->setSpacing(2); - connect(m_headerWidget, &ClickableWidget::clicked, this, - [this]() { emit deviceSelected(m_uuid); }); - - // Device name label - QHBoxLayout *nameLayout = new QHBoxLayout(); - nameLayout->setContentsMargins(0, 0, 0, 0); - m_deviceLabel = new QLabel(m_deviceName); -#ifndef WIN32 - m_deviceLabel->setStyleSheet("QLabel { font-weight: bold; }"); -#endif - m_deviceLabel->setWordWrap(true); - nameLayout->addWidget(m_deviceLabel); - if (m_wireless) { - m_wirelessIcon = new ZIconLabel( - QIcon(":/resources/icons/QlementineIconsWireless116.png"), - "Wireless", 1.0, this); - nameLayout->setSpacing(5); - nameLayout->addWidget(m_wirelessIcon); - } - headerLayout->addLayout(nameLayout); - - // Toggle button - m_toggleButton = new QPushButton(); - m_toggleButton->setFlat(true); - m_toggleButton->setMaximumHeight(20); - m_toggleButton->setStyleSheet("QPushButton { " - " text-align: left; " - " padding: 2px 5px; " -#ifdef WIN32 - " min-height: 0; " -#endif - " border: none; " - " color: #666; " - " font-size: 11px; " - "} " - "QPushButton:hover { " - " background-color: #f0f0f0; " - " border-radius: 3px; " - "}"); - connect(m_toggleButton, &QPushButton::clicked, this, - &DeviceSidebarItem::onToggleCollapse); - headerLayout->addWidget(m_toggleButton); - - m_mainLayout->addWidget(m_headerWidget); - - // Options section (collapsible) - m_optionsWidget = new QWidget(); - QVBoxLayout *optionsLayout = new QVBoxLayout(m_optionsWidget); - optionsLayout->setContentsMargins(5, 5, 5, 5); - optionsLayout->setSpacing(3); - - // Create navigation buttons - m_infoButton = new QPushButton("Info"); - m_appsButton = new QPushButton("Apps"); - m_galleryButton = new QPushButton("Gallery"); - m_filesButton = new QPushButton("Files"); - - // Create button group for exclusive selection - m_navigationGroup = new QButtonGroup(this); - m_navigationGroup->addButton(m_infoButton, 0); - m_navigationGroup->addButton(m_appsButton, 1); - m_navigationGroup->addButton(m_galleryButton, 2); - m_navigationGroup->addButton(m_filesButton, 3); - - // Style the navigation buttons - QList navButtons = {m_infoButton, m_appsButton, - m_galleryButton, m_filesButton}; - for (QPushButton *btn : navButtons) { - btn->setCheckable(true); - btn->setMaximumHeight(25); - btn->setStyleSheet( - QString("QPushButton { " - " background-color: rgba(255, 255, 255, 120); " - " border: 1px solid rgba(255, 255, 255, 200); " -#ifdef WIN32 - " min-height: 0; " -#endif - " padding: 4px 8px; " - " text-align: center; " - " border-radius: 6px; " - " font-size: 11px; " - " color: #212529; " - "} " - "QPushButton:checked { " - " background-color: %1; " - " color: white; " - " border: 1px solid %1; " - "} " - "QPushButton:hover:!checked { " - " background-color: rgba(255, 255, 255, 180); " - " border-color: %2; " - "} " - "QPushButton:checked:hover { " - " background-color: %2; " - "}") - .arg(COLOR_ACCENT_BLUE.name(), COLOR_BLUE.name())); - - connect(btn, &QPushButton::clicked, this, - &DeviceSidebarItem::onNavigationButtonClicked); - optionsLayout->addWidget(btn); - } - - // Set default selection - m_infoButton->setChecked(true); - - m_mainLayout->addWidget(m_optionsWidget); - - // Initialize UI state to match m_collapsed value - // This ensures consistency regardless of initial m_collapsed value - updateToggleButton(); - toggleCollapse(); - - // Context menu: Remove - setContextMenuPolicy(Qt::CustomContextMenu); - connect(this, &QWidget::customContextMenuRequested, this, - [this](const QPoint &pos) { - QMenu menu(this); - QAction *removeAct = menu.addAction("Remove"); - connect(removeAct, &QAction::triggered, this, [this]() { - AppContext::sharedInstance()->removeDevice(m_uuid, true); - }); - menu.exec(mapToGlobal(pos)); - }); - - setSelected(false); -} - -void DeviceSidebarItem::gotWired() -{ - m_wireless = false; - if (m_wirelessIcon) { - m_wirelessIcon->deleteLater(); - m_wirelessIcon = nullptr; - } -} - -void DeviceSidebarItem::setSelected(bool selected) -{ - m_selected = selected; - bool dark = isDarkMode(); - -#ifndef WIN32 - if (selected) { - setStyleSheet(QString("QFrame#DeviceSidebarItem { " - "background-color: rgba(255, 255, 255, 45); " - "border-radius: 5px; " - "border: 2px solid %1; " - "}") - .arg(COLOR_ACCENT_BLUE.name())); - } else { - setStyleSheet("QFrame#DeviceSidebarItem { " - "background-color: rgba(255, 255, 255, 16); " - "border-radius: 5px; " - "border: 2px solid transparent; " - "}"); - } -#else - if (selected) { - if (!dark) { - setStyleSheet("QFrame#DeviceSidebarItem { background-color: " - "rgba(0, 0, 0, 30); }"); - } else { - setStyleSheet( - "QFrame#DeviceSidebarItem { background-color: rgba(255, " - "255, 255, 45); }"); - } - } else { - if (!dark) { - setStyleSheet("QFrame#DeviceSidebarItem { background-color: " - "rgba(0, 0, 0, 10); }"); - } else { - setStyleSheet( - "QFrame#DeviceSidebarItem { background-color: rgba(255, " - "255, 255, 16); }"); - } - } -#endif -} - -void DeviceSidebarItem::setCollapsed(bool collapsed) -{ - if (m_collapsed == collapsed) - return; - - m_collapsed = collapsed; - updateToggleButton(); - toggleCollapse(); -} - -void DeviceSidebarItem::updateToggleButton() -{ - if (m_collapsed) { - m_toggleButton->setText("▶ Options"); - } else { - m_toggleButton->setText("▼ Options"); - } -} - -void DeviceSidebarItem::toggleCollapse() -{ - if (m_collapsed) { - m_optionsWidget->hide(); - m_optionsWidget->setMaximumHeight(0); - } else { - m_optionsWidget->show(); - m_optionsWidget->setMaximumHeight(QWIDGETSIZE_MAX); - } -} - -void DeviceSidebarItem::onToggleCollapse() { setCollapsed(!m_collapsed); } - -void DeviceSidebarItem::onNavigationButtonClicked() -{ - QPushButton *button = qobject_cast(sender()); - if (button) { - // Only emit navigationRequested - this will handle both device - // selection and tab switching - emit navigationRequested(m_uuid, button->text()); - // Remove this line: emit deviceSelected(m_uuid); - } -} - -const QString &DeviceSidebarItem::getDeviceUuid() const { return m_uuid; } - -RecoveryDeviceSidebarItem::RecoveryDeviceSidebarItem(uint64_t ecid, - QWidget *parent) - : QFrame(parent), m_ecid(ecid) -{ - setObjectName("RecoveryDeviceSidebarItem"); - setupUI(); - setSelected(false); -} - -void RecoveryDeviceSidebarItem::setupUI() -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(5, 5, 5, 5); - mainLayout->setSpacing(5); - - ClickableWidget *headerWidget = new ClickableWidget(); - connect(headerWidget, &ClickableWidget::clicked, this, - [this]() { emit recoveryDeviceSelected(m_ecid); }); - QVBoxLayout *headerLayout = new QVBoxLayout(headerWidget); - headerLayout->setContentsMargins(0, 0, 0, 0); - - QLabel *titleLabel = new QLabel("Recovery Mode"); - titleLabel->setStyleSheet("QLabel { font-weight: bold; }"); - titleLabel->setWordWrap(true); - headerLayout->addWidget(titleLabel); - - mainLayout->addWidget(headerWidget); - - // FIXME: ecid - // Context menu: Remove - // setContextMenuPolicy(Qt::CustomContextMenu); - // connect(this, &QWidget::customContextMenuRequested, this, - // [this](const QPoint &pos) { - // QMenu menu(this); - // QAction *removeAct = menu.addAction("Remove"); - // connect(removeAct, &QAction::triggered, this, [this]() { - // AppContext::sharedInstance()->removeDevice( - // QString::number(m_ecid)); - // }); - // menu.exec(mapToGlobal(pos)); - // }); -} - -void RecoveryDeviceSidebarItem::setSelected(bool selected) -{ - m_selected = selected; - bool dark = isDarkMode(); - -#ifndef WIN32 - if (selected) { - setStyleSheet(QString("QFrame#RecoveryDeviceSidebarItem { " - "background-color: rgba(255, 255, 255, 45); " - "border-radius: 5px; " - "border: 2px solid %1; " - "}") - .arg(COLOR_ACCENT_BLUE.name())); - } else { - setStyleSheet("QFrame#RecoveryDeviceSidebarItem { " - "background-color: rgba(255, 255, 255, 16); " - "border-radius: 5px; " - "border: 2px solid transparent; " - "}"); - } -#else - if (selected) { - if (!dark) { - setStyleSheet( - "QFrame#RecoveryDeviceSidebarItem { background-color: " - "rgba(0, 0, 0, 30); }"); - } else { - setStyleSheet("QFrame#RecoveryDeviceSidebarItem { " - "background-color: rgba(255, " - "255, 255, 45); }"); - } - } else { - if (!dark) { - setStyleSheet( - "QFrame#RecoveryDeviceSidebarItem { background-color: " - "rgba(0, 0, 0, 10); }"); - } else { - setStyleSheet("QFrame#RecoveryDeviceSidebarItem { " - "background-color: rgba(255, " - "255, 255, 16); }"); - } - } -#endif -} - -// DeviceSidebarWidget Implementation -DeviceSidebarWidget::DeviceSidebarWidget(QWidget *parent) - : QWidget(parent), m_currentSelection(DeviceSelection("")) -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(10, 10, 10, 10); - mainLayout->setSpacing(0); - - m_scrollArea = new QScrollArea(); - m_scrollArea->setWidgetResizable(true); - m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_scrollArea->setFrameStyle(QFrame::NoFrame); - - // Make the scroll area and its viewport transparent - m_scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - m_scrollArea->viewport()->setStyleSheet("background: transparent;"); - - m_contentWidget = new QWidget(); - m_contentLayout = new QVBoxLayout(m_contentWidget); - m_contentLayout->setContentsMargins(5, 5, 5, 5); - m_contentLayout->setSpacing(10); - m_contentLayout->addStretch(); // Push items to top - - m_contentWidget->setStyleSheet("background: transparent;"); - - m_scrollArea->setWidget(m_contentWidget); - mainLayout->addWidget(m_scrollArea); - - setMinimumWidth(200); - setMaximumWidth(200); - - // Listen to AppContext selection changes - connect(AppContext::sharedInstance(), - &AppContext::currentDeviceSelectionChanged, this, - &DeviceSidebarWidget::setCurrentSelection); - - // Device became wired -> update sidebar item - connect(AppContext::sharedInstance(), &AppContext::deviceBecameWired, this, - [this](const QString &udid) { - if (m_deviceItems.contains(udid)) { - m_deviceItems[udid]->gotWired(); - } - }); -} - -DeviceSidebarItem *DeviceSidebarWidget::addDevice(const QString &deviceName, - const QString &uuid, - bool isWireless) -{ - DeviceSidebarItem *item = - new DeviceSidebarItem(deviceName, uuid, isWireless, this); - - // Connect to unified handler - connect( - item, &DeviceSidebarItem::deviceSelected, this, - [this](const QString &uuid) { onItemSelected(DeviceSelection(uuid)); }); - connect(item, &DeviceSidebarItem::navigationRequested, this, - [this](const QString &uuid, const QString §ion) { - onItemSelected(DeviceSelection(uuid, section)); - }); - - m_deviceItems[uuid] = item; - m_contentLayout->insertWidget(m_contentLayout->count() - 1, item); - - return item; -} - -DevicePendingSidebarItem * -DeviceSidebarWidget::addPendingDevice(const QString &uuid) -{ - DevicePendingSidebarItem *item = new DevicePendingSidebarItem(uuid, this); - connect(item, &DevicePendingSidebarItem::clicked, this, - [this, uuid]() { onItemSelected(DeviceSelection::pending(uuid)); }); - m_pendingItems[uuid] = item; - m_contentLayout->insertWidget(m_contentLayout->count() - 1, item); - return item; -} - -RecoveryDeviceSidebarItem *DeviceSidebarWidget::addRecoveryDevice(uint64_t ecid) -{ - RecoveryDeviceSidebarItem *item = new RecoveryDeviceSidebarItem(ecid, this); - - connect(item, &RecoveryDeviceSidebarItem::recoveryDeviceSelected, this, - [this](uint64_t ecid) { onItemSelected(DeviceSelection(ecid)); }); - - m_recoveryItems[ecid] = item; - m_contentLayout->insertWidget(m_contentLayout->count() - 1, item); - return item; -} - -void DeviceSidebarWidget::removeDevice(const QString &uuid) -{ - if (m_deviceItems.contains(uuid)) { - DeviceSidebarItem *item = m_deviceItems[uuid]; - m_deviceItems.remove(uuid); - m_contentLayout->removeWidget(item); - item->deleteLater(); - } -} - -void DeviceSidebarWidget::removePendingDevice(const QString &uuid) -{ - if (m_pendingItems.contains(uuid)) { - DevicePendingSidebarItem *item = m_pendingItems[uuid]; - m_pendingItems.remove(uuid); - m_contentLayout->removeWidget(item); - item->deleteLater(); - } -} - -void DeviceSidebarWidget::removeRecoveryDevice(uint64_t ecid) -{ - if (m_recoveryItems.contains(ecid)) { - RecoveryDeviceSidebarItem *item = m_recoveryItems[ecid]; - m_recoveryItems.remove(ecid); - m_contentLayout->removeWidget(item); - item->deleteLater(); - } -} - -void DeviceSidebarWidget::setCurrentSelection(const DeviceSelection &selection) -{ - m_currentSelection = selection; - updateSelection(); -} - -void DeviceSidebarWidget::onItemSelected(const DeviceSelection &selection) -{ - AppContext::sharedInstance()->setCurrentDeviceSelection(selection); -} - -void DeviceSidebarWidget::updateSelection() -{ - // Clear all selections first - for (auto item : m_deviceItems) { - item->setSelected(false); - } - for (auto item : m_recoveryItems) { - item->setSelected(false); - } - for (auto item : m_pendingItems) { - item->setSelected(false); - } - - // Set selection based on current selection - if (m_currentSelection.type == DeviceSelection::Normal && - m_deviceItems.contains(m_currentSelection.udid)) { - m_deviceItems[m_currentSelection.udid]->setSelected(true); - } else if (m_currentSelection.type == DeviceSelection::Recovery && - m_recoveryItems.contains(m_currentSelection.ecid)) { - m_recoveryItems[m_currentSelection.ecid]->setSelected(true); - } else if (m_currentSelection.type == DeviceSelection::Pending && - m_pendingItems.contains(m_currentSelection.udid)) { - m_pendingItems[m_currentSelection.udid]->setSelected(true); - } -} - -DevicePendingSidebarItem::DevicePendingSidebarItem(const QString &udid, - QWidget *parent) - : QFrame(parent), m_udid(udid) -{ - QHBoxLayout *layout = new QHBoxLayout(this); - layout->setContentsMargins(10, 10, 10, 10); - layout->setSpacing(1); - - QProcessIndicator *spinner = new QProcessIndicator(this); - spinner->setFixedSize(26, 26); - spinner->setType(QProcessIndicator::line_rotate); - spinner->start(); - - QLabel *label = new QLabel("Pairing...", this); - - layout->addWidget(label); - layout->addWidget(spinner); - - setLayout(layout); - setObjectName("DevicePendingSidebarItem"); - setSelected(false); -} - -void DevicePendingSidebarItem::setSelected(bool selected) -{ - m_selected = selected; - bool dark = isDarkMode(); - -#ifndef WIN32 - if (selected) { - setStyleSheet(QString("QFrame#DevicePendingSidebarItem { " - "background-color: rgba(255, 255, 255, 45); " - "border-radius: 5px; " - "border: 2px solid %1; " - "}") - .arg(COLOR_ACCENT_BLUE.name())); - } else { - setStyleSheet("QFrame#DevicePendingSidebarItem { " - "background-color: rgba(255, 255, 255, 16); " - "border-radius: 5px; " - "border: 2px solid transparent; " - "}"); - } -#else - if (selected) { - if (!dark) { - setStyleSheet( - "QFrame#DevicePendingSidebarItem { background-color: " - "rgba(0, 0, 0, 30); }"); - } else { - setStyleSheet("QFrame#DevicePendingSidebarItem { " - "background-color: rgba(255, " - "255, 255, 45); }"); - } - } else { - if (!dark) { - setStyleSheet( - "QFrame#DevicePendingSidebarItem { background-color: " - "rgba(0, 0, 0, 10); }"); - } else { - setStyleSheet("QFrame#DevicePendingSidebarItem { " - "background-color: rgba(255, " - "255, 255, 16); }"); - } - } -#endif -} - -void DevicePendingSidebarItem::mousePressEvent(QMouseEvent *event) -{ - emit clicked(); - QFrame::mousePressEvent(event); -} \ No newline at end of file diff --git a/src/devicesidebarwidget.h b/src/devicesidebarwidget.h deleted file mode 100644 index 4deda05..0000000 --- a/src/devicesidebarwidget.h +++ /dev/null @@ -1,197 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DEVICESIDEBARWIDGET_H -#define DEVICESIDEBARWIDGET_H - -#include "iDescriptor-ui.h" -#include -#include -#include -#include -#include -#include -#include -#include - -class DeviceSidebarItem : public QFrame -{ - Q_OBJECT - -public: - explicit DeviceSidebarItem(const QString &deviceName, const QString &uuid, - bool isWireless, QWidget *parent = nullptr); - const QString &getDeviceUuid() const; - - void setSelected(bool selected); - bool isSelected() const { return m_selected; } - void setCollapsed(bool collapsed); - bool isCollapsed() const { return m_collapsed; } - - void gotWired(); -signals: - void deviceSelected(const QString &uuid); - void navigationRequested(const QString &uuid, const QString §ion); - -private slots: - void onToggleCollapse(); - void onNavigationButtonClicked(); - -private: - void setupUI(); - void updateToggleButton(); - void toggleCollapse(); - - QString m_uuid; - QString m_deviceName; - bool m_selected; - bool m_collapsed; - QVBoxLayout *m_mainLayout; - ClickableWidget *m_headerWidget; - QWidget *m_optionsWidget; - QPushButton *m_toggleButton; - QLabel *m_deviceLabel; - bool m_wireless; - - // Navigation buttons - QPushButton *m_infoButton; - QPushButton *m_appsButton; - QPushButton *m_galleryButton; - QPushButton *m_filesButton; - QButtonGroup *m_navigationGroup; - ZIconLabel *m_wirelessIcon; -}; - -#ifndef DEVICEPENDINGSIDEBARITEM_H -#define DEVICEPENDINGSIDEBARITEM_H -class DevicePendingSidebarItem : public QFrame -{ - Q_OBJECT -public: - explicit DevicePendingSidebarItem(const QString &udid, - QWidget *parent = nullptr); - void setSelected(bool selected); - bool isSelected() const { return m_selected; } - -signals: - void clicked(); - -protected: - void mousePressEvent(QMouseEvent *event) override; - -private: - QString m_udid; - bool m_selected = false; -}; -#endif // DEVICEPENDINGSIDEBARITEM_H - -#ifndef RECOVERYDEVICESIDEBARITEM_H -#define RECOVERYDEVICESIDEBARITEM_H -class RecoveryDeviceSidebarItem : public QFrame -{ - Q_OBJECT -public: - explicit RecoveryDeviceSidebarItem(uint64_t ecid, - QWidget *parent = nullptr); - - void setSelected(bool selected); - bool isSelected() const { return m_selected; } - -private: - void setupUI(); - uint64_t m_ecid; - bool m_selected = false; -signals: - void recoveryDeviceSelected(uint64_t ecid); -}; -#endif // RECOVERYDEVICESIDEBARITEM_H - -// Unified device selection data -struct DeviceSelection { - enum Type { Normal, Recovery, Pending }; - Type type; - QString udid; - uint64_t ecid = 0; - QString section = "Info"; - - bool valid() const - { - if (type == Normal) { - return !udid.isEmpty(); - } else if (type == Recovery) { - return ecid != 0; - } else if (type == Pending) { - return !udid.isEmpty(); - } - return false; - } - - DeviceSelection(const QString &deviceUdid, const QString &nav = "") - : type(Normal), udid(deviceUdid), section(nav) - { - } - DeviceSelection(uint64_t recoveryEcid) : type(Recovery), ecid(recoveryEcid) - { - } - static DeviceSelection pending(const QString &deviceUuid) - { - DeviceSelection sel(deviceUuid); - sel.type = Pending; - return sel; - } -}; - -class DeviceSidebarWidget : public QWidget -{ - Q_OBJECT - -public: - explicit DeviceSidebarWidget(QWidget *parent = nullptr); - - // Unified interface - DeviceSidebarItem *addDevice(const QString &deviceName, const QString &uuid, - bool isWireless); - DevicePendingSidebarItem *addPendingDevice(const QString &uuid); - RecoveryDeviceSidebarItem *addRecoveryDevice(uint64_t ecid); - - void removeDevice(const QString &uuid); - void removePendingDevice(const QString &uuid); - void removeRecoveryDevice(uint64_t ecid); - - void setCurrentSelection(const DeviceSelection &selection); - -public slots: - void onItemSelected(const DeviceSelection &selection); - -signals: - void deviceSelectionChanged(const DeviceSelection &selection); - -private: - void updateSelection(); - QScrollArea *m_scrollArea; - QWidget *m_contentWidget; - QVBoxLayout *m_contentLayout; - - DeviceSelection m_currentSelection; - QMap m_deviceItems; - QMap m_pendingItems; - QMap m_recoveryItems; -}; - -#endif // DEVICESIDEBARWIDGET_H \ No newline at end of file diff --git a/src/devicesleepwarningwidget.cpp b/src/devicesleepwarningwidget.cpp deleted file mode 100644 index 79c7bff..0000000 --- a/src/devicesleepwarningwidget.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "devicesleepwarningwidget.h" - -DeviceSleepWarningWidget::DeviceSleepWarningWidget(QWidget *parent) - : QDialog{parent} -{ -#ifdef WIN32 - setupWinWindow(this); -#endif - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - setMinimumWidth(400); - - m_loadingWidget = new ZLoadingWidget(false, this); - mainLayout->addWidget(m_loadingWidget); - - QWidget *contentContainer = new QWidget(this); - QVBoxLayout *contentLayout = new QVBoxLayout(contentContainer); - contentLayout->setContentsMargins(0, 0, 0, 0); - contentLayout->setSpacing(0); - m_loadingWidget->setupContentWidget(contentContainer); - - m_mediaPlayer = new QMediaPlayer(this); - m_videoWidget = new QVideoWidget(this); - m_videoWidget->setMinimumSize(300, 400); - m_videoWidget->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - m_videoWidget->setAspectRatioMode(Qt::KeepAspectRatio); - m_mediaPlayer->setVideoOutput(m_videoWidget); - - connect(m_mediaPlayer, - QOverload::of( - &QMediaPlayer::mediaStatusChanged), - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::EndOfMedia) { - m_mediaPlayer->setPosition(0); - m_mediaPlayer->play(); - } - }); - - QLabel *title = new QLabel("Keep Your Device Awake", this); - title->setAlignment(Qt::AlignCenter); - title->setStyleSheet("QLabel { font-size: 18px; font-weight: bold; }"); - - QLabel *description = new QLabel( - R"(Please keep your device awake or unlocked while connected wirelessly to avoid disconnection)", - this); - description->setAlignment(Qt::AlignCenter); - description->setWordWrap(true); - - QVBoxLayout *textLayout = new QVBoxLayout(); - textLayout->setContentsMargins(20, 20, 20, 20); - textLayout->setSpacing(10); - contentLayout->addWidget(title); - textLayout->addWidget(description); - - contentLayout->addLayout(textLayout); - contentLayout->addWidget(m_videoWidget); - - contentLayout->addSpacing(10); - - QCheckBox *dontShowAgain = new QCheckBox("Don't show this again", this); - connect(dontShowAgain, &QCheckBox::toggled, this, [this](bool checked) { - SettingsManager::sharedInstance()->setIsSleepyDeviceWarningDismissed( - checked); - }); - - contentLayout->addWidget(dontShowAgain, 0, Qt::AlignCenter); - contentLayout->addSpacing(10); - m_mediaPlayer->setSource(QUrl("qrc:/resources/unlock.mp4")); - QTimer::singleShot(500, this, &DeviceSleepWarningWidget::init); -} - -void DeviceSleepWarningWidget::init() -{ - m_mediaPlayer->play(); - m_loadingWidget->stop(); -} diff --git a/src/devicesleepwarningwidget.h b/src/devicesleepwarningwidget.h deleted file mode 100644 index 896d5ea..0000000 --- a/src/devicesleepwarningwidget.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef DEVICESLEEPWARNINGWIDGET_H -#define DEVICESLEEPWARNINGWIDGET_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include - -class DeviceSleepWarningWidget : public QDialog -{ - Q_OBJECT -public: - explicit DeviceSleepWarningWidget(QWidget *parent = nullptr); - -private: - ZLoadingWidget *m_loadingWidget; - QMediaPlayer *m_mediaPlayer; - QVideoWidget *m_videoWidget; - - void init(); -}; - -#endif // DEVICESLEEPWARNINGWIDGET_H diff --git a/src/devmodewidget.cpp b/src/devmodewidget.cpp deleted file mode 100644 index 29d2064..0000000 --- a/src/devmodewidget.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "devmodewidget.h" - -DevModeWidget::DevModeWidget(std::shared_ptr device, - QWidget *parent) - : QDialog{parent} -{ -#ifdef WIN32 - setupWinWindow(this); -#endif - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 20, 0, 0); - mainLayout->setSpacing(0); - - m_loadingWidget = new ZLoadingWidget(false, this); - mainLayout->addWidget(m_loadingWidget); - - QWidget *contentContainer = new QWidget(this); - QVBoxLayout *contentLayout = new QVBoxLayout(contentContainer); - contentLayout->setContentsMargins(0, 0, 0, 0); - contentLayout->setSpacing(0); - m_loadingWidget->setupContentWidget(contentContainer); - - m_mediaPlayer = new QMediaPlayer(this); - m_videoWidget = new QVideoWidget(this); - m_videoWidget->setMinimumSize(300, 500); - m_videoWidget->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - m_videoWidget->setAspectRatioMode(Qt::KeepAspectRatio); - m_mediaPlayer->setVideoOutput(m_videoWidget); - - connect(m_mediaPlayer, - QOverload::of( - &QMediaPlayer::mediaStatusChanged), - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::EndOfMedia) { - m_mediaPlayer->setPosition(0); - m_mediaPlayer->play(); - } - }); - - QLabel *title = - new QLabel("Enable Developer Mode on your iOS device", this); - title->setAlignment(Qt::AlignCenter); - title->setStyleSheet("QLabel { font-size: 18px; font-weight: bold; }"); - - QLabel *description = new QLabel( - R"(In order to use this feature on your device, you need to enable Developer Mode in the Settings app. - This allows iDescriptor to access advanced features on your device. - Please follow the instructions in the video below to enable Developer Mode.)", - this); - description->setAlignment(Qt::AlignCenter); - description->setWordWrap(true); - - QVBoxLayout *textLayout = new QVBoxLayout(); - textLayout->setContentsMargins(20, 20, 20, 20); - textLayout->setSpacing(10); - contentLayout->addWidget(title); - textLayout->addWidget(description); - - contentLayout->addLayout(textLayout); - contentLayout->addWidget(m_videoWidget); - - m_mediaPlayer->setSource(QUrl("qrc:/resources/dev-mode.mp4")); - - connect( - device->service_manager, - &CXX::ServiceManager::developer_mode_option_revealed, this, - [this](bool revealed) { - if (revealed) { - QTimer::singleShot(500, this, &DevModeWidget::init); - } else { - m_loadingWidget->showError( - "Failed to reveal Developer Mode option in UI. Please try " - "again later."); - } - }, - Qt::SingleShotConnection); - - device->service_manager->reveal_developer_mode_option_in_ui(); -} - -void DevModeWidget::init() -{ - m_mediaPlayer->play(); - m_loadingWidget->stop(); -} diff --git a/src/devmodewidget.h b/src/devmodewidget.h deleted file mode 100644 index 7262993..0000000 --- a/src/devmodewidget.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef DEVMODEWIDGET_H -#define DEVMODEWIDGET_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include - -class DevModeWidget : public QDialog -{ - Q_OBJECT -public: - explicit DevModeWidget(std::shared_ptr device, - QWidget *parent = nullptr); - -private: - ZLoadingWidget *m_loadingWidget; - QMediaPlayer *m_mediaPlayer; - QVideoWidget *m_videoWidget; - - void init(); -}; - -#endif // DEVMODEWIDGET_H diff --git a/src/diagnosedialog.cpp b/src/diagnosedialog.cpp deleted file mode 100644 index 598e501..0000000 --- a/src/diagnosedialog.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "diagnosedialog.h" -#include "iDescriptor-ui.h" -#include - -DiagnoseDialog::DiagnoseDialog(QWidget *parent) : QDialog(parent) -{ - setupUI(); - setWindowTitle("System Dependencies"); - setModal(true); - setAttribute(Qt::WA_DeleteOnClose, true); -} - -void DiagnoseDialog::setupUI() -{ - setMinimumSize(MIN_MAIN_WINDOW_SIZE.width(), MIN_MAIN_WINDOW_SIZE.height() / 2); - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(10, 10, 10, 10); - - /* - TODO: either subclass DiagnoseWidget or - modify its layout to better fit dialog - */ - m_diagnoseWidget = new DiagnoseWidget(); - - QHBoxLayout *buttonLayout = new QHBoxLayout(); - buttonLayout->addStretch(); - - m_closeButton = new QPushButton("Close"); - m_closeButton->setMinimumWidth(80); - connect(m_closeButton, &QPushButton::clicked, this, - &DiagnoseDialog::onCloseClicked); - - buttonLayout->addWidget(m_closeButton); - - mainLayout->addWidget(m_diagnoseWidget); - mainLayout->addLayout(buttonLayout); -} - -void DiagnoseDialog::onCloseClicked() { accept(); } \ No newline at end of file diff --git a/src/diagnosedialog.h b/src/diagnosedialog.h deleted file mode 100644 index d448cfc..0000000 --- a/src/diagnosedialog.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DIAGNOSE_DIALOG_H -#define DIAGNOSE_DIALOG_H - -#include "diagnosewidget.h" -#include -#include -#include -#include - -class DiagnoseDialog : public QDialog -{ - Q_OBJECT - -public: - explicit DiagnoseDialog(QWidget *parent = nullptr); - -private slots: - void onCloseClicked(); - -private: - void setupUI(); - - DiagnoseWidget *m_diagnoseWidget; - QPushButton *m_closeButton; -}; - -#endif // DIAGNOSE_DIALOG_H \ No newline at end of file diff --git a/src/diagnosewidget.cpp b/src/diagnosewidget.cpp deleted file mode 100644 index 910130c..0000000 --- a/src/diagnosewidget.cpp +++ /dev/null @@ -1,913 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "diagnosewidget.h" -#ifdef WIN32 -#include "platform/windows/win_common.h" -#include -#include -#endif - -DependencyItem::DependencyItem(const QString &name, const QString &description, - bool optional, QWidget *parent) - : QWidget(parent), m_name(name) -{ - QHBoxLayout *layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - - QHBoxLayout *infoLayout = new QHBoxLayout(); - - m_nameLabel = new QLabel(name); - QFont nameFont = m_nameLabel->font(); - nameFont.setBold(true); - nameFont.setPointSize(nameFont.pointSize() + 1); - m_nameLabel->setFont(nameFont); - - m_descriptionLabel = new QLabel(QString("(%1)").arg(description)); - if (optional) { - m_descriptionLabel->setText(m_descriptionLabel->text() + " (Optional)"); - } - m_descriptionLabel->setWordWrap(false); - - infoLayout->addWidget(m_nameLabel); - infoLayout->addWidget(m_descriptionLabel); - - // Middle - status - m_statusLabel = new QLabel("Checking..."); - m_statusLabel->setMinimumWidth(100); - m_statusLabel->setAlignment(Qt::AlignCenter); - - // Right side - actions - QHBoxLayout *actionLayout = new QHBoxLayout(); - actionLayout->setContentsMargins(0, 0, 0, 0); - - m_installButton = new QPushButton("Install"); - m_installButton->setMinimumWidth(80); - m_installButton->setVisible(false); - connect(m_installButton, &QPushButton::clicked, this, - &DependencyItem::onInstallClicked); - - m_processIndicator = new QProcessIndicator(); - m_processIndicator->setType(QProcessIndicator::line_rotate); - m_processIndicator->setFixedSize(24, 24); - m_processIndicator->setVisible(false); - - actionLayout->addWidget(m_processIndicator); - actionLayout->addWidget(m_installButton); - - layout->addLayout(infoLayout); - layout->addStretch(); - layout->addWidget(m_statusLabel); - layout->addLayout(actionLayout); -} - -void DependencyItem::setInstalled(SERVICE_AVAILABILITY availability, - bool isRequired) -{ - setChecking(false); - m_availability = availability; - - switch (availability) { - case SERVICE_AVAILABLE: - /* code */ - if (m_name == "Avahi Daemon" || m_name == "Bonjour Service") { - m_statusLabel->setText("Activated"); - } else { - m_statusLabel->setText("Installed"); - } -#ifndef WIN32 - m_statusLabel->setStyleSheet("color: green;"); -#else - // FIXME: if we call this multiple times, the styles will keep stacking - // and become a mess, need a better way to handle this - m_statusLabel->setStyleSheet(mergeStyles( - m_statusLabel, - QString("QLabel { color: %1; }").arg(COLOR_GREEN.name()))); -#endif - m_installButton->setVisible(false); - return; - break; - - case SERVICE_AVAILABLE_BUT_NOT_RUNNING: - m_statusLabel->setText("Not running"); - m_installButton->setText("Enable"); - break; - case UNABLE_TO_CHECK: - m_statusLabel->setText("Action needed"); - m_installButton->setText("Info"); - break; - case SERVICE_UNAVAILABLE: - if (isRequired) { - m_statusLabel->setText("Not Installed"); - } else { - m_statusLabel->setText("Not Installed (Optional)"); - } - break; - default: - break; - } - -#ifndef WIN32 - if (isRequired) { - m_statusLabel->setStyleSheet("color: red;"); - } -#else - // FIXME: if we call this multiple times, the styles will keep stacking - // and become a mess, need a better way to handle this - m_statusLabel->setStyleSheet(mergeStyles( - m_statusLabel, QString("QLabel { color: %1; }").arg(COLOR_RED.name()))); -#endif - m_installButton->setVisible(true); -} - -void DependencyItem::setChecking(bool checking) -{ - if (checking) { - m_statusLabel->setText("Checking..."); - m_statusLabel->setStyleSheet("color: gray;"); - m_installButton->setVisible(false); - m_processIndicator->setVisible(false); - m_processIndicator->stop(); - } -} - -void DependencyItem::setInstalling(bool installing) -{ - if (installing) { - m_statusLabel->setText("Installing..."); - m_statusLabel->setStyleSheet("color: gray;"); - m_installButton->setVisible(false); - m_processIndicator->setVisible(true); - m_processIndicator->start(); - } else { - m_processIndicator->stop(); - m_processIndicator->setVisible(false); - } -} - -void DependencyItem::setActivating(bool activating) -{ - if (activating) { - m_statusLabel->setText("Activating..."); - m_statusLabel->setStyleSheet("color: gray;"); - m_installButton->setVisible(false); - m_processIndicator->setVisible(true); - m_processIndicator->start(); - } else { - m_processIndicator->stop(); - m_processIndicator->setVisible(false); - } -} - -void DependencyItem::setProgress(const QString &message) -{ - m_statusLabel->setText(message); - m_statusLabel->setStyleSheet("color: gray;"); -} - -void DependencyItem::onInstallClicked() -{ - emit installRequested(m_name, m_availability); -} - -DiagnoseWidget::DiagnoseWidget(QWidget *parent) - : QWidget(parent), m_isExpanded(false) -{ - setupUI(); - -#ifdef WIN32 - addDependencyItem( - "Bonjour Service", - "Required for AirPlay, wireless devices and network service discovery"); - addDependencyItem("Apple Mobile Device Support", - "Required for iOS device communication"); - addDependencyItem("WinFsp", "Required for mounting your device as a drive", - true); -#endif - -#ifdef __linux__ - addDependencyItem("Avahi Daemon", "Required for AirPlay, wireless devices"); -#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT - addDependencyItem("UDEV rules", - "Required for recovery devices requires manual setup", - true); -#endif -#endif - - // Auto-check on startup - QTimer::singleShot(100, this, [this]() { checkDependencies(); }); -} - -void DiagnoseWidget::setupUI() -{ - setObjectName("diagnoseWidget"); - setContentsMargins(20, 2, 20, 0); - setAutoFillBackground(true); - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setSpacing(5); - - // Title and summary - QLabel *titleLabel = new QLabel("Dependency Check"); - QFont titleFont = titleLabel->font(); - titleFont.setBold(true); - titleFont.setPointSize(titleFont.pointSize() + 2); - titleLabel->setFont(titleFont); - - m_summaryLabel = new QLabel("Checking system dependencies..."); - - // Check button - m_checkButton = new QPushButton("Refresh Check(s)"); - m_checkButton->setMaximumWidth(m_checkButton->sizeHint().width()); - connect(m_checkButton, &QPushButton::clicked, this, - [this]() { checkDependencies(false); }); - - // Toggle button - m_toggleButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsLightKeyboardArrowDown.png"), - "Expand/Collapse"); - // m_toggleButton->setFixedSize(24, 24); - m_toggleButton->setCheckable(true); - connect(m_toggleButton, &QPushButton::clicked, this, - &DiagnoseWidget::onToggleExpand); - - m_itemsWidget = new QWidget(); - m_itemsLayout = new QVBoxLayout(m_itemsWidget); - m_itemsLayout->setSpacing(10); - m_itemsLayout->addStretch(); - m_itemsWidget->setVisible(m_isExpanded); - - // Layout assembly - QHBoxLayout *headerLayout = new QHBoxLayout(); - headerLayout->addWidget(titleLabel); - headerLayout->addWidget(m_checkButton); - headerLayout->addWidget(m_toggleButton); - - m_mainLayout->addLayout(headerLayout); - m_mainLayout->addWidget(m_summaryLabel); - m_mainLayout->addWidget(m_itemsWidget); -} - -void DiagnoseWidget::addDependencyItem(const QString &name, - const QString &description, - bool optional) -{ - DependencyItem *item = new DependencyItem(name, description, optional); - item->setProperty("name", name); - item->setProperty("optional", optional); - connect(item, &DependencyItem::installRequested, this, - &DiagnoseWidget::onInstallRequested); - - m_dependencyItems[name] = item; - - // Insert before the stretch - m_itemsLayout->insertWidget(m_itemsLayout->count() - 1, item); -} - -void DiagnoseWidget::checkDependencies(bool autoExpand) -{ - m_summaryLabel->setText("Checking system dependencies..."); - m_checkButton->setEnabled(false); - - for (DependencyItem *item : m_dependencyItems) { - item->setChecking(true); - } - - QTimer::singleShot(500, [this, autoExpand]() { - int installedCount = 0; - int totalCount = 0; - int optionalInstalledCount = 0; - int optionalTotalCount = 0; - - for (DependencyItem *item : m_dependencyItems) { - SERVICE_AVAILABILITY installed = SERVICE_UNAVAILABLE; - QString itemName = item->property("name").toString(); - -#ifdef WIN32 - if (itemName == "Bonjour Service") { - installed = IsBonjourServiceInstalled(); - } else if (itemName == "Apple Mobile Device Support") { - installed = IsAppleMobileDeviceSupportInstalled(); - } else if (itemName == "WinFsp") { - installed = IsWinFspInstalled(); - } -#endif - -#ifdef __linux__ - if (itemName == "UDEV rules") { - installed = checkUdevRulesInstalled(); - } else if (itemName == "Avahi Daemon") { - installed = checkAvahiDaemonRunning(); - } -#endif - - bool isRequired = item->property("optional").toBool() == false; - if (!isRequired) { - ++optionalTotalCount; - if (installed == SERVICE_AVAILABILITY::SERVICE_AVAILABLE) { - ++optionalInstalledCount; - } - } else { - ++totalCount; - if (installed == SERVICE_AVAILABILITY::SERVICE_AVAILABLE) { - ++installedCount; - } - } - - item->setInstalled(installed, isRequired); - } - - if (installedCount == totalCount) { - if (optionalTotalCount != optionalInstalledCount) { - int optionalMissingCount = - optionalTotalCount - optionalInstalledCount; - QString optionalText = optionalMissingCount == 1 - ? "optional capability is" - : "optional capabilities are"; - m_summaryLabel->setText(QString("%1 %2 available") - .arg(optionalMissingCount) - .arg(optionalText)); - } else { - m_summaryLabel->setText( - "All required dependencies are installed"); - } - m_summaryLabel->setStyleSheet( - QString("color: %1; font-weight: bold;") - .arg(COLOR_GREEN.name())); - if (m_isExpanded && autoExpand) { - onToggleExpand(); - } - } else { - m_summaryLabel->setText( - QString("Missing required dependencies (%1/%2 installed)") - .arg(installedCount) - .arg(totalCount)); - m_summaryLabel->setStyleSheet( - QString("color: %1; font-weight: bold;").arg(COLOR_RED.name())); - if (!m_isExpanded && autoExpand) { - onToggleExpand(); - } - } - - m_checkButton->setEnabled(true); - }); -} - -void DiagnoseWidget::onInstallRequested(const QString &name) -{ - DependencyItem *itemToInstall = m_dependencyItems.value(name); - if (!itemToInstall) { - QMessageBox::warning(this, "Error", - "Dependency item not found: " + name); - return; - } - SERVICE_AVAILABILITY availability = itemToInstall->availability(); -#ifdef WIN32 - if (name == "Bonjour Service") { - if (availability == SERVICE_AVAILABLE_BUT_NOT_RUNNING) { - itemToInstall->setActivating(true); - - QProcess *proc = new QProcess(this); - connect( - proc, &QProcess::finished, this, - [this, proc, itemToInstall](int exitCode, - QProcess::ExitStatus status) { - itemToInstall->setActivating(false); - if (status != QProcess::NormalExit || exitCode != 0) { - QString err = proc->readAllStandardError(); - if (err.isEmpty()) - err = proc->readAllStandardOutput(); - QMessageBox::warning( - this, "Activation Failed", - "Failed to start Bonjour Service.\n\nDetails:\n" + - err.trimmed()); - } - checkDependencies(false); - proc->deleteLater(); - }); - - QString ps = "Set-Service -Name 'Bonjour Service' " - "-StartupType Automatic; " - "Start-Service -Name 'Bonjour Service'"; - - QStringList args; - args << "-NoProfile" - << "-ExecutionPolicy" - << "Bypass" - << "-Command" - << QString( - "Start-Process -FilePath powershell.exe -Verb RunAs " - "-ArgumentList \"%1\" -Wait") - .arg(ps.replace("\"", "\\\"")); - - proc->start("powershell.exe", args); - return; - } - - installBonjourRuntime(); - return; - } - - if (name == "Apple Mobile Device Support") { - if (availability == SERVICE_AVAILABLE_BUT_NOT_RUNNING) { - itemToInstall->setActivating(true); - - QProcess *proc = new QProcess(this); - connect(proc, &QProcess::finished, this, - [this, proc, itemToInstall](int exitCode, - QProcess::ExitStatus status) { - itemToInstall->setActivating(false); - if (status != QProcess::NormalExit || exitCode != 0) { - QString err = proc->readAllStandardError(); - if (err.isEmpty()) - err = proc->readAllStandardOutput(); - QMessageBox::warning( - this, "Activation Failed", - "Failed to start Apple Mobile Device " - "Service.\n\nDetails:\n" + - err.trimmed()); - } - checkDependencies(false); - proc->deleteLater(); - }); - - QString ps = "Set-Service -Name 'Apple Mobile Device Service' " - "-StartupType Automatic; " - "Start-Service -Name 'Apple Mobile Device Service'"; - - QStringList args; - args << "-NoProfile" - << "-ExecutionPolicy" - << "Bypass" - << "-Command" - << QString( - "Start-Process -FilePath powershell.exe -Verb RunAs " - "-ArgumentList \"%1\" -Wait") - .arg(ps.replace("\"", "\\\"")); - - proc->start("powershell.exe", args); - return; - } - - itemToInstall->setInstalling(true); - - QString scriptPath = QCoreApplication::applicationDirPath() + - "/install-apple-drivers.ps1"; - - QProcess *installProcess = new QProcess(this); - connect(installProcess, &QProcess::finished, this, - [this, installProcess, - itemToInstall](int exitCode, QProcess::ExitStatus exitStatus) { - if (exitStatus != QProcess::NormalExit || exitCode != 0) { - QString errorOutput = - installProcess->readAllStandardError(); - if (errorOutput.isEmpty()) { - errorOutput = - installProcess->readAllStandardOutput(); - } - QMessageBox::warning( - this, "Installation Failed", - "This might be a " - "permissions issue or an internal error.\n\n" - "Details: " + - errorOutput.trimmed()); - } - checkDependencies(false); - installProcess->deleteLater(); - }); - - QString command = - QString("Start-Process -FilePath powershell.exe -Verb RunAs " - "-ArgumentList '-NoProfile -ExecutionPolicy Bypass -File " - "\"%1\"' -Wait") - .arg(scriptPath); - - QStringList args; - args << "-NoProfile" - << "-ExecutionPolicy" - << "Bypass" - << "-Command" << command; - - installProcess->start("powershell.exe", args); - return; - } - - if (name == "WinFsp") { - if (availability == SERVICE_AVAILABLE_BUT_NOT_RUNNING) { - itemToInstall->setActivating(true); - - QProcess *proc = new QProcess(this); - connect( - proc, &QProcess::finished, this, - [this, proc, itemToInstall](int exitCode, - QProcess::ExitStatus status) { - itemToInstall->setActivating(false); - if (status != QProcess::NormalExit || exitCode != 0) { - QString err = proc->readAllStandardError(); - if (err.isEmpty()) - err = proc->readAllStandardOutput(); - QMessageBox::warning( - this, "Activation Failed", - "Failed to start WinFsp.Launcher.\n\nDetails:\n" + - err.trimmed()); - } - checkDependencies(false); - proc->deleteLater(); - }); - - // Use single quotes around the service name; no Read-Host - QString ps = "Set-Service -Name 'WinFsp.Launcher' " - "-StartupType Automatic; " - "Start-Service -Name 'WinFsp.Launcher'"; - - QStringList args; - args << "-NoProfile" - << "-ExecutionPolicy" - << "Bypass" - << "-Command" - << QString( - "Start-Process -FilePath powershell.exe -Verb RunAs " - "-ArgumentList \"%1\" -Wait") - .arg(ps.replace("\"", "\\\"")); - - proc->start("powershell.exe", args); - return; - } - itemToInstall->setInstalling(true); - - QString scriptPath = QCoreApplication::applicationDirPath() + - "/install-win-fsp.silent.bat"; - - QProcess *installProcess = new QProcess(this); - connect( - installProcess, &QProcess::finished, this, - [this, installProcess](int exitCode, - QProcess::ExitStatus exitStatus) { - if (exitStatus != QProcess::NormalExit || exitCode != 0) { - QMessageBox::warning( - this, "Installation Failed", - "The installation script failed to run correctly. " - "This might be because the action was cancelled or an " - "error occurred.\n\nPlease try again."); - } - checkDependencies(false); - installProcess->deleteLater(); - }); - - QStringList args; - args << "-NoProfile" - << "-ExecutionPolicy" - << "Bypass" - << "-Command" - << QString("Start-Process -FilePath \"%1\" -Verb RunAs -Wait;") - .arg(scriptPath); - - installProcess->start("powershell.exe", args); - } -#endif - -#ifdef __linux__ - if (name == "UDEV rules") { - QMessageBox msgBox(this); - msgBox.setWindowTitle("Manual configuration required"); - msgBox.setText( - "USB device permissions are required for recovery " - "devices.\n\n" - "Due to the variety of Linux distributions and package managers, " - "you should configure these permissions manually.\n\n" - "Please refer to the UDEV.md file in the project repository for " - "detailed instructions."); - msgBox.setInformativeText( - "Would you like to open the instructions now?"); - - QPushButton *openButton = - msgBox.addButton("Open Instructions", QMessageBox::ActionRole); - msgBox.addButton("Close", QMessageBox::RejectRole); - - msgBox.exec(); - - if (msgBox.clickedButton() == openButton) { - QDesktopServices::openUrl(QUrl( - "https://github.com/uncor3/iDescriptor/blob/main/UDEV.md")); - } - } - - if (name == "Avahi Daemon") { - if (availability == SERVICE_AVAILABLE_BUT_NOT_RUNNING) { - QProcess *installProcess = new QProcess(this); - connect( - installProcess, &QProcess::finished, this, - [this, installProcess, - itemToInstall](int exitCode, QProcess::ExitStatus exitStatus) { - if (exitStatus != QProcess::NormalExit || exitCode != 0) { - QString errorOutput = - installProcess->readAllStandardError(); - if (errorOutput.isEmpty()) { - errorOutput = - installProcess->readAllStandardOutput(); - } - QMessageBox::warning(this, "Error", - "Failed to enable Avahi daemon. " - "This might be because the action " - "was cancelled or an " - "error occurred.\n\nDetails: " + - errorOutput.trimmed()); - checkDependencies(false); - } else { - checkDependencies(false); - } - itemToInstall->setInstalling(false); - installProcess->deleteLater(); - }); - - QStringList args; - args << "systemctl" - << "enable" - << "--now" - << "avahi-daemon.service"; - installProcess->start("pkexec", args); - return; - } - QMessageBox::information(this, "Avahi Daemon", - "The Avahi daemon is responsible for network " - "service discovery and is required for " - "AirPlay and wireless device features.\n\n" - "Please use your distribution's package " - "manager to install 'avahi'"); - } - -#endif -} - -#ifdef __linux__ -SERVICE_AVAILABILITY DiagnoseWidget::checkUdevRulesInstalled() -{ - // Check if udev rules file exists - QFile rulesFile("/etc/udev/rules.d/99-idevice.rules"); - if (!rulesFile.exists()) { - return UNABLE_TO_CHECK; - } - - // Check if the file contains the correct rule - if (!rulesFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - return UNABLE_TO_CHECK; - } - - QTextStream in(&rulesFile); - QString content = in.readAll(); - rulesFile.close(); - - // Check for the essential parts of the rule - bool hasUsbSubsystem = content.contains("SUBSYSTEM==\"usb\""); - bool hasAppleVendor = content.contains("ATTR{idVendor}==\"05ac\""); - bool hasMode = content.contains("MODE=\"0666\""); - - if (!hasUsbSubsystem || !hasAppleVendor || !hasMode) { - return UNABLE_TO_CHECK; - } - - // Check if current user is in the idevice group - QProcess groupsProcess; - groupsProcess.start("groups"); - groupsProcess.waitForFinished(3000); - - if (groupsProcess.exitCode() != 0) { - // If we can't check groups, consider it not installed - return UNABLE_TO_CHECK; - } - - QString groupsOutput = - QString::fromUtf8(groupsProcess.readAllStandardOutput()).trimmed(); - QStringList groups = - groupsOutput.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); - - bool isInIdeviceGroup = groups.contains("idevice"); - return isInIdeviceGroup ? SERVICE_AVAILABLE : UNABLE_TO_CHECK; -} - -SERVICE_AVAILABILITY DiagnoseWidget::checkAvahiDaemonRunning() -{ - // Connect to the system bus - QDBusConnection systemBus = QDBusConnection::systemBus(); - if (!systemBus.isConnected()) { - return UNABLE_TO_CHECK; - } - - QDBusConnectionInterface *iface = systemBus.interface(); - if (!iface) { - return UNABLE_TO_CHECK; - } - - // Avahi daemon D-Bus name - const QString avahiService = QStringLiteral("org.freedesktop.Avahi"); - - // If the service is registered, Avahi is running - if (iface->isServiceRegistered(avahiService).value()) { - return SERVICE_AVAILABLE; - } - - // maybe installed ? - bool hasBinary = - !QStandardPaths::findExecutable(QStringLiteral("avahi-browse")) - .isEmpty(); - - return hasBinary ? SERVICE_AVAILABLE_BUT_NOT_RUNNING : SERVICE_UNAVAILABLE; -} -#endif - -void DiagnoseWidget::onToggleExpand() -{ - m_isExpanded = !m_isExpanded; - m_itemsWidget->setVisible(m_isExpanded); - m_toggleButton->setIcon( - m_isExpanded - ? QIcon(":/resources/icons/MaterialSymbolsLightKeyboardArrowUp.png") - : QIcon(":/resources/icons/" - "MaterialSymbolsLightKeyboardArrowDown.png")); - m_itemsWidget->updateGeometry(); - adjustSize(); -} - -#ifdef WIN32 -void DiagnoseWidget::installBonjourRuntime() -{ - DependencyItem *itemToInstall = m_dependencyItems.value("Bonjour Service"); - - if (!itemToInstall) - return; - - itemToInstall->setInstalling(true); - itemToInstall->setProgress("Downloading..."); - - // Download Bonjour SDK - QNetworkAccessManager *manager = new QNetworkAccessManager(this); - QNetworkRequest request( - QUrl("https://github.com/tempx-x/bonjour-sdk/raw/refs/heads/main/" - "bonjoursdksetup.exe")); - - QNetworkReply *reply = manager->get(request); - - connect(reply, &QNetworkReply::downloadProgress, this, - [itemToInstall](qint64 bytesReceived, qint64 bytesTotal) { - if (bytesTotal > 0) { - int percent = (bytesReceived * 100) / bytesTotal; - itemToInstall->setProgress( - QString("Downloading... %1%").arg(percent)); - } - }); - - connect( - reply, &QNetworkReply::finished, this, - [this, reply, manager, itemToInstall]() { - reply->deleteLater(); - manager->deleteLater(); - - if (reply->error() != QNetworkReply::NoError) { - QMessageBox::critical(this, "Download Failed", - "Failed to download Bonjour SDK: " + - reply->errorString()); - checkDependencies(false); - return; - } - - itemToInstall->setProgress("Verifying..."); - - // Verify MD5 checksum - QByteArray data = reply->readAll(); - QByteArray hash = - QCryptographicHash::hash(data, QCryptographicHash::Md5); - QString actualHash = hash.toHex(); - QString expectedHash = "4ff2aae8205aec31b06743782cfcadce"; - - if (actualHash != expectedHash) { - QMessageBox::critical( - this, "Checksum Mismatch", - QString("Downloaded file checksum does not match!\n" - "Expected: %1\n" - "Got: %2") - .arg(expectedHash, actualHash)); - checkDependencies(false); - return; - } - - itemToInstall->setProgress("Extracting..."); - - // Create temp directory - QString tempDir = - QStandardPaths::writableLocation(QStandardPaths::TempLocation) + - "/bonjour_install"; - QDir().mkpath(tempDir); - - // Save the downloaded file - QString exePath = tempDir + "/bonjoursdksetup.exe"; - QFile file(exePath); - if (!file.open(QIODevice::WriteOnly)) { - QMessageBox::critical(this, "Error", - "Failed to save downloaded file"); - checkDependencies(false); - return; - } - file.write(data); - file.close(); - - // Extract using libarchive - struct archive *a = archive_read_new(); - archive_read_support_format_all(a); - archive_read_support_filter_all(a); - - struct archive *ext = archive_write_disk_new(); - archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME); - - if (archive_read_open_filename(a, exePath.toUtf8().constData(), - 10240) != ARCHIVE_OK) { - QMessageBox::critical(this, "Extraction Failed", - QString("Failed to open archive: %1") - .arg(archive_error_string(a))); - archive_read_free(a); - archive_write_free(ext); - checkDependencies(false); - return; - } - - struct archive_entry *entry; - QString msiPath; - - while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { - QString entryName = - QString::fromUtf8(archive_entry_pathname(entry)); - - if (entryName.endsWith("Bonjour64.msi", Qt::CaseInsensitive)) { - QString fullPath = tempDir + "/" + entryName; - archive_entry_set_pathname(entry, - fullPath.toUtf8().constData()); - - if (archive_write_header(ext, entry) != ARCHIVE_OK) { - qWarning() << "Failed to write header for" << entryName; - } else { - const void *buff; - size_t size; - la_int64_t offset; - - while (archive_read_data_block(a, &buff, &size, - &offset) == ARCHIVE_OK) { - archive_write_data_block(ext, buff, size, offset); - } - } - archive_write_finish_entry(ext); - msiPath = fullPath; - break; // Only need Bonjour64.msi - } else { - archive_read_data_skip(a); - } - } - - archive_read_free(a); - archive_write_free(ext); - - if (msiPath.isEmpty()) { - QMessageBox::critical(this, "Extraction Failed", - "Could not find Bonjour64.msi in the " - "archive"); - QDir(tempDir).removeRecursively(); - checkDependencies(false); - return; - } - - itemToInstall->setProgress("Installing..."); - - // Launch the MSI via the shell (same behavior as double-click) - itemToInstall->setInstalling(false); // we can't track MSI process - - if (!QDesktopServices::openUrl(QUrl::fromLocalFile(msiPath))) { - QMessageBox::warning(this, "Installation Failed", - "Failed to launch Bonjour installer.\n\n" - "You can also run it manually from:\n" + - msiPath); - checkDependencies(false); - return; - } - - QMessageBox::information( - this, "Installation Started", - "The Bonjour installer has been launched.\n" - "Please complete the setup, then re-run the dependency check."); - - itemToInstall->setProgress("Refresh to verify installation."); - }); -} -#endif diff --git a/src/diagnosewidget.h b/src/diagnosewidget.h deleted file mode 100644 index dcc6c53..0000000 --- a/src/diagnosewidget.h +++ /dev/null @@ -1,129 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DIAGNOSE_WIDGET_H -#define DIAGNOSE_WIDGET_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "qprocessindicator.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef __linux__ -#include -#include -#include -#endif - -#include "service.h" - -class DependencyItem : public QWidget -{ - Q_OBJECT - -public: - explicit DependencyItem(const QString &name, const QString &description, - bool optional = false, QWidget *parent = nullptr); - void setInstalled(SERVICE_AVAILABILITY availability, bool isRequired); - void setChecking(bool checking); - void setInstalling(bool installing); - void setActivating(bool activating); - void setProgress(const QString &message); - SERVICE_AVAILABILITY availability() const { return m_availability; } - -signals: - void installRequested(const QString &name, - SERVICE_AVAILABILITY availability); - -private slots: - void onInstallClicked(); - -private: - QString m_name; - QLabel *m_nameLabel; - QLabel *m_descriptionLabel; - QLabel *m_statusLabel; - QPushButton *m_installButton; - QProcessIndicator *m_processIndicator; - SERVICE_AVAILABILITY m_availability = SERVICE_UNAVAILABLE; -}; - -class DiagnoseWidget : public QWidget -{ - Q_OBJECT - -public: - explicit DiagnoseWidget(QWidget *parent = nullptr); - -public slots: - void checkDependencies(bool autoExpand = true); - -private slots: - void onInstallRequested(const QString &name); - void onToggleExpand(); - -private: - void setupUI(); - void addDependencyItem(const QString &name, const QString &description, - bool optional = false); - -#ifdef WIN32 - void installBonjourRuntime(); -#endif - -#ifdef __linux__ - SERVICE_AVAILABILITY checkUdevRulesInstalled(); - SERVICE_AVAILABILITY checkAvahiDaemonRunning(); -#endif - - QVBoxLayout *m_mainLayout; - QVBoxLayout *m_itemsLayout; - QPushButton *m_checkButton; - ZIconWidget *m_toggleButton; - QLabel *m_summaryLabel; - QWidget *m_itemsWidget; - bool m_isExpanded; - - QMap m_dependencyItems; -}; - -#endif // DIAGNOSE_WIDGET_H diff --git a/src/diskusagebar.cpp b/src/diskusagebar.cpp deleted file mode 100644 index fa9d857..0000000 --- a/src/diskusagebar.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifdef __APPLE__ -#include "diskusagebar.h" -#include "platform/macos/macos.h" - -#include -#include -#include - -DiskUsageBar::DiskUsageBar(QWidget *parent) : QWidget(parent), m_percentage(0.0) -{ - m_hoverTimer = new QTimer(this); - m_hoverTimer->setSingleShot(true); - m_hoverTimer->setInterval(500); // 500ms delay before showing popover - connect(m_hoverTimer, &QTimer::timeout, this, &DiskUsageBar::showPopover); - setAttribute(Qt::WA_Hover, true); - - QHBoxLayout *layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - // Add an invisible spacer to give the widget content - QWidget *spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - layout->addWidget(spacer); - - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); -} - -void DiskUsageBar::setUsageInfo(const QString &type, - const QString &formattedSize, - const QString &color, double percentage) -{ - m_type = type; - m_formattedSize = formattedSize; - m_color = color; - m_percentage = percentage; -} - -void DiskUsageBar::enterEvent(QEnterEvent *event) -{ - Q_UNUSED(event); - m_hoverTimer->start(); - QWidget::enterEvent(event); -} - -void DiskUsageBar::leaveEvent(QEvent *event) -{ - m_hoverTimer->stop(); - hidePopoverForBarWidget(); - QWidget::leaveEvent(event); -} - -void DiskUsageBar::showPopover() -{ - if (m_type.isEmpty()) - return; - - UsageInfo info; - info.type = m_type; - info.formattedSize = m_formattedSize; - info.color = m_color; - info.percentage = m_percentage; - - showPopoverForBarWidget(this, info); -} -#endif \ No newline at end of file diff --git a/src/diskusagebar.h b/src/diskusagebar.h deleted file mode 100644 index f672f04..0000000 --- a/src/diskusagebar.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifdef __APPLE__ -#ifndef DISKUSAGEBAR_H -#define DISKUSAGEBAR_H - -#include -#include - -class DiskUsageBar : public QWidget -{ - Q_OBJECT - -public: - explicit DiskUsageBar(QWidget *parent = nullptr); - - void setUsageInfo(const QString &type, const QString &formattedSize, - const QString &color, double percentage); - -protected: - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - -private slots: - void showPopover(); - -private: - QString m_type; - QString m_formattedSize; - QString m_color; - double m_percentage; - QTimer *m_hoverTimer; -}; - -#endif // DISKUSAGEBAR_H -#endif // __APPLE__ \ No newline at end of file diff --git a/src/diskusagewidget.cpp b/src/diskusagewidget.cpp deleted file mode 100644 index 7843f74..0000000 --- a/src/diskusagewidget.cpp +++ /dev/null @@ -1,441 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "diskusagewidget.h" -#include "diskusagebar.h" -#include "iDescriptor.h" -extern "C" { -#include -} - -#include -#include -#include -#include -#include - -using namespace iDescriptor; - -DiskUsageWidget::DiskUsageWidget( - const std::shared_ptr device, QWidget *parent) - : QWidget(parent), m_device(device), m_state(Loading), m_totalCapacity(0), - m_systemUsage(0), m_appsUsage(0), m_mediaUsage(0), m_othersUsage(0), - m_freeSpace(0) -{ - setMinimumHeight(80); - setupUI(); - connect(m_device->service_manager, - &CXX::ServiceManager::disk_usage_retrieved, this, - &DiskUsageWidget::onDiskUsageRetrieved); - QTimer::singleShot(100, this, [this] { - /* - on older devices if Photos.sqlite is high in size and the device is - connected wirelessly it takes ~5 minutes to read the entire file - maybe skip on wireless connections on old devices than iPhone10,1 - (iPhone 8) - */ - bool skipGalleryUsage = - m_device->deviceInfo.is_iPhone && m_device->deviceInfo.isWireless && - !iDescriptor::Utils::isProductTypeNewer( - m_device->deviceInfo.rawProductType, "iPhone10,1"); - if (skipGalleryUsage) - qDebug() << "Skipping gallery usage calculation"; - m_device->service_manager->fetch_disk_usage(skipGalleryUsage); - }); -} - -void DiskUsageWidget::setupUI() -{ - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(0, 0, 14, 10); - m_mainLayout->setSpacing(0); - - // Title - m_titleLabel = new QLabel("Disk Usage", this); - QFont titleFont = font(); - titleFont.setBold(true); - m_titleLabel->setFont(titleFont); - m_titleLabel->setAlignment(Qt::AlignCenter); - m_mainLayout->addWidget(m_titleLabel); - - // Stacked widget for different states - m_stackedWidget = new QStackedWidget(this); - m_mainLayout->addWidget(m_stackedWidget); - - // Loading/Error page - m_loadingErrorPage = new QWidget(); - m_loadingErrorLayout = new QVBoxLayout(m_loadingErrorPage); - m_loadingErrorLayout->setContentsMargins(0, 0, 0, 0); - m_loadingErrorLayout->setSpacing(5); - - m_processIndicator = new QProcessIndicator(m_loadingErrorPage); - m_processIndicator->setFixedSize(24, 24); - m_processIndicator->start(); - - m_statusLabel = new QLabel(m_loadingErrorPage); - m_statusLabel->setAlignment(Qt::AlignCenter); - m_statusLabel->setText("Loading disk usage..."); - - m_loadingErrorLayout->addStretch(); - m_loadingErrorLayout->addWidget(m_processIndicator, 0, Qt::AlignCenter); - m_loadingErrorLayout->addWidget(m_statusLabel); - m_loadingErrorLayout->addStretch(); - - m_stackedWidget->addWidget(m_loadingErrorPage); - - // Data page - m_dataPage = new QWidget(); - m_dataLayout = new QVBoxLayout(m_dataPage); - m_dataLayout->setContentsMargins(0, 0, 0, 0); - m_dataLayout->setSpacing(0); - - // Disk usage bar container - m_diskBarContainer = new QWidget(this); - m_diskBarContainer->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Fixed); - m_diskBarContainer->setFixedHeight(20); - m_diskBarLayout = new QHBoxLayout(m_diskBarContainer); - m_diskBarLayout->setContentsMargins(0, 0, 0, 0); - m_diskBarLayout->setSpacing(0); - - /* - FIXME: There is a bug with qt, related to NSPopover on macOS - need to revisit this when we find a fix - */ - // #ifdef Q_OS_MAC - // m_systemBar = new DiskUsageBar(); - // m_appsBar = new DiskUsageBar(); - // m_mediaBar = new DiskUsageBar(); - // m_othersBar = new DiskUsageBar(); - // m_freeBar = new DiskUsageBar(); - - // m_systemBar->setStyleSheet( - // " background-color: #a1384d; border: 1px solid" - // "#e64a5b; padding: 0; margin: 0; border-top-left-radius: 3px; " - // "border-bottom-left-radius: 3px; "); - // m_appsBar->setStyleSheet("background-color: #4f869f; border: 1px - // solid " - // "#63b4da; padding: 0; margin: 0; "); - // m_mediaBar->setStyleSheet("background-color: #2ECC71; " - // "border: none; padding: 0; margin: 0; "); - // m_othersBar->setStyleSheet("background-color: #a28729; border: 1px - // solid " - // "#c4a32d; padding: 0; margin: 0; "); - // m_freeBar->setStyleSheet( - // "background-color: #6e6d6d; border: 1px solid " - // "#4f4f4f; padding: 0; margin: 0; border-top-right-radius: 3px; " - // "border-bottom-right-radius: 3px; "); - - m_systemBar = new QWidget(); - m_appsBar = new QWidget(); - m_mediaBar = new QWidget(); - m_galleryBar = new QWidget(); - m_othersBar = new QWidget(); - m_freeBar = new QWidget(); - - // required for tooltips to have default styling - m_systemBar->setObjectName("systemBar"); - m_appsBar->setObjectName("appsBar"); - m_mediaBar->setObjectName("mediaBar"); - m_galleryBar->setObjectName("galleryBar"); - m_othersBar->setObjectName("othersBar"); - m_freeBar->setObjectName("freeBar"); - - bool dark = isDarkMode(); - - // Set colors - m_systemBar->setStyleSheet( - "QWidget#systemBar { background-color: #a1384d; border: 1px solid" - "#e64a5b; padding: 0; margin: 0; border-radius:0px; " - "border-top-left-radius: 3px; " - "border-bottom-left-radius: 3px; }"); - m_appsBar->setStyleSheet( - "QWidget#appsBar { background-color: #4f869f; border: 1px solid " - "#63b4da; border-radius:0px; padding: 0; margin: 0; }"); - m_mediaBar->setStyleSheet("QWidget#mediaBar { background-color: #2ECC71; " - "border: none; padding: 0; margin: 0; }"); - m_galleryBar->setStyleSheet( - "QWidget#galleryBar { background-color: #9b59b6; border: 1px solid " - "#b36cd1; border-radius:0px; padding: 0; margin: 0; }"); - m_othersBar->setStyleSheet( - "QWidget#othersBar { background-color: #a28729; border: 1px solid " - "#c4a32d; border-radius:0px; padding: 0; margin: 0; }"); - m_freeBar->setStyleSheet( - QString("QWidget#freeBar { background-color: %1; border: " - "1px solid " - "#4f4f4f4f; padding: 0; margin: 0; border-radius:0px; " - "border-top-right-radius: 3px; " - "border-bottom-right-radius: 3px; }") - .arg(dark ? "rgba(255, 255, 255, 10)" : "rgba(0, 0, 0, 25)")); - - // remove padding margin from layout - m_systemBar->setContentsMargins(0, 0, 0, 0); - m_appsBar->setContentsMargins(0, 0, 0, 0); - m_mediaBar->setContentsMargins(0, 0, 0, 0); - m_galleryBar->setContentsMargins(0, 0, 0, 0); - m_othersBar->setContentsMargins(0, 0, 0, 0); - m_freeBar->setContentsMargins(0, 0, 0, 0); - - m_diskBarLayout->addWidget(m_systemBar); - m_diskBarLayout->addWidget(m_appsBar); - m_diskBarLayout->addWidget(m_mediaBar); - m_diskBarLayout->addWidget(m_galleryBar); - m_diskBarLayout->addWidget(m_othersBar); - m_diskBarLayout->addWidget(m_freeBar); - - m_dataLayout->addWidget(m_diskBarContainer); - - QWidget *m_legendWidget = new QWidget(); - m_legendWidget->setContentsMargins(0, 0, 0, 0); - m_legendWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - - m_legendLayout = new QHBoxLayout(m_legendWidget); - m_legendLayout->setSpacing(0); - m_legendLayout->setContentsMargins(0, 0, 0, 0); - - m_systemLabel = new QLabel("System", m_legendWidget); - m_appsLabel = new QLabel("Apps", m_legendWidget); - m_mediaLabel = new QLabel("Media", m_legendWidget); - m_galleryLabel = new QLabel("Gallery", m_legendWidget); - m_othersLabel = new QLabel("Others", m_legendWidget); - m_freeLabel = new QLabel("Free", m_legendWidget); - - QString labelStyle = - "margin: 0px; padding: 0px 4px 0px 0px; font-size: 10px;"; - m_systemLabel->setStyleSheet(labelStyle); - m_appsLabel->setStyleSheet(labelStyle); - m_mediaLabel->setStyleSheet(labelStyle); - m_galleryLabel->setStyleSheet(labelStyle); - m_othersLabel->setStyleSheet(labelStyle); - m_freeLabel->setStyleSheet(labelStyle); - - // FIXME:switch to zloadingwidget and remove unnecessary stretches when - // m_galleryLabel is invisible - m_legendLayout->addWidget(m_systemLabel); - m_legendLayout->addStretch(); - m_legendLayout->addWidget(m_appsLabel); - m_legendLayout->addStretch(); - m_legendLayout->addWidget(m_mediaLabel); - m_legendLayout->addStretch(); - m_legendLayout->addWidget(m_galleryLabel); - m_legendLayout->addStretch(); - m_legendLayout->addWidget(m_othersLabel); - m_legendLayout->addStretch(); - m_legendLayout->addWidget(m_freeLabel); - - // Add the legend widget (not the layout) to the data layout - m_dataLayout->addWidget(m_legendWidget); - - m_stackedWidget->addWidget(m_dataPage); - - // Initially show loading page - m_stackedWidget->setCurrentWidget(m_loadingErrorPage); -} - -void DiskUsageWidget::updateUI() -{ - if (m_state == Loading) { - m_processIndicator->start(); - m_statusLabel->setText("Loading disk usage..."); - m_stackedWidget->setCurrentWidget(m_loadingErrorPage); - return; - } - - if (m_state == Error) { - m_processIndicator->stop(); - m_statusLabel->setText("Error: " + m_errorMessage); - m_stackedWidget->setCurrentWidget(m_loadingErrorPage); - return; - } - - if (m_totalCapacity == 0) { - m_processIndicator->stop(); - m_processIndicator->hide(); - m_statusLabel->setText("No disk information available."); - m_stackedWidget->setCurrentWidget(m_loadingErrorPage); - return; - } - - // Show data page - m_stackedWidget->setCurrentWidget(m_dataPage); - - // Calculate proportions for each segment - int totalWidth = m_diskBarContainer->width(); - - int systemWidth = - (int)((double)m_systemUsage / m_totalCapacity * totalWidth); - int appsWidth = (int)((double)m_appsUsage / m_totalCapacity * totalWidth); - int mediaWidth = (int)((double)m_mediaUsage / m_totalCapacity * totalWidth); - int galleryWidth = - (int)((double)m_galleryUsage / m_totalCapacity * totalWidth); - int othersWidth = - (int)((double)m_othersUsage / m_totalCapacity * totalWidth); - int freeWidth = (int)((double)m_freeSpace / m_totalCapacity * totalWidth); - - // Ensure at least 1 pixel width for non-zero values - if (m_systemUsage > 0 && systemWidth == 0) - systemWidth = 1; - if (m_appsUsage > 0 && appsWidth == 0) - appsWidth = 1; - if (m_mediaUsage > 0 && mediaWidth == 0) - mediaWidth = 1; - if (m_othersUsage > 0 && othersWidth == 0) - othersWidth = 1; - if (m_freeSpace > 0 && freeWidth == 0) - freeWidth = 1; - if (m_galleryUsage > 0 && galleryWidth == 0) - galleryWidth = 1; - - m_diskBarLayout->setStretchFactor(m_systemBar, systemWidth); - m_diskBarLayout->setStretchFactor(m_appsBar, appsWidth); - m_diskBarLayout->setStretchFactor(m_mediaBar, mediaWidth); - m_diskBarLayout->setStretchFactor(m_galleryBar, galleryWidth); - m_diskBarLayout->setStretchFactor(m_othersBar, othersWidth); - m_diskBarLayout->setStretchFactor(m_freeBar, freeWidth); - - // Hide segments with zero usage - m_systemBar->setVisible(m_systemUsage > 0); - m_systemLabel->setVisible(m_systemUsage > 0); - - m_appsBar->setVisible(m_appsUsage > 0); - m_appsLabel->setVisible(m_appsUsage > 0); - - m_mediaBar->setVisible(m_mediaUsage > 0); - m_mediaLabel->setVisible(m_mediaUsage > 0); - - m_galleryBar->setVisible(m_galleryUsage > 0); - m_galleryLabel->setVisible(m_galleryUsage > 0); - - m_othersBar->setVisible(m_othersUsage > 0); - m_othersLabel->setVisible(m_othersUsage > 0); - - m_freeBar->setVisible(m_freeSpace > 0); - m_freeLabel->setVisible(m_freeSpace > 0); - - // Update legend labels with sizes - m_systemLabel->setText( - QString("System (%1)").arg(Utils::formatSize(m_systemUsage))); - m_appsLabel->setText( - QString("Apps (%1)").arg(Utils::formatSize(m_appsUsage))); - m_mediaLabel->setText( - QString("Media (%1)").arg(Utils::formatSize(m_mediaUsage))); - m_othersLabel->setText( - QString("Others (%1)").arg(Utils::formatSize(m_othersUsage))); - m_freeLabel->setText( - QString("Free (%1)").arg(Utils::formatSize(m_freeSpace))); - m_galleryLabel->setText( - QString("Gallery (%1)").arg(Utils::formatSize(m_galleryUsage))); - - qDebug() << "Disk Usage Updated:" - << "System:" << m_systemUsage << "Apps:" << m_appsUsage - << "Media:" << m_mediaUsage << "Others:" << m_othersUsage - << "Gallery:" << m_galleryUsage << "Free:" << m_freeSpace; - - // Set stretch factors and ensure minimum visibility - int systemStretch = std::max( - 1, (int)(m_systemUsage / 1000000)); // Convert to MB for stretch - int appsStretch = std::max(1, (int)(m_appsUsage / 1000000)); - int mediaStretch = std::max(1, (int)(m_mediaUsage / 1000000)); - int galleryStretch = std::max(1, (int)(m_galleryUsage / 1000000)); - int othersStretch = std::max(1, (int)(m_othersUsage / 1000000)); - int freeStretch = std::max(1, (int)(m_freeSpace / 1000000)); - - m_diskBarLayout->setStretchFactor(m_systemBar, systemStretch); - m_diskBarLayout->setStretchFactor(m_appsBar, appsStretch); - m_diskBarLayout->setStretchFactor(m_mediaBar, mediaStretch); - m_diskBarLayout->setStretchFactor(m_galleryBar, galleryStretch); - m_diskBarLayout->setStretchFactor(m_othersBar, othersStretch); - m_diskBarLayout->setStretchFactor(m_freeBar, freeStretch); - - /* FIXME: NSPopover bug */ - // #ifdef Q_OS_MAC - // m_systemBar->setUsageInfo("System", formatSize(m_systemUsage), - // "#a1384d", - // (double)m_systemUsage / m_totalCapacity); - // m_appsBar->setUsageInfo("Apps", formatSize(m_appsUsage), "#3498DB", - // (double)m_appsUsage / m_totalCapacity); - // m_mediaBar->setUsageInfo("Media", formatSize(m_mediaUsage), - // "#2ECC71", - // (double)m_mediaUsage / m_totalCapacity); - // m_othersBar->setUsageInfo("Others", formatSize(m_othersUsage), - // "#F39C12", - // (double)m_othersUsage / m_totalCapacity); - // m_freeBar->setUsageInfo("Free", formatSize(m_freeSpace), "#BDC3C7", - // (double)m_freeSpace / m_totalCapacity); - // #else - m_systemBar->setToolTip( - QString("System: %1 (%2%)") - .arg(Utils::formatSize(m_systemUsage)) - .arg(QString::number((double)m_systemUsage / m_totalCapacity * 100, - 'f', 1))); - m_appsBar->setToolTip( - QString("Apps: %1 (%2%)") - .arg(Utils::formatSize(m_appsUsage)) - .arg(QString::number((double)m_appsUsage / m_totalCapacity * 100, - 'f', 1))); - m_mediaBar->setToolTip( - QString("Media: %1 (%2%)") - .arg(Utils::formatSize(m_mediaUsage)) - .arg(QString::number((double)m_mediaUsage / m_totalCapacity * 100, - 'f', 1))); - m_galleryBar->setToolTip( - QString("Gallery: %1 (%2%)") - .arg(Utils::formatSize(m_galleryUsage)) - .arg(QString::number((double)m_galleryUsage / m_totalCapacity * 100, - 'f', 1))); - m_othersBar->setToolTip( - QString("Others: %1 (%2%)") - .arg(Utils::formatSize(m_othersUsage)) - .arg(QString::number((double)m_othersUsage / m_totalCapacity * 100, - 'f', 1))); - m_freeBar->setToolTip( - QString("Free: %1 (%2%)") - .arg(Utils::formatSize(m_freeSpace)) - .arg(QString::number((double)m_freeSpace / m_totalCapacity * 100, - 'f', 1))); -} -void DiskUsageWidget::onDiskUsageRetrieved(bool success, uint64_t apps_usage, - uint64_t gallery_usage) -{ - if (!success) { - m_state = Error; - m_errorMessage = "Failed to retrieve disk usage data."; - } else { - m_state = Ready; - m_totalCapacity = m_device->deviceInfo.diskInfo.totalDiskCapacity; - qDebug() << "Total Capacity:" << m_totalCapacity; - m_systemUsage = m_device->deviceInfo.diskInfo.totalSystemCapacity; - m_appsUsage = apps_usage; - // m_mediaUsage = result["mediaUsage"].toULongLong(); - m_mediaUsage = 0; - m_freeSpace = m_device->deviceInfo.diskInfo.totalDataAvailable; - // m_galleryUsage = result["galleryUsage"].toULongLong(); - m_galleryUsage = gallery_usage; - - uint64_t usedKnown = - m_systemUsage + m_appsUsage + m_mediaUsage + m_galleryUsage; - if (m_totalCapacity > (m_freeSpace + usedKnown)) { - m_othersUsage = m_totalCapacity - m_freeSpace - usedKnown; - } else { - m_othersUsage = 0; - } - } - - updateUI(); -} diff --git a/src/diskusagewidget.h b/src/diskusagewidget.h deleted file mode 100644 index 2a967e8..0000000 --- a/src/diskusagewidget.h +++ /dev/null @@ -1,103 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef DISKUSAGEWIDGET_H -#define DISKUSAGEWIDGET_H -#include "diskusagebar.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "qprocessindicator.h" - -#include -#include -#include -#include -#include -#include -#include - -class DiskUsageWidget : public QWidget -{ - Q_OBJECT -public: - explicit DiskUsageWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - -private: - void onDiskUsageRetrieved(bool success, uint64_t apps_usage, - uint64_t gallery_usage); - void setupUI(); - void updateUI(); - - enum State { Loading, Ready, Error }; - - const std::shared_ptr m_device; - State m_state; - QString m_errorMessage; - - // UI widgets - QVBoxLayout *m_mainLayout; - QLabel *m_titleLabel; - QStackedWidget *m_stackedWidget; - - // Loading/Error page - QWidget *m_loadingErrorPage; - QVBoxLayout *m_loadingErrorLayout; - QProcessIndicator *m_processIndicator; - QLabel *m_statusLabel; - - // Data page - QWidget *m_dataPage; - QVBoxLayout *m_dataLayout; - QWidget *m_diskBarContainer; - QHBoxLayout *m_diskBarLayout; - /*FIXME: NSPopover bug */ - // #ifdef Q_OS_MAC - // DiskUsageBar *m_systemBar; - // DiskUsageBar *m_appsBar; - // DiskUsageBar *m_mediaBar; - // DiskUsageBar *m_othersBar; - // DiskUsageBar *m_freeBar; - // #else - QWidget *m_systemBar; - QWidget *m_appsBar; - QWidget *m_mediaBar; - QWidget *m_othersBar; - QWidget *m_freeBar; - QWidget *m_galleryBar; - // #endif - - QHBoxLayout *m_legendLayout; - QLabel *m_systemLabel; - QLabel *m_appsLabel; - QLabel *m_mediaLabel; - QLabel *m_galleryLabel; - QLabel *m_othersLabel; - QLabel *m_freeLabel; - - uint64_t m_totalCapacity; - uint64_t m_systemUsage; - uint64_t m_appsUsage; - uint64_t m_mediaUsage; - uint64_t m_othersUsage; - uint64_t m_freeSpace; - uint64_t m_galleryUsage; -}; - -#endif // DISKUSAGEWIDGET_H diff --git a/src/exportalbum.cpp b/src/exportalbum.cpp deleted file mode 100644 index 9f97a64..0000000 --- a/src/exportalbum.cpp +++ /dev/null @@ -1,251 +0,0 @@ -#include "exportalbum.h" - -void AlbumScanWorker::scanAlbums(const QStringList &paths) -{ - { - QMutexLocker locker(&m_cancelMutex); - m_cancelRequested = false; - } - - ScanResult res{true, 0, {}}; - - for (const QString &path : paths) { - if (isCancelled()) { - return; - } - - QList items = m_device->afc_backend->list_files_flat(path); - - if (items.isEmpty()) { - res.ok = false; - continue; - } - - for (const QString &item : items) { - if (isCancelled()) { - return; - } - if (item.isEmpty()) - continue; - res.items.append(item); - } - res.count += static_cast(items.size()); - } - - emit scanFinished(res.ok, res.count, res.items); -} - -void AlbumScanWorker::calculateTotalSize(const QStringList &items) -{ - { - QMutexLocker locker(&m_cancelMutex); - m_cancelRequested = false; - } - - quint64 totalSize = 0; - - for (const QString &item : items) { - if (isCancelled()) { - return; - } - - int size = m_device->afc_backend->get_file_size(item); - if (size > 0) { - totalSize += static_cast(size); - } - emit totalSizeProgress(totalSize); - } - - emit totalSizeFinished(totalSize); -} - -void AlbumScanWorker::cancel() -{ - QMutexLocker locker(&m_cancelMutex); - m_cancelRequested = true; -} - -bool AlbumScanWorker::isCancelled() -{ - QMutexLocker locker(&m_cancelMutex); - return m_cancelRequested; -} - -ExportAlbum::ExportAlbum(const std::shared_ptr device, - const QStringList &paths, QWidget *parent) - : QDialog(parent), m_device(device), m_listCount(paths.size()) -{ - setWindowTitle("Export Album"); - setMaximumSize(600, 400); -#ifdef WIN32 - setupWinWindow(this); -#endif - - m_loadingWidget = new ZLoadingWidget(false, this); - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->addWidget(m_loadingWidget); - - m_workerThread = new QThread(this); - m_worker = new AlbumScanWorker(m_device); - m_worker->moveToThread(m_workerThread); - - connect(m_workerThread, &QThread::finished, m_worker, - &QObject::deleteLater); - connect(this, &ExportAlbum::requestScan, m_worker, - &AlbumScanWorker::scanAlbums, Qt::QueuedConnection); - connect(this, &ExportAlbum::requestTotalSize, m_worker, - &AlbumScanWorker::calculateTotalSize, Qt::QueuedConnection); - connect(this, &ExportAlbum::requestCancelWorker, m_worker, - &AlbumScanWorker::cancel, Qt::QueuedConnection); - - connect(m_worker, &AlbumScanWorker::scanFinished, this, - [this](bool ok, quint64 count, const QStringList &items) { - qDebug() << "Total photo count:" << count << "with" - << (ok ? 0 : 1) << "errors"; - - if (m_exiting) { - return; - } - - if (ok) { - m_exportItems = items; - updateInfoLabel(count); - calculateTotalExportSize(); - m_loadingWidget->stop(); - } else { - QMessageBox::warning( - nullptr, "Error", - "Failed to read directory: cannot export album(s)"); - reject(); - } - }); - - connect( - m_worker, &AlbumScanWorker::totalSizeProgress, this, - [this](quint64 totalSize) { - if (m_exiting) { - return; - } - m_totalExportSize = totalSize; - m_totalSizeExportLabel->setText( - QString("Total size to export: %1") - .arg(iDescriptor::Utils::formatSize(m_totalExportSize))); - }); - - connect( - m_worker, &AlbumScanWorker::totalSizeFinished, this, - [this](quint64 totalSize) { - if (m_exiting) { - return; - } - m_totalExportSize = totalSize; - m_totalSizeExportLabel->setText( - QString("Total size to export: %1") - .arg(iDescriptor::Utils::formatSize(m_totalExportSize))); - m_loadingIndicator->stop(); - m_loadingIndicator->hide(); - }); - - m_workerThread->start(); - - connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, - [this](const QString &udid) { - if (udid == m_device->udid) { - m_exiting = true; - emit requestCancelWorker(); - QTimer::singleShot(0, this, [this]() { close(); }); - } - }); - - QWidget *contentWidget = new QWidget(this); - QVBoxLayout *contentLayout = new QVBoxLayout(contentWidget); - - m_infoLabel = new QLabel(this); - contentLayout->addWidget(m_infoLabel); - - QHBoxLayout *buttonLayout = new QHBoxLayout(); - QPushButton *cancelButton = new QPushButton("Cancel", this); - QPushButton *exportButton = new QPushButton("Export", this); - buttonLayout->addWidget(exportButton); - buttonLayout->addWidget(cancelButton); - m_dirPickerLabel = new ZDirPickerLabel(); - - contentLayout->addWidget(m_dirPickerLabel); - - QHBoxLayout *sizeLayout = new QHBoxLayout(); - - m_totalSizeExportLabel = new QLabel("Total size to export: 0 MB", this); - sizeLayout->addWidget(m_totalSizeExportLabel); - - m_loadingIndicator = new QProcessIndicator(this); - m_loadingIndicator->setType(QProcessIndicator::line_rotate); - m_loadingIndicator->setFixedSize(32, 16); - sizeLayout->addWidget(m_loadingIndicator); - sizeLayout->addStretch(); - - contentLayout->addLayout(sizeLayout); - contentLayout->addLayout(buttonLayout); - - connect(cancelButton, &QPushButton::clicked, this, [this]() { - m_exiting = true; - emit requestCancelWorker(); - QTimer::singleShot(0, this, [this]() { close(); }); - }); - connect(exportButton, &QPushButton::clicked, this, [this, exportButton]() { - m_exiting = true; - emit requestCancelWorker(); - exportButton->setEnabled(false); - QTimer::singleShot(0, this, [this]() { - startExport(); - accept(); - }); - }); - - m_loadingWidget->setupContentWidget(contentWidget); - - getTotalPhotoCount(paths); - - connect(this, &QDialog::finished, this, [this](int) { - m_exiting = true; - deleteLater(); - }); -} - -void ExportAlbum::getTotalPhotoCount(const QStringList &paths) -{ - emit requestScan(paths); -} - -void ExportAlbum::updateInfoLabel(quint64 photoCount) -{ - m_infoLabel->setText(QString("Are you sure you want to export %1 album(s) " - "with %2 photo(s)/video(s) ?") - .arg(m_listCount) - .arg(photoCount)); -} - -void ExportAlbum::startExport() -{ - IOManagerClient::sharedInstance()->startExport( - m_device, m_exportItems, m_dirPickerLabel->getOutputDir(), - "Exporting Album(s)"); -} - -void ExportAlbum::calculateTotalExportSize() -{ - m_totalExportSize = 0; - m_totalSizeExportLabel->setText("Total size to export: 0 MB"); - m_loadingIndicator->start(); - emit requestTotalSize(m_exportItems); -} - -ExportAlbum::~ExportAlbum() -{ - m_exiting = true; - emit requestCancelWorker(); - - if (m_workerThread && m_workerThread->isRunning()) { - m_workerThread->quit(); - m_workerThread->wait(); - } -} \ No newline at end of file diff --git a/src/exportalbum.h b/src/exportalbum.h deleted file mode 100644 index a795ef9..0000000 --- a/src/exportalbum.h +++ /dev/null @@ -1,85 +0,0 @@ -#ifndef EXPORTALBUM_H -#define EXPORTALBUM_H - -#include "appcontext.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "iomanagerclient.h" -#include "qprocessindicator.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include - - -struct ScanResult { - bool ok; - quint64 count; - QStringList items; -}; - -class AlbumScanWorker : public QObject -{ - Q_OBJECT -public: - explicit AlbumScanWorker(const std::shared_ptr &device) - : m_device(device) - { - } - -public slots: - void scanAlbums(const QStringList &paths); - void calculateTotalSize(const QStringList &items); - void cancel(); - -signals: - void scanFinished(bool ok, quint64 count, const QStringList &items); - void totalSizeProgress(quint64 totalSize); - void totalSizeFinished(quint64 totalSize); - -private: - bool isCancelled(); - - const std::shared_ptr m_device; - QMutex m_cancelMutex; - bool m_cancelRequested = false; -}; - -class ExportAlbum : public QDialog -{ - Q_OBJECT -public: - explicit ExportAlbum(const std::shared_ptr device, - const QStringList &paths, QWidget *parent = nullptr); - ~ExportAlbum(); - -signals: - void requestScan(const QStringList &paths); - void requestTotalSize(const QStringList &items); - void requestCancelWorker(); - -private: - QThread *m_workerThread = nullptr; - AlbumScanWorker *m_worker = nullptr; - ZLoadingWidget *m_loadingWidget; - const std::shared_ptr m_device; - QLabel *m_infoLabel; - size_t m_listCount; - QList m_exportItems; - ZDirPickerLabel *m_dirPickerLabel; - QLabel *m_totalSizeExportLabel; - QProcessIndicator *m_loadingIndicator = nullptr; - quint64 m_totalExportSize = 0; - bool m_exiting = false; - void getTotalPhotoCount(const QStringList &paths); - void updateInfoLabel(quint64 photoCount); - // startExport(const QStringList &paths, const QString &exportDir); - void startExport(); - void calculateTotalExportSize(); -}; - -#endif // EXPORTALBUM_H diff --git a/src/fileexplorerwidget.cpp b/src/fileexplorerwidget.cpp deleted file mode 100644 index 2e87c20..0000000 --- a/src/fileexplorerwidget.cpp +++ /dev/null @@ -1,259 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "fileexplorerwidget.h" -#include "afcexplorerwidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "mediapreviewdialog.h" -#include "settingsmanager.h" - -FileExplorerWidget::FileExplorerWidget( - const std::shared_ptr device, QWidget *parent) - : QWidget(parent), m_device(device) -{ - QVBoxLayout *rootLayout = new QVBoxLayout(this); - rootLayout->setContentsMargins(0, 0, 0, 0); - - m_loadingWidget = new ZLoadingWidget(false, this); - rootLayout->addWidget(m_loadingWidget); -} - -void FileExplorerWidget::init() -{ - if (m_loaded) { - qDebug() - << "[FileExplorerWidget]: Already initialized, skipping init()"; - return; - } - m_loaded = true; - m_mainSplitter = new ModernSplitter(Qt::Horizontal); - - // Main layout - QWidget *contentContainer = new QWidget(this); - QHBoxLayout *mainLayout = new QHBoxLayout(contentContainer); - mainLayout->addWidget(m_mainSplitter); - mainLayout->setContentsMargins(0, 0, 0, 0); - - m_loadingWidget->setupContentWidget(contentContainer); - - setupSidebar(); - - // Create stacked widget with AFC explorers - m_stackedWidget = new QStackedWidget(); - - // Add normal AFC explorer (index 0) - AfcExplorerWidget *afcExplorer = - new AfcExplorerWidget(m_device, true, std::nullopt, false, "/", this); - connect(afcExplorer, &AfcExplorerWidget::favoritePlaceAdded, this, - &FileExplorerWidget::saveFavoritePlace); - - m_stackedWidget->addWidget(afcExplorer); - - // Add AFC2 explorer (index 1) - AfcExplorerWidget *afc2Explorer = - new AfcExplorerWidget(m_device, true, std::nullopt, true, "/", this); - connect(afc2Explorer, &AfcExplorerWidget::favoritePlaceAdded, this, - &FileExplorerWidget::saveFavoritePlaceAfc2); - m_stackedWidget->addWidget(afc2Explorer); - - // Start with normal AFC client - m_stackedWidget->setCurrentIndex(0); - - // Add widgets to splitter - m_mainSplitter->addWidget(m_sidebarTree); - m_mainSplitter->addWidget(m_stackedWidget); - m_mainSplitter->setSizes({400, 800}); - setLayout(mainLayout); - - connect(SettingsManager::sharedInstance(), - &SettingsManager::favoritePlacesChanged, this, - &FileExplorerWidget::loadFavoritePlaces); - - m_loadingWidget->stop(true); -} - -void FileExplorerWidget::setupSidebar() -{ - m_sidebarTree = new QTreeWidget(); - m_sidebarTree->setHeaderLabel("Files"); - m_sidebarTree->setMinimumWidth(50); - m_sidebarTree->setMaximumWidth(250); - m_sidebarTree->setContextMenuPolicy(Qt::CustomContextMenu); - - QTreeWidgetItem *explorersRoot = new QTreeWidgetItem(m_sidebarTree); - explorersRoot->setText(0, "Explorer"); - explorersRoot->setIcon(0, QIcon::fromTheme("folder")); - explorersRoot->setExpanded(true); - - m_defaultAfcItem = new QTreeWidgetItem(explorersRoot); - m_defaultAfcItem->setText(0, "Default"); - m_defaultAfcItem->setIcon(0, QIcon::fromTheme("folder")); - - m_jailbrokenAfcItem = new QTreeWidgetItem(explorersRoot); - m_jailbrokenAfcItem->setText(0, "Jailbroken (AFC2)"); - m_jailbrokenAfcItem->setIcon(0, QIcon::fromTheme("folder")); - - // Common Places section - QTreeWidgetItem *commonPlacesItem = new QTreeWidgetItem(m_sidebarTree); - commonPlacesItem->setText(0, "Common Places"); - commonPlacesItem->setIcon(0, QIcon::fromTheme("places-bookmarks")); - commonPlacesItem->setExpanded(true); - - QTreeWidgetItem *wallpapersItem = new QTreeWidgetItem(commonPlacesItem); - QVariantMap dataMap; - dataMap["path"] = "/DCIM"; - dataMap["alias"] = "Pictures"; - dataMap["afc2"] = false; - wallpapersItem->setText(0, "Pictures"); - wallpapersItem->setIcon(0, QIcon::fromTheme("image-x-generic")); - wallpapersItem->setData(0, Qt::UserRole, QVariant::fromValue(dataMap)); - - // Favorite Places section - m_favoritePlacesItem = new QTreeWidgetItem(m_sidebarTree); - m_favoritePlacesItem->setText(0, "Favorite Places"); - m_favoritePlacesItem->setIcon(0, QIcon::fromTheme("user-bookmarks")); - m_favoritePlacesItem->setExpanded(true); - - loadFavoritePlaces(); - - connect(m_sidebarTree, &QTreeWidget::itemClicked, this, - &FileExplorerWidget::onSidebarItemClicked); - connect(m_sidebarTree, &QTreeWidget::customContextMenuRequested, this, - &FileExplorerWidget::onSidebarContextMenuRequested); -} - -void FileExplorerWidget::loadFavoritePlaces() -{ - m_favoritePlacesItem->takeChildren(); - SettingsManager *settings = SettingsManager::sharedInstance(); - QList> favorites = - settings->getFavoritePlaces("favorite_places/"); - - for (const auto &favorite : favorites) { - QString path = favorite.first; - QString alias = favorite.second; - QVariantMap dataMap; - dataMap["path"] = path; - dataMap["alias"] = alias; - dataMap["afc2"] = false; - - QTreeWidgetItem *favoriteItem = - new QTreeWidgetItem(m_favoritePlacesItem); - favoriteItem->setText(0, alias); - favoriteItem->setIcon(0, QIcon::fromTheme("folder-favorites")); - favoriteItem->setData(0, Qt::UserRole, QVariant::fromValue(dataMap)); - } - - QList> favorites_afc2 = - settings->getFavoritePlaces("favorite_places_afc2/"); - - for (const auto &favorite : favorites_afc2) { - QString path = favorite.first; - QString alias = favorite.second; - QVariantMap dataMap; - dataMap["path"] = path; - dataMap["alias"] = alias; - dataMap["afc2"] = true; - QTreeWidgetItem *favoriteItem = - new QTreeWidgetItem(m_favoritePlacesItem); - favoriteItem->setText(0, alias); - favoriteItem->setIcon(0, QIcon::fromTheme("folder-favorites")); - favoriteItem->setData(0, Qt::UserRole, QVariant::fromValue(dataMap)); - } -} - -void FileExplorerWidget::onSidebarItemClicked(QTreeWidgetItem *item, int column) -{ - Q_UNUSED(column); - - if (item == m_defaultAfcItem) { - static_cast(m_stackedWidget->widget(0))->goHome(); - m_stackedWidget->setCurrentIndex(0); - } else if (item == m_jailbrokenAfcItem) { - static_cast(m_stackedWidget->widget(1))->goHome(); - m_stackedWidget->setCurrentIndex(1); - } - - QVariant data = item->data(0, Qt::UserRole); - if (data.isValid()) { - QVariantMap dataMap = data.toMap(); - QString path = dataMap.value("path").toString(); - bool afc2 = dataMap.value("afc2").toBool(); - if (afc2) { - m_stackedWidget->setCurrentIndex(1); - } else { - m_stackedWidget->setCurrentIndex(0); - } - AfcExplorerWidget *currentExplorer = - qobject_cast(m_stackedWidget->currentWidget()); - if (currentExplorer) { - currentExplorer->navigateToPath(path); - } - } -} - -void FileExplorerWidget::saveFavoritePlace(const QString &alias, - const QString &path) -{ - qDebug() << "Saving favorite place:" << alias << "->" << path; - SettingsManager *settings = SettingsManager::sharedInstance(); - settings->saveFavoritePlace(path, alias, "favorite_places/"); -} - -void FileExplorerWidget::saveFavoritePlaceAfc2(const QString &alias, - const QString &path) -{ - SettingsManager *settings = SettingsManager::sharedInstance(); - settings->saveFavoritePlace(path, alias, "favorite_places_afc2/"); -} - -void FileExplorerWidget::onSidebarContextMenuRequested(const QPoint &pos) -{ - QTreeWidgetItem *item = m_sidebarTree->itemAt(pos); - - // Only show a context menu for items that are direct children of the - // favorites list. - if (!item || item->parent() != m_favoritePlacesItem) { - return; - } - - QVariant data = item->data(0, Qt::UserRole); - if (!data.isValid()) { - return; - } - - QMenu contextMenu; - QAction *removeAction = contextMenu.addAction("Remove from Favorites"); - QAction *selectedAction = - contextMenu.exec(m_sidebarTree->viewport()->mapToGlobal(pos)); - - if (selectedAction == removeAction) { - QVariantMap dataMap = data.toMap(); - QString path = dataMap.value("path").toString(); - bool afc2 = dataMap.value("afc2").toBool(); - - SettingsManager *settings = SettingsManager::sharedInstance(); - if (afc2) { - settings->removeFavoritePlace("favorite_places_afc2/", path); - } else { - settings->removeFavoritePlace("favorite_places/", path); - } - } -} \ No newline at end of file diff --git a/src/fileexplorerwidget.h b/src/fileexplorerwidget.h deleted file mode 100644 index 16b9328..0000000 --- a/src/fileexplorerwidget.h +++ /dev/null @@ -1,83 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef FILEEXPLORERWIDGET_H -#define FILEEXPLORERWIDGET_H - -#include "iDescriptor.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class FileExplorerWidget : public QWidget -{ - Q_OBJECT -public: - explicit FileExplorerWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - void init(); - -private slots: - void onSidebarItemClicked(QTreeWidgetItem *item, int column); - -private: - ZLoadingWidget *m_loadingWidget; - QSplitter *m_mainSplitter; - QStackedWidget *m_stackedWidget; - // AfcClientHandle *currentAfcClient; - QTreeWidget *m_sidebarTree; - const std::shared_ptr m_device; - - // Tree items - QTreeWidgetItem *m_defaultAfcItem; - QTreeWidgetItem *m_jailbrokenAfcItem; - QTreeWidgetItem *m_favoritePlacesItem; - - void setupSidebar(); - void loadFavoritePlaces(); - void saveFavoritePlace(const QString &alias, const QString &path); - void saveFavoritePlaceAfc2(const QString &alias, const QString &path); - void onSidebarContextMenuRequested(const QPoint &pos); - bool m_loaded = false; -}; - -#endif // FILEEXPLORERWIDGET_H diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp deleted file mode 100644 index fbcc991..0000000 --- a/src/gallerywidget.cpp +++ /dev/null @@ -1,737 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "gallerywidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "imageloader.h" -#include "iomanagerclient.h" -#include "mediapreviewdialog.h" -#include "photomodel.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* - FIXME: this needs to be refactored once we - figure out how to query Photos.sqlite - Check out: - https://github.com/ScottKjr3347/iOS_Local_PL_Photos.sqlite_Queries -*/ -GalleryWidget::GalleryWidget(const std::shared_ptr device, - QWidget *parent) - : QWidget{parent}, m_device(device) - -{ - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(0, 0, 0, 0); - m_loadingWidget = new ZLoadingWidget(true, this); - setupControlsLayout(); - m_mainLayout->addWidget(m_loadingWidget); - - // Setup album selection view - setupAlbumSelectionView(); - - // Setup photo gallery view - setupPhotoGalleryView(); - - // Add stacked widget to main layout - setLayout(m_mainLayout); - - connect(m_loadingWidget, &ZLoadingWidget::retryClicked, this, - &GalleryWidget::refresh); - - setControlsEnabled(false); // Disable controls until album is selected -} - -void GalleryWidget::refresh() -{ - bool inAlbumSelection = - (m_loadingWidget->currentWidget() == m_albumSelectionWidget); - - m_loadingWidget->showLoading(); - // refresh the album list - if (inAlbumSelection) { - qDebug() << "Refreshing album list..."; - QTimer::singleShot(100, this, &GalleryWidget::reload); - return; - } - if (m_model) { - qDebug() << "Refreshing current album:" << m_currentAlbumPath; - m_model->setAlbumPath(m_currentAlbumPath); - } -} - -void GalleryWidget::reload() -{ - m_loaded = false; - load(); -} - -/*Load is called when the tab is active*/ -void GalleryWidget::load() -{ - if (m_loaded) - return; - - m_loaded = true; - connect( - m_device->afc_backend, &CXX::AfcBackend::album_list_loaded, this, - [this](QString udid, QList album_list) { - onAlbumListLoaded(album_list); - }, - Qt::SingleShotConnection); - m_device->afc_backend->load_album_list(); -} - -void GalleryWidget::setupControlsLayout() -{ - m_controlsLayout = new QHBoxLayout(); - m_controlsLayout->setSpacing(5); - m_controlsLayout->setContentsMargins(7, 7, 7, 7); - - m_importButton = new QPushButton("Import"); - - // Sort order combo box - QLabel *sortLabel = new QLabel("Sort:"); - QFont sortFont = sortLabel->font(); - sortFont.setWeight(QFont::DemiBold); - sortLabel->setFont(sortFont); - - m_sortComboBox = new QComboBox(); - m_sortComboBox->addItem("Newest First", - static_cast(PhotoModel::NewestFirst)); - m_sortComboBox->addItem("Oldest First", - static_cast(PhotoModel::OldestFirst)); - m_sortComboBox->setCurrentIndex(0); // Default to Newest First - m_sortComboBox->setMinimumWidth(100); // Ensure text fits - m_sortComboBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - // Filter combo box - QLabel *filterLabel = new QLabel("Filter:"); - QFont filterFont = filterLabel->font(); - filterFont.setWeight(QFont::DemiBold); - filterLabel->setFont(filterFont); - m_filterComboBox = new QComboBox(); - m_filterComboBox->addItem("All Media", static_cast(PhotoModel::All)); - m_filterComboBox->addItem("Images Only", - static_cast(PhotoModel::ImagesOnly)); - m_filterComboBox->addItem("Videos Only", - static_cast(PhotoModel::VideosOnly)); - m_filterComboBox->setCurrentIndex( - static_cast(PhotoModel::All)); // Default to All - m_filterComboBox->setMinimumWidth(90); // Ensure text fits - m_filterComboBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - // Export buttons - m_exportSelectedButton = new QPushButton("Export Selected"); - m_exportSelectedButton->setEnabled(false); - m_exportSelectedButton->setSizePolicy(QSizePolicy::Preferred, - QSizePolicy::Fixed); - m_exportAllButton = new QPushButton("Export All"); - m_exportAllButton->setEnabled(false); - - // Back button - m_backButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsArrowLeftAlt.png"), - "Back to Albums"); - m_backButton->setMaximumWidth(30); - m_backButton->hide(); // Hidden initially - - // Refresh button - m_refreshButton = new ZIconWidget( - QIcon(":/resources/icons/IcOutlineRefresh.png"), "Refresh Album"); - m_refreshButton->setMaximumWidth(30); - connect(m_refreshButton, &ZIconWidget::clicked, this, - &GalleryWidget::refresh); - - // Connect signals - connect(m_sortComboBox, QOverload::of(&QComboBox::currentIndexChanged), - this, &GalleryWidget::onSortOrderChanged); - connect(m_filterComboBox, - QOverload::of(&QComboBox::currentIndexChanged), this, - &GalleryWidget::onFilterChanged); - connect(m_exportSelectedButton, &QPushButton::clicked, this, - &GalleryWidget::onExportSelected); - connect(m_exportAllButton, &QPushButton::clicked, this, - &GalleryWidget::onExportAll); - connect(m_backButton, &ZIconWidget::clicked, this, - &GalleryWidget::onBackToAlbums); - - connect(m_importButton, &QPushButton::clicked, this, - &GalleryWidget::handleImport); - - // Add widgets to layout - m_controlsLayout->addWidget(m_backButton); - m_controlsLayout->addWidget(m_refreshButton); - m_controlsLayout->addWidget(m_importButton); - m_controlsLayout->addWidget(sortLabel); - m_controlsLayout->addWidget(m_sortComboBox); - m_controlsLayout->addWidget(filterLabel); - m_controlsLayout->addWidget(m_filterComboBox); - m_controlsLayout->addStretch(); // Push export buttons to the right - m_controlsLayout->addWidget(m_exportSelectedButton); - m_controlsLayout->addWidget(m_exportAllButton); - - QWidget *controlsWidget = new QWidget(); - controlsWidget->setLayout(m_controlsLayout); - controlsWidget->setObjectName("controlsWidget"); - controlsWidget->setStyleSheet("QWidget#controlsWidget { " - " padding: 2px; " - "}"); - - m_mainLayout->addWidget(controlsWidget); -} - -void GalleryWidget::onSortOrderChanged() -{ - if (!m_model) - return; - - int sortValue = m_sortComboBox->currentData().toInt(); - PhotoModel::SortOrder order = static_cast(sortValue); - m_model->setSortOrder(order); - - qDebug() << "Sort order changed to:" - << (order == PhotoModel::NewestFirst ? "Newest First" - : "Oldest First"); -} - -PhotoModel::FilterType GalleryWidget::getCurrentFilterType() const -{ - int filterValue = m_filterComboBox->currentData().toInt(); - return static_cast(filterValue); -} - -void GalleryWidget::onFilterChanged() -{ - if (!m_model) - return; - - PhotoModel::FilterType filter = getCurrentFilterType(); - m_model->setFilterType(filter); - - QString filterName = m_filterComboBox->currentText(); - qDebug() << "Filter changed to:" << filterName; -} - -void GalleryWidget::onExportSelected() -{ - // if we are exporting from album selection view - if (m_loadingWidget->currentWidget() == m_albumSelectionWidget) { - - QModelIndexList selectedIndexes = - m_albumListView->selectionModel()->selectedIndexes(); - // QStringList filePaths = - // m_albumModel->getSelectedFilePaths(selectedIndexes); - - QStringList paths; - for (const QModelIndex &index : selectedIndexes) { - if (index.isValid() && - index.row() < m_albumListView->model()->rowCount()) { - paths.append(index.data(Qt::UserRole).toString()); - } else { - qDebug() << "Invalid index in selection:" << index; - } - } - - auto *exportAlbum = new ExportAlbum(m_device, paths, this); - exportAlbum->show(); - return; - } - - if (!m_model || !m_listView->selectionModel()->hasSelection()) { - QMessageBox::information(this, "No Selection", - "Please select photos to export."); - return; - } - - QModelIndexList selectedIndexes = - m_listView->selectionModel()->selectedIndexes(); - QStringList filePaths = m_model->getSelectedFilePaths(selectedIndexes); - - if (filePaths.isEmpty()) { - QMessageBox::information(this, "No Items", - "No valid items selected for export."); - return; - } - - QString exportDir = selectExportDirectory(); - if (exportDir.isEmpty()) { - return; - } - - QList exportItems; - for (const QString &filePath : filePaths) { - exportItems.append(filePath); - } - - qDebug() << "Starting export of selected files:" << exportItems.size() - << "items to" << exportDir; - - IOManagerClient::sharedInstance()->startExport( - m_device, exportItems, exportDir, "Exporting from gallery"); -} - -void GalleryWidget::onExportAll() -{ - // if we are exporting from album selection view - if (m_loadingWidget->currentWidget() == m_albumSelectionWidget) { - - // get all available albums - QStringList paths; - for (int row = 0; row < m_albumListView->model()->rowCount(); ++row) { - QModelIndex index = m_albumListView->model()->index(row, 0); - if (index.isValid()) { - paths.append(index.data(Qt::UserRole).toString()); - } - } - - if (paths.isEmpty()) { - QMessageBox::information(this, "No Albums", - "No albums available for export."); - return; - } - - auto *exportAlbum = new ExportAlbum(m_device, paths, this); - exportAlbum->show(); - return; - } - - if (!m_model) - return; - - QList exportItems = m_model->getFilteredFilePaths(); - - if (exportItems.isEmpty()) { - QMessageBox::information(this, "No Items", "No items to export."); - return; - } - QString message = - QString("Export all %1 items currently shown?").arg(exportItems.size()); - int reply = QMessageBox::question(this, "Export All", message, - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No); - - if (reply != QMessageBox::Yes) { - return; - } - - QString exportDir = selectExportDirectory(); - if (exportDir.isEmpty()) { - return; - } - - qDebug() << "Starting export of:" << exportItems.size() << "items to" - << exportDir; - - IOManagerClient::sharedInstance()->startExport( - m_device, exportItems, exportDir, "Exporting from gallery"); -} - -QString GalleryWidget::selectExportDirectory() -{ - QString defaultDir = - QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); - - QString selectedDir = QFileDialog::getExistingDirectory( - this, "Select Export Directory", defaultDir, - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - - return selectedDir; -} - -void GalleryWidget::setupAlbumSelectionView() -{ - m_albumSelectionWidget = new QWidget(); - QVBoxLayout *layout = new QVBoxLayout(m_albumSelectionWidget); - layout->setContentsMargins(0, 0, 0, 0); - // Add instructions label - QLabel *instructionLabel = new QLabel("Select a photo album:"); - QFont instructionFont = instructionLabel->font(); - instructionFont.setWeight(QFont::Bold); - instructionLabel->setFont(instructionFont); - layout->addWidget(instructionLabel); - - m_albumListView = new QListView(); - m_albumListView->setViewMode(QListView::IconMode); - m_albumListView->setFlow(QListView::LeftToRight); - m_albumListView->setWrapping(true); - m_albumListView->setResizeMode(QListView::Adjust); - m_albumListView->setIconSize(QSize(200, 230)); - m_albumListView->setGridSize(QSize(210, 260)); - - m_albumListView->setSelectionMode(QAbstractItemView::ExtendedSelection); - m_albumListView->setUniformItemSizes(true); - - m_albumListView->setStyleSheet("QListView { " - " border-top: 1px solid #c1c1c1ff; " - " background-color: transparent; " - " border-radius: 0px;" - " padding: 0px;" - " padding-top: 5px;" - "} " - "QListView::item { " - " width: 200px; " - " height: 270px; " - " margin: 2px; " - "}"); - - layout->addWidget(m_albumListView); - - m_loadingWidget->setupContentWidget(m_albumSelectionWidget); - - connect(m_albumListView, &QListView::doubleClicked, this, - [this](const QModelIndex &index) { - if (!index.isValid()) - return; - QString albumPath = index.data(Qt::UserRole).toString(); - onAlbumSelected(albumPath); - }); -} - -void GalleryWidget::setupPhotoGalleryView() -{ - m_photoGalleryWidget = new QWidget(); - QVBoxLayout *layout = new QVBoxLayout(m_photoGalleryWidget); - layout->setContentsMargins(0, 0, 0, 0); - - // Create list view for photos - m_listView = new QListView(); - m_listView->setViewMode(QListView::IconMode); - m_listView->setFlow(QListView::LeftToRight); - m_listView->setWrapping(true); - m_listView->setResizeMode(QListView::Adjust); - m_listView->setIconSize(QSize(200, 230)); - m_listView->setGridSize(QSize(210, 260)); - m_listView->setSelectionMode(QAbstractItemView::ExtendedSelection); - m_listView->setUniformItemSizes(true); - m_listView->setContextMenuPolicy(Qt::CustomContextMenu); - - m_listView->setStyleSheet("QListView { " - " border-top: 1px solid #c1c1c1ff; " - " background-color: transparent; " - " border-radius: 0px;" - " padding: 0px;" - " padding-top: 5px;" - "} " - "QListView::item { " - " width: 200px; " - " height: 270px; " - " margin: 2px; " - "}"); - - layout->addWidget(m_listView); - - // Add the photo gallery widget to stacked widget - m_loadingWidget->setupAditionalWidget(m_photoGalleryWidget); - - // Connect double-click to open preview dialog - connect(m_listView, &QListView::doubleClicked, this, - [this](const QModelIndex &index) { - if (!index.isValid()) - return; - - QString filePath = - m_model->data(index, Qt::UserRole).toString(); - if (filePath.isEmpty()) - return; - - qDebug() << "Opening preview for" << filePath; - auto *previewDialog = new MediaPreviewDialog( - m_device, filePath, std::nullopt, false, this); - previewDialog->show(); - }); - - connect(m_listView, &QListView::customContextMenuRequested, this, - &GalleryWidget::onPhotoContextMenu); - - m_albumModel = new QStandardItemModel(this); -} - -void GalleryWidget::onError() -{ - m_loadingWidget->showError(); - QMessageBox::warning(this, "Error", - "Could not access DCIM directory on device."); - return; -} - -void GalleryWidget::onAlbumListLoaded(const QList &dcimTree) -{ - qDebug() << "Albums loaded:" << dcimTree.size(); - if (dcimTree.isEmpty()) { - m_loadingWidget->showError("No albums found on device"); - return; - } - - m_albumModel->clear(); - - for (const QString &albumName : dcimTree) { - auto *item = new QStandardItem(albumName); - QString fullPath = QString("/DCIM/%1").arg(albumName); - item->setData(fullPath, Qt::UserRole); - - item->setIcon(QIcon(":/resources/icons/" - "MaterialSymbolsLightImageOutlineSharp.png")); - m_albumModel->appendRow(item); - - loadAlbumThumbnailAsync(fullPath, item); - } - - m_albumListView->setModel(m_albumModel); - m_loadingWidget->stop(); - m_loadingWidget->switchToWidget(m_albumSelectionWidget); - m_exportAllButton->setEnabled(m_albumModel->rowCount() > 0); - - connect(m_albumListView->selectionModel(), - &QItemSelectionModel::selectionChanged, this, [this]() { - bool hasSelection = - m_albumListView->selectionModel()->hasSelection(); - m_exportSelectedButton->setEnabled(hasSelection); - }); -} - -void GalleryWidget::onAlbumSelected(const QString &albumPath) -{ - m_currentAlbumPath = albumPath; - - // Create model if not exists - if (!m_model) { - m_model = new PhotoModel(m_device, getCurrentFilterType(), this); - m_listView->setModel(m_model); - - connect(m_model, &PhotoModel::albumPathSet, this, [this]() { - // Switch to photo gallery view once album is loaded - m_loadingWidget->stop(false); - m_loadingWidget->switchToWidget(m_photoGalleryWidget); - // Enable controls and show back button - setControlsEnabled(true); - m_backButton->show(); - }); - - connect(m_model, &PhotoModel::albumPathSetFailed, this, [this]() { - m_loadingWidget->stop(false); - m_backButton->show(); - m_loadingWidget->showError("Failed to load album"); - }); - - // Update export button states based on selection - connect(m_listView->selectionModel(), - &QItemSelectionModel::selectionChanged, this, [this]() { - bool hasSelection = - m_listView->selectionModel()->hasSelection(); - m_exportSelectedButton->setEnabled(hasSelection); - }); - } - - // Set album path and load photos - m_model->setAlbumPath(albumPath); - - m_loadingWidget->showLoading(); -} - -void GalleryWidget::onBackToAlbums() -{ - // Switch back to album selection view - m_loadingWidget->switchToWidget(m_albumSelectionWidget); - - if (m_model) { - m_model->clear(); - } - - // Disable controls and hide back button - setControlsEnabled(false); - m_backButton->hide(); - // Clear current album path - m_currentAlbumPath.clear(); -} - -void GalleryWidget::setControlsEnabled(bool enabled) -{ - m_sortComboBox->setEnabled(enabled); - m_filterComboBox->setEnabled(enabled); - - const bool hasSelection = m_listView && m_listView->selectionModel() && - m_listView->selectionModel()->hasSelection(); - - m_exportSelectedButton->setEnabled(enabled && hasSelection); -} - -/* - FIXME: this needs to be refactored once we - figure out how to query Photos.sqlite - Check out: - https://github.com/ScottKjr3347/iOS_Local_PL_Photos.sqlite_Queries -*/ -QImage -GalleryWidget::loadAlbumThumbnail(const QString &albumPath, - std::shared_ptr device) -{ - if (QCoreApplication::closingDown() || !QGuiApplication::instance()) { - return {}; - } - - QList albumTree = device->afc_backend->list_dir(albumPath); - if (albumTree.isEmpty()) { - qDebug() << "Failed to read album directory:" << albumPath; - return {}; - } - - QString firstImagePath; - for (const QString &fileName : albumTree) { - bool isDir = - device->afc_backend->is_directory((albumPath + "/" + fileName)); - if (!isDir && (fileName.endsWith(".JPG", Qt::CaseInsensitive) || - fileName.endsWith(".PNG", Qt::CaseInsensitive) || - fileName.endsWith(".HEIC", Qt::CaseInsensitive))) { - firstImagePath = albumPath + "/" + fileName; - break; - } - } - - if (firstImagePath.isEmpty()) { - return {}; - } - - QByteArray imageData = device->afc_backend->file_to_buffer(firstImagePath); - - QImage thumbnail; - if (firstImagePath.endsWith(".HEIC", Qt::CaseInsensitive)) { - thumbnail = load_heic(imageData); - } else { - thumbnail.loadFromData(imageData); - } - - return thumbnail; -} - -void GalleryWidget::loadAlbumThumbnailAsync(const QString &albumPath, - QStandardItem *item) -{ - Q_UNUSED(item); - - auto *watcher = new QFutureWatcher(this); - const auto device = m_device; - - connect(watcher, &QFutureWatcher::finished, this, - [this, watcher, albumPath]() { - const QImage result = watcher->result(); - watcher->deleteLater(); - - if (result.isNull() || !m_albumModel || - QCoreApplication::closingDown() || - !QGuiApplication::instance()) { - return; - } - - for (int row = 0; row < m_albumModel->rowCount(); ++row) { - QModelIndex idx = m_albumModel->index(row, 0); - if (idx.data(Qt::UserRole).toString() == albumPath) { - if (auto *it = m_albumModel->itemFromIndex(idx)) { - it->setIcon(QIcon(QPixmap::fromImage(result))); - } - break; - } - } - }); - - QFuture future = QtConcurrent::run([albumPath, device]() { - return loadAlbumThumbnail(albumPath, device); - }); - - watcher->setFuture(future); -} - -void GalleryWidget::onPhotoContextMenu(const QPoint &pos) -{ - QModelIndex index = m_listView->indexAt(pos); - if (!index.isValid()) { - return; - } - - // Make sure the item is selected - if (!m_listView->selectionModel()->isSelected(index)) { - m_listView->selectionModel()->select( - index, QItemSelectionModel::ClearAndSelect); - } - - QMenu contextMenu(this); - QAction *previewAction = contextMenu.addAction("Preview"); - contextMenu.addSeparator(); - QAction *exportAction = contextMenu.addAction("Export"); - - exportAction->setEnabled(m_listView->selectionModel()->hasSelection()); - - connect(previewAction, &QAction::triggered, this, [this, index]() { - // Re-use the double-click logic - if (!index.isValid()) - return; - - QString filePath = m_model->data(index, Qt::UserRole).toString(); - if (filePath.isEmpty()) - return; - - qDebug() << "Opening preview for" << filePath; - auto *previewDialog = new MediaPreviewDialog(m_device, filePath); - previewDialog->show(); - }); - - connect(exportAction, &QAction::triggered, this, - &GalleryWidget::onExportSelected); - - contextMenu.exec(m_listView->viewport()->mapToGlobal(pos)); -} - -void GalleryWidget::handleImport() -{ - QStringList filePaths = QFileDialog::getOpenFileNames( - this, "Select Photos to Import", - QStandardPaths::writableLocation(QStandardPaths::PicturesLocation), - "Images (*.jpg *.jpeg *.png *.heic);;All Files (*)"); - - if (filePaths.isEmpty()) { - return; - } - - qDebug() << "Selected files for import:" << filePaths; - - PhotoImportDialog dialog(filePaths, this); - dialog.exec(); -} - -GalleryWidget::~GalleryWidget() -{ - qDebug() << "GalleryWidget destructor called"; -} diff --git a/src/gallerywidget.h b/src/gallerywidget.h deleted file mode 100644 index 97c08d5..0000000 --- a/src/gallerywidget.h +++ /dev/null @@ -1,113 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef GALLERYWIDGET_H -#define GALLERYWIDGET_H - -#include "exportalbum.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "photoimportdialog.h" -#include "photomodel.h" -#include "zloadingwidget.h" -#include -#include - -QT_BEGIN_NAMESPACE -class QListView; -class QComboBox; -class QPushButton; -class QHBoxLayout; -class QVBoxLayout; -class QStackedWidget; -class QLabel; -class QStandardItem; -class QStandardItemModel; -QT_END_NAMESPACE - -class ExportManager; - -class GalleryWidget : public QWidget -{ - Q_OBJECT - -public: - explicit GalleryWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - void load(); - ~GalleryWidget(); - -private slots: - void onSortOrderChanged(); - void onFilterChanged(); - void onExportSelected(); - void onExportAll(); - void onAlbumSelected(const QString &albumPath); - void onBackToAlbums(); - -private: - void reload(); - void refresh(); - void setupControlsLayout(); - void setupAlbumSelectionView(); - void setupPhotoGalleryView(); - void onAlbumListLoaded(const QList &dcimTree); - void setControlsEnabled(bool enabled); - QString selectExportDirectory(); - static QImage loadAlbumThumbnail(const QString &albumPath, - std::shared_ptr device); - void loadAlbumThumbnailAsync(const QString &albumPath, QStandardItem *item); - void onPhotoContextMenu(const QPoint &pos); - PhotoModel::FilterType getCurrentFilterType() const; - void handleImport(); - void onError(); - - const std::shared_ptr m_device; - bool m_loaded = false; - QString m_currentAlbumPath; - - // UI components - QVBoxLayout *m_mainLayout; - QHBoxLayout *m_controlsLayout; - ZLoadingWidget *m_loadingWidget; - QPushButton *m_importButton; - - // Album selection view - QWidget *m_albumSelectionWidget = nullptr; - QListView *m_albumListView = nullptr; - - // Photo gallery view - QWidget *m_photoGalleryWidget = nullptr; - QListView *m_listView = nullptr; - PhotoModel *m_model = nullptr; - QStandardItemModel *m_albumModel; - - // Control widgets - QComboBox *m_sortComboBox; - QComboBox *m_filterComboBox; - QPushButton *m_exportSelectedButton; - QPushButton *m_exportAllButton; - ZIconWidget *m_backButton = nullptr; - ZIconWidget *m_refreshButton = nullptr; - - // Export manager - ExportManager *m_exportManager; -}; - -#endif // GALLERYWIDGET_H diff --git a/src/rust/src/hause_arrest.rs b/src/hause_arrest.rs similarity index 100% rename from src/rust/src/hause_arrest.rs rename to src/hause_arrest.rs diff --git a/src/rust/src/heic_to_image.cc b/src/heic_to_image.cc similarity index 100% rename from src/rust/src/heic_to_image.cc rename to src/heic_to_image.cc diff --git a/src/howtoconnectdialog.cpp b/src/howtoconnectdialog.cpp deleted file mode 100644 index 858dc26..0000000 --- a/src/howtoconnectdialog.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "howtoconnectdialog.h" - -HowToConnectDialog::HowToConnectDialog(QWidget *parent) : QDialog{parent} -{ -#ifdef WIN32 - setupWinWindow(this); -#endif - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 20, 0, 0); - mainLayout->setSpacing(0); - - auto *contentLayout = new QVBoxLayout(); - m_loadingWidget = new ZLoadingWidget(false, this); - m_loadingWidget->setupContentWidget(contentLayout); - mainLayout->addWidget(m_loadingWidget); - - contentLayout->setContentsMargins(16, 16, 16, 16); - contentLayout->setSpacing(12); - - m_stackedWidget = new QStackedWidget(this); - m_stackedWidget->addWidget(createPage("Connect your device", - ":resources/connect.png", - QSize(200, 200))); // first page - m_stackedWidget->addWidget(createPage("Accept the pairing dialog", - ":/resources/trust.png")); // second - - QString message; - -#ifdef WIN32 - message = - "You can now unplug the device. iDescriptor will connect to it " - "automatically (requires iOS 15 or later and the Bonjour service)."; -#elif __linux__ - message = "You can now unplug the device. iDescriptor will connect to it " - "automatically (requires iOS 15 or later and the Avahi daemon)."; -#else - message = "You can now unplug the device. iDescriptor will connect to it " - "automatically (requires iOS 15 or later)."; -#endif - - m_stackedWidget->addWidget( - createPage(message, - ":/resources/ios-version.png")); // third - contentLayout->addWidget(m_stackedWidget, 1); - - auto *navLayout = new QHBoxLayout(); - navLayout->setContentsMargins(0, 0, 0, 0); - navLayout->setSpacing(10); - navLayout->addStretch(); - - m_prevButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsArrowLeftAlt.png"), "Previous"); - - m_nextButton = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsArrowRightAlt.png"), "Next"); - - m_prevButton->setFixedWidth(48); - m_nextButton->setFixedWidth(48); - - navLayout->addWidget(m_prevButton); - navLayout->addWidget(m_nextButton); - navLayout->addStretch(); - contentLayout->addLayout(navLayout); - - connect(m_prevButton, &QPushButton::clicked, this, [this]() { - const int current = m_stackedWidget->currentIndex(); - if (current > 0) { - m_stackedWidget->setCurrentIndex(current - 1); - } - updateNavigationButtons(); - }); - - connect(m_nextButton, &QPushButton::clicked, this, [this]() { - const int current = m_stackedWidget->currentIndex(); - if (current < m_stackedWidget->count() - 1) { - m_stackedWidget->setCurrentIndex(current + 1); - } - updateNavigationButtons(); - }); - - connect(m_stackedWidget, &QStackedWidget::currentChanged, this, - [this](int) { updateNavigationButtons(); }); - - updateNavigationButtons(); - - QTimer::singleShot(500, this, &HowToConnectDialog::init); -} - -void HowToConnectDialog::init() { m_loadingWidget->stop(); } - -QWidget *HowToConnectDialog::createPage(const QString &text, - const QString &imagePath, - const QSize &imageSize) -{ - auto *page = new QWidget(this); - page->setMaximumSize(500, 500); - auto *layout = new QVBoxLayout(page); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(10); - - auto *label = new QLabel(text, page); - label->setAlignment(Qt::AlignCenter); - label->setWordWrap(true); - QFont font = label->font(); - font.setPointSize(16); - font.setBold(true); - label->setFont(font); - - auto *imageLabel = new ResponsiveQLabel(page); - imageLabel->setPixmap(QPixmap(imagePath)); - imageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - // imageLabel->setMinimumSize(400, 200); - imageLabel->setMinimumSize(imageSize); - // imageLabel->setMaximumSize(600, 200); - imageLabel->setScaledContents(true); - - layout->addStretch(); - layout->addWidget(label); - layout->addWidget(imageLabel, 1, Qt::AlignHCenter); - layout->addStretch(); - - return page; -} - -void HowToConnectDialog::updateNavigationButtons() -{ - const int index = m_stackedWidget->currentIndex(); - const int last = m_stackedWidget->count() - 1; - m_prevButton->setEnabled(index > 0); - m_nextButton->setEnabled(index < last); -} diff --git a/src/howtoconnectdialog.h b/src/howtoconnectdialog.h deleted file mode 100644 index 6bc5e34..0000000 --- a/src/howtoconnectdialog.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef HOWTOCONNECTDIALOG_H -#define HOWTOCONNECTDIALOG_H - -#include "iDescriptor-ui.h" -#include "responsiveqlabel.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class QPushButton; -class QStackedWidget; -class QWidget; - -class HowToConnectDialog : public QDialog -{ - Q_OBJECT - -public: - explicit HowToConnectDialog(QWidget *parent = nullptr); - -private: - void init(); - QWidget *createPage(const QString &text, const QString &imagePath, - const QSize &imageSize = QSize(400, 200)); - void updateNavigationButtons(); - ZLoadingWidget *m_loadingWidget{nullptr}; - QStackedWidget *m_stackedWidget{nullptr}; - ZIconWidget *m_prevButton{nullptr}; - ZIconWidget *m_nextButton{nullptr}; -}; - -#endif // HOWTOCONNECTDIALOG_H diff --git a/src/httpserver.cpp b/src/httpserver.cpp deleted file mode 100644 index 75ab0f9..0000000 --- a/src/httpserver.cpp +++ /dev/null @@ -1,262 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "httpserver.h" -#include "iDescriptor.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -HttpServer::HttpServer(QObject *parent) - : QObject(parent), server(new QTcpServer(this)), port(8080) -{ - connect(server, &QTcpServer::newConnection, this, - &HttpServer::onNewConnection); -} - -HttpServer::~HttpServer() { stop(); } - -void HttpServer::start(const QStringList &files) -{ - fileList = files; - - // Generate unique JSON filename - QString timestamp = - QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss"); - jsonFileName = QString("%1-idescriptor-import.json").arg(timestamp); - - // Try to bind to port from settings, if fails try other ports - int startPort = SettingsManager::sharedInstance()->wirelessFileServerPort(); - qDebug() << "Starting HTTP server on port" << startPort; - for (int tryPort = startPort; tryPort <= startPort + 10; ++tryPort) { - if (server->listen(QHostAddress::Any, tryPort)) { - port = tryPort; - emit serverStarted(); - return; - } - } - - emit serverError(QString("Could not bind to any port between %1-%2") - .arg(startPort) - .arg(startPort + 10)); -} - -void HttpServer::stop() -{ - if (server->isListening()) { - server->close(); - } -} - -int HttpServer::getPort() const { return port; } - -void HttpServer::onNewConnection() -{ - QTcpSocket *socket = server->nextPendingConnection(); - connect(socket, &QTcpSocket::readyRead, this, &HttpServer::onReadyRead); - connect(socket, &QTcpSocket::disconnected, this, - &HttpServer::onDisconnected); -} - -void HttpServer::onReadyRead() -{ - QTcpSocket *socket = qobject_cast(sender()); - if (!socket) - return; - - QByteArray data = socket->readAll(); - QString request = QString::fromUtf8(data); - - // Parse HTTP request - QStringList lines = request.split("\r\n"); - if (lines.isEmpty()) - return; - - QString requestLine = lines.first(); - QStringList parts = requestLine.split(" "); - if (parts.size() < 2) - return; - - QString method = parts[0]; - QString path = parts[1]; - - if (method == "GET") { - handleRequest(socket, path); - } else { - sendResponse(socket, 405, "text/plain", "Method Not Allowed"); - } -} - -void HttpServer::onDisconnected() -{ - QTcpSocket *socket = qobject_cast(sender()); - if (socket) { - socket->deleteLater(); - } -} - -void HttpServer::handleRequest(QTcpSocket *socket, const QString &path) -{ - // Serve JSON manifest - if (path == QString("/%1").arg(jsonFileName)) { - sendJsonManifest(socket); - return; - } - - // Serve files from /serve/ directory - if (path.startsWith("/serve/")) { - QString encodedFileName = path.mid(7); // Remove "/serve/" - QString fileName = QUrl::fromPercentEncoding(encodedFileName.toUtf8()); - - // Find the file in our list - QString targetFile; - for (const QString &file : fileList) { - QFileInfo info(file); - if (info.fileName() == fileName) { - targetFile = file; - break; - } - } - - if (!targetFile.isEmpty()) { - sendFile(socket, targetFile); - return; - } - } - - sendResponse(socket, 404, "text/html", - "

404 Not Found

The requested file was " - "not found.

"); -} - -void HttpServer::sendResponse(QTcpSocket *socket, int statusCode, - const QString &contentType, - const QByteArray &data) -{ - QString statusText; - switch (statusCode) { - case 200: - statusText = "OK"; - break; - case 404: - statusText = "Not Found"; - break; - case 405: - statusText = "Method Not Allowed"; - break; - case 500: - statusText = "Internal Server Error"; - break; - default: - statusText = "Unknown"; - break; - } - - QString response = - QString("HTTP/1.1 %1 %2\r\n").arg(statusCode).arg(statusText); - response += QString("Content-Type: %1\r\n").arg(contentType); - response += QString("Content-Length: %1\r\n").arg(data.size()); - response += "Access-Control-Allow-Origin: *\r\n"; - response += "Connection: close\r\n"; - response += "\r\n"; - - socket->write(response.toUtf8()); - socket->write(data); - socket->disconnectFromHost(); -} - -void HttpServer::sendFile(QTcpSocket *socket, const QString &filePath) -{ - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly)) { - sendResponse(socket, 404, "text/plain", "File not found"); - return; - } - - QByteArray data = file.readAll(); - QString mimeType = getMimeType(filePath); - - // Emit progress signal - QFileInfo info(filePath); - emit downloadProgress(info.fileName(), data.size(), data.size()); - - sendResponse(socket, 200, mimeType, data); -} - -void HttpServer::sendJsonManifest(QTcpSocket *socket) -{ - QString jsonContent = generateJsonManifest(); - sendResponse(socket, 200, "application/json", jsonContent.toUtf8()); -} - -QString HttpServer::generateJsonManifest() const -{ - QString serverIP = getLocalIP(); - - QJsonObject manifest; - QJsonArray items; - - for (const QString &file : fileList) { - QFileInfo info(file); - QJsonObject item; - item["path"] = QString("http://%1:%2/serve/%3") - .arg(serverIP) - .arg(port) - .arg(QString::fromUtf8( - QUrl::toPercentEncoding(info.fileName()))); - items.append(item); - } - - manifest["items"] = items; - - QJsonDocument doc(manifest); - return doc.toJson(); -} - -QString HttpServer::getLocalIP() -{ - foreach (const QNetworkInterface &netIf, - QNetworkInterface::allInterfaces()) { - if (netIf.flags().testFlag(QNetworkInterface::IsUp) && - !netIf.flags().testFlag(QNetworkInterface::IsLoopBack)) { - foreach (const QNetworkAddressEntry &entry, - netIf.addressEntries()) { - if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) { - return entry.ip().toString(); - } - } - } - } - return "127.0.0.1"; -} - -QString HttpServer::getMimeType(const QString &filePath) const -{ - QMimeDatabase db; - QMimeType type = db.mimeTypeForFile(filePath); - return type.name(); -} diff --git a/src/httpserver.h b/src/httpserver.h deleted file mode 100644 index ebafc87..0000000 --- a/src/httpserver.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef HTTPSERVER_H -#define HTTPSERVER_H - -#include -#include -#include -#include -#include -#include - -class HttpServer : public QObject -{ - Q_OBJECT - -public: - explicit HttpServer(QObject *parent = nullptr); - ~HttpServer(); - - void start(const QStringList &files); - void stop(); - int getPort() const; - QString getJsonFileName() const { return jsonFileName; } - static QString getLocalIP(); - -signals: - void serverStarted(); - void serverError(const QString &error); - void downloadProgress(const QString &fileName, int bytesDownloaded, - int totalBytes); - -private slots: - void onNewConnection(); - void onReadyRead(); - void onDisconnected(); - -private: - QTcpServer *server; - QStringList fileList; - int port; - QString jsonFileName; - QMap downloadTracker; - - void handleRequest(QTcpSocket *socket, const QString &path); - void sendResponse(QTcpSocket *socket, int statusCode, - const QString &contentType, const QByteArray &data); - void sendFile(QTcpSocket *socket, const QString &filePath); - void sendJsonManifest(QTcpSocket *socket); - QString generateJsonManifest() const; - QString getMimeType(const QString &filePath) const; -}; - -#endif // HTTPSERVER_H diff --git a/src/ifusediskunmountbutton.cpp b/src/ifusediskunmountbutton.cpp deleted file mode 100644 index 2dbb4b0..0000000 --- a/src/ifusediskunmountbutton.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "ifusediskunmountbutton.h" -#include "iDescriptor-ui.h" -#include -#include - -iFuseDiskUnmountButton::iFuseDiskUnmountButton(const QString &path, - QWidget *parent) - : ZIconWidget{QIcon(":/resources/icons/ClarityHardDiskSolidAlerted.png"), - "Unmount iFuse at " + path, 1.0, parent} -{ - setCursor(Qt::PointingHandCursor); - setFixedSize(24, 24); -} diff --git a/src/ifusediskunmountbutton.h b/src/ifusediskunmountbutton.h deleted file mode 100644 index 2b1c180..0000000 --- a/src/ifusediskunmountbutton.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef IFUSEDISKUNMOUNTBUTTON_H -#define IFUSEDISKUNMOUNTBUTTON_H - -#include "iDescriptor-ui.h" - -class iFuseDiskUnmountButton : public ZIconWidget -{ - Q_OBJECT -public: - explicit iFuseDiskUnmountButton(const QString &path, - QWidget *parent = nullptr); - -signals: -}; - -#endif // IFUSEDISKUNMOUNTBUTTON_H diff --git a/src/ifusemanager.cpp b/src/ifusemanager.cpp deleted file mode 100644 index 2514508..0000000 --- a/src/ifusemanager.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "ifusemanager.h" -#include -#include - -QStringList iFuseManager::getMountArg(std::string &udid, QString &path) -{ - return QStringList() << "-u" << QString::fromStdString(udid) << path; -} - -#ifdef __linux__ -QList iFuseManager::getMountPoints() -{ - QProcess mountProcess; - mountProcess.start("mount", QStringList() << "-t" - << "fuse.ifuse"); - mountProcess.waitForFinished(); - - QString output = mountProcess.readAllStandardOutput(); - - if (output.trimmed().isEmpty()) { - qDebug() << "[iFuseWidget] No existing ifuse mounts found."; - return {}; - } - - QStringList mountPoints; - QStringList lines = output.split('\n', Qt::SkipEmptyParts); - for (const QString &line : lines) { - // A typical line is: "ifuse on /path/to/mount type fuse.ifuse (...)" - QString mountPath = line.section(" on ", 1).section(" type ", 0, 0); - if (!mountPath.isEmpty()) { - qDebug() << "[iFuseWidget] - Mount point:" << mountPath; - mountPoints.append(mountPath); - } - } - return mountPoints; -} -#endif - -bool iFuseManager::linuxUnmount(const QString &path) -{ - QProcess umountProcess; - umountProcess.start("fusermount", QStringList() << "-u" << path); - umountProcess.waitForFinished(); - - if (umountProcess.exitCode() != 0) { - qWarning() << "[iFuseWidget] Failed to unmount" << path << ":" - << umountProcess.readAllStandardError().trimmed(); - return false; - } - - qDebug() << "[iFuseWidget] Successfully unmounted" << path; - return true; -} \ No newline at end of file diff --git a/src/ifusemanager.h b/src/ifusemanager.h deleted file mode 100644 index 9198474..0000000 --- a/src/ifusemanager.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef IFUSEMANAGER_H -#define IFUSEMANAGER_H - -#include - -class iFuseManager : public QObject -{ - Q_OBJECT -public: -// explicit iFuseManager(QObject *parent = nullptr); -#ifdef Q_OS_LINUX - static QList getMountPoints(); -#endif - static QStringList getMountArg(std::string &udid, QString &path); - // TODO: need to implement a cross-platform mount and unmount method - static bool linuxUnmount(const QString &path); -signals: -}; - -#endif // IFUSEMANAGER_H diff --git a/src/ifusewidget.cpp b/src/ifusewidget.cpp deleted file mode 100644 index 0065cc2..0000000 --- a/src/ifusewidget.cpp +++ /dev/null @@ -1,520 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "ifusewidget.h" -#include "diagnosedialog.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "ifusediskunmountbutton.h" -#include "ifusemanager.h" -#include "mainwindow.h" -#include -#include -#include -#include -#include -#ifdef WIN32 -#include "platform/windows/win_common.h" -#endif - -iFuseWidget::iFuseWidget(const std::shared_ptr device, - QWidget *parent) - : Tool(parent), m_mainLayout(nullptr), m_ifuseProcess(nullptr), - m_device(device) -{ - setupUI(); - updateUI(); - - connect(AppContext::sharedInstance(), &AppContext::deviceChange, this, - &iFuseWidget::updateUI); -} - -void iFuseWidget::setupUI() -{ - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setSpacing(15); - m_mainLayout->setContentsMargins(20, 20, 20, 20); - - // Description label - m_descriptionLabel = new QLabel("This tool allows you to mount your " - "iPhone's disk as a drive on your PC"); - m_descriptionLabel->setWordWrap(true); - m_descriptionLabel->setStyleSheet( - "font-size: 14px; color: #666; margin-bottom: 10px;"); - m_mainLayout->addWidget(m_descriptionLabel); - - // Status label - m_statusLabel = new QLabel(); - m_statusLabel->setWordWrap(true); - m_statusLabel->hide(); - m_statusLabel->setStyleSheet( - "padding: 8px; border-radius: 4px; margin: 5px 0;"); - m_mainLayout->addWidget(m_statusLabel); - - // Device selection - QWidget *deviceWidget = new QWidget(); - QHBoxLayout *deviceLayout = new QHBoxLayout(deviceWidget); - deviceLayout->setContentsMargins(0, 0, 0, 0); - - QLabel *deviceLabel = new QLabel("Select Device:"); - deviceLabel->setMinimumWidth(100); - m_deviceComboBox = new QComboBox(); - m_deviceComboBox->setMinimumHeight(35); - - deviceLayout->addWidget(deviceLabel); - deviceLayout->addWidget(m_deviceComboBox, 1); - m_mainLayout->addWidget(deviceWidget); - - // Mount path selection - QWidget *pathWidget = new QWidget(); - QHBoxLayout *pathLayout = new QHBoxLayout(pathWidget); - pathLayout->setContentsMargins(0, 0, 0, 0); - - m_mountPathLabel = new ZLabel(this); - m_mountPathLabel->setCursor(Qt::PointingHandCursor); - m_mountPathLabel->setText("Mount directory will be shown here"); - m_mountPathLabel->setStyleSheet("QLabel { " - "border: 1px solid #ccc; " - "padding: 8px; " - "border-radius: 4px; " - "}"); - m_mountPathLabel->setMinimumHeight(35); - - m_folderPickerButton = new QPushButton("Browse..."); - m_folderPickerButton->setMinimumHeight(35); - - pathLayout->addWidget(m_mountPathLabel, 1); - pathLayout->addWidget(m_folderPickerButton); - m_mainLayout->addWidget(pathWidget); - - // Mount button - m_mountButton = new QPushButton("Mount Device"); - m_mountButton->setMinimumHeight(40); - m_mountButton->setDefault(true); - m_mainLayout->addWidget(m_mountButton); - - // Add stretch to push everything to the top - m_mainLayout->addStretch(); - - // Connect signals - connect(m_folderPickerButton, &QPushButton::clicked, this, - &iFuseWidget::onFolderPickerClicked); - connect(m_mountPathLabel, &ZLabel::clicked, this, - &iFuseWidget::onMountPathClicked); - connect(m_mountButton, &QPushButton::clicked, this, - &iFuseWidget::onMountClicked); - - connect(m_deviceComboBox, &QComboBox::currentTextChanged, this, - &iFuseWidget::onDeviceChanged); - - QString homeDir = - QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - QString productType = - QString::fromStdString(m_device->deviceInfo.productType); - QString defaultMountPath = QDir(homeDir).absoluteFilePath(productType); - m_mountPathLabel->setText(defaultMountPath); - -/* FIXME: this can be handled better, also check for linux */ -#ifdef WIN32 - if (IsWinFspInstalled() != SERVICE_AVAILABLE) { - DiagnoseDialog *diagnoseDialog = new DiagnoseDialog(this); - diagnoseDialog->setAttribute(Qt::WA_DeleteOnClose); - diagnoseDialog->show(); - } -#endif -} - -void iFuseWidget::updateDeviceComboBox() -{ - QList> devices = - AppContext::sharedInstance()->getAllDevices(); - - m_deviceComboBox->blockSignals(true); - m_deviceComboBox->clear(); - m_deviceComboBox->setEnabled(true); - m_mountButton->setEnabled(true); - - for (std::shared_ptr device : devices) { - - if (device->deviceInfo.isWireless) { - continue; // Skip wireless devices since ifuse only works with USB - } - - QString displayText = - QString::fromStdString(device->deviceInfo.productType) + " / " + - device->udid; - m_deviceComboBox->addItem(displayText, device->udid); - } - m_deviceComboBox->blockSignals(false); - - // Try to find and select the device passed to the widget - int deviceIndex = -1; - if (m_device) { - deviceIndex = m_deviceComboBox->findData(m_device->udid); - } - - if (deviceIndex != -1) { - // Found the pre-selected device, so select it. - m_deviceComboBox->setCurrentIndex(deviceIndex); - } else if (!devices.isEmpty()) { - // Pre-selected device not found or not provided, so select the first - // one. - m_device = devices.first(); - m_deviceComboBox->setCurrentIndex(0); - } -} - -void iFuseWidget::onFolderPickerClicked() -{ -#ifdef WIN32 - QString currentPath = m_mountPathLabel->text(); - // On Windows, ifuse requires a non-existent directory. - // We can use getSaveFileName to allow the user to specify one. - QString dir = QFileDialog::getSaveFileName(this, "Select Mount Directory", - currentPath); - if (!dir.isEmpty()) { - m_mountPathLabel->setText(dir); - } -#endif - -#ifdef __linux__ - QString currentPath = m_mountPathLabel->text(); - QString dir = QFileDialog::getExistingDirectory( - this, "Select Mount Directory", currentPath); - if (!dir.isEmpty()) { - m_mountPathLabel->setText(dir); - } -#endif -} - -void iFuseWidget::onMountPathClicked() -{ - QString currentPath = m_mountPathLabel->text(); - if (!currentPath.isEmpty() && QDir(currentPath).exists()) { - QDesktopServices::openUrl(QUrl::fromLocalFile(currentPath)); - } -} - -void iFuseWidget::onMountClicked() -{ - if (!validateInputs()) { - return; - } - - m_ifuseProcess = new QProcess(); - connect(m_ifuseProcess, - QOverload::of(&QProcess::finished), this, - &iFuseWidget::onProcessFinished); - connect(m_ifuseProcess, &QProcess::errorOccurred, this, - &iFuseWidget::onProcessError); - - QString ifuseExecutablePath; - QString fullMountPath = m_mountPathLabel->text(); - -// On Windows we ship with a bundled win-ifuse.exe -#ifdef WIN32 - ifuseExecutablePath = - QCoreApplication::applicationDirPath() + "/win-ifuse.exe"; - qDebug() << "Looking for bundled win-ifuse.exe at" << ifuseExecutablePath; - if (!QFileInfo::exists(ifuseExecutablePath)) { - setStatusMessage("Error: win-ifuse.exe not found at expected path: " + - ifuseExecutablePath, - true); - return; - } -#endif - -#ifdef __linux__ - /* - Check if running in AppImage - this is set by the plugin script - */ - if (qEnvironmentVariableIsSet("IFUSE_BIN_APPIMAGE")) { - ifuseExecutablePath = qgetenv("IFUSE_BIN_APPIMAGE"); - if (ifuseExecutablePath.isEmpty()) { - setStatusMessage("Error: Running in AppImage mode, but " - "IFUSE_BIN_APPIMAGE is not set.", - true); - return; - } - - if (!QFileInfo(ifuseExecutablePath).isExecutable()) { - setStatusMessage("Error: ifuse not found or is not executable.", - true); - return; - } - } else { - ifuseExecutablePath = QStandardPaths::findExecutable("ifuse"); - if (ifuseExecutablePath.isEmpty()) { - setStatusMessage( - "Error: ifuse binary not found. Please install ifuse first.", - true); - return; - } - } -#endif - -#ifdef WIN32 - // On Windows, the mount path must not exist. - if (QFileInfo(fullMountPath).exists()) { - setStatusMessage("Error: Mount directory must not exist on Windows: " + - fullMountPath, - true); - return; - } -#endif - - QDir dir; -// on Linux, we need to create the mount directory if it doesn't exist -#ifdef __linux__ - if (!QDir(fullMountPath).exists()) { - if (!dir.mkpath(fullMountPath)) { - setStatusMessage("Error: Failed to create mount directory: " + - fullMountPath, - true); - return; - } - } -#endif - - m_currentMountPath = fullMountPath; - - QString deviceUdid = getSelectedDeviceUdid(); - - setStatusMessage("Mounting device...", false); - m_mountButton->setText("Mounting..."); - m_mountButton->setEnabled(false); - - // Run ifuse command - QStringList arguments; - arguments << "-u" << deviceUdid << fullMountPath; - - m_ifuseProcess->start(ifuseExecutablePath, arguments); - -#ifdef WIN32 - // On Windows, the process runs in the foreground. We wait for it to start. - // If it fails to start, onProcessError will be called. - if (!m_ifuseProcess->waitForStarted()) { - return; - } - - // Process started successfully, so we treat it as a successful mount - setStatusMessage("Device mounted successfully at: " + m_currentMountPath, - false); - - m_mountButton->setText("Mount"); - m_mountButton->setEnabled(true); - - auto *b = new iFuseDiskUnmountButton(m_currentMountPath); - MainWindow::sharedInstance()->statusBar()->addPermanentWidget(b); - - QProcess *processToKill = m_ifuseProcess; - connect(b, &iFuseDiskUnmountButton::clicked, b, [processToKill, b]() { - if (processToKill) { - processToKill->kill(); - } - MainWindow::sharedInstance()->statusBar()->removeWidget(b); - b->deleteLater(); - }); - connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, b, - [processToKill]() { - if (processToKill && - processToKill->state() == QProcess::Running) { - processToKill->kill(); - } - }); - - QTimer::singleShot(1000, [this]() { - QDesktopServices::openUrl(QUrl::fromLocalFile(m_currentMountPath)); - }); -#endif -} - -void iFuseWidget::onProcessFinished(int exitCode, - QProcess::ExitStatus exitStatus) -{ - m_mountButton->setText("Mount Device"); - m_mountButton->setEnabled(true); - - if (exitStatus == QProcess::CrashExit) { - setStatusMessage("Error: ifuse process crashed", true); - return; - } - -#ifdef WIN32 - // On Windows, this is just a confirmation that the process has been - // terminated. - setStatusMessage("Device unmounted from " + m_currentMountPath, false); -#endif - -#ifdef __linux__ - if (exitCode == 0) { - setStatusMessage( - "Device mounted successfully at: " + m_currentMountPath, false); - - auto *b = new iFuseDiskUnmountButton(m_currentMountPath); - QProcess *processToKill = m_ifuseProcess; - QString currentMountPath = m_currentMountPath; - connect(b, &iFuseDiskUnmountButton::clicked, this, - [b, processToKill, currentMountPath]() { - qDebug() << "Unmounting" << currentMountPath; - bool ok = iFuseManager::linuxUnmount(currentMountPath); - if (!ok) { - QMessageBox::warning(nullptr, "Unmount Failed", - "Failed to unmount iFuse at " + - currentMountPath + - ". Please try again."); - return; - } - MainWindow::sharedInstance()->statusBar()->removeWidget(b); - b->deleteLater(); - }); - MainWindow::sharedInstance()->statusBar()->addPermanentWidget(b); - QDesktopServices::openUrl(QUrl::fromLocalFile(currentMountPath)); - } else { - QString errorOutput = m_ifuseProcess->readAllStandardError(); - setStatusMessage("Mount failed: " + errorOutput, true); - } -#endif - - m_ifuseProcess->deleteLater(); - m_ifuseProcess = nullptr; -} - -void iFuseWidget::onProcessError(QProcess::ProcessError error) -{ - m_mountButton->setText("Mount Device"); - m_mountButton->setEnabled(true); - - QString errorMessage; - switch (error) { - case QProcess::FailedToStart: - errorMessage = "Failed to start ifuse. Make sure it's installed."; - break; - case QProcess::Crashed: - errorMessage = "ifuse process crashed."; - break; - case QProcess::Timedout: - errorMessage = "ifuse process timed out."; - break; - default: - errorMessage = "Unknown error occurred."; - break; - } - - setStatusMessage("Error: " + errorMessage, true); - - if (m_ifuseProcess) { - m_ifuseProcess->deleteLater(); - m_ifuseProcess = nullptr; - } -} - -void iFuseWidget::updatePath() -{ - QString homeDir = - QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - QString productType = - QString::fromStdString(m_device->deviceInfo.productType); - QString defaultMountPath = QDir(homeDir).absoluteFilePath(productType); - m_mountPathLabel->setText(defaultMountPath); -} - -void iFuseWidget::updateUI() -{ - QList> devices = - AppContext::sharedInstance()->getAllDevices(); - - if (devices.isEmpty()) { - m_device = nullptr; - m_deviceComboBox->clear(); - m_deviceComboBox->setEnabled(false); - m_mountButton->setEnabled(false); - m_mountPathLabel->setText("No device connected."); - deleteLater(); - return; - } - updateDeviceComboBox(); - updatePath(); -} - -bool iFuseWidget::validateInputs() -{ - if (m_deviceComboBox->currentData().toString().isEmpty()) { - setStatusMessage("Error: No device selected", true); - return false; - } - - return true; -} - -QString iFuseWidget::getSelectedDeviceUdid() -{ - return m_deviceComboBox->currentData().toString(); -} - -void iFuseWidget::setStatusMessage(const QString &message, bool isError) -{ - m_statusLabel->setText(message); - m_statusLabel->show(); - - if (isError) { - m_statusLabel->setStyleSheet( - "background-color: #ffe6e6; color: #d00; border: 1px solid " - "#ffcccc; padding: 8px; border-radius: 4px; margin: 5px 0;"); - } else { - m_statusLabel->setStyleSheet( - "background-color: #e6ffe6; color: #060; border: 1px solid " - "#ccffcc; padding: 8px; border-radius: 4px; margin: 5px 0;"); - } - - // Auto-hide status after 5 seconds for non-error messages - if (!isError) { - QTimer::singleShot(5000, [this]() { m_statusLabel->hide(); }); - } -} - -void iFuseWidget::onDeviceChanged(const QString &text) -{ - QString selectedUdid = m_deviceComboBox->currentData().toString(); - QList> devices = - AppContext::sharedInstance()->getAllDevices(); - - for (const std::shared_ptr &device : devices) { - if (device->udid == selectedUdid) { - m_device = device; - - // Update mount path to reflect new device - QString homeDir = - QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - QString productType = - QString::fromStdString(device->deviceInfo.productType); - QString newMountPath = QDir(homeDir).absoluteFilePath(productType); - m_mountPathLabel->setText(newMountPath); - - break; - } - } -} - -bool iFuseWidget::canOpenForDevice( - const std::shared_ptr &device) -{ - return !device->deviceInfo.isWireless; -} \ No newline at end of file diff --git a/src/ifusewidget.h b/src/ifusewidget.h deleted file mode 100644 index 3270483..0000000 --- a/src/ifusewidget.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef IFUSEWIDGET_H -#define IFUSEWIDGET_H - -#include "appcontext.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "service.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class iFuseWidget : public Tool -{ - Q_OBJECT - -public: - static bool - canOpenForDevice(const std::shared_ptr &device); - explicit iFuseWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - -private slots: - void onFolderPickerClicked(); - void onMountPathClicked(); - void onMountClicked(); - void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); - void onProcessError(QProcess::ProcessError error); - void updateUI(); - -private: - void setupUI(); - void updatePath(); - void updateDeviceComboBox(); - bool validateInputs(); - QString getSelectedDeviceUdid(); - void setStatusMessage(const QString &message, bool isError = false); - void onDeviceChanged(const QString &deviceName); - // UI Components - QVBoxLayout *m_mainLayout; - QLabel *m_descriptionLabel; - QLabel *m_statusLabel; - QComboBox *m_deviceComboBox; - ZLabel *m_mountPathLabel; - QPushButton *m_folderPickerButton; - QLabel *m_folderNameLabel; - QPushButton *m_mountButton; - std::shared_ptr m_device; - - // Data - QString m_selectedPath; - QProcess *m_ifuseProcess; - QString m_currentMountPath; -}; - -#endif // IFUSEWIDGET_H \ No newline at end of file diff --git a/src/image_cache.rs b/src/image_cache.rs new file mode 100644 index 0000000..32ca646 --- /dev/null +++ b/src/image_cache.rs @@ -0,0 +1,27 @@ +use once_cell::sync::Lazy; +use qttypes::QImage; +use std::collections::HashMap; +use std::sync::Mutex; + +static CACHE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +fn key(udid: &str, path: &str) -> String { + // stable + avoids accidental collisions + format!("{udid}\u{1f}{path}") +} + +pub fn get(udid: &str, path: &str) -> Option { + CACHE.lock().ok()?.get(&key(udid, path)).cloned() +} + +pub fn insert(udid: &str, path: &str, img: QImage) { + if let Ok(mut guard) = CACHE.lock() { + guard.insert(key(udid, path), img); + } +} + +pub fn clear() { + if let Ok(mut guard) = CACHE.lock() { + guard.clear(); + } +} diff --git a/src/rust/src/image_loader.rs b/src/image_loader.rs similarity index 62% rename from src/rust/src/image_loader.rs rename to src/image_loader.rs index 8eaff52..835e3c1 100644 --- a/src/rust/src/image_loader.rs +++ b/src/image_loader.rs @@ -1,12 +1,9 @@ -use cxx_qt::Threading; -use cxx_qt_lib::{QByteArray, QImage, QString}; -use idevice::afc::{self, AfcClient}; - -use crate::{APP_DEVICE_STATE, RUNTIME}; -use core::ffi; +use idevice::afc::AfcClient; use idevice::afc::opcode::AfcFopenMode; use once_cell::sync::Lazy; use priority_queue::PriorityQueue; +use qmetaobject::prelude::*; +use qttypes::{QImage, QString}; use std::cmp::Reverse; use std::collections::HashMap; use std::sync::{ @@ -18,35 +15,15 @@ use tokio::{ sync::{Notify, Semaphore}, }; -#[cxx_qt::bridge] -mod qobject { - unsafe extern "C++" { - include!("cxx-qt-lib/qstring.h"); - include!("cxx-qt-lib/qbytearray.h"); - include!("cxx-qt-lib/qimage.h"); +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}; - type QImage = cxx_qt_lib::QImage; - type QString = cxx_qt_lib::QString; - type QByteArray = cxx_qt_lib::QByteArray; - } +#[derive(Default, QObject)] +pub struct ImageLoader { + base: qt_base_class!(trait QObject), - extern "RustQt" { - #[qobject] - type ImageBackend = super::ImageRustBackend; - - #[qinvokable] - fn request_thumbnail( - self: Pin<&mut ImageBackend>, - udid: &QString, - file_path: &QString, - row: u32, - ); - - #[qsignal] - fn thumbnail_ready(self: Pin<&mut ImageBackend>, file_path: QString, img: QImage, row: u32); - } - - impl cxx_qt::Threading for ImageBackend {} + thumbnailReady: qt_signal!(file_path: QString, row: u32), } static POOL_SEM: Lazy> = Lazy::new(|| Arc::new(Semaphore::new(10))); @@ -54,9 +31,14 @@ static SCHEDULER: Lazy> = Lazy::new(|| Arc::new(Scheduler::new()) static WORKER_STARTED: AtomicBool = AtomicBool::new(false); static NEXT_SEQ: AtomicU64 = AtomicU64::new(0); -#[derive(Default)] -pub struct ImageRustBackend; - +impl QtThreading for ImageLoader { + fn qt_thread(&self) -> crate::qt_threading::QtThread + where + Self: Sized, + { + QtThread::new(self) + } +} #[derive(Clone, Debug, Hash, Eq, PartialEq)] struct JobKey { udid: String, @@ -66,7 +48,7 @@ struct JobKey { struct JobPayload { row: u32, path_for_qt: QString, - qt_thread: cxx_qt::CxxQtThread, + qt_thread: QtThread, } struct QueueState { @@ -121,6 +103,7 @@ fn ensure_worker_started() { .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .is_ok() { + // FIXME: use std::thread ? RUNTIME.spawn(async { loop { let Some((key, payload)) = SCHEDULER.pop_next() else { @@ -160,32 +143,22 @@ fn ensure_worker_started() { device.afc.clone() }; - let mut afc = afc_arc.lock().await; - - let info = afc.get_file_info(&key.path).await; - - let size = match info { - Ok(i) => i.size, - Err(_) => anyhow::bail!("File has no size ?"), - }; - - drop(afc); - let mut img = QImage::default(); if is_video_file(&key.path) { // FIXME: can we do something better here ? let reader = - crate::bridge::AfcReader::new(key.udid.clone(), key.path.clone()); + AfcReader::new(key.udid.clone(), key.path.clone(), afc_arc); + + // let reader_for_block = reader; + let f_size = reader.get_size().await; + if !(f_size > 0) { + anyhow::bail!("File size is invalid for {}", key.path); + }; - let reader_for_block = reader; - let size_for_block = size as i32; img = tokio::task::spawn_blocking(move || { - crate::bridge::bridge::generate_thumbnail_with_reader( - &reader_for_block, - size_for_block, - // FIXME: sizes aren't respected - 320, - 240, + generate_thumbnail( + &reader, f_size, // FIXME: use consts for sizes + 320, 240, ) }) .await @@ -193,23 +166,24 @@ fn ensure_worker_started() { } else { let mut afc = afc_arc.lock().await; if key.path.to_ascii_lowercase().ends_with(".heic") { - let mut fd = afc.open(key.path, AfcFopenMode::RdOnly).await?; + let mut fd = afc.open(&key.path, AfcFopenMode::RdOnly).await?; let buf = fd.read_entire().await?; - img = crate::bridge::bridge::heic_to_image(&buf); + //FIXME: + // img = crate::bridge::bridge::heic_to_image(&buf); } else { - img = file_to_image(&mut afc, key.path).await; + img = file_to_image(&mut afc, &key.path).await; } } + crate::image_cache::insert(&key.udid.as_str(), &key.path, img); + let row = payload.row; let path_for_qt = payload.path_for_qt; let qt_thread = payload.qt_thread; - if let Err(e) = qt_thread.queue(move |mut backend_qobj| { - backend_qobj.thumbnail_ready(path_for_qt, img, row); - }) { - eprintln!("image_loader: failed to queue thumbnail_ready: {e}"); - } + qt_thread.queue(move |backend_qobj| { + backend_qobj.thumbnailReady(path_for_qt, row); + }); Ok(()) } @@ -219,59 +193,10 @@ fn ensure_worker_started() { }); } } -//FIXME:move to utils -fn is_video_file(path: &str) -> bool { - let ext = path - .rsplit_once('.') - .map(|(_, e)| e.to_ascii_lowercase()) - .unwrap_or_default(); - - matches!( - ext.as_str(), - "mp4" - | "mov" - | "m4v" - | "avi" - | "mkv" - | "webm" - | "flv" - | "wmv" - | "3gp" - | "mpeg" - | "mpg" - | "ts" - | "mts" - | "m2ts" - ) -} - -// async fn read_file_via_afc(udid: String, path: String) -> Vec { -// 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!("image_loader::read_file_via_afc: failed to read {path}: {e}"); -// buf.clear(); -// break; -// } -// }; - -// if n == 0 { -// break; -// } - -// buf.extend_from_slice(&chunk[..n]); -// } - -// buf -// } // FIXME: move or remove async fn file_to_buffer(afc: &mut AfcClient, path: String) -> Vec { - let mut buf = Vec::new(); + let mut buf: Vec = Vec::new(); let mut fd = match afc.open(path, AfcFopenMode::RdOnly).await { Ok(f) => f, @@ -302,7 +227,7 @@ async fn file_to_buffer(afc: &mut AfcClient, path: String) -> Vec { } //FIXME: move -async fn file_to_image(afc: &mut AfcClient, path: String) -> QImage { +async fn file_to_image(afc: &mut AfcClient, path: &str) -> QImage { let mut buf = Vec::new(); let mut fd = match afc.open(path, AfcFopenMode::RdOnly).await { @@ -331,19 +256,11 @@ async fn file_to_image(afc: &mut AfcClient, path: String) -> QImage { } fd.close().await.ok(); - match QImage::from_data(&buf, None) { - Some(img) => img, - None => QImage::default(), - } + create_image_from_buffer(&buf) } -impl qobject::ImageBackend { - fn request_thumbnail( - self: ::std::pin::Pin<&mut Self>, - udid: &QString, - file_path: &QString, - row: u32, - ) { +impl ImageLoader { + pub fn request_thumbnail(&self, udid: QString, file_path: QString, row: u32) { ensure_worker_started(); let udid_string = udid.to_string(); diff --git a/src/image_provider.rs b/src/image_provider.rs new file mode 100644 index 0000000..9145afc --- /dev/null +++ b/src/image_provider.rs @@ -0,0 +1,98 @@ +use qmetaobject::*; +use qttypes::{QImage, QSize, QString}; +use std::cell::RefCell; +use std::collections::HashMap; +use url::Url; +use url::form_urlencoded; + +use crate::image_loader::ImageLoader; +use crate::qquickimageprovider_imp::*; + +#[derive(Default, Clone)] +struct QSizeRef { + inner: QSize, +} + +impl QMetaType for QSizeRef {} + +impl From for QSizeRef { + fn from(size: QSize) -> Self { + Self { inner: size } + } +} + +#[derive(QObject)] +pub struct ImageProvider { + base: qt_base_class!(trait QQuickImageProvider), + loader: QObjectBox, +} + +impl ImageProvider { + pub fn default(loader: QObjectBox) -> Self { + Self { + loader: loader, + base: Default::default(), + } + } +} + +fn parse_image_id(id: &str) -> Option<(String, u32, String)> { + let (path, query) = id.split_once('?').unwrap_or((id, "")); + let mut udid = String::new(); + let mut index: u32 = 0; + + for (k, v) in form_urlencoded::parse(query.as_bytes()) { + match k.as_ref() { + "udid" => udid = v.into_owned(), + "index" => index = v.parse().unwrap_or(0), + _ => {} + } + } + + Some((udid, index, path.to_string())) +} + +impl QQuickImageProvider for ImageProvider { + fn request_image(&self, id: &str, requested_size: &QSize) -> (QSize, QImage) { + let placeholder = ( + QSize { + width: 500, + height: 500, + }, + QImage::load_from_file(QString::from( + ":/resources/icons/MaterialSymbolsLightImageOutlineSharp.png", + )), + ); + + let (udid, index, path) = match parse_image_id(id) { + Some(v) => v, + None => { + println!("Failed to parse image id: {}", id); + return placeholder; + } + }; + + if let Some(img) = crate::image_cache::get(&udid, &path) { + return ( + QSize { + width: 500, + height: 500, + }, + img, + ); + } + + 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), + index, + ); + + placeholder + } +} diff --git a/src/imageloader.cpp b/src/imageloader.cpp deleted file mode 100644 index f3603cc..0000000 --- a/src/imageloader.cpp +++ /dev/null @@ -1,509 +0,0 @@ -#include "imageloader.h" -#include "iDescriptor.h" -#include "imagetask.h" -#include -#include -#include - -extern "C" { -#include -#include -#include -#include -#include -} - -ImageLoader::ImageLoader(QObject *parent) : QObject(parent) -{ - // TODO: maybe finetune to hardware ? - m_pool.setMaxThreadCount(15); - - if (qApp) { - connect(qApp, &QCoreApplication::aboutToQuit, this, - [this]() { clear(); }); - } -} - -bool ImageLoader::isLoading(const QString &path) -{ - QMutexLocker locker(&m_mutex); - return m_pendingTasks.contains(path); -} - -void ImageLoader::requestThumbnail( - const std::shared_ptr device, const QString &path, - unsigned int row) -{ - { - QMutexLocker locker(&m_mutex); - if (m_pendingTasks.contains(path)) - return; - } - - auto *task = new ImageTask(device, path, row); - - { - QMutexLocker locker(&m_mutex); - m_pendingTasks[path] = task; - } - - connect(task, &ImageTask::finished, this, &ImageLoader::onTaskFinished, - Qt::QueuedConnection); - - // Use row as priority - m_pool.start(task, row); -} - -/* - this method should not load from cache - because cached images are already scaled down - we need the original image -*/ -void ImageLoader::requestImageWithCallback( - const std::shared_ptr device, const QString &path, - int priority, std::function callback, - std::optional> hause_arrest, bool useAfc2) -{ - auto *task = - new ImageTask(device, path, priority, false, hause_arrest, useAfc2); - - connect( - task, &ImageTask::finished, this, - [callback](const QString &, const QImage &image, unsigned int) { - if (QCoreApplication::closingDown() || - !QGuiApplication::instance()) { - callback(QPixmap()); - return; - } - callback(image.isNull() ? QPixmap() : QPixmap::fromImage(image)); - }, - Qt::QueuedConnection); - - m_pool.start(task, priority); -} - -void ImageLoader::cancelThumbnail(const QString &path) -{ - qDebug() << "Attempting to cancel thumbnail loading for" << path; - - ImageTask *task = nullptr; - { - QMutexLocker locker(&m_mutex); - task = m_pendingTasks.take(path); - } - - if (!task) { - return; - } - - if (m_pool.tryTake(task)) { - qDebug() << "Cancelled thumbnail loading for" << path; - // should be safe to delete - delete task; - } -} - -void ImageLoader::clear() -{ - qDebug() << "Clearing ImageLoader cache and pending tasks"; - m_pool.clear(); - m_pool.waitForDone(); - - QMutexLocker locker(&m_mutex); - m_pendingTasks.clear(); -} - -void ImageLoader::onTaskFinished(const QString &path, const QImage &image, - unsigned int row) -{ - { - QMutexLocker locker(&m_mutex); - if (!m_pendingTasks.contains(path)) { - return; - } - m_pendingTasks.remove(path); - } - - if (QCoreApplication::closingDown() || !QGuiApplication::instance()) { - return; - } - - const QPixmap pixmap = - image.isNull() ? QPixmap() : QPixmap::fromImage(image); - emit thumbnailReady(path, pixmap, row); -} - -// almost a copy of loadThumbnailFromDevice but without any scaling logic -QImage ImageLoader::loadImage( - const std::shared_ptr device, const QString &filePath, - std::optional> hause_arrest, bool useAfc2) -{ - if (QCoreApplication::closingDown() || !QGuiApplication::instance()) { - return {}; - } - QByteArray imageData; - - if (useAfc2) { - imageData = device->afc2_backend->file_to_buffer(filePath); - } else if (hause_arrest.has_value() && hause_arrest.value()) { - qDebug() << "Loading image using HauseArrest for:" << filePath; - imageData = hause_arrest.value()->file_to_buffer(filePath); - } else { - imageData = device->afc_backend->file_to_buffer(filePath); - } - - if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) { - QImage img = load_heic(imageData); - return img.isNull() ? QImage() : img; - } - - QBuffer buffer(&imageData); - buffer.open(QIODevice::ReadOnly); - - QImageReader reader(&buffer); - if (reader.canRead()) { - QImage image = reader.read(); - if (!image.isNull()) { - return image; - } - } - - QImage fallback; - if (fallback.loadFromData(imageData)) { - return fallback; - } - - return {}; -} - -QImage ImageLoader::loadThumbnailFromDevice( - const std::shared_ptr device, const QString &filePath, - const QSize &size, - std::optional> hause_arrest, bool useAfc2) -{ - if (QCoreApplication::closingDown() || !QGuiApplication::instance()) { - return {}; - } - - QByteArray imageData; - - if (useAfc2) { - imageData = device->afc2_backend->file_to_buffer(filePath); - } else if (hause_arrest.has_value() && hause_arrest.value()) { - qDebug() << "Loading thumbnail using HauseArrest for:" << filePath; - imageData = hause_arrest.value()->file_to_buffer(filePath); - } else { - imageData = device->afc_backend->file_to_buffer(filePath); - } - - if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) { - QImage img = load_heic(imageData); - return img.isNull() ? QImage() - : img.scaled(size, Qt::KeepAspectRatio, - Qt::SmoothTransformation); - } - - QBuffer buffer(&imageData); - buffer.open(QIODevice::ReadOnly); - - QImageReader reader(&buffer); - if (reader.canRead()) { - QImage image = reader.read(); - if (!image.isNull()) { - return image.scaled(size, Qt::KeepAspectRatio, - Qt::SmoothTransformation); - } - } - - QImage fallback; - if (fallback.loadFromData(imageData)) { - return fallback.scaled(size, Qt::KeepAspectRatio, - Qt::SmoothTransformation); - } - - return {}; -} - -QImage ImageLoader::generateVideoThumbnailFFmpeg( - const std::shared_ptr device, const QString &filePath, - const QSize &requestedSize, - std::optional> hause_arrest, bool useAfc2) -{ - QImage thumbnail; - if (QCoreApplication::closingDown()) { - qDebug() << "Application is closing, aborting " - "generateVideoThumbnailFFmpeg for" - << filePath; - return thumbnail; - } - - /* - FIXME: other afc clients are not respected here, we need to handle this - better, currently only the normal afc client is used for video thumbnail - generation - */ - CXX::AfcBackend *afc = device->afc_backend; - - const qint64 fileSize = afc->get_file_size(filePath); - if (fileSize <= 0) { - qWarning() << "Invalid video file size for thumbnail:" << filePath; - return {}; - } - - AVFormatContext *formatCtx = avformat_alloc_context(); - if (!formatCtx) { - qWarning() << "Failed to allocate format context"; - return {}; - } - - struct StreamContext { - CXX::AfcBackend *backend; - QString path; - qint64 fileSize; - qint64 currentPos; - }; - - auto *streamCtx = new StreamContext{afc, filePath, fileSize, 0}; - - auto readPacket = [](void *opaque, uint8_t *buf, int bufSize) -> int { - auto *ctx = static_cast(opaque); - - if (ctx->currentPos >= ctx->fileSize) { - return AVERROR_EOF; - } - - qint64 toRead = - std::min(bufSize, ctx->fileSize - ctx->currentPos); - QByteArray chunk = - ctx->backend->read_file_range(ctx->path, ctx->currentPos, toRead); - - if (chunk.isEmpty()) { - // IO error - return AVERROR(EIO); - } - - const int n = std::min(chunk.size(), bufSize); - memcpy(buf, chunk.constData(), n); - ctx->currentPos += n; - return n; - }; - - auto seekPacket = [](void *opaque, int64_t offset, int whence) -> int64_t { - auto *ctx = static_cast(opaque); - - if (whence == AVSEEK_SIZE) { - return ctx->fileSize; - } - - qint64 newPos = 0; - switch (whence) { - case SEEK_SET: - newPos = offset; - break; - case SEEK_CUR: - newPos = ctx->currentPos + offset; - break; - case SEEK_END: - newPos = ctx->fileSize + offset; - break; - default: - return -1; - } - - if (newPos < 0 || newPos > ctx->fileSize) { - return -1; - } - - ctx->currentPos = newPos; - return newPos; - }; - - const int avioBufferSize = 32768; - unsigned char *avioBuffer = - static_cast(av_malloc(avioBufferSize)); - if (!avioBuffer) { - delete streamCtx; - avformat_free_context(formatCtx); - return {}; - } - - AVIOContext *avioCtx = - avio_alloc_context(avioBuffer, avioBufferSize, 0, streamCtx, readPacket, - nullptr, seekPacket); - if (!avioCtx) { - av_free(avioBuffer); - delete streamCtx; - avformat_free_context(formatCtx); - return {}; - } - - formatCtx->pb = avioCtx; - formatCtx->flags |= AVFMT_FLAG_CUSTOM_IO; - - // Open input - if (avformat_open_input(&formatCtx, nullptr, nullptr, nullptr) < 0) { - qWarning() << "Failed to open video format"; - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - avformat_free_context(formatCtx); - return {}; - } - - // Find stream info - if (avformat_find_stream_info(formatCtx, nullptr) < 0) { - qWarning() << "Failed to find stream info"; - avformat_close_input(&formatCtx); - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - return {}; - } - - // Find video stream - int videoStreamIndex = -1; - const AVCodec *codec = nullptr; - AVCodecParameters *codecParams = nullptr; - - for (unsigned int i = 0; i < formatCtx->nb_streams; i++) { - if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - videoStreamIndex = i; - codecParams = formatCtx->streams[i]->codecpar; - codec = avcodec_find_decoder(codecParams->codec_id); - break; - } - } - - if (videoStreamIndex == -1 || !codec) { - qWarning() << "No video stream found"; - avformat_close_input(&formatCtx); - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - return {}; - } - - // Allocate codec context - AVCodecContext *codecCtx = avcodec_alloc_context3(codec); - if (!codecCtx) { - avformat_close_input(&formatCtx); - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - return {}; - } - - if (avcodec_parameters_to_context(codecCtx, codecParams) < 0) { - avcodec_free_context(&codecCtx); - avformat_close_input(&formatCtx); - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - return {}; - } - - // Open codec - if (avcodec_open2(codecCtx, codec, nullptr) < 0) { - avcodec_free_context(&codecCtx); - avformat_close_input(&formatCtx); - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - return {}; - } - - // Allocate frame - AVFrame *frame = av_frame_alloc(); - AVPacket *packet = av_packet_alloc(); - - if (!frame || !packet) { - if (frame) - av_frame_free(&frame); - if (packet) - av_packet_free(&packet); - avcodec_free_context(&codecCtx); - avformat_close_input(&formatCtx); - av_free(avioCtx->buffer); - avio_context_free(&avioCtx); - return {}; - } - - // Read frames until we get a valid one - bool frameDecoded = false; - while (av_read_frame(formatCtx, packet) >= 0) { - if (packet->stream_index == videoStreamIndex) { - if (avcodec_send_packet(codecCtx, packet) >= 0) { - if (avcodec_receive_frame(codecCtx, frame) >= 0) { - frameDecoded = true; - av_packet_unref(packet); - break; - } - } - } - av_packet_unref(packet); - } - - if (frameDecoded) { - // Get rotation from display matrix - double rotation = 0.0; - if (AVFrameSideData *sd = - av_frame_get_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX)) { - rotation = - -av_display_rotation_get(reinterpret_cast(sd->data)); - } - - // Convert frame to RGB24 - SwsContext *swsCtx = - sws_getContext(frame->width, frame->height, - static_cast(frame->format), - frame->width, frame->height, AV_PIX_FMT_RGB24, - SWS_BILINEAR, nullptr, nullptr, nullptr); - - if (swsCtx) { - AVFrame *rgbFrame = av_frame_alloc(); - if (rgbFrame) { - rgbFrame->format = AV_PIX_FMT_RGB24; - rgbFrame->width = frame->width; - rgbFrame->height = frame->height; - - if (av_frame_get_buffer(rgbFrame, 0) >= 0) { - sws_scale(swsCtx, frame->data, frame->linesize, 0, - frame->height, rgbFrame->data, - rgbFrame->linesize); - - // Convert to QImage - QImage img(rgbFrame->data[0], rgbFrame->width, - rgbFrame->height, rgbFrame->linesize[0], - QImage::Format_RGB888); - - // Create a deep copy since AVFrame will be freed - QImage imgCopy = img.copy(); - - // Apply rotation - if (rotation != 0.0) { - QTransform transform; - transform.rotate(rotation); - imgCopy = imgCopy.transformed(transform); - } - - // Scale to requested size - /* - TODO: scaling might become optional - if we ever needed the raw frame, - might need to abstract the main logic to get the frame - and handle scaling separately - */ - thumbnail = - imgCopy.scaled(requestedSize, Qt::KeepAspectRatio, - Qt::SmoothTransformation); - } - - av_frame_free(&rgbFrame); - } - - sws_freeContext(swsCtx); - } - } - - // Cleanup - av_frame_free(&frame); - av_packet_free(&packet); - avcodec_free_context(&codecCtx); - avformat_close_input(&formatCtx); - - return thumbnail; -} \ No newline at end of file diff --git a/src/imageloader.h b/src/imageloader.h deleted file mode 100644 index 8f75b70..0000000 --- a/src/imageloader.h +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef IMAGELOADER_H -#define IMAGELOADER_H - -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class ImageTask; - -class ImageLoader : public QObject -{ - Q_OBJECT -public: - explicit ImageLoader(QObject *parent = nullptr); - static ImageLoader &sharedInstance() - { - static ImageLoader instance; - return instance; - } - void requestThumbnail(const std::shared_ptr device, - const QString &path, unsigned int row = 0); - void requestImageWithCallback( - const std::shared_ptr device, const QString &path, - int priority, std::function callback, - std::optional> hause_arrest = - std::nullopt, - bool useAfc2 = false); - void cancelThumbnail(const QString &path); - bool isLoading(const QString &path); - void clear(); - static QImage loadThumbnailFromDevice( - const std::shared_ptr device, - const QString &filePath, const QSize &size, - std::optional> hause_arrest = - std::nullopt, - bool useAfc2 = false); - - static QImage generateVideoThumbnailFFmpeg( - const std::shared_ptr device, - const QString &filePath, const QSize &size, - std::optional> hause_arrest = - std::nullopt, - bool useAfc2 = false); - - static QImage loadImage(const std::shared_ptr device, - const QString &filePath, - std::optional> - hause_arrest = std::nullopt, - bool useAfc2 = false); -signals: - void thumbnailReady(const QString &path, const QPixmap &image, - unsigned int row); - -private slots: - void onTaskFinished(const QString &path, const QImage &image, - unsigned int row); - -private: - QThreadPool m_pool; - QHash m_pendingTasks; - QMutex m_mutex; -}; - -#endif // IMAGELOADER_H \ No newline at end of file diff --git a/src/imagetask.h b/src/imagetask.h deleted file mode 100644 index 85b63b8..0000000 --- a/src/imagetask.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef IMAGETASK_H -#define IMAGETASK_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "imageloader.h" -#include -#include -#include -#include -#include -#include - -class ImageTask : public QObject, public QRunnable -{ - Q_OBJECT -public: - ImageTask(const std::shared_ptr device, - const QString &path, unsigned int row, bool scale = true, - std::optional> hause_arrest = - std::nullopt, - bool useAfc2 = false) - : m_device(device), m_path(path), m_isThumbnail(scale), m_row(row), - m_hause_arrest(hause_arrest), m_useAfc2(useAfc2) - { - setAutoDelete(true); - } - -signals: - void finished(const QString &path, const QImage &image, unsigned int row); - -protected: - void run() override - { - if (QCoreApplication::closingDown()) { - return; - } - - const bool isVideo = iDescriptor::Utils::isVideoFile(m_path); - - if (isVideo) { - QImage image = ImageLoader::generateVideoThumbnailFFmpeg( - m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest, m_useAfc2); - emit finished(m_path, image, m_row); - return; - } - - if (m_isThumbnail) { - QImage image = ImageLoader::loadThumbnailFromDevice( - m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest, m_useAfc2); - emit finished(m_path, image, m_row); - } else { - QImage image = ImageLoader::loadImage(m_device, m_path, - m_hause_arrest, m_useAfc2); - emit finished(m_path, image, m_row); - } - } - -private: - const std::shared_ptr m_device; - QString m_path; - bool m_isThumbnail; - unsigned int m_row; - std::optional> m_hause_arrest; - bool m_useAfc2; -}; - -#endif // IMAGETASK_H diff --git a/src/include/bridge.h b/src/include/bridge.h new file mode 100644 index 0000000..3edfea9 --- /dev/null +++ b/src/include/bridge.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*AfcReadCallback)(const void *reader_ptr, int64_t offset, + int32_t size, uint8_t *out_buf, + int32_t *out_len); + +QImage generate_thumbnail_with_reader_ffi(const void *reader_ptr, + int32_t file_size, + int32_t requested_w, + int32_t requested_h); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/infolabel.cpp b/src/infolabel.cpp deleted file mode 100644 index 95af921..0000000 --- a/src/infolabel.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "infolabel.h" -#include -#include -#include -#include - -InfoLabel::InfoLabel(const QString &text, const QString &textToCopy, - QWidget *parent) - : QLabel(text, parent), m_originalText(text), - m_textToCopy(!textToCopy.isEmpty() ? textToCopy : text) -{ - setCursor(Qt::PointingHandCursor); - setStyleSheet(m_style); - m_restoreTimer = new QTimer(this); - m_restoreTimer->setSingleShot(true); - connect(m_restoreTimer, &QTimer::timeout, this, - &InfoLabel::restoreOriginalText); -} - -void InfoLabel::mousePressEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) { - int originalWidth = width(); - - QClipboard *clipboard = QApplication::clipboard(); - clipboard->setText(m_textToCopy); - - // prevent layout shifts - setMinimumWidth(originalWidth); - setText("Copied!"); -#ifdef WIN32 - setStyleSheet(QStringLiteral( - "QLabel { color: #4CAF50; font-weight: bold; font-size: 14px; }" - "QLabel:hover { background-color: rgba(255, 255, 255, 0.1); " - "border-radius: 2px; }")); -#else - setStyleSheet("QLabel { color: #4CAF50; font-weight: bold; } " - "QLabel:hover { background-color: rgba(255, 255, 255, " - "0.1); border-radius: 2px; }"); -#endif - m_restoreTimer->start(1000); // Show "Copied!" for 1 second - } - QLabel::mousePressEvent(event); -} - -void InfoLabel::restoreOriginalText() -{ - setText(m_originalText); - setMinimumWidth(0); - setStyleSheet(m_style); -} - -void InfoLabel::setOriginalText(const QString &text) { m_originalText = text; } - -void InfoLabel::setTextToCopy(const QString &textToCopy) -{ - m_textToCopy = textToCopy; -} \ No newline at end of file diff --git a/src/infolabel.h b/src/infolabel.h deleted file mode 100644 index 01aa217..0000000 --- a/src/infolabel.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef INFOLABEL_H -#define INFOLABEL_H - -#include -#include -#include - -class InfoLabel : public QLabel -{ - Q_OBJECT - -public: - explicit InfoLabel(const QString &text = QString(), - const QString &textToCopy = QString(), - QWidget *parent = nullptr); - - void setOriginalText(const QString &text); - void setTextToCopy(const QString &textToCopy); - -protected: - void mousePressEvent(QMouseEvent *event) override; - -private slots: - void restoreOriginalText(); - -private: - QString m_originalText; - QString m_textToCopy; - QTimer *m_restoreTimer; - QString m_style = -#ifdef WIN32 - QStringLiteral( - "QLabel:hover { background-color: rgba(255, 255, 255, 0.1); " - "border-radius: 2px; }" - "QLabel { " - "font-size: 14px;}"); -#else - QStringLiteral( - "QLabel:hover { background-color: rgba(255, 255, 255, 0.1); " - "border-radius: 2px; }"); -#endif -}; - -#endif // INFOLABEL_H \ No newline at end of file diff --git a/src/installedappswidget.cpp b/src/installedappswidget.cpp deleted file mode 100644 index 9ee470a..0000000 --- a/src/installedappswidget.cpp +++ /dev/null @@ -1,699 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "installedappswidget.h" -#include "afcexplorerwidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "qprocessindicator.h" -#include "zlineedit.h" - -AppTabWidget::AppTabWidget(const QString &appName, const QString &bundleId, - const QString &version, const QPixmap &icon, - QWidget *parent) - : QWidget(parent), m_appName(appName), m_bundleId(bundleId), - m_version(version), m_selected(false) -{ -#ifndef WIN32 - setFixedHeight(60); -#else - setMinimumHeight(60); -#endif - setMinimumWidth(100); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_StyledBackground, true); - setObjectName("AppTabWidget"); - setupUI(icon); -} - -void AppTabWidget::setSelected(bool selected) -{ - m_selected = selected; - updateStyles(); -} - -void AppTabWidget::setupUI(const QPixmap &icon) -{ - QHBoxLayout *mainLayout = new QHBoxLayout(this); - mainLayout->setContentsMargins(10, 8, 10, 8); - mainLayout->setSpacing(10); - - m_iconLabel = new IDLoadingIconLabel(this); - m_iconLabel->setFixedSize(32, 32); - - if (!icon.isNull()) { - m_iconLabel->setLoadedPixmap(icon); - } - mainLayout->addWidget(m_iconLabel); - - // Text container - QVBoxLayout *textLayout = new QVBoxLayout(); - textLayout->setContentsMargins(0, 0, 0, 0); - textLayout->setSpacing(2); - - // App name label - m_nameLabel = new QLabel(); - QFont nameFont = m_nameLabel->font(); - nameFont.setWeight(QFont::Medium); - m_nameLabel->setFont(nameFont); - - QString displayText = m_appName; - if (displayText.length() > 20) { - displayText = displayText.left(17) + "..."; - } - m_nameLabel->setText(displayText); - textLayout->addWidget(m_nameLabel); - - // Version label - if (!m_version.isEmpty()) { - m_versionLabel = new QLabel(m_version); - m_versionLabel->setStyleSheet("font-size: 11px;"); - textLayout->addWidget(m_versionLabel); - } else { - m_versionLabel = nullptr; - } - - mainLayout->addLayout(textLayout); - mainLayout->addStretch(); - - updateStyles(); -} - -void AppTabWidget::setIcon(const QPixmap &icon) -{ - if (!m_iconLabel) - return; - - if (!icon.isNull()) { - m_iconLabel->setLoadedPixmap(icon); - } else { - m_iconLabel->setLoadFailed(); - } - m_hasIcon = true; -} - -void AppTabWidget::mousePressEvent(QMouseEvent *event) -{ - Q_UNUSED(event) - emit clicked(); -} - -void AppTabWidget::updateStyles() -{ - QString style; -#ifndef WIN32 - QColor bgColor = isDarkMode() ? qApp->palette().color(QPalette::Light) - : qApp->palette().color(QPalette::Dark); -#else - QColor bgColor = - isDarkMode() ? QColor(255, 255, 255, 25) : QColor(0, 0, 0, 25); -#endif - if (m_selected) { - style = - "#AppTabWidget { background-color: " + COLOR_ACCENT_BLUE.name() + - "; border-radius: " - "10px; border : 1px solid " + - bgColor.lighter().name() + "; }"; - } else { - style = "#AppTabWidget { background-color: " + - bgColor.name(QColor::HexArgb) + - "; border-radius: 10px; border: 1px solid " + - bgColor.lighter().name() + "; }"; - } - // prevent infinite loop - if (style != styleSheet()) { - setStyleSheet(style); - } -} - -InstalledAppsWidget::InstalledAppsWidget( - const std::shared_ptr device, QWidget *parent) - : QWidget(parent), m_device(device) -{ - QVBoxLayout *rootLayout = new QVBoxLayout(this); - rootLayout->setContentsMargins(0, 0, 0, 0); - - m_zloadingWidget = new ZLoadingWidget(true, this); - rootLayout->addWidget(m_zloadingWidget); - initInternal(); -} - -void InstalledAppsWidget::initInternal() -{ - setupUI(); - - connect(m_device->service_manager, - &CXX::ServiceManager::installed_apps_retrieved, this, - &InstalledAppsWidget::onAppsDataReady); - connect(m_device->service_manager, &CXX::ServiceManager::app_icon_loaded, - this, &InstalledAppsWidget::onAppIconLoaded); - setStyleSheet("InstalledAppsWidget { background: transparent; }"); -} - -void InstalledAppsWidget::init() -{ - if (m_loaded) { - qDebug() - << "[InstalledAppsWidget]: Already initialized, skipping init()"; - return; - } - m_loaded = true; - m_device->service_manager->fetch_installed_apps(); -} - -void InstalledAppsWidget::refresh() -{ - m_zloadingWidget->showLoading(); - m_device->service_manager->fetch_installed_apps(); -} - -InstalledAppsWidget::~InstalledAppsWidget() -{ - cleanupHouseArrestClients(); - - if (m_device && m_device->service_manager) { - disconnect(m_device->service_manager, nullptr, this, nullptr); - } - - m_iconLoadQueue.clear(); - m_iconLoading = false; -} - -void InstalledAppsWidget::setupUI() -{ - QWidget *contentContainer = new QWidget(this); - m_mainLayout = new QHBoxLayout(contentContainer); - m_mainLayout->setContentsMargins(0, 0, 0, 0); - m_mainLayout->setSpacing(0); - - m_zloadingWidget->setupContentWidget(contentContainer); - - // Create stacked widget for different states - m_stackedWidget = new QStackedWidget(this); - m_mainLayout->addWidget(m_stackedWidget); - - // Create loading widget - createLoadingWidget(); - - // Create error widget - createErrorWidget(); - - // Create content widget - createContentWidget(); - - // Start in loading state - showLoadingState(); -} - -void InstalledAppsWidget::showLoadingState() -{ - m_stackedWidget->setCurrentWidget(m_loadingWidget); -} - -void InstalledAppsWidget::showErrorState(const QString &error) -{ - m_zloadingWidget->stop(true); - m_errorLabel->setText(QString("Error loading apps: %1").arg(error)); - m_stackedWidget->setCurrentWidget(m_errorWidget); -} - -void InstalledAppsWidget::createLoadingWidget() -{ - m_loadingWidget = new QWidget(); - QVBoxLayout *loadingLayout = new QVBoxLayout(m_loadingWidget); - loadingLayout->setAlignment(Qt::AlignCenter); - - QProcessIndicator *spinner = new QProcessIndicator(); - spinner->setType(QProcessIndicator::line_rotate); - spinner->setFixedSize(48, 48); - spinner->start(); - loadingLayout->addWidget(spinner, 0, Qt::AlignCenter); - - QLabel *loadingLabel = new QLabel("Loading installed apps..."); - loadingLabel->setAlignment(Qt::AlignCenter); - loadingLabel->setStyleSheet( - "font-size: 14px; color: #666; margin-top: 10px;"); - loadingLayout->addWidget(loadingLabel); - - m_stackedWidget->addWidget(m_loadingWidget); -} - -void InstalledAppsWidget::createErrorWidget() -{ - m_errorWidget = new QWidget(); - QVBoxLayout *errorLayout = new QVBoxLayout(m_errorWidget); - errorLayout->setAlignment(Qt::AlignCenter); - - m_errorLabel = new QLabel(); - m_errorLabel->setAlignment(Qt::AlignCenter); - m_errorLabel->setStyleSheet( - "font-size: 14px; color: #d32f2f; margin: 20px;"); - m_errorLabel->setWordWrap(true); - errorLayout->addWidget(m_errorLabel); - - QPushButton *retryButton = new QPushButton("Retry"); - retryButton->setFixedSize(100, 30); - connect(retryButton, &QPushButton::clicked, this, - &InstalledAppsWidget::refresh); - errorLayout->addWidget(retryButton, 0, Qt::AlignCenter); - - m_stackedWidget->addWidget(m_errorWidget); -} - -void InstalledAppsWidget::createContentWidget() -{ - m_contentWidget = new QWidget(); - QHBoxLayout *contentLayout = new QHBoxLayout(m_contentWidget); - contentLayout->setContentsMargins(0, 0, 0, 0); - contentLayout->setSpacing(0); - - // Create main splitter - m_splitter = new ModernSplitter(Qt::Horizontal, m_contentWidget); - m_splitter->setChildrenCollapsible(false); - contentLayout->addWidget(m_splitter); - - // Left side - App list - createLeftPanel(); - - // Right side - Content area - createRightPanel(); - - // Set initial splitter - m_splitter->setSizes({400, 600}); - - // Connect signals - connect(m_searchEdit, &QLineEdit::textChanged, this, - &InstalledAppsWidget::filterApps); - connect(m_fileSharingCheckBox, &QCheckBox::toggled, this, - &InstalledAppsWidget::onFileSharingFilterChanged); - - m_stackedWidget->addWidget(m_contentWidget); -} - -void InstalledAppsWidget::onAppsDataReady(const QMap &result) -{ - m_zloadingWidget->stop(true); - if (result.isEmpty()) { - showErrorState("No apps found or failed to retrieve apps."); - return; - } - - m_stackedWidget->setCurrentWidget(m_contentWidget); - - // Clear existing tabs - if (m_appsListWidget) - m_appsListWidget->clear(); - m_appTabs.clear(); - m_appItems.clear(); - m_selectedTab = nullptr; - m_iconLoadQueue.clear(); - m_iconLoading = false; - - // Create tabs for each app - for (const QVariant &appVariant : result) { - QJsonParseError error; - QJsonDocument doc = - QJsonDocument::fromJson(appVariant.toString().toUtf8(), &error); - - if (error.error != QJsonParseError::NoError) { - qDebug() << "JSON parse error:" << error.errorString(); - continue; - } - - QString displayName = doc["CFBundleDisplayName"].toString(); - QString bundleId = doc["bundle_id"].toString(); - QString version = doc["CFBundleShortVersionString"].toString(); - QString appType = doc["app_type"].toString(); - bool fileSharingEnabled = doc["UIFileSharingEnabled"].toBool(); - - /* - Always fails to load Fitness app container - even though file sharing is enabled - */ - if (bundleId == "com.apple.Fitness") { - continue; - } - - // // Filter by file sharing status if checkbox is checked - if (m_fileSharingCheckBox->isChecked() && !fileSharingEnabled) { - continue; - } - - if (displayName.isEmpty()) { - displayName = bundleId; - } - - // Create tab name with type indicator - QString tabName = displayName; - if (appType == "System") { - tabName += " (System)"; - } - - createAppTab(tabName, bundleId, version, QPixmap()); - } - - // Select first tab if available - if (!m_appTabs.isEmpty()) { - selectAppTab(m_appTabs.first()); - } - - QTimer::singleShot(0, this, &InstalledAppsWidget::updateVisibleIcons); -} - -void InstalledAppsWidget::createAppTab(const QString &appName, - const QString &bundleId, - const QString &version, - const QPixmap &icon) -{ - if (!m_appsListWidget) - return; - - auto *tabWidget = - new AppTabWidget(appName, bundleId, version, icon, m_appsListWidget); - connect(tabWidget, &AppTabWidget::clicked, this, - &InstalledAppsWidget::onAppTabClicked); - - auto *item = new QListWidgetItem(m_appsListWidget); - item->setSizeHint(tabWidget->sizeHint()); - - m_appsListWidget->addItem(item); - m_appsListWidget->setItemWidget(item, tabWidget); - - m_appTabs[bundleId] = tabWidget; - m_appItems[bundleId] = item; -} - -void InstalledAppsWidget::onAppTabClicked() -{ - AppTabWidget *clickedTab = qobject_cast(sender()); - if (clickedTab) { - selectAppTab(clickedTab); - } -} - -void InstalledAppsWidget::selectAppTab(AppTabWidget *tab) -{ - // Deselect previous tab - if (m_selectedTab) { - m_selectedTab->setSelected(false); - } - - // Select new tab - m_selectedTab = tab; - tab->setSelected(true); - - QString bundleId = tab->getBundleId(); - - // Load app container data - loadAppContainer(bundleId); -} - -void InstalledAppsWidget::filterApps(const QString &searchText) -{ - QString lowerSearchText = searchText.toLower(); - - for (AppTabWidget *tab : m_appTabs) { - bool shouldShow = false; - - if (lowerSearchText.isEmpty()) { - shouldShow = true; - } else { - QString appName = tab->getAppName().toLower(); - QString bundleId = tab->getBundleId().toLower(); - - shouldShow = appName.contains(lowerSearchText) || - bundleId.contains(lowerSearchText); - } - - QListWidgetItem *item = m_appItems.value(tab->getBundleId(), nullptr); - if (item) - item->setHidden(!shouldShow); - - tab->setVisible(shouldShow); - } - - updateVisibleIcons(); -} - -void InstalledAppsWidget::loadAppContainer(const QString &bundleId) -{ - if (!m_device || m_loadingContainer) { - return; - } - m_loadingContainer = true; - - disableTabs(true); - // Clean up previous house arrest clients before creating a new one - cleanupHouseArrestClients(); - - clearContainerLayout(); - - // Create a centered loading widget - QWidget *loadingWidget = new QWidget(); - QVBoxLayout *loadingLayout = new QVBoxLayout(loadingWidget); - loadingLayout->setAlignment(Qt::AlignCenter); - - QProcessIndicator *l = new QProcessIndicator(); - l->setType(QProcessIndicator::line_rotate); - l->setFixedSize(32, 32); - l->start(); - loadingLayout->addWidget(l, 0, Qt::AlignCenter); - - m_containerLayout->addWidget(loadingWidget); - - m_houseArrestAfcClient = - std::make_shared(m_device->udid, bundleId); - - connect(m_houseArrestAfcClient.get(), - &CXX::HauseArrest::init_session_finished, this, - &InstalledAppsWidget::onContainerDataReady, - Qt::SingleShotConnection); - - m_houseArrestAfcClient->init_session(); -} - -void InstalledAppsWidget::onContainerDataReady(bool success) -{ - clearContainerLayout(); - - m_loadingContainer = false; - disableTabs(false); - - if (!success) { - qDebug() << "Error loading app container:"; - QLabel *errorLabel = new QLabel("No data available for this app"); - errorLabel->setAlignment(Qt::AlignCenter); - m_containerLayout->addWidget(errorLabel); - return; - } - - // Create AfcExplorerWidget with the house arrest AFC client - AfcExplorerWidget *explorer = new AfcExplorerWidget( - m_device, true, m_houseArrestAfcClient, false, "/Documents", this); - explorer->setStyleSheet("border :none;"); - m_containerLayout->addWidget(explorer); -} - -void InstalledAppsWidget::onAppIconLoaded(const QString &bundleId, - const QByteArray &icon) -{ - qDebug() << "Icon loaded for bundle ID:" << bundleId; - AppTabWidget *tab = m_appTabs.value(bundleId, nullptr); - if (tab) { - qDebug() << "Setting icon for bundle ID:" << bundleId; - QPixmap pixmap; - pixmap.loadFromData(icon); - tab->setIcon(pixmap); - } - - startNextIconLoad(); -} - -void InstalledAppsWidget::onFileSharingFilterChanged(bool enabled) -{ - Q_UNUSED(enabled) - m_iconLoadQueue.clear(); - m_iconLoading = false; - m_device->service_manager->fetch_installed_apps(); -} - -void InstalledAppsWidget::cleanupHouseArrestClients() -{ - if (m_houseArrestAfcClient) { - m_houseArrestAfcClient = nullptr; - } -} - -void InstalledAppsWidget::createLeftPanel() -{ - QWidget *tabWidget = new QWidget(); - tabWidget->setMinimumWidth(100); - tabWidget->setMaximumWidth(500); - - QVBoxLayout *tabWidgetLayout = new QVBoxLayout(tabWidget); - tabWidgetLayout->setContentsMargins(0, 0, 0, 0); - tabWidgetLayout->setSpacing(0); - - // Search container - QWidget *searchContainer = new QWidget(); - searchContainer->setFixedHeight(60); - QHBoxLayout *searchLayout = new QHBoxLayout(searchContainer); - searchLayout->setContentsMargins(5, 0, 5, 5); - - // Search box - m_searchEdit = new ZLineEdit(); - m_searchEdit->setPlaceholderText("Search apps..."); - searchLayout->addWidget(m_searchEdit); - - // File sharing filter checkbox - m_fileSharingCheckBox = new QCheckBox("Show Only File Sharing Enabled"); - m_fileSharingCheckBox->setChecked(true); - m_fileSharingCheckBox->setStyleSheet("QCheckBox { font-size: 10px; }"); - searchLayout->addWidget(m_fileSharingCheckBox); - - tabWidgetLayout->addWidget(searchContainer); - - // App list view - m_appsListWidget = new QListWidget(); - m_appsListWidget->setSelectionMode(QAbstractItemView::NoSelection); - m_appsListWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - m_appsListWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_appsListWidget->setFrameShape(QFrame::NoFrame); - m_appsListWidget->setSpacing(10); - m_appsListWidget->setStyleSheet( - "QListWidget { background: transparent; border: none; }" - "QListWidget::item { margin: 0px; }"); - - tabWidgetLayout->addWidget(m_appsListWidget); - - m_splitter->addWidget(tabWidget); - - connect(m_appsListWidget->verticalScrollBar(), &QScrollBar::valueChanged, - this, &InstalledAppsWidget::updateVisibleIcons); -} - -void InstalledAppsWidget::createRightPanel() -{ - QWidget *rightContentWidget = new QWidget(); - - QVBoxLayout *contentLayout = new QVBoxLayout(rightContentWidget); - contentLayout->setContentsMargins(0, 0, 0, 5); - contentLayout->setSpacing(0); - - m_containerWidget = new QWidget(); - m_containerWidget->setObjectName("containerWidget"); - m_containerWidget->setStyleSheet( - "QWidget#containerWidget { border: none; }"); - m_containerLayout = new QVBoxLayout(m_containerWidget); - m_containerLayout->setContentsMargins(0, 0, 0, 0); - m_containerLayout->setSpacing(0); - - contentLayout->addWidget(m_containerWidget); - - m_splitter->addWidget(rightContentWidget); -} - -void InstalledAppsWidget::disableTabs(bool disable) -{ - for (AppTabWidget *tab : m_appTabs) { - tab->setEnabled(!disable); - } -} - -void InstalledAppsWidget::enqueueIconLoad(const QString &bundleId) -{ - if (bundleId.isEmpty()) - return; - - if (!m_iconLoadQueue.contains(bundleId)) { - m_iconLoadQueue.enqueue(bundleId); - } - - if (!m_iconLoading) { - startNextIconLoad(); - } -} - -void InstalledAppsWidget::updateVisibleIcons() -{ - if (!m_appsListWidget) - return; - - QWidget *viewport = m_appsListWidget->viewport(); - if (!viewport) - return; - - const QRect viewportRect = viewport->rect(); - - for (auto it = m_appTabs.cbegin(); it != m_appTabs.cend(); ++it) { - AppTabWidget *tab = it.value(); - if (!tab) - continue; - - if (tab->hasIcon()) - continue; - - const QString bundleId = tab->getBundleId(); - if (m_iconLoadQueue.contains(bundleId)) - continue; - - QListWidgetItem *item = m_appItems.value(bundleId, nullptr); - if (!item) - continue; - - QRect itemRect = m_appsListWidget->visualItemRect(item); - if (viewportRect.intersects(itemRect)) { - qDebug() << "Enqueuing icon load for visible tab:" << bundleId; - enqueueIconLoad(bundleId); - } - } -} - -void InstalledAppsWidget::clearContainerLayout() -{ - if (!m_containerLayout) - return; - - QLayoutItem *item; - while ((item = m_containerLayout->takeAt(0)) != nullptr) { - if (item->widget()) { - item->widget()->deleteLater(); - } - delete item; - } -} - -void InstalledAppsWidget::startNextIconLoad() -{ - if (!m_device || QCoreApplication::closingDown()) { - m_iconLoading = false; - return; - } - - if (m_iconLoadQueue.isEmpty()) { - m_iconLoading = false; - return; - } - - m_iconLoading = true; - const QString bundleId = m_iconLoadQueue.dequeue(); - - // Use the working API - m_device->service_manager->fetch_app_icon(bundleId); -} \ No newline at end of file diff --git a/src/installedappswidget.h b/src/installedappswidget.h deleted file mode 100644 index a3162c4..0000000 --- a/src/installedappswidget.h +++ /dev/null @@ -1,180 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef INSTALLEDAPPSWIDGET_H -#define INSTALLEDAPPSWIDGET_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "idescriptor_rust_codebase/src/hause_arrest.cxxqt.h" -#include "zlineedit.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class AppTabWidget : public QWidget -{ - Q_OBJECT - -public: - AppTabWidget(const QString &appName, const QString &bundleId, - const QString &version, const QPixmap &icon = QPixmap(), - QWidget *parent = nullptr); - - void setSelected(bool selected); - bool isSelected() const { return m_selected; } - - QString getBundleId() const { return m_bundleId; } - QString getAppName() const { return m_appName; } - QString getVersion() const { return m_version; } - - void setIcon(const QPixmap &icon); - - void updateStyles(); - bool hasIcon() const { return m_hasIcon; } - -signals: - void clicked(); - -protected: - void mousePressEvent(QMouseEvent *event) override; - void changeEvent(QEvent *event) override - { - if (event->type() == QEvent::PaletteChange) { - updateStyles(); - } - QWidget::changeEvent(event); - }; - -private: - void setupUI(const QPixmap &icon); - - QString m_appName; - QString m_bundleId; - QString m_version; - bool m_selected = false; - bool m_hasIcon = false; - IDLoadingIconLabel *m_iconLabel; - QLabel *m_nameLabel; - QLabel *m_versionLabel; - QNetworkAccessManager *m_networkManager = new QNetworkAccessManager(this); -}; - -class InstalledAppsWidget : public QWidget -{ - Q_OBJECT - -public: - explicit InstalledAppsWidget( - const std::shared_ptr device, - QWidget *parent = nullptr); - void init(); - ~InstalledAppsWidget(); - -private slots: - void onAppsDataReady(const QMap &apps); - void onAppTabClicked(); - void onContainerDataReady(bool success); - void onFileSharingFilterChanged(bool enabled); - -private: - void setupUI(); - void initInternal(); - void refresh(); - void createLoadingWidget(); - void createErrorWidget(); - void createContentWidget(); - void createLeftPanel(); - void createRightPanel(); - void createAppTab(const QString &appName, const QString &bundleId, - const QString &version, const QPixmap &icon = QPixmap()); - void showLoadingState(); - void showErrorState(const QString &error); - void selectAppTab(AppTabWidget *tab); - void filterApps(const QString &searchText); - void loadAppContainer(const QString &bundleId); - void cleanupHouseArrestClients(); - void disableTabs(bool disable); - void enqueueIconLoad(const QString &bundleId); - void startNextIconLoad(); - void onAppIconLoaded(const QString &bundleId, const QByteArray &icon); - void updateVisibleIcons(); - void clearContainerLayout(); - - const std::shared_ptr m_device; - QHBoxLayout *m_mainLayout; - QStackedWidget *m_stackedWidget; - QWidget *m_loadingWidget; - QWidget *m_errorWidget; - QWidget *m_contentWidget; - QLabel *m_errorLabel; - ZLineEdit *m_searchEdit; - QCheckBox *m_fileSharingCheckBox; - QListWidget *m_appsListWidget; - QProgressBar *m_progressBar; - QScrollArea *m_containerScrollArea; - QWidget *m_containerWidget; - QVBoxLayout *m_containerLayout; - QSplitter *m_splitter; - ZLoadingWidget *m_zloadingWidget; - - std::shared_ptr m_houseArrestAfcClient = nullptr; - // App data storage - QMap m_appTabs; - QMap m_appItems; - AppTabWidget *m_selectedTab = nullptr; - - QQueue m_iconLoadQueue; - bool m_iconLoading = false; - - bool m_loadingContainer = false; - bool m_loaded = false; -}; - -#endif // INSTALLEDAPPSWIDGET_H \ No newline at end of file diff --git a/src/rust/src/io_manager.rs b/src/io_manager.rs similarity index 100% rename from src/rust/src/io_manager.rs rename to src/io_manager.rs diff --git a/src/iomanagerclient.cpp b/src/iomanagerclient.cpp deleted file mode 100644 index ae473e0..0000000 --- a/src/iomanagerclient.cpp +++ /dev/null @@ -1,286 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "iomanagerclient.h" -#include "statusballoon.h" - -IOManagerClient *IOManagerClient::sharedInstance() -{ - static IOManagerClient self; - return &self; -} -IOManagerClient::IOManagerClient(QObject *parent) : QObject(parent) {} - -void IOManagerClient::startExport( - const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &exportTitle, std::optional> onComplete) -{ - qDebug() << "startExport() entry - items:" << items.size() - << "dest:" << destinationPath; - if (!device) { - qWarning() << "Invalid device provided to ExportManager"; - QMessageBox::critical(nullptr, "Export Error", - "Invalid device specified for export."); - return; - } - - if (items.isEmpty()) { - qWarning() << "No items provided for export"; - QMessageBox::information(nullptr, "Export Error", - "No items selected for export."); - return; - } - - QDir destDir(destinationPath); - if (!destDir.exists()) { - if (!destDir.mkpath(".")) { - qWarning() << "Could not create destination directory:" - << destinationPath; - QMessageBox::critical(nullptr, "Export Error", - "Could not create destination directory."); - - return; - } - } - - QUuid jobId = QUuid::createUuid(); - - StatusBalloon::sharedInstance()->startProcess( - exportTitle, items.size(), destinationPath, ProcessType::Export, jobId, - onComplete); - - AppContext::sharedInstance()->ioManager->start_export( - device->udid, jobId, items, destinationPath); - - qDebug() << "Started export job" << jobId << "for" << items.size() - << "items"; -} - -/* hause_arrest */ -void IOManagerClient::startExport( - const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &exportTitle, const QString &bundleId, - std::optional> onComplete) -{ - qDebug() << "startExport() hause_arrest entry - items:" << items.size() - << "dest:" << destinationPath; - if (!device) { - qWarning() << "Invalid device provided to ExportManager"; - QMessageBox::critical(nullptr, "Export Error", - "Invalid device specified for export."); - return; - } - - if (items.isEmpty()) { - qWarning() << "No items provided for export"; - QMessageBox::information(nullptr, "Export Error", - "No items selected for export."); - return; - } - - QDir destDir(destinationPath); - if (!destDir.exists()) { - if (!destDir.mkpath(".")) { - qWarning() << "Could not create destination directory:" - << destinationPath; - QMessageBox::critical(nullptr, "Export Error", - "Could not create destination directory."); - - return; - } - } - - QUuid jobId = QUuid::createUuid(); - - StatusBalloon::sharedInstance()->startProcess( - exportTitle, items.size(), destinationPath, ProcessType::Export, jobId, - onComplete); - - AppContext::sharedInstance()->ioManager->start_export_with_hause_arrest_afc( - device->udid, jobId, items, destinationPath, bundleId); - - qDebug() << "Started export job with hause_arrest_afc" << jobId << "for" - << items.size() << "items"; -} - -/* afc2 */ -void IOManagerClient::startExport( - const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &exportTitle, bool useAfc2, - std::optional> onComplete) -{ - qDebug() << "startExport() afc2 - items:" << items.size() - << "dest:" << destinationPath; - if (!device) { - qWarning() << "Invalid device provided to ExportManager"; - QMessageBox::critical(nullptr, "Export Error", - "Invalid device specified for export."); - return; - } - - if (items.isEmpty()) { - qWarning() << "No items provided for export"; - QMessageBox::information(nullptr, "Export Error", - "No items selected for export."); - return; - } - - QDir destDir(destinationPath); - if (!destDir.exists()) { - if (!destDir.mkpath(".")) { - qWarning() << "Could not create destination directory:" - << destinationPath; - QMessageBox::critical(nullptr, "Export Error", - "Could not create destination directory."); - - return; - } - } - - QUuid jobId = QUuid::createUuid(); - - StatusBalloon::sharedInstance()->startProcess( - exportTitle, items.size(), destinationPath, ProcessType::Export, jobId, - onComplete); - - AppContext::sharedInstance()->ioManager->start_export_with_afc2( - device->udid, jobId, items, destinationPath); - - qDebug() << "Started export job with afc2" << jobId << "for" << items.size() - << "items"; -} - -void IOManagerClient::startImport( - const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &importTitle, std::optional> onComplete) -{ - qDebug() << "startImport() entry - items:" << items.size() - << "dest:" << destinationPath; - if (!device) { - qWarning() << "Invalid device provided to ExportManager"; - QMessageBox::critical(nullptr, "Import Error", - "Invalid device specified for import."); - return; - } - - if (items.isEmpty()) { - qWarning() << "No items provided for export"; - QMessageBox::information(nullptr, "Import Error", - "No items selected for import."); - return; - } - - QUuid jobId = QUuid::createUuid(); - - StatusBalloon::sharedInstance()->startProcess( - importTitle, items.size(), destinationPath, ProcessType::Import, jobId, - onComplete); - - AppContext::sharedInstance()->ioManager->start_import( - device->udid, jobId, items, destinationPath); - - qDebug() << "Started import job" << jobId << "for" << items.size() - << "items"; -} - -/* hause_arrest */ -void IOManagerClient::startImport( - const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &importTitle, const QString &bundleId, - std::optional> onComplete) -{ - qDebug() << "startImport() entry - items:" << items.size() - << "dest:" << destinationPath; - if (!device) { - qWarning() << "Invalid device provided to ExportManager"; - QMessageBox::critical(nullptr, "Import Error", - "Invalid device specified for import."); - return; - } - - if (items.isEmpty()) { - qWarning() << "No items provided for export"; - QMessageBox::information(nullptr, "Import Error", - "No items selected for import."); - return; - } - - QUuid jobId = QUuid::createUuid(); - - StatusBalloon::sharedInstance()->startProcess( - importTitle, items.size(), destinationPath, ProcessType::Import, jobId, - onComplete); - - AppContext::sharedInstance()->ioManager->start_import_with_hause_arrest_afc( - device->udid, jobId, items, destinationPath, bundleId); - - qDebug() << "Started import job" << jobId << "for" << items.size() - << "items"; -} - -/* afc2 */ -void IOManagerClient::startImport( - const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &importTitle, bool useAfc2, - std::optional> onComplete) -{ - qDebug() << "startImport() entry - items:" << items.size() - << "dest:" << destinationPath; - if (!device) { - qWarning() << "Invalid device provided to ExportManager"; - QMessageBox::critical(nullptr, "Import Error", - "Invalid device specified for import."); - return; - } - - if (items.isEmpty()) { - qWarning() << "No items provided for export"; - QMessageBox::information(nullptr, "Import Error", - "No items selected for import."); - return; - } - - QUuid jobId = QUuid::createUuid(); - - StatusBalloon::sharedInstance()->startProcess( - importTitle, items.size(), destinationPath, ProcessType::Import, jobId, - onComplete); - - AppContext::sharedInstance()->ioManager->start_import_with_afc2( - device->udid, jobId, items, destinationPath); - - qDebug() << "Started import job" << jobId << "for" << items.size() - << "items"; -} - -void IOManagerClient::cancel(const QUuid &jobId) -{ - AppContext::sharedInstance()->ioManager->cancel_job(jobId); -} - -void IOManagerClient::cancelAllJobs() -{ - AppContext::sharedInstance()->ioManager->cancel_all_jobs(); -} \ No newline at end of file diff --git a/src/iomanagerclient.h b/src/iomanagerclient.h deleted file mode 100644 index 7f075a9..0000000 --- a/src/iomanagerclient.h +++ /dev/null @@ -1,88 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef IOMANAGERCLIENT_H -#define IOMANAGERCLIENT_H - -#include "appcontext.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include - -class IOManagerClient : public QObject -{ - Q_OBJECT - -public: - static IOManagerClient *sharedInstance(); - - IOManagerClient(const IOManagerClient &) = delete; - IOManagerClient &operator=(const IOManagerClient &) = delete; - - void - startExport(const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &jobTitle, - std::optional> onComplete = std::nullopt); - /* afc2 */ - void - startExport(const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &jobTitle, bool useAfc2, - std::optional> onComplete = std::nullopt); - /* hause_arrest_afc*/ - void - startExport(const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &exportTitle, const QString &bundleId, - std::optional> onComplete = std::nullopt); - - void - startImport(const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &jobTitle, - std::optional> onComplete = std::nullopt); - /* hause_arrest_afc */ - void - startImport(const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &jobTitle, const QString &bundleId, - std::optional> onComplete = std::nullopt); - /* afc2 */ - void - startImport(const std::shared_ptr device, - const QList &items, const QString &destinationPath, - const QString &jobTitle, bool useAfc2, - std::optional> onComplete = std::nullopt); - - void cancel(const QUuid &jobId); - void cancelAllJobs(); - static QString generateUniqueOutputPath(const QString &basePath); - -private: - explicit IOManagerClient(QObject *parent = nullptr); -}; - -#endif // IOMANAGERCLIENT_H diff --git a/src/jailbrokenwidget.cpp b/src/jailbrokenwidget.cpp deleted file mode 100644 index 78fb0ed..0000000 --- a/src/jailbrokenwidget.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "jailbrokenwidget.h" - -JailbrokenWidget::JailbrokenWidget(QWidget *parent) : QWidget{parent} -{ - QGridLayout *mainLayout = new QGridLayout(this); - mainLayout->setContentsMargins(10, 10, 10, 10); - mainLayout->setSpacing(10); - - // Define all the tools you want to display - QList tools; - tools.append({"SSH Terminal", "Connect to your device via SSH", - ":/resources/icons/TablerDatabaseExport.png"}); - tools.append({"More Tools Coming", "New features will be added soon", - ":/resources/icons/TablerDatabaseExport.png", - false}); // Disabled placeholder - - const int maxColumns = 3; - for (int i = 0; i < tools.size(); ++i) { - const auto &toolInfo = tools[i]; - ClickableWidget *toolWidget = createJailbreakTool(toolInfo); - - int row = i / maxColumns; - int col = i % maxColumns; - mainLayout->addWidget(toolWidget, row, col); - } - - // Add a stretch to the last row and column to push everything to the - // top-left - mainLayout->setRowStretch(mainLayout->rowCount(), 1); - mainLayout->setColumnStretch(mainLayout->columnCount(), 1); -} - -ClickableWidget * -JailbrokenWidget::createJailbreakTool(const JailbreakToolInfo &info) -{ - ClickableWidget *b = new ClickableWidget(); - b->setCursor(Qt::PointingHandCursor); - b->setEnabled(info.enabled); - - // Use a theme-aware stylesheet for the background and hover effect - b->setStyleSheet("ClickableWidget {" - " border-radius: 8px;" - " padding: 10px;" - "}"); - - QVBoxLayout *layout = new QVBoxLayout(b); - - // Icon (using the theme-aware ZIcon pattern) - // ZIconLabel *iconLabel = new ZIconLabel(); - ZIconLabel *iconLabel = new ZIconLabel(QIcon(), nullptr, 1.5, this); - - // iconLabel->setAlignment(Qt::AlignCenter); - // ZIcon toolIcon(QIcon(info.iconPath)); - - // auto updateIcon = [b, iconLabel, toolIcon]() { - // iconLabel->setPixmap( - // toolIcon.getThemedPixmap(QSize(45, 45), b->palette())); - // }; - // updateIcon(); - // connect(qApp, &QApplication::paletteChanged, b, updateIcon); - - // Title - QLabel *titleLabel = new QLabel(info.title); - titleLabel->setAlignment(Qt::AlignCenter); - QFont titleFont = titleLabel->font(); - titleFont.setBold(true); - titleLabel->setFont(titleFont); - - // Description (using a theme-aware palette color) - QLabel *descLabel = new QLabel(info.description); - descLabel->setWordWrap(true); - descLabel->setAlignment(Qt::AlignCenter); - descLabel->setStyleSheet("font-size: 12px;"); - - layout->addWidget(iconLabel, 0, Qt::AlignCenter); - layout->addWidget(titleLabel); - layout->addWidget(descLabel); - - // TODO: Connect the clicked signal to a slot - if (info.title == "SSH Terminal") { - iconLabel->setIcon(QIcon(":/resources/icons/BxBxsTerminal.png")); - - connect(b, &ClickableWidget::clicked, this, [this]() { - if (m_sshTerminalWidget) { - m_sshTerminalWidget->raise(); - m_sshTerminalWidget->activateWindow(); - return; - } - m_sshTerminalWidget = new SSHTerminalTool(); - m_sshTerminalWidget->setAttribute(Qt::WA_DeleteOnClose); - m_sshTerminalWidget->show(); - m_sshTerminalWidget->raise(); - m_sshTerminalWidget->activateWindow(); - connect(m_sshTerminalWidget, &QObject::destroyed, this, - [this]() { m_sshTerminalWidget = nullptr; }); - }); - } else if (info.title == "More Tools Coming") { - iconLabel->setIcon( - QIcon(":/resources/icons/IconParkTwotoneMoreTwo.png")); - } - iconLabel->setIconSizeMultiplier(2); - return b; -} - -JailbrokenWidget::~JailbrokenWidget() {} \ No newline at end of file diff --git a/src/jailbrokenwidget.h b/src/jailbrokenwidget.h deleted file mode 100644 index 22dd4bc..0000000 --- a/src/jailbrokenwidget.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef JAILBROKENWIDGET_H -#define JAILBROKENWIDGET_H - -#include "appcontext.h" -#include "responsiveqlabel.h" -#include "sshterminaltool.h" - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class ClickableWidget; - -struct JailbreakToolInfo { - QString title; - QString description; - QString iconPath; - bool enabled = true; -}; - -class JailbrokenWidget : public QWidget -{ - Q_OBJECT - -public: - JailbrokenWidget(QWidget *parent = nullptr); - ~JailbrokenWidget(); - -private slots: -private: - ClickableWidget *createJailbreakTool(const JailbreakToolInfo &info); - SSHTerminalTool *m_sshTerminalWidget = nullptr; -}; - -#endif // JAILBROKENWIDGET_H \ No newline at end of file diff --git a/src/keychaindialog.cpp b/src/keychaindialog.cpp deleted file mode 100644 index 9a58cd6..0000000 --- a/src/keychaindialog.cpp +++ /dev/null @@ -1,157 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "keychaindialog.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include - -KeychainDialog::KeychainDialog(QWidget *parent) - : QDialog(parent), m_player(nullptr), m_videoWidget(nullptr), - m_mainLayout(nullptr), m_okButton(nullptr), m_titleLabel(nullptr), - m_descriptionLabel(nullptr), m_dontShowAgainCheckbox(nullptr) -{ - setupUI(); - setupVideo(); -} - -KeychainDialog::~KeychainDialog() -{ - if (m_player) { - m_player->stop(); - } -} - -void KeychainDialog::setupUI() -{ - setWindowTitle("Keychain Access Required"); - setModal(true); - setMinimumSize(600, 450); - - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(20, 20, 20, 20); - m_mainLayout->setSpacing(15); - - // Title label - m_titleLabel = new QLabel("Keychain Access Required"); - m_titleLabel->setObjectName("titleLabel"); - m_titleLabel->setAlignment(Qt::AlignCenter); - m_titleLabel->setStyleSheet( - "QLabel#titleLabel { " - " font-size: 18px; font-weight: bold; margin-bottom: 10px;" - "}"); - m_mainLayout->addWidget(m_titleLabel); - - // Description label - m_descriptionLabel = new QLabel( - "In order to sign in to App Store we use the keychain backend to " - "safely store and retrieve your credentials. Please click on \"Always " - "Allow\" when prompted. " - "This is a security feature to protect your Apple ID credentials. You " - "can disable this in Settings."); - m_descriptionLabel->setAlignment(Qt::AlignCenter); - m_descriptionLabel->setWordWrap(true); - m_mainLayout->addWidget(m_descriptionLabel); - - // Video widget - m_videoWidget = new QVideoWidget(); - m_videoWidget->setObjectName("videoWidget"); - m_videoWidget->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - m_videoWidget->setAspectRatioMode( - Qt::AspectRatioMode::KeepAspectRatioByExpanding); - m_videoWidget->setStyleSheet( - "QVideoWidget#videoWidget { background-color: transparent; }"); - m_videoWidget->setMinimumHeight(250); - m_mainLayout->addWidget(m_videoWidget, 1); - - m_dontShowAgainCheckbox = new QCheckBox("Don't show this again"); - m_mainLayout->addWidget(m_dontShowAgainCheckbox, 0, Qt::AlignCenter); - - QHBoxLayout *buttonsLayout = new QHBoxLayout(); - m_skipSigningInButton = new QPushButton("Skip For Now"); - m_skipSigningInButton->setFixedHeight(40); - - m_okButton = new QPushButton("OK, I understand"); - m_okButton->setDefault(true); - m_okButton->setFixedHeight(40); - - buttonsLayout->addWidget(m_skipSigningInButton); - buttonsLayout->addWidget(m_okButton); - - m_mainLayout->addLayout(buttonsLayout, Qt::AlignCenter); - - connect(m_okButton, &QPushButton::clicked, this, - &KeychainDialog::onOkClicked); - connect(m_skipSigningInButton, &QPushButton::clicked, this, - &KeychainDialog::onSkipSigningInClicked); -} - -void KeychainDialog::setupVideo() -{ - m_player = new QMediaPlayer(this); - m_player->setVideoOutput(m_videoWidget); - m_player->setSource(QUrl("qrc:/resources/keychain.mp4")); - - // Loop the video - connect(m_player, &QMediaPlayer::mediaStatusChanged, this, - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::EndOfMedia) { - m_player->setPosition(0); - m_player->play(); - } - }); - - // Auto-play when ready - connect(m_player, &QMediaPlayer::mediaStatusChanged, this, - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::LoadedMedia) { - m_player->play(); - } - }); -} - -void KeychainDialog::onOkClicked() -{ - if (m_dontShowAgainCheckbox && m_dontShowAgainCheckbox->isChecked()) { - SettingsManager::sharedInstance()->setShowKeychainDialog(false); - } - - if (m_player) { - m_player->stop(); - } - accept(); -} - -void KeychainDialog::onSkipSigningInClicked() -{ - if (m_dontShowAgainCheckbox && m_dontShowAgainCheckbox->isChecked()) { - SettingsManager::sharedInstance()->setShowKeychainDialog(false); - } - - if (m_player) { - m_player->stop(); - } - reject(); -} \ No newline at end of file diff --git a/src/keychaindialog.h b/src/keychaindialog.h deleted file mode 100644 index 8ab7299..0000000 --- a/src/keychaindialog.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef KEYCHAIN_DIALOG_H -#define KEYCHAIN_DIALOG_H - -#include -#include -#include -#include -#include -#include -#include - -class KeychainDialog : public QDialog -{ - Q_OBJECT - -public: - explicit KeychainDialog(QWidget *parent = nullptr); - ~KeychainDialog(); - -private slots: - void onOkClicked(); - void onSkipSigningInClicked(); - -private: - void setupUI(); - void setupVideo(); - - QMediaPlayer *m_player; - QVideoWidget *m_videoWidget; - QVBoxLayout *m_mainLayout; - QPushButton *m_okButton; - QPushButton *m_skipSigningInButton; - QLabel *m_titleLabel; - QLabel *m_descriptionLabel; - QCheckBox *m_dontShowAgainCheckbox; -}; - -#endif // KEYCHAIN_DIALOG_H \ No newline at end of file diff --git a/src/live_reload.cpp b/src/live_reload.cpp new file mode 100644 index 0000000..2ba05dd --- /dev/null +++ b/src/live_reload.cpp @@ -0,0 +1,80 @@ +// Taken from +// https://github.com/gyroflow/gyroflow/blob/master/src/ui_live_reload.cpp +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2021-2022 Adrian + +#include +#include +#include +#include +#include +#include +#include +#include + +void init_live_reload(QQmlApplicationEngine *engine, const QString &path) +{ + QFileSystemWatcher *w = new QFileSystemWatcher(); + QDirIterator it(path, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + auto i = it.fileInfo(); + if (i.isFile()) + w->addPath(i.absoluteFilePath()); + } + + QUrl mainPath = QUrl::fromLocalFile(path + "/Main.qml"); + + qDebug() << mainPath; + + QObject::connect( + w, &QFileSystemWatcher::fileChanged, [=](const QString &file) { + QTimer::singleShot(50, [=] { + static QQuickItem *previousItem = nullptr; + auto wnd = + qobject_cast(engine->rootObjects().first()); + w->addPath(file); + + // auto children = wnd->contentItem()->childItems(); + // if (!children.isEmpty()) { + // auto itm = children.first(); + // qDebug() << itm->objectName(); + // if (itm->objectName() == "Main" || + // itm->objectName() == "AppLoader") { + // itm->setParentItem(nullptr); + // if (itm == previousItem) + // previousItem = nullptr; + // delete itm; + // } + // } + + for (auto *itm : wnd->contentItem()->childItems()) { + if (itm->objectName() == "Main" || + itm->objectName() == "AppLoader") { + + itm->setParentItem(nullptr); + if (itm == previousItem) + previousItem = nullptr; + + delete itm; + } + } + + if (previousItem) { + auto toDelete = previousItem; + QTimer::singleShot(5000, [=] { + toDelete->setParentItem(nullptr); + delete toDelete; + }); + } + engine->clearComponentCache(); + + QQmlComponent component(engine, mainPath, wnd); + previousItem = qobject_cast(component.create()); + if (previousItem) { + previousItem->setObjectName("Main"); + previousItem->setParentItem(wnd->contentItem()); + } + }); + }); +} \ No newline at end of file diff --git a/src/livescreenwidget.cpp b/src/livescreenwidget.cpp deleted file mode 100644 index c80515b..0000000 --- a/src/livescreenwidget.cpp +++ /dev/null @@ -1,180 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "livescreenwidget.h" - -LiveScreenWidget::LiveScreenWidget( - const std::shared_ptr device, QWidget *parent) - : Tool{parent, false}, m_device(device) -{ - setWindowTitle("Live Screen - iDescriptor"); - setAttribute(Qt::WA_DeleteOnClose); - - connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, - [this, device](const QString &removed_uuid) { - if (device->udid == removed_uuid) { - this->close(); - this->deleteLater(); - } - }); - - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - - m_loadingWidget = new ZLoadingWidget(true, this); - mainLayout->addWidget(m_loadingWidget); - - // Setup UI - QVBoxLayout *contentLayout = new QVBoxLayout(this); - contentLayout->setContentsMargins(10, 10, 10, 10); - contentLayout->setSpacing(10); - m_loadingWidget->setupContentWidget(contentLayout); - - // Status label - m_statusLabel = new QLabel("Connecting to screenshot service..."); - m_statusLabel->setAlignment(Qt::AlignCenter); - contentLayout->addWidget(m_statusLabel); - - // Screenshot display - m_imageLabel = new QLabel(); - m_imageLabel->setMinimumSize(300, 600); - m_imageLabel->setAlignment(Qt::AlignCenter); - contentLayout->addWidget(m_imageLabel, 1); - - // Controls (rotate / mirror), initially hidden, shown when capturing starts - m_controlsWidget = new QWidget(this); - auto *controlsLayout = new QHBoxLayout(m_controlsWidget); - controlsLayout->setContentsMargins(0, 0, 0, 0); - controlsLayout->setSpacing(5); - - m_rotateCwButton = new QPushButton("Rotate ↻", m_controlsWidget); - m_rotateCcwButton = new QPushButton("Rotate ↺", m_controlsWidget); - m_mirrorButton = new QPushButton("Mirror", m_controlsWidget); - - controlsLayout->addWidget(m_rotateCwButton); - controlsLayout->addWidget(m_rotateCcwButton); - controlsLayout->addWidget(m_mirrorButton); - controlsLayout->addStretch(1); - - m_controlsWidget->setVisible(false); - contentLayout->addWidget(m_controlsWidget); - - // button actions - connect(m_rotateCwButton, &QPushButton::clicked, this, [this]() { - m_rotationDegrees = (m_rotationDegrees + 90) % 360; - applyTransformAndDisplay(); - }); - connect(m_rotateCcwButton, &QPushButton::clicked, this, [this]() { - m_rotationDegrees = (m_rotationDegrees + 270) % 360; // -90 mod 360 - applyTransformAndDisplay(); - }); - connect(m_mirrorButton, &QPushButton::clicked, this, [this]() { - m_mirrorHorizontal = !m_mirrorHorizontal; - applyTransformAndDisplay(); - }); - - m_client = - new CXX::ScreenshotBackend(m_device->udid, m_device->ios_version); - - connect(m_client, &CXX::ScreenshotBackend::screenshot_captured, this, - [this](const QByteArray bytes) { - m_lastPixmap = QPixmap::fromImage(QImage::fromData(bytes)); - applyTransformAndDisplay(); - }); - - connect(m_loadingWidget, &ZLoadingWidget::retryClicked, this, [this]() { - m_loadingWidget->showLoading(); - QTimer::singleShot(200, this, &LiveScreenWidget::startInitialization); - }); - connect(m_client, &CXX::ScreenshotBackend::init_failed, this, - &LiveScreenWidget::handleFailedInitialization); - QTimer::singleShot(200, this, &LiveScreenWidget::startInitialization); -} - -void LiveScreenWidget::startInitialization() -{ - m_loadingWidget->stop(); - m_client->start_capture(); - - m_statusLabel->setText("Capturing"); - m_controlsWidget->setVisible(true); -} - -void LiveScreenWidget::handleFailedInitialization() -{ - m_loadingWidget->showLoading(); - bool dvt = m_device->ios_version >= 17; - if (!dvt) { - if (m_tries >= 2) { - m_loadingWidget->showError( - "Failed to initialize screenshot capture"); - return; - } - ++m_tries; - auto *helper = new DevDiskImageHelper(m_device, this); - - connect(helper, &DevDiskImageHelper::mountingCompleted, this, - [this, helper](bool success) { - if (success) { - QTimer::singleShot( - 400, this, &LiveScreenWidget::startInitialization); - } else { - m_statusLabel->setText( - "Failed to mount developer disk image"); - } - }); - - helper->start(); - - } else { - DevModeWidget(m_device, this).exec(); - m_loadingWidget->showError( - "Failed to initialize screenshot capture. Please ensure the device " - "has developer mode enabled."); - } -} - -LiveScreenWidget::~LiveScreenWidget() { delete m_client; } - -void LiveScreenWidget::applyTransformAndDisplay() -{ - if (m_lastPixmap.isNull() || !m_imageLabel) - return; - - QTransform transform; - transform.rotate(m_rotationDegrees); - - QPixmap transformed = - m_lastPixmap.transformed(transform, Qt::SmoothTransformation); - - if (m_mirrorHorizontal) { - QTransform mirrorTransform; - mirrorTransform.scale(-1, 1); - transformed = - transformed.transformed(mirrorTransform, Qt::SmoothTransformation); - } - - const QSize targetSize = m_imageLabel->size(); - if (!targetSize.isEmpty()) { - transformed = transformed.scaled(targetSize, Qt::KeepAspectRatio, - Qt::SmoothTransformation); - } - - m_imageLabel->setPixmap(transformed); -} \ No newline at end of file diff --git a/src/livescreenwidget.h b/src/livescreenwidget.h deleted file mode 100644 index 0123f47..0000000 --- a/src/livescreenwidget.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef LIVESCREEN_H -#define LIVESCREEN_H - -#include "appcontext.h" -#include "devdiskimagehelper.h" -#include "devdiskmanager.h" -#include "devmodewidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class LiveScreenWidget : public Tool -{ - Q_OBJECT -public: - explicit LiveScreenWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - ~LiveScreenWidget(); - -private: - void updateScreenshot(); - void startCapturing(); - void applyTransformAndDisplay(); - void handleFailedInitialization(); - - std::shared_ptr m_device; - QLabel *m_imageLabel; - QLabel *m_statusLabel; - CXX::ScreenshotBackend *m_client; - ZLoadingWidget *m_loadingWidget; - - // controls for rotation / mirroring - QWidget *m_controlsWidget = nullptr; - QPushButton *m_rotateCwButton = nullptr; - QPushButton *m_rotateCcwButton = nullptr; - QPushButton *m_mirrorButton = nullptr; - - // transformation state - QPixmap m_lastPixmap; - int m_rotationDegrees = 0; // 0, 90, 180, 270 - bool m_mirrorHorizontal = false; - int m_tries = 0; - -private: - void startInitialization(); -}; - -#endif // LIVESCREEN_H diff --git a/src/loadingspinnerwidget.cpp b/src/loadingspinnerwidget.cpp deleted file mode 100644 index b6c1df2..0000000 --- a/src/loadingspinnerwidget.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "loadingspinnerwidget.h" -#include - -LoadingSpinnerWidget::LoadingSpinnerWidget(QWidget *parent) - : QWidget(parent), m_angle(0), m_color(Qt::gray) -{ - connect(&m_timer, &QTimer::timeout, this, - &LoadingSpinnerWidget::updateRotation); - m_timer.setInterval(15); // Update every 15ms for smooth animation - m_timer.start(); - setFixedSize(24, 24); // Default size -} - -void LoadingSpinnerWidget::setColor(const QColor &color) -{ - m_color = color; - update(); // Trigger a repaint with the new color -} - -void LoadingSpinnerWidget::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - int penWidth = 2; - QRectF rect(penWidth / 2.0, penWidth / 2.0, width() - penWidth, - height() - penWidth); - - QPen pen(m_color); - pen.setWidth(penWidth); - pen.setCapStyle(Qt::RoundCap); - painter.setPen(pen); - - // Draw a 270-degree arc, rotating the start angle - painter.drawArc(rect, m_angle * 16, 270 * 16); -} - -void LoadingSpinnerWidget::updateRotation() -{ - m_angle = (m_angle + 10) % 360; - update(); // Schedule a repaint -} \ No newline at end of file diff --git a/src/loadingspinnerwidget.h b/src/loadingspinnerwidget.h deleted file mode 100644 index 5697ee4..0000000 --- a/src/loadingspinnerwidget.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef LOADINGSPINNERWIDGET_H -#define LOADINGSPINNERWIDGET_H - -#include -#include -#include - -class LoadingSpinnerWidget : public QWidget -{ - Q_OBJECT - -public: - explicit LoadingSpinnerWidget(QWidget *parent = nullptr); - void setColor(const QColor &color); - -protected: - void paintEvent(QPaintEvent *event) override; - -private slots: - void updateRotation(); - -private: - QTimer m_timer; - int m_angle; - QColor m_color; -}; - -#endif // LOADINGSPINNERWIDGET_H \ No newline at end of file diff --git a/src/logindialog.cpp b/src/logindialog.cpp deleted file mode 100644 index f886dad..0000000 --- a/src/logindialog.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "logindialog.h" -#include "appstoremanager.h" -#include "qprocessindicator.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include "platform/windows/win_common.h" -#endif - -LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent) -{ - setWindowTitle("Login to App Store - iDescriptor"); - setModal(true); - setFixedWidth(400); - -#ifdef WIN32 - setupWinWindow(this); -#endif - - QVBoxLayout *layout = new QVBoxLayout(this); - layout->setSpacing(15); - layout->setContentsMargins(20, 20, 20, 20); - - // Email - QLabel *emailLabel = new QLabel("Email:"); - emailLabel->setStyleSheet("font-size: 14px"); - layout->addWidget(emailLabel); - - m_emailEdit = new QLineEdit(); - m_emailEdit->setPlaceholderText("Enter your email"); -#ifndef WIN32 - m_emailEdit->setStyleSheet("padding: 8px; border: 1px solid #ddd; " - "border-radius: 4px; font-size: 14px;"); -#endif - - layout->addWidget(m_emailEdit); - - // Password - QLabel *passwordLabel = new QLabel("Password:"); - passwordLabel->setStyleSheet("font-size: 14px"); - layout->addWidget(passwordLabel); - - m_passwordEdit = new QLineEdit(); - m_passwordEdit->setPlaceholderText("Enter your password"); - m_passwordEdit->setEchoMode(QLineEdit::Password); -#ifndef WIN32 - m_passwordEdit->setStyleSheet("padding: 8px; border: 1px solid #ddd; " - "border-radius: 4px; font-size: 14px;"); -#endif - layout->addWidget(m_passwordEdit); - - // Description - QLabel *descriptionLabel = new QLabel( - "You shouldn't be using your main account here and don't worry, " - "your credentials won't be " - "stored or shared anywhere. This App is open-source."); - descriptionLabel->setStyleSheet("font-size: 10px; font-weight: thin;"); - descriptionLabel->setAlignment(Qt::AlignLeft); - descriptionLabel->setWordWrap(true); - layout->addWidget(descriptionLabel); - - QWidget *signInContainer = new QWidget(this); - m_signInStackedLayout = new QStackedLayout(signInContainer); - m_signInStackedLayout->setContentsMargins(0, 0, 0, 0); - - // Create the actual "Sign In" button - m_signInButton = new QPushButton("Sign In"); -#ifndef WIN32 - m_signInButton->setStyleSheet( - "QPushButton { padding: 8px 16px; font-size: 14px; border-radius: 4px; " - "background-color: #007AFF; color: white; border: none; min-width: " - "80px; }" - "QPushButton:hover { background-color: #0056CC; }"); -#endif - // Create the indicator - QWidget *indicatorWidget = new QWidget(); - QVBoxLayout *indicatorLayout = new QVBoxLayout(indicatorWidget); - indicatorLayout->setContentsMargins(0, 0, 0, 0); - indicatorLayout->setAlignment(Qt::AlignCenter); - m_indicator = new QProcessIndicator(this); - m_indicator->setType(QProcessIndicator::line_rotate); - m_indicator->setFixedSize(48, 24); - indicatorLayout->addWidget(m_indicator); - - // Add button and indicator to the stacked layout - m_signInStackedLayout->addWidget(m_signInButton); - m_signInStackedLayout->addWidget(indicatorWidget); - - // Ensure the container has the same size as the button - signInContainer->setFixedSize(m_signInButton->sizeHint()); - - m_cancelButton = new QPushButton("Cancel"); -#ifndef WIN32 - m_cancelButton->setStyleSheet( - "QPushButton { padding: 8px 16px; font-size: 14px; border-radius: 4px; " - "background-color: #f0f0f0; color: #333; border: 1px solid #ddd; " - "min-width: 80px; }" - "QPushButton:disabled { background-color: #eee; color: #aaa; border: " - "1px solid #ddd; }"); -#endif - - // Layout for the buttons - QHBoxLayout *buttonLayout = new QHBoxLayout(); - buttonLayout->addStretch(); - buttonLayout->addWidget(m_cancelButton); - buttonLayout->addWidget(signInContainer); - - connect(m_signInButton, &QPushButton::clicked, this, &LoginDialog::signIn); - connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); - - layout->addLayout(buttonLayout); -} - -QString LoginDialog::getEmail() const { return m_emailEdit->text(); } -QString LoginDialog::getPassword() const { return m_passwordEdit->text(); } - -void LoginDialog::signIn() -{ - QString email = getEmail(); - QString password = getPassword(); - if (email.isEmpty() || password.isEmpty()) { - QMessageBox::warning(this, "Login Failed", - "Email and password cannot be empty."); - return; - } - - AppStoreManager *manager = AppStoreManager::sharedInstance(); - if (!manager) { - QMessageBox::critical(this, "Error", - "Failed to initialize App Store manager."); - return; - } - - // Show indicator and disable cancel button - m_signInStackedLayout->setCurrentIndex(1); - m_indicator->start(); - m_cancelButton->setEnabled(false); - - manager->loginWithCallback( - email, password, [this](bool success, const QJsonObject &accountInfo) { - // Hide indicator and re-enable buttons - m_indicator->stop(); - m_signInStackedLayout->setCurrentIndex(0); - m_cancelButton->setEnabled(true); - - if (success) { - qDebug() << "Login successful"; - accept(); - } else { - QMessageBox::warning(this, "Login Failed", - "Login failed. Please check your " - "credentials and 2FA code."); - } - }); -} \ No newline at end of file diff --git a/src/logindialog.h b/src/logindialog.h deleted file mode 100644 index fcf014d..0000000 --- a/src/logindialog.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef LOGINDIALOG_H -#define LOGINDIALOG_H - -#include "qprocessindicator.h" -#include -#include -#include -#include - -class LoginDialog : public QDialog -{ - Q_OBJECT - -public: - LoginDialog(QWidget *parent = nullptr); - - QString getEmail() const; - QString getPassword() const; - -private slots: - void signIn(); - -private: - QLineEdit *m_emailEdit; - QLineEdit *m_passwordEdit; - QPushButton *m_signInButton; - QPushButton *m_cancelButton; - QProcessIndicator *m_indicator; - QStackedLayout *m_signInStackedLayout; -}; - -#endif // LOGINDIALOG_H diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 9ad8567..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,147 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "constants.h" -#include "iDescriptor.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef WIN32 -#include "platform/windows/win_common.h" -#endif -#include "networkdeviceprovider.h" -// #include "thumbnailmodel.h" -#include "thumbnailprovider.h" -#include -#include -#include -#include -#include -#define FLUENTUI_BUILD_STATIC_LIB 1 - -int main(int argc, char *argv[]) -{ -#ifdef WIN32 - // ::SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); - qputenv("QT_QPA_PLATFORM", "windows:darkmode=2"); -#endif -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - qputenv("QT_QUICK_CONTROLS_STYLE", "Basic"); -#else - qputenv("QT_QUICK_CONTROLS_STYLE", "Default"); -#endif -#ifdef Q_OS_LINUX - // fix bug UOSv20 v-sync does not work - qputenv("QSG_RENDER_LOOP", "basic"); -#endif - -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); -#endif -#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) - QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - QApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif -#endif - - QApplication a(argc, argv); - QCoreApplication::setOrganizationName("iDescriptor"); - QCoreApplication::setApplicationName("iDescriptor"); - QCoreApplication::setApplicationVersion(APP_VERSION); - if (a.arguments().contains("--reset-settings")) { - // SettingsManager::sharedInstance()->clear(); - QMessageBox::information(nullptr, "Settings Reset", - "All application settings have been reset to " - "their default values."); - } - QQmlApplicationEngine engine; - -#ifdef WIN32 - - QString appPath = QCoreApplication::applicationDirPath(); - QString gstPluginPath = - QDir::toNativeSeparators(appPath + "/gstreamer-1.0"); - QString gstPluginScannerPath = QDir::toNativeSeparators( - appPath + "/gstreamer-1.0/libexec/gst-plugin-scanner.exe"); - - const char *oldPath = getenv("PATH"); - QString newPath = appPath + ";" + QString(oldPath); - qputenv("PATH", newPath.toUtf8()); - - qputenv("GST_PLUGIN_PATH", gstPluginPath.toUtf8()); - qDebug() << "GST_PLUGIN_PATH=" << gstPluginPath; - qputenv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", "no"); - qDebug() << "GST_REGISTRY_REUSE_PLUGIN_SCANNER=no"; - qputenv("GST_PLUGIN_SYSTEM_PATH", gstPluginPath.toUtf8()); - qDebug() << "GST_PLUGIN_SYSTEM_PATH=" << gstPluginPath; - qputenv("GST_DEBUG", "GST_PLUGIN_LOADING:5"); - qDebug() << "GST_DEBUG=GST_PLUGIN_LOADING:5"; - qputenv("GST_PLUGIN_SCANNER_1_0", gstPluginScannerPath.toUtf8()); - qDebug() << "GST_PLUGIN_SCANNER_1_0=" << gstPluginScannerPath; -#endif -#ifdef __APPLE__ - QString appPath = QCoreApplication::applicationDirPath(); - QString frameworksPath = - QDir::toNativeSeparators(appPath + "/../Frameworks"); - QString gstPluginPath = - QDir::toNativeSeparators(frameworksPath + "/gstreamer"); - QString gstPluginScannerPath = - QDir::toNativeSeparators(frameworksPath + "/gst-plugin-scanner"); - - setenv("GST_PLUGIN_PATH", gstPluginPath.toUtf8().constData(), 1); - setenv("GST_PLUGIN_SYSTEM_PATH", gstPluginPath.toUtf8().constData(), 1); - setenv("GST_PLUGIN_SCANNER", gstPluginScannerPath.toUtf8().constData(), 1); -#endif - - const QUrl url(QStringLiteral("qrc:/src/qml/Main.qml")); - QObject::connect( - &engine, &QQmlApplicationEngine::objectCreated, &a, - [url](QObject *obj, const QUrl &objUrl) { - if (!obj && url == objUrl) { - QCoreApplication::exit(-1); - } - }, - Qt::QueuedConnection); - -// FIXME: for some reason we have to set this -// dont let this end up in final build -#ifdef WIN32 - engine.addImportPath("C:/Qt/6.8.3/mingw_64/qml"); -#endif - Constants constants; - // qmlRegisterType("iDescriptor", 1, 0, "ThumbnailModel"); - engine.rootContext()->setContextProperty("CONSTANTS", &constants); - engine.addImageProvider("thumb", ThumbnailProvider::sharedInstance()); - engine.rootContext()->setContextProperty( - "ThumbnailProvider", ThumbnailProvider::sharedInstance()); - engine.rootContext()->setContextProperty( - "NetworkDeviceProvider", NetworkDeviceProvider::sharedInstance()); - engine.load(url); - - return a.exec(); -} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..30392af --- /dev/null +++ b/src/main.rs @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2021-2022 Adrian + +#![recursion_limit = "4096"] +#![windows_subsystem = "windows"] + +use cpp::*; +use idevice::{ + afc::AfcClient, diagnostics_relay::DiagnosticsRelayClient, lockdown::LockdownClient, +}; +use qmetaobject::*; + +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +use std::future::Future; +use std::sync::mpsc; +use tokio::runtime::Runtime; +use tokio::sync::oneshot; +use tokio::task::JoinHandle; + +use once_cell::sync::Lazy; + +use crate::qquickimageprovider_imp::AddImageProvider; + +pub mod afc_services; +pub mod apps; +pub mod core; +pub mod image_cache; +pub mod image_loader; +pub mod image_provider; +pub mod qquickimageprovider_imp; +pub mod qrc; +pub mod qt_threading; +pub mod query_sqlite; +pub mod service_factory; +pub mod service_manager; +pub mod utils; + +pub const POSSIBLE_ROOT: &str = "../../../../"; +pub const APP_LABEL: &str = "iDescriptor"; +pub const EV_CONNECTED: u32 = 1; +pub const EV_DISCONNECTED: u32 = 2; +pub const EV_PAIRING_PENDING: u32 = 3; +pub const EV_FAIL: u32 = 4; + +// TODO +// #[global_allocator] +// static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +cpp! {{ + #include + #include + #include + #include + #include + #include + + #include "src/live_reload.cpp" +}} + +#[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() + .build() + .unwrap() +}); + +pub fn run_sync(fut: F) -> R +where + F: Future + Send + 'static, + R: Send + 'static, +{ + let (tx, rx) = mpsc::sync_channel(1); + + RUNTIME.spawn(async move { + let res = fut.await; + let _ = tx.send(res); + }); + + rx.recv().expect("Tokio runtime worker panicked") +} + +fn main() { + let ui_live_reload = true; + // #[cfg(target_os = "windows")] + // unsafe { + // use windows::Win32::System::Console::*; + // if AttachConsole(ATTACH_PARENT_PROCESS).is_err() && cli::will_run_in_console() { + // let _ = AllocConsole(); + // } + // } + + // let _ = util::install_crash_handler(); + // utils::init_logging(); + qmetaobject::log::init_qt_to_rust(); + + cpp!(unsafe [] { + + #define FLUENTUI_BUILD_STATIC_LIB 1 + + #ifdef WIN32 + // ::SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); + qputenv("QT_QPA_PLATFORM", "windows:darkmode=2"); + #endif + #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + qputenv("QT_QUICK_CONTROLS_STYLE", "Basic"); + #else + qputenv("QT_QUICK_CONTROLS_STYLE", "Default"); + #endif + #ifdef Q_OS_LINUX + // fix bug UOSv20 v-sync does not work + qputenv("QSG_RENDER_LOOP", "basic"); + #endif + + // 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); + // #endif + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + QApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + #endif + #endif + QCoreApplication::setOrganizationName("iDescriptor"); + QCoreApplication::setApplicationName("iDescriptor"); + + // FIXME + // QCoreApplication::setApplicationVersion(VERSION); + // FIXME + // if (a.arguments().contains("--reset-settings")) { + // // SettingsManager::sharedInstance()->clear(); + // QMessageBox::information(nullptr, "Settings Reset", + // "All application settings have been reset to " + // "their default values."); + // } + // QQmlApplicationEngine engine; + + #ifdef WIN32 + QString appPath = QCoreApplication::applicationDirPath(); + QString gstPluginPath = + QDir::toNativeSeparators(appPath + "/gstreamer-1.0"); + QString gstPluginScannerPath = QDir::toNativeSeparators( + appPath + "/gstreamer-1.0/libexec/gst-plugin-scanner.exe"); + + const char *oldPath = getenv("PATH"); + QString newPath = appPath + ";" + QString(oldPath); + qputenv("PATH", newPath.toUtf8()); + + qputenv("GST_PLUGIN_PATH", gstPluginPath.toUtf8()); + qDebug() << "GST_PLUGIN_PATH=" << gstPluginPath; + qputenv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", "no"); + qDebug() << "GST_REGISTRY_REUSE_PLUGIN_SCANNER=no"; + qputenv("GST_PLUGIN_SYSTEM_PATH", gstPluginPath.toUtf8()); + qDebug() << "GST_PLUGIN_SYSTEM_PATH=" << gstPluginPath; + qputenv("GST_DEBUG", "GST_PLUGIN_LOADING:5"); + qDebug() << "GST_DEBUG=GST_PLUGIN_LOADING:5"; + qputenv("GST_PLUGIN_SCANNER_1_0", gstPluginScannerPath.toUtf8()); + qDebug() << "GST_PLUGIN_SCANNER_1_0=" << gstPluginScannerPath; + #endif + #ifdef __APPLE__ + QString appPath = QCoreApplication::applicationDirPath(); + QString frameworksPath = + QDir::toNativeSeparators(appPath + "/../Frameworks"); + QString gstPluginPath = + QDir::toNativeSeparators(frameworksPath + "/gstreamer"); + QString gstPluginScannerPath = + QDir::toNativeSeparators(frameworksPath + "/gst-plugin-scanner"); + + setenv("GST_PLUGIN_PATH", gstPluginPath.toUtf8().constData(), 1); + setenv("GST_PLUGIN_SYSTEM_PATH", gstPluginPath.toUtf8().constData(), 1); + setenv("GST_PLUGIN_SCANNER", gstPluginScannerPath.toUtf8().constData(), 1); + #endif + }); + + if cfg!(compiled_qml) { + // For some reason on some devices QML detects that debugger is connected and fails to load pre-compiled qml files + cpp!(unsafe [] { qputenv("QML_FORCE_DISK_CACHE", "1"); }); + } + + crate::qrc::rsrc(); + // #[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!("Query"), + ); + + let mut engine = QmlEngine::new(); + // let mut dpi = cpp!(unsafe[] -> f64 as "double" { return QGuiApplication::primaryScreen()->logicalDotsPerInch() / 96.0; }); + + let obj = QObjectBox::new(image_loader::ImageLoader::default()); + engine.set_object_property("imageLoader".into(), obj.pinned()); + + let apps_impl = QObjectBox::new(apps::Apps::new_with_state()); + engine.set_object_property("apps".into(), apps_impl.pinned()); + + let provider_ref_cell = QObjectBox::new(image_provider::ImageProvider::default(obj)); + engine.add_image_provider("thumb", provider_ref_cell); + + let engine_ptr = engine.cpp_ptr(); + + let service_factory = QObjectBox::new(crate::service_factory::ServiceFactory::new(engine_ptr)); + engine.set_object_property("serviceFactory".into(), service_factory.pinned()); + + if !ui_live_reload { + use std::path::PathBuf; + // Try to load from disk first + let path = (|| -> Option { + let path = if cfg!(any(target_os = "macos", target_os = "ios")) { + PathBuf::from("../Resources/ui/Main.qml") + } else { + PathBuf::from("./ui/Main.qml") + }; + let final_path = std::env::current_exe().ok()?.parent()?.join(path); + if final_path.exists() { + Some(String::from(final_path.to_str()?)) + } else { + None + } + })(); + if let Some(path) = path { + engine.load_file(path.into()); + } else { + // Load from resources + engine.load_url(QString::from("qrc:/src/ui/Main.qml").into()); + } + } else { + engine.load_file(format!("{}/src/ui/Main.qml", env!("CARGO_MANIFEST_DIR")).into()); + let ui_path = QString::from(format!("{}/src/ui", env!("CARGO_MANIFEST_DIR"))); + cpp!(unsafe [engine_ptr as "QQmlApplicationEngine *", ui_path as "QString"] { init_live_reload(engine_ptr, ui_path); }); + } + + engine.exec(); +} diff --git a/src/mediapreviewdialog.cpp b/src/mediapreviewdialog.cpp deleted file mode 100644 index 9e4c206..0000000 --- a/src/mediapreviewdialog.cpp +++ /dev/null @@ -1,705 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "mediapreviewdialog.h" -#include "appcontext.h" -#include "iDescriptor-ui.h" -#include "imageloader.h" -#include "mediastreamermanager.h" -#include "photomodel.h" - -MediaPreviewDialog::MediaPreviewDialog( - const std::shared_ptr device, const QString &filePath, - std::optional> hause_arrest, bool useAfc2, - QWidget *parent) - : QDialog(parent), m_device(device), m_filePath(filePath), - m_isVideo(iDescriptor::Utils::isVideoFile(filePath)), - m_hause_arrest(hause_arrest), m_useAfc2(useAfc2) -{ - setWindowTitle(QFileInfo(filePath).fileName() + " - iDescriptor"); -#ifdef WIN32 - setupWinWindow(this); -#endif - setAttribute(Qt::WA_DeleteOnClose); - // Make dialog fullscreen - setWindowState(Qt::WindowMaximized); - setWindowFlags(Qt::Window | Qt::WindowMaximizeButtonHint | - Qt::WindowCloseButtonHint); - - // Use full screen size - const QSize screenSize = QApplication::primaryScreen()->size(); - resize(screenSize); - - setupUI(); - loadMedia(); - connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, - [this](const QString &udid) { - if (udid == m_device->udid) { - close(); - } - }); -} - -MediaPreviewDialog::~MediaPreviewDialog() -{ - // Release the streamer if it was used for video - if (m_isVideo) { - MediaStreamerManager::sharedInstance()->releaseStreamer(m_device->udid, - m_filePath); - } -} - -void MediaPreviewDialog::setupUI() -{ - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(0, 0, 0, 0); - m_mainLayout->setSpacing(0); - - m_loadingWidget = new ZLoadingWidget(); - m_mainLayout->addWidget(m_loadingWidget); - - if (m_isVideo) { - setupVideoView(); - } else { - setupImageView(); - } -} - -void MediaPreviewDialog::setupImageView() -{ - // Graphics view for image display with zoom/pan - m_imageScene = new QGraphicsScene(this); - m_imageView = new QGraphicsView(m_imageScene, this); - m_imageView->setDragMode(QGraphicsView::ScrollHandDrag); - m_imageView->setRenderHint(QPainter::Antialiasing); - m_imageView->setVisible(false); - m_loadingWidget->setupContentWidget(m_imageView); - - // Controls layout - m_controlsLayout = new QHBoxLayout(); - m_controlsLayout->setContentsMargins(10, 5, 10, 5); - m_controlsLayout->setSpacing(10); - - m_zoomInBtn = new QPushButton("Zoom In", this); - m_zoomOutBtn = new QPushButton("Zoom Out", this); - m_zoomResetBtn = new QPushButton("100%", this); - m_fitToWindowBtn = new QPushButton("Fit to Window", this); - - m_controlsLayout->addWidget(m_zoomInBtn); - m_controlsLayout->addWidget(m_zoomOutBtn); - m_controlsLayout->addWidget(m_zoomResetBtn); - m_controlsLayout->addWidget(m_fitToWindowBtn); - m_controlsLayout->addStretch(); - - m_mainLayout->addLayout(m_controlsLayout); - - // Connect signals - connect(m_zoomInBtn, &QPushButton::clicked, this, - &MediaPreviewDialog::zoomIn); - connect(m_zoomOutBtn, &QPushButton::clicked, this, - &MediaPreviewDialog::zoomOut); - connect(m_zoomResetBtn, &QPushButton::clicked, this, - &MediaPreviewDialog::zoomReset); - connect(m_fitToWindowBtn, &QPushButton::clicked, this, - &MediaPreviewDialog::fitToWindow); -} - -void MediaPreviewDialog::setupVideoView() -{ - // Video widget - m_videoWidget = new QVideoWidget(this); - m_videoWidget->setVisible(false); - m_videoWidget->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - m_loadingWidget->setupContentWidget(m_videoWidget); - - // Media player - m_mediaPlayer = new QMediaPlayer(this); - m_mediaPlayer->setVideoOutput(m_videoWidget); - - // Set up audio output explicitly - auto *audioOutput = new QAudioOutput(this); - audioOutput->setVolume(1.0); // Full volume - m_mediaPlayer->setAudioOutput(audioOutput); - - // Setup video controls - setupVideoControls(); - - // Connect media player signals - connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, - &MediaPreviewDialog::onMediaPlayerDurationChanged); - connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, - &MediaPreviewDialog::onMediaPlayerPositionChanged); - connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, - &MediaPreviewDialog::onMediaPlayerStateChanged); - connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, - [this](QMediaPlayer::Error error, const QString &errorString) { - m_loadingWidget->showError("Error playing video: " + - errorString); - }); - - m_progressTimer = new QTimer(this); - connect(m_progressTimer, &QTimer::timeout, this, - &MediaPreviewDialog::updateVideoProgress); -} - -void MediaPreviewDialog::loadMedia() -{ - if (m_isVideo) { - return loadVideo(); - } - loadImage(); -} - -void MediaPreviewDialog::loadImage() -{ - QPointer safeThis(this); - auto callback = [this, safeThis](const QPixmap &pixmap) { - if (!safeThis) { - return; - } - if (!pixmap.isNull()) { - onImageLoaded(pixmap); - } else { - onImageLoadFailed(); - } - }; - // 99999 is so that it gets the highest priority in the queue - unsigned int priority = 99999; - ImageLoader::sharedInstance().requestImageWithCallback( - m_device, m_filePath, priority, callback, m_hause_arrest, m_useAfc2); -} - -void MediaPreviewDialog::loadVideo() -{ - m_videoWidget->setVisible(true); - - // Get streamer URL from the singleton manager - QUrl streamUrl = MediaStreamerManager::sharedInstance()->getStreamUrl( - m_device, m_hause_arrest, m_useAfc2, m_filePath); - qDebug() << "Streaming video from URL:" << streamUrl; - if (streamUrl.isEmpty()) { - // TODO: connect to retry signal to attempt restarting the stream - m_loadingWidget->showError("Failed to start video stream"); - return; - } - - m_mediaPlayer->setSource(streamUrl); - m_mediaPlayer->play(); - m_loadingWidget->stop(); - // m_statusLabel->setText( - // QString("Playing: %1").arg(QFileInfo(m_filePath).fileName())); -} - -void MediaPreviewDialog::onImageLoaded(const QPixmap &pixmap) -{ - m_originalPixmap = pixmap; - - m_imageView->setVisible(true); - - // Add pixmap to scene - m_pixmapItem = m_imageScene->addPixmap(m_originalPixmap); - m_imageScene->setSceneRect(m_originalPixmap.rect()); - - // Fit to window initially - // TODO:why QTimer::singleShot is required here ? - // fitToWindow(); - QTimer::singleShot(0, this, &MediaPreviewDialog::fitToWindow); - m_loadingWidget->stop(); - // Update status - // m_statusLabel->setText(QString("Image: %1 (%2x%3)") - // .arg(QFileInfo(m_filePath).fileName()) - // .arg(m_originalPixmap.width()) - // .arg(m_originalPixmap.height())); -} - -void MediaPreviewDialog::onImageLoadFailed() -{ - // TODO: connect to retry signal to attempt reloading the image - m_loadingWidget->showError("Failed to load image"); - // m_statusLabel->setText("Error loading image"); -} - -void MediaPreviewDialog::wheelEvent(QWheelEvent *event) -{ - if (!m_isVideo && m_imageView && m_imageView->isVisible()) { - if (event->modifiers() & Qt::ControlModifier) { - // Zoom with Ctrl + Mouse Wheel - const double scaleFactor = 1.15; - if (event->angleDelta().y() > 0) { - zoom(scaleFactor); - } else { - zoom(1.0 / scaleFactor); - } - event->accept(); - return; - } - } - QDialog::wheelEvent(event); -} - -void MediaPreviewDialog::keyPressEvent(QKeyEvent *event) -{ - // Image shortcuts - if (!m_isVideo && m_imageView) { - switch (event->key()) { - case Qt::Key_Plus: - case Qt::Key_Equal: - zoomIn(); - event->accept(); - return; - case Qt::Key_Minus: - zoomOut(); - event->accept(); - return; - case Qt::Key_0: - zoomReset(); - event->accept(); - return; - case Qt::Key_F: - fitToWindow(); - event->accept(); - return; - } - } - - // Video shortcuts - if (m_isVideo && m_mediaPlayer) { - switch (event->key()) { - case Qt::Key_Space: - onPlayPauseClicked(); - event->accept(); - return; - case Qt::Key_S: - onStopClicked(); - event->accept(); - return; - case Qt::Key_R: - m_repeatBtn->toggle(); - event->accept(); - return; - case Qt::Key_Left: - // Seek backward 10 seconds - if (m_videoDuration > 0) { - qint64 newPos = qMax(0LL, m_mediaPlayer->position() - 10000); - m_mediaPlayer->setPosition(newPos); - } - event->accept(); - return; - case Qt::Key_Right: - // Seek forward 10 seconds - if (m_videoDuration > 0) { - qint64 newPos = - qMin(m_videoDuration, m_mediaPlayer->position() + 10000); - m_mediaPlayer->setPosition(newPos); - } - event->accept(); - return; - } - } - - // Global shortcuts - if (event->key() == Qt::Key_Escape) { - close(); - event->accept(); - return; - } - - QDialog::keyPressEvent(event); -} - -void MediaPreviewDialog::resizeEvent(QResizeEvent *event) -{ - QDialog::resizeEvent(event); - - // Auto-fit when window is resized if we're close to fit-to-window size - if (!m_isVideo && m_imageView && m_imageView->isVisible() && - !m_originalPixmap.isNull()) { - const QSize viewSize = m_imageView->viewport()->size(); - const QSize pixmapSize = m_originalPixmap.size(); - const double fitScale = - qMin(static_cast(viewSize.width()) / pixmapSize.width(), - static_cast(viewSize.height()) / pixmapSize.height()); - - // If current zoom is close to fit-to-window, re-fit - if (qAbs(m_zoomFactor - fitScale) < 0.1) { - fitToWindow(); - } - } -} - -void MediaPreviewDialog::zoomIn() { zoom(1.25); } - -void MediaPreviewDialog::zoomOut() { zoom(1.0 / 1.25); } - -void MediaPreviewDialog::zoomReset() -{ - if (m_imageView && m_originalPixmap.isNull() == false) { - m_imageView->resetTransform(); - m_zoomFactor = 1.0; - updateZoomStatus(); - } -} - -void MediaPreviewDialog::fitToWindow() -{ - if (!m_imageView || m_originalPixmap.isNull()) - return; - - const QSize viewSize = m_imageView->viewport()->size(); - const QSize pixmapSize = m_originalPixmap.size(); - - const double scaleX = - static_cast(viewSize.width()) / pixmapSize.width(); - const double scaleY = - static_cast(viewSize.height()) / pixmapSize.height(); - const double scale = qMin(scaleX, scaleY); - - m_imageView->resetTransform(); - m_imageView->scale(scale, scale); - m_zoomFactor = scale; - updateZoomStatus(); -} - -void MediaPreviewDialog::zoom(double factor) -{ - if (!m_imageView) - return; - - m_imageView->scale(factor, factor); - m_zoomFactor *= factor; - updateZoomStatus(); -} - -void MediaPreviewDialog::updateZoomStatus() -{ - if (!m_isVideo && !m_originalPixmap.isNull()) { - // m_statusLabel->setText(QString("Image: %1 (%2x%3) - Zoom: %4%") - // .arg(QFileInfo(m_filePath).fileName()) - // .arg(m_originalPixmap.width()) - // .arg(m_originalPixmap.height()) - // .arg(qRound(m_zoomFactor * 100))); - } -} - -void MediaPreviewDialog::setupVideoControls() -{ - // Create video controls layout - m_videoControlsLayout = new QHBoxLayout(); - m_videoControlsLayout->setContentsMargins(10, 5, 10, 5); - m_videoControlsLayout->setSpacing(10); - - // Play/Pause button - m_playPauseBtn = new QPushButton("⏸️", this); - m_playPauseBtn->setMaximumWidth(40); - m_playPauseBtn->setMinimumHeight(30); - m_playPauseBtn->setToolTip("Play/Pause (Space)"); - m_playPauseBtn->setStyleSheet("QPushButton { font-size: 14px; }"); - connect(m_playPauseBtn, &QPushButton::clicked, this, - &MediaPreviewDialog::onPlayPauseClicked); - - // Stop button - m_stopBtn = new QPushButton("⏹️", this); - m_stopBtn->setMaximumWidth(40); - m_stopBtn->setMinimumHeight(30); - m_stopBtn->setToolTip("Stop (S)"); - m_stopBtn->setStyleSheet("QPushButton { font-size: 14px; }"); - connect(m_stopBtn, &QPushButton::clicked, this, - &MediaPreviewDialog::onStopClicked); - - // Repeat button - m_repeatBtn = new QPushButton("🔁", this); - m_repeatBtn->setMaximumWidth(40); - m_repeatBtn->setMinimumHeight(30); - m_repeatBtn->setCheckable(true); - m_repeatBtn->setToolTip("Toggle Repeat (R)"); - m_repeatBtn->setStyleSheet("QPushButton { font-size: 14px; }"); - connect(m_repeatBtn, &QPushButton::toggled, this, - &MediaPreviewDialog::onRepeatToggled); - - // Timeline slider - m_timelineSlider = new ZSlider(Qt::Horizontal, this); - m_timelineSlider->setMinimum(0); - m_timelineSlider->setMaximum(1000); - m_timelineSlider->setValue(0); - m_timelineSlider->setToolTip("Seek timeline"); - connect(m_timelineSlider, &ZSlider::valueChanged, this, - &MediaPreviewDialog::onTimelineValueChanged); - connect(m_timelineSlider, &ZSlider::sliderPressed, this, - &MediaPreviewDialog::onTimelinePressed); - connect(m_timelineSlider, &ZSlider::sliderReleased, this, - &MediaPreviewDialog::onTimelineReleased); - - // Time label - m_timeLabel = new QLabel("00:00 / 00:00", this); - m_timeLabel->setMinimumWidth(100); - m_timeLabel->setStyleSheet("QLabel { font-family: monospace; }"); - - // Volume slider - m_volumeSlider = new QSlider(Qt::Horizontal, this); - m_volumeSlider->setMinimum(0); - m_volumeSlider->setMaximum(100); - m_volumeSlider->setValue(100); // Default to full volume - m_volumeSlider->setMaximumWidth(100); - m_volumeSlider->setToolTip("Volume"); - connect(m_volumeSlider, &QSlider::valueChanged, this, - &MediaPreviewDialog::onVolumeChanged); - - // Volume label - m_volumeLabel = new QLabel("🔊", this); - m_volumeLabel->setStyleSheet("QLabel { font-size: 14px; }"); - - // Add widgets to layout - m_videoControlsLayout->addWidget(m_playPauseBtn); - m_videoControlsLayout->addWidget(m_stopBtn); - m_videoControlsLayout->addWidget(m_repeatBtn); - m_videoControlsLayout->addWidget(m_timelineSlider, 1); - m_videoControlsLayout->addWidget(m_timeLabel); - m_videoControlsLayout->addWidget(m_volumeLabel); - m_videoControlsLayout->addWidget(m_volumeSlider); - - // Add controls layout to main layout - m_mainLayout->addLayout(m_videoControlsLayout); -} - -void MediaPreviewDialog::onPlayPauseClicked() -{ - if (!m_mediaPlayer) - return; - - if (m_mediaPlayer->playbackState() == QMediaPlayer::PlayingState) { - m_mediaPlayer->pause(); - } else { - m_mediaPlayer->play(); - } -} - -void MediaPreviewDialog::onStopClicked() -{ - if (!m_mediaPlayer) - return; - - m_mediaPlayer->stop(); - if (m_progressTimer) { - m_progressTimer->stop(); - } -} - -void MediaPreviewDialog::onRepeatToggled(bool enabled) -{ - m_isRepeatEnabled = enabled; - m_repeatBtn->setStyleSheet( - enabled ? "QPushButton { background-color: #4CAF50; color: white; }" - : ""); -} - -void MediaPreviewDialog::onTimelinePressed() -{ - m_isDraggingTimeline = true; - if (m_progressTimer) { - m_progressTimer->stop(); - } -} - -void MediaPreviewDialog::onTimelineReleased() -{ - m_isDraggingTimeline = false; - if (m_mediaPlayer && m_videoDuration > 0) { - // Seek to the selected position - qint64 position = (m_timelineSlider->value() * m_videoDuration) / 1000; - m_mediaPlayer->setPosition(position); - } - - // Restart progress timer if playing - if (m_mediaPlayer && - m_mediaPlayer->playbackState() == QMediaPlayer::PlayingState) { - m_progressTimer->start(100); // Update every 100ms - } -} - -void MediaPreviewDialog::onTimelineValueChanged(int value) -{ - if (m_isDraggingTimeline && m_videoDuration > 0) { - // Update time display while dragging - qint64 position = (value * m_videoDuration) / 1000; - updateVideoTimeDisplay(); - } -} - -void MediaPreviewDialog::updateVideoProgress() -{ - if (!m_mediaPlayer || m_isDraggingTimeline) - return; - - qint64 position = m_mediaPlayer->position(); - if (m_videoDuration > 0) { - int sliderValue = static_cast((position * 1000) / m_videoDuration); - m_timelineSlider->setValue(sliderValue); - } - - updateVideoTimeDisplay(); -} - -void MediaPreviewDialog::onMediaPlayerStateChanged() -{ - if (!m_mediaPlayer) - return; - - QMediaPlayer::PlaybackState state = m_mediaPlayer->playbackState(); - - switch (state) { - case QMediaPlayer::PlayingState: - m_playPauseBtn->setText("⏸️"); - m_playPauseBtn->setToolTip("Pause (Space)"); - if (m_progressTimer) { - m_progressTimer->start(100); - } - break; - case QMediaPlayer::PausedState: - m_playPauseBtn->setText("▶️"); - m_playPauseBtn->setToolTip("Play (Space)"); - if (m_progressTimer) { - m_progressTimer->stop(); - } - break; - case QMediaPlayer::StoppedState: - m_playPauseBtn->setText("▶️"); - m_playPauseBtn->setToolTip("Play (Space)"); - if (m_progressTimer) { - m_progressTimer->stop(); - } - m_timelineSlider->setValue(0); - - // Handle repeat functionality - if (m_isRepeatEnabled) { - QTimer::singleShot(100, this, [this]() { - if (m_mediaPlayer) { - m_mediaPlayer->play(); - } - }); - } - break; - } -} - -void MediaPreviewDialog::onMediaPlayerDurationChanged(qint64 duration) -{ - m_videoDuration = duration; - updateVideoTimeDisplay(); - - // Update status with video info - if (duration > 0) { - QString durationStr; - formatTime(duration, durationStr); - // m_statusLabel->setText(QString("Video: %1 - Duration: %2") - // .arg(QFileInfo(m_filePath).fileName()) - // .arg(durationStr)); - } -} - -void MediaPreviewDialog::onMediaPlayerPositionChanged(qint64 position) -{ - if (!m_isDraggingTimeline) { - updateVideoProgress(); - } -} - -void MediaPreviewDialog::updateVideoTimeDisplay() -{ - if (!m_mediaPlayer) - return; - - qint64 currentPos = - m_isDraggingTimeline - ? (m_timelineSlider->value() * m_videoDuration) / 1000 - : m_mediaPlayer->position(); - - QString currentTimeStr, durationStr; - formatTime(currentPos, currentTimeStr); - formatTime(m_videoDuration, durationStr); - - m_timeLabel->setText(QString("%1 / %2").arg(currentTimeStr, durationStr)); -} - -void MediaPreviewDialog::formatTime(qint64 milliseconds, QString &timeString) -{ - qint64 seconds = milliseconds / 1000; - qint64 minutes = seconds / 60; - qint64 hours = minutes / 60; - - seconds %= 60; - minutes %= 60; - - if (hours > 0) { - timeString = QString("%1:%2:%3") - .arg(hours, 2, 10, QChar('0')) - .arg(minutes, 2, 10, QChar('0')) - .arg(seconds, 2, 10, QChar('0')); - } else { - timeString = QString("%1:%2") - .arg(minutes, 2, 10, QChar('0')) - .arg(seconds, 2, 10, QChar('0')); - } -} - -void MediaPreviewDialog::onVolumeChanged(int value) -{ - if (!m_mediaPlayer) - return; - - QAudioOutput *audioOutput = m_mediaPlayer->audioOutput(); - if (audioOutput) { - float volume = static_cast(value) / 100.0f; - audioOutput->setVolume(volume); - - // Update volume icon based on level - if (value == 0) { - m_volumeLabel->setText("🔇"); - } else if (value < 30) { - m_volumeLabel->setText("🔈"); - } else if (value < 70) { - m_volumeLabel->setText("🔉"); - } else { - m_volumeLabel->setText("🔊"); - } - - qDebug() << "Volume changed to:" << value << "%" << "(" << volume - << ")"; - } -} - -#ifdef __APPLE__ -bool MediaPreviewDialog::event(QEvent *event) -{ - // TODO: implement this on all dialogs - // catch platform close (Cmd+W on macOS) - if (event->type() == QEvent::ShortcutOverride) { - if (auto *ke = dynamic_cast(event)) { - const Qt::KeyboardModifiers mods = ke->modifiers(); - if (ke->key() == Qt::Key_W && - (mods & (Qt::MetaModifier | Qt::ControlModifier))) { - ke->accept(); - close(); - return true; - } - } - } - return QDialog::event(event); -} -#endif \ No newline at end of file diff --git a/src/mediapreviewdialog.h b/src/mediapreviewdialog.h deleted file mode 100644 index 6179c8c..0000000 --- a/src/mediapreviewdialog.h +++ /dev/null @@ -1,153 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef MEDIAPREVIEWDIALOG_H -#define MEDIAPREVIEWDIALOG_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "zloadingwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class MediaPreviewDialog : public QDialog -{ - Q_OBJECT - -public: - explicit MediaPreviewDialog(const std::shared_ptr device, - const QString &filePath, - std::optional> - hause_arrest = std::nullopt, - bool useAfc2 = false, - QWidget *parent = nullptr); - ~MediaPreviewDialog(); - -protected: - void wheelEvent(QWheelEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - void resizeEvent(QResizeEvent *event) override; -#ifdef __APPLE__ - bool event(QEvent *event) override; -#endif -private slots: - void onImageLoaded(const QPixmap &pixmap); - void onImageLoadFailed(); - void zoomIn(); - void zoomOut(); - void zoomReset(); - void fitToWindow(); - - // Video control slots - void onPlayPauseClicked(); - void onStopClicked(); - void onRepeatToggled(bool enabled); - void onTimelineValueChanged(int value); - void onTimelinePressed(); - void onTimelineReleased(); - void onVolumeChanged(int value); - void updateVideoProgress(); - void onMediaPlayerStateChanged(); - void onMediaPlayerDurationChanged(qint64 duration); - void onMediaPlayerPositionChanged(qint64 position); - -private: - void setupUI(); - void setupImageView(); - void setupVideoView(); - void setupVideoControls(); - void loadMedia(); - void loadImage(); - void loadVideo(); - void zoom(double factor); - void updateZoomStatus(); - void updateVideoTimeDisplay(); - void formatTime(qint64 milliseconds, QString &timeString); - - // Core data - std::shared_ptr m_device; - std::optional> m_hause_arrest; - bool m_useAfc2; - QString m_filePath; - bool m_isVideo; - - // UI components - QVBoxLayout *m_mainLayout; - QHBoxLayout *m_controlsLayout; - - // Image viewing components - QGraphicsView *m_imageView = nullptr; - QGraphicsScene *m_imageScene = nullptr; - QGraphicsPixmapItem *m_pixmapItem = nullptr; - - // Video viewing components - QVideoWidget *m_videoWidget = nullptr; - QMediaPlayer *m_mediaPlayer = nullptr; - // Video control components - QHBoxLayout *m_videoControlsLayout; - QPushButton *m_playPauseBtn; - QPushButton *m_stopBtn; - QPushButton *m_repeatBtn; - ZSlider *m_timelineSlider; - QLabel *m_timeLabel; - QSlider *m_volumeSlider; - QLabel *m_volumeLabel; - QTimer *m_progressTimer; - - // Control buttons - QPushButton *m_zoomInBtn; - QPushButton *m_zoomOutBtn; - QPushButton *m_zoomResetBtn; - QPushButton *m_fitToWindowBtn; - - // State - double m_zoomFactor = 1.0; - QPixmap m_originalPixmap; - - // Video state - bool m_isRepeatEnabled = true; - bool m_isDraggingTimeline = false; - qint64 m_videoDuration = 0; - - ZLoadingWidget *m_loadingWidget; -}; - -#endif // MEDIAPREVIEWDIALOG_H diff --git a/src/mediastreamermanager.cpp b/src/mediastreamermanager.cpp deleted file mode 100644 index 290505a..0000000 --- a/src/mediastreamermanager.cpp +++ /dev/null @@ -1,107 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "mediastreamermanager.h" -#include -#include - -MediaStreamerManager::~MediaStreamerManager() { cleanup(); } - -MediaStreamerManager *MediaStreamerManager::sharedInstance() -{ - static MediaStreamerManager instance; - return &instance; -} - -QUrl MediaStreamerManager::getStreamUrl( - const std::shared_ptr device, - std::optional> hause_arrest, bool useAfc2, - const QString &filePath) -{ - QString rustUrl; - - if (useAfc2) { - qDebug() << "Requesting stream URL using Afc2Backend for:" << filePath; - rustUrl = device->afc2_backend->start_video_stream(filePath); - } else if (hause_arrest.has_value() && hause_arrest.value()) { - qDebug() << "Requesting stream URL using HauseArrest for:" << filePath; - rustUrl = hause_arrest.value()->start_video_stream(filePath); - } else { - qDebug() << "Requesting stream URL using AfcBackend for:" << filePath; - rustUrl = device->afc_backend->start_video_stream(filePath); - } - - if (rustUrl.isEmpty()) { - qWarning() << "MediaStreamerManager: start_video_stream failed for" - << filePath; - return {}; - } - - QMutexLocker locker(&m_streamersMutex); - auto it = m_streamers.find(filePath); - if (it != m_streamers.end()) { - it->refCount++; - qDebug() << "MediaStreamerManager: Reusing existing streamer for" - << filePath << "refCount:" << it->refCount; - } else { - qDebug() << "MediaStreamerManager: Creating new streamer for" - << filePath; - StreamerInfo info{rustUrl, 1}; - m_streamers.insert(filePath, info); - } - - return QUrl(rustUrl); -} - -void MediaStreamerManager::releaseStreamer(const QString &udid, - const QString &filePath) -{ - QMutexLocker locker(&m_streamersMutex); - auto it = m_streamers.find(filePath); - if (it != m_streamers.end()) { - it->refCount--; - qDebug() << "MediaStreamerManager: Released streamer for" << filePath - << "refCount:" << it->refCount; - - if (it->refCount <= 0) { - qDebug() << "MediaStreamerManager: Deleting streamer for" - << filePath; - // delete it->streamer; - AppContext::sharedInstance()->ioManager->release_video_streamer( - udid, it.value().rustUrl); - m_streamers.erase(it); - } - } -} - -void MediaStreamerManager::cleanup() -{ - QMutexLocker locker(&m_streamersMutex); - auto it = m_streamers.begin(); - while (it != m_streamers.end()) { - qDebug() << "MediaStreamerManager: Cleaning up streamer for" - << it.key(); - // FIXME: what to do here? - // if (it->streamer) { - // delete it->streamer; - // } - // release_streamer(it.value().rustUrl); - it = m_streamers.erase(it); - } -} \ No newline at end of file diff --git a/src/mediastreamermanager.h b/src/mediastreamermanager.h deleted file mode 100644 index f1358c5..0000000 --- a/src/mediastreamermanager.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef MEDIASTREAMERMANAGER_H -#define MEDIASTREAMERMANAGER_H - -#include "appcontext.h" -#include "iDescriptor.h" -#include -#include -#include -#include - -class MediaStreamerManager -{ -public: - static MediaStreamerManager *sharedInstance(); - - QUrl - getStreamUrl(const std::shared_ptr device, - std::optional> hause_arrest, - bool useAfc2, const QString &filePath); - - void releaseStreamer(const QString &udid, const QString &filePath); - - void cleanup(); - -private: - ~MediaStreamerManager(); - -private: - struct StreamerInfo { - QString rustUrl; - int refCount; - }; - static QMutex s_instanceMutex; - - QMap m_streamers; - QMutex m_streamersMutex; -}; - -#endif // MEDIASTREAMERMANAGER_H diff --git a/src/networkdevicestoconnectwidget.cpp b/src/networkdevicestoconnectwidget.cpp deleted file mode 100644 index a67e769..0000000 --- a/src/networkdevicestoconnectwidget.cpp +++ /dev/null @@ -1,392 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "networkdevicestoconnectwidget.h" - -#include "appcontext.h" -#include -#include -#include -#include -#include -#include -#include - -NetworkDeviceCard::NetworkDeviceCard(const NetworkDevice &device, - QWidget *parent) - : QWidget(parent), m_device(device) -{ - QVBoxLayout *cardLayout = new QVBoxLayout(this); - cardLayout->setContentsMargins(12, 10, 12, 10); - cardLayout->setSpacing(4); - - // Device name - QLabel *nameLabel = new QLabel(device.name); - nameLabel->setWordWrap(true); - QFont nameFont = nameLabel->font(); - nameFont.setPointSize(13); - nameFont.setWeight(QFont::Medium); - nameLabel->setFont(nameFont); - QPalette namePalette = nameLabel->palette(); - namePalette.setColor(QPalette::WindowText, - palette().color(QPalette::WindowText)); - nameLabel->setPalette(namePalette); - - // Device info container - QWidget *infoContainer = new QWidget(); - QHBoxLayout *infoLayout = new QHBoxLayout(infoContainer); - infoLayout->setContentsMargins(0, 0, 0, 0); - infoLayout->setSpacing(12); - - // Address info - QLabel *addressLabel = new QLabel(QString("IP: %1").arg(device.address)); - QFont addressFont = addressLabel->font(); - addressFont.setPointSize(11); - addressLabel->setFont(addressFont); - QPalette addressPalette = addressLabel->palette(); - QColor secondaryColor = palette().color(QPalette::WindowText); - secondaryColor.setAlpha(180); - addressPalette.setColor(QPalette::WindowText, secondaryColor); - addressLabel->setPalette(addressPalette); - - // Port info - QLabel *portLabel = new QLabel(QString("Port: %1").arg(device.port)); - portLabel->setFont(addressFont); - portLabel->setPalette(addressPalette); - - infoLayout->addWidget(addressLabel); - infoLayout->addWidget(portLabel); - infoLayout->addStretch(); - - m_connectButton = new QPushButton("Connect"); - m_connectButton->setDefault(true); - connect(m_connectButton, &QPushButton::clicked, this, [this, device]() { - m_connectButton->setText("Connecting..."); - m_connectButton->setEnabled(false); - AppContext::sharedInstance()->tryToConnectToNetworkDevice(device); - }); - infoLayout->addWidget(m_connectButton); - infoLayout->addSpacing(5); - - QLabel *statusIndicator = new QLabel("●"); - statusIndicator->setStyleSheet( - QString("QLabel { font-size: 14px; color: %1; }") -#ifdef WIN32 - .arg(COLOR_ACCENT_BLUE.name())); -#else - .arg(COLOR_GREEN.name())); -#endif - - infoLayout->addWidget(statusIndicator); - - cardLayout->addWidget(nameLabel); - cardLayout->addWidget(infoContainer); -} - -void NetworkDeviceCard::failed() -{ - m_connectButton->setText("Failed to connect"); - m_connectButton->setEnabled(false); - - QTimer::singleShot(2000, this, [this]() { - m_connectButton->setText("Connect"); - m_connectButton->setEnabled(true); - }); -} - -void NetworkDeviceCard::noPairingFile() -{ - // TODO: add a button or hint to explain how to create a pairing file for - // this device - m_connectButton->setText("No pairing file"); - m_connectButton->setEnabled(false); - - QTimer::singleShot(5000, this, [this]() { - m_connectButton->setText("Connect"); - m_connectButton->setEnabled(true); - }); -} - -void NetworkDeviceCard::connected() -{ - m_connectButton->setText("Connected"); - m_connectButton->setEnabled(false); - - QTimer::singleShot(10000, this, [this]() { - m_connectButton->setText("Connect"); - m_connectButton->setEnabled(true); - }); -} - -void NetworkDeviceCard::alreadyExists() -{ - m_connectButton->setText("Already connected"); - m_connectButton->setEnabled(false); - - QTimer::singleShot(3000, this, [this]() { - m_connectButton->setText("Connect"); - m_connectButton->setEnabled(true); - }); -} - -void NetworkDeviceCard::initStarted() -{ - m_connectButton->setText("Connecting..."); - m_connectButton->setEnabled(false); -} - -NetworkDevicesToConnectWidget::NetworkDevicesToConnectWidget(QWidget *parent) - : QWidget(parent) -{ - setupUI(); - - connect(NetworkDeviceProvider::sharedInstance(), - &NetworkDeviceProvider::deviceAdded, this, - &NetworkDevicesToConnectWidget::onWirelessDeviceAdded); - connect(NetworkDeviceProvider::sharedInstance(), - &NetworkDeviceProvider::deviceRemoved, this, - &NetworkDevicesToConnectWidget::onWirelessDeviceRemoved); - - updateDeviceList(); - - // in case the backend fails to find pairing file - connect(AppContext::sharedInstance()->core, &CXX::Core::no_pairing_file, - this, - &NetworkDevicesToConnectWidget::onNoPairingFileForWirelessDevice); - - connect(AppContext::sharedInstance(), - &AppContext::noPairingFileForWirelessDevice, this, - &NetworkDevicesToConnectWidget::onNoPairingFileForWirelessDevice); - - connect(AppContext::sharedInstance()->core, &CXX::Core::init_failed, this, - &NetworkDevicesToConnectWidget::onDeviceInitFailed); - connect(AppContext::sharedInstance(), &AppContext::initStarted, this, - &NetworkDevicesToConnectWidget::onDeviceInitStarted); - connect(AppContext::sharedInstance(), &AppContext::deviceAdded, this, - &NetworkDevicesToConnectWidget::onDeviceAdded); - connect(AppContext::sharedInstance(), &AppContext::deviceAlreadyExistsMAC, - this, &NetworkDevicesToConnectWidget::onDeviceAlreadyExists); - - // eval interval - QTimer *evalTimer = new QTimer(this); - connect(evalTimer, &QTimer::timeout, this, - &NetworkDevicesToConnectWidget::eval); - evalTimer->start(30000); // evaluate every 30 seconds -} - -NetworkDevicesToConnectWidget::~NetworkDevicesToConnectWidget() {} - -void NetworkDevicesToConnectWidget::setupUI() -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(10, 10, 10, 10); - mainLayout->setSpacing(10); - - // Status label - m_statusLabel = new QLabel("Scanning for network devices..."); - QFont statusFont = m_statusLabel->font(); - statusFont.setPointSize(12); - statusFont.setWeight(QFont::Medium); - m_statusLabel->setFont(statusFont); - m_statusLabel->setAlignment(Qt::AlignCenter); - mainLayout->addWidget(m_statusLabel); - - // Device group - m_deviceGroup = new QGroupBox("Network Devices"); - QFont groupFont = m_deviceGroup->font(); - groupFont.setPointSize(14); - groupFont.setWeight(QFont::Bold); - m_deviceGroup->setFont(groupFont); - - QVBoxLayout *groupLayout = new QVBoxLayout(m_deviceGroup); - groupLayout->setContentsMargins(5, 15, 5, 5); - groupLayout->setSpacing(0); - - // Scroll area - m_scrollArea = new QScrollArea(); - m_scrollArea->setWidgetResizable(true); - m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - m_scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - m_scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - /* FIXME: We need a better approach to theme awareness */ - connect(qApp, &QApplication::paletteChanged, this, [this]() { - m_scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - }); - - // Scroll content - m_scrollContent = new QWidget(); - m_scrollContent->setContentsMargins(0, 0, 0, 0); - m_deviceLayout = new QVBoxLayout(m_scrollContent); - m_deviceLayout->setContentsMargins(5, 5, 5, 5); - m_deviceLayout->setSpacing(8); - m_deviceLayout->addStretch(); - - m_scrollArea->setWidget(m_scrollContent); - groupLayout->addWidget(m_scrollArea, 1); - - mainLayout->addWidget(m_deviceGroup, 1); -} - -void NetworkDevicesToConnectWidget::createDeviceCard( - const NetworkDevice &device) -{ - NetworkDeviceCard *card = new NetworkDeviceCard(device); - - m_deviceLayout->insertWidget(m_deviceLayout->count() - 1, card); - m_deviceCards[device.macAddress] = card; -} - -void NetworkDevicesToConnectWidget::clearDeviceCards() -{ - for (QWidget *card : m_deviceCards) { - if (card) { - card->deleteLater(); - } - } - m_deviceCards.clear(); -} -void NetworkDevicesToConnectWidget::updateDeviceList() -{ - clearDeviceCards(); - - QMap devices = - NetworkDeviceProvider::sharedInstance()->getNetworkDevices(); - - if (devices.isEmpty()) { - m_statusLabel->setText("No network devices found"); - } else { - m_statusLabel->setText( - QString("Found %1 network device(s)").arg(devices.count())); - - for (const NetworkDevice &device : devices) { - createDeviceCard(device); - } - } -} - -void NetworkDevicesToConnectWidget::onWirelessDeviceAdded( - const NetworkDevice &device) -{ - if (m_deviceCards.contains(device.macAddress)) { - qDebug() << "Device with MAC" << device.macAddress - << "already exists in the list. Skipping addition."; - return; - } - createDeviceCard(device); - - // Update status - int deviceCount = m_deviceCards.count(); - m_statusLabel->setText( - QString("Found %1 network device(s)").arg(deviceCount)); -} - -void NetworkDevicesToConnectWidget::onWirelessDeviceRemoved( - const QString &macAddress) -{ - // Find and remove the corresponding card - NetworkDeviceCard *card = m_deviceCards[macAddress]; - m_deviceCards.remove(macAddress); - if (card) { - card->deleteLater(); - } - - // Update status - int deviceCount = m_deviceCards.count(); - if (deviceCount == 0) { - m_statusLabel->setText("No network devices found"); - } else { - m_statusLabel->setText( - QString("Found %1 network device(s)").arg(deviceCount)); - } -} - -void NetworkDevicesToConnectWidget::eval() -{ - if (QCoreApplication::closingDown() || - !SettingsManager::sharedInstance()->autoConnectWirelessDevices()) - return; - bool forceCache = true; - for (const auto &card : m_deviceCards) { - if (card) { - NetworkDevice device = card->getDevice(); - AppContext::sharedInstance()->tryToConnectToNetworkDevice( - device, forceCache, false); - forceCache = false; // force cache once - } - } -} - -void NetworkDevicesToConnectWidget::onNoPairingFileForWirelessDevice( - const QString &macAddress) -{ - NetworkDeviceCard *deviceCard = m_deviceCards[macAddress]; - if (deviceCard) { - qDebug() << "Calling noPairingFile() on device card for" << macAddress; - deviceCard->noPairingFile(); - } -} - -// udid or mac address -void NetworkDevicesToConnectWidget::onDeviceInitFailed(const QString &uniq) -{ - NetworkDeviceCard *deviceCard = m_deviceCards[uniq]; - if (deviceCard) { - qDebug() << "Calling failed() on device card for" << uniq; - deviceCard->failed(); - } -} - -void NetworkDevicesToConnectWidget::onDeviceInitStarted(const QString &uniq) -{ - NetworkDeviceCard *deviceCard = m_deviceCards[uniq]; - if (deviceCard) { - qDebug() << "Calling initStarted() on device card for" << uniq; - deviceCard->initStarted(); - } -} - -void NetworkDevicesToConnectWidget::onDeviceAdded( - const std::shared_ptr device) -{ - NetworkDeviceCard *deviceCard = m_deviceCards[QString::fromStdString( - device->deviceInfo.wifiMacAddress)]; - if (deviceCard) { - qDebug() << "Calling connected() on device card for" - << QString::fromStdString(device->deviceInfo.wifiMacAddress); - deviceCard->connected(); - return; - } - qDebug() << "No device card found for" - << QString::fromStdString(device->deviceInfo.wifiMacAddress); -} - -void NetworkDevicesToConnectWidget::onDeviceAlreadyExists( - const iDescriptor::Uniq &uniq) -{ - NetworkDeviceCard *deviceCard = m_deviceCards[QString(uniq.get())]; - if (deviceCard) { - qDebug() << "Calling alreadyExists() on device card for" << uniq.get(); - deviceCard->alreadyExists(); - return; - } - qDebug() << "No device card found for" << uniq.get(); -} diff --git a/src/networkdevicestoconnectwidget.h b/src/networkdevicestoconnectwidget.h deleted file mode 100644 index 97c0fa1..0000000 --- a/src/networkdevicestoconnectwidget.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef NETWORKDEVICESTOCONNECTWIDGET_H -#define NETWORKDEVICESTOCONNECTWIDGET_H - -#include "iDescriptor-ui.h" -#include "networkdeviceprovider.h" - -#include -#include -#include -#include -#include -#include - -class NetworkDeviceCard : public QWidget -{ - Q_OBJECT -public: - explicit NetworkDeviceCard(const NetworkDevice &device, - QWidget *parent = nullptr); - -private: - QPushButton *m_connectButton = nullptr; - NetworkDevice m_device; - -public: - void failed(); - void noPairingFile(); - void initStarted(); - void connected(); - void alreadyExists(); - NetworkDevice getDevice() { return m_device; }; -}; - -class NetworkDevicesToConnectWidget : public QWidget -{ - Q_OBJECT - -public: - explicit NetworkDevicesToConnectWidget(QWidget *parent = nullptr); - ~NetworkDevicesToConnectWidget(); - -private slots: - void onWirelessDeviceAdded(const NetworkDevice &device); - void onWirelessDeviceRemoved(const QString &deviceName); - void onNoPairingFileForWirelessDevice(const QString &macAddress); - void onDeviceInitFailed(const QString &udid); - void onDeviceInitStarted(const QString &udid); - void onDeviceAdded(const std::shared_ptr device); - void onDeviceAlreadyExists(const iDescriptor::Uniq &uniq); - -private: - void setupUI(); - void createDeviceCard(const NetworkDevice &device); - void clearDeviceCards(); - void updateDeviceList(); - void eval(); - QGroupBox *m_deviceGroup = nullptr; - QScrollArea *m_scrollArea = nullptr; - QWidget *m_scrollContent = nullptr; - QVBoxLayout *m_deviceLayout = nullptr; - QLabel *m_statusLabel = nullptr; - - QMap m_deviceCards; -}; - -#endif // NETWORKDEVICESTOCONNECTWIDGET_H \ No newline at end of file diff --git a/src/networkdeviceswidget.cpp b/src/networkdeviceswidget.cpp deleted file mode 100644 index 94f865d..0000000 --- a/src/networkdeviceswidget.cpp +++ /dev/null @@ -1,230 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "networkdeviceswidget.h" -#ifdef __linux__ -#include "core/services/avahi/avahi_service.h" -#else -#include "core/services/dnssd/dnssd_service.h" -#endif - -#include -#include -#include -#include -#include - -NetworkDevicesWidget::NetworkDevicesWidget(QWidget *parent) : Tool(parent) -{ - setWindowTitle("Network Devices - iDescriptor"); - setMinimumSize(300, 300); - setMaximumSize(500, 500); - setupUI(); - - connect(NetworkDeviceProvider::sharedInstance(), - &NetworkDeviceProvider::deviceAdded, this, - &NetworkDevicesWidget::onWirelessDeviceAdded); - connect(NetworkDeviceProvider::sharedInstance(), - &NetworkDeviceProvider::deviceRemoved, this, - &NetworkDevicesWidget::onWirelessDeviceRemoved); - - updateDeviceList(); - setMaximumHeight(sizeHint().height()); -} - -NetworkDevicesWidget::~NetworkDevicesWidget() {} - -void NetworkDevicesWidget::setupUI() -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(10, 10, 10, 10); - mainLayout->setSpacing(10); - - // Status label - m_statusLabel = new QLabel("Scanning for network devices..."); - QFont statusFont = m_statusLabel->font(); - statusFont.setPointSize(12); - statusFont.setWeight(QFont::Medium); - m_statusLabel->setFont(statusFont); - m_statusLabel->setAlignment(Qt::AlignCenter); - mainLayout->addWidget(m_statusLabel); - - // Device group - m_deviceGroup = new QGroupBox("Network Devices"); - QFont groupFont = m_deviceGroup->font(); - groupFont.setPointSize(14); - groupFont.setWeight(QFont::Bold); - m_deviceGroup->setFont(groupFont); - - QVBoxLayout *groupLayout = new QVBoxLayout(m_deviceGroup); - groupLayout->setContentsMargins(5, 15, 5, 5); - groupLayout->setSpacing(0); - - // Scroll area - m_scrollArea = new QScrollArea(); - m_scrollArea->setWidgetResizable(true); - m_scrollArea->setMinimumHeight(200); - m_scrollArea->setMaximumHeight(400); - m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - m_scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - - // Scroll content - m_scrollContent = new QWidget(); - m_deviceLayout = new QVBoxLayout(m_scrollContent); - m_deviceLayout->setContentsMargins(5, 5, 5, 5); - m_deviceLayout->setSpacing(8); - m_deviceLayout->addStretch(); - - m_scrollArea->setWidget(m_scrollContent); - groupLayout->addWidget(m_scrollArea); - - mainLayout->addWidget(m_deviceGroup); - mainLayout->addStretch(); -} - -void NetworkDevicesWidget::createDeviceCard(const NetworkDevice &device) -{ - // Main card frame - QWidget *card = new QWidget(); - - QVBoxLayout *cardLayout = new QVBoxLayout(card); - cardLayout->setContentsMargins(12, 10, 12, 10); - cardLayout->setSpacing(4); - - // Device name (primary) - QLabel *nameLabel = new QLabel(device.name); - nameLabel->setWordWrap(true); - QFont nameFont = nameLabel->font(); - nameFont.setPointSize(13); - nameFont.setWeight(QFont::Medium); - nameLabel->setFont(nameFont); - QPalette namePalette = nameLabel->palette(); - namePalette.setColor(QPalette::WindowText, - palette().color(QPalette::WindowText)); - nameLabel->setPalette(namePalette); - - // Device info container - QWidget *infoContainer = new QWidget(); - QHBoxLayout *infoLayout = new QHBoxLayout(infoContainer); - infoLayout->setContentsMargins(0, 0, 0, 0); - infoLayout->setSpacing(12); - - // Address info - QLabel *addressLabel = new QLabel(QString("IP: %1").arg(device.address)); - QFont addressFont = addressLabel->font(); - addressFont.setPointSize(11); - addressLabel->setFont(addressFont); - QPalette addressPalette = addressLabel->palette(); - QColor secondaryColor = palette().color(QPalette::WindowText); - secondaryColor.setAlpha(180); - addressPalette.setColor(QPalette::WindowText, secondaryColor); - addressLabel->setPalette(addressPalette); - - // Port info - QLabel *portLabel = new QLabel(QString("Port: %1").arg(device.port)); - portLabel->setFont(addressFont); - portLabel->setPalette(addressPalette); - - infoLayout->addWidget(addressLabel); - infoLayout->addWidget(portLabel); - infoLayout->addStretch(); - - QLabel *statusIndicator = new QLabel("●"); - statusIndicator->setStyleSheet( - QString("QLabel { font-size: 14px; color: %1; }") -#ifdef WIN32 - .arg(COLOR_ACCENT_BLUE.name())); -#else - .arg(COLOR_GREEN.name())); -#endif - - infoLayout->addWidget(statusIndicator); - - cardLayout->addWidget(nameLabel); - cardLayout->addWidget(infoContainer); - - // Store the device info as property for later removal - card->setProperty("deviceName", device.name); - card->setProperty("deviceAddress", device.address); - - // Insert before the stretch - m_deviceLayout->insertWidget(m_deviceLayout->count() - 1, card); - m_deviceCards.append(card); -} - -void NetworkDevicesWidget::clearDeviceCards() -{ - for (QWidget *card : m_deviceCards) { - card->deleteLater(); - } - m_deviceCards.clear(); -} - -void NetworkDevicesWidget::updateDeviceList() -{ - clearDeviceCards(); - - QMap devices = - NetworkDeviceProvider::sharedInstance()->getNetworkDevices(); - - if (devices.isEmpty()) { - m_statusLabel->setText("No network devices found"); - } else { - m_statusLabel->setText( - QString("Found %1 network device(s)").arg(devices.count())); - - for (const NetworkDevice &device : devices) { - createDeviceCard(device); - } - } -} - -void NetworkDevicesWidget::onWirelessDeviceAdded(const NetworkDevice &device) -{ - createDeviceCard(device); - - // Update status - int deviceCount = m_deviceCards.count(); - m_statusLabel->setText( - QString("Found %1 network device(s)").arg(deviceCount)); -} - -void NetworkDevicesWidget::onWirelessDeviceRemoved(const QString &deviceName) -{ - // Find and remove the corresponding card - for (int i = 0; i < m_deviceCards.count(); ++i) { - QWidget *card = m_deviceCards[i]; - if (card->property("deviceName").toString() == deviceName) { - m_deviceCards.removeAt(i); - card->deleteLater(); - break; - } - } - - // Update status - int deviceCount = m_deviceCards.count(); - if (deviceCount == 0) { - m_statusLabel->setText("No network devices found"); - } else { - m_statusLabel->setText( - QString("Found %1 network device(s)").arg(deviceCount)); - } -} \ No newline at end of file diff --git a/src/networkdeviceswidget.h b/src/networkdeviceswidget.h deleted file mode 100644 index ec1226c..0000000 --- a/src/networkdeviceswidget.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef NETWORKDEVICESWIDGET_H -#define NETWORKDEVICESWIDGET_H - -#include "iDescriptor-ui.h" -#include "networkdeviceprovider.h" -#include -#include -#include -#include -#include -#include - -class NetworkDevicesWidget : public Tool -{ - Q_OBJECT - -public: - explicit NetworkDevicesWidget(QWidget *parent = nullptr); - ~NetworkDevicesWidget(); - -private slots: - void onWirelessDeviceAdded(const NetworkDevice &device); - void onWirelessDeviceRemoved(const QString &deviceName); - -private: - void setupUI(); - void createDeviceCard(const NetworkDevice &device); - void clearDeviceCards(); - void updateDeviceList(); - - QGroupBox *m_deviceGroup = nullptr; - QScrollArea *m_scrollArea = nullptr; - QWidget *m_scrollContent = nullptr; - QVBoxLayout *m_deviceLayout = nullptr; - QLabel *m_statusLabel = nullptr; - - QList m_deviceCards; - -protected: - void changeEvent(QEvent *event) override - { - if (event->type() == QEvent::PaletteChange) { - if (m_scrollArea) { - m_scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - } - } - QWidget::changeEvent(event); - }; -}; - -#endif // NETWORKDEVICESWIDGET_H \ No newline at end of file diff --git a/src/photoimportdialog.cpp b/src/photoimportdialog.cpp deleted file mode 100644 index 6a8248b..0000000 --- a/src/photoimportdialog.cpp +++ /dev/null @@ -1,286 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "photoimportdialog.h" -#include "httpserver.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -PhotoImportDialog::PhotoImportDialog(const QStringList &files, QWidget *parent) - : QDialog(parent), selectedFiles(files) -{ - setupUI(); - setModal(true); - resize(600, 600); - setWindowTitle("Import Photos to iDevice - iDescriptor"); -#ifdef WIN32 - setupWinWindow(this); -#endif -} - -PhotoImportDialog::~PhotoImportDialog() -{ - if (m_mediaPlayer) { - m_mediaPlayer->stop(); - delete m_mediaPlayer; - } - if (m_httpServer) { - m_httpServer->stop(); - delete m_httpServer; - } -} - -void PhotoImportDialog::setupUI() -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // File list - QLabel *listLabel = new QLabel( - QString("Files to be served (%1 items):").arg(selectedFiles.size()), - this); - mainLayout->addWidget(listLabel); - - fileList = new QListWidget(this); - fileList->setMaximumHeight(150); - for (const QString &file : selectedFiles) { - QFileInfo info(file); - fileList->addItem(info.fileName()); - } - mainLayout->addWidget(fileList); - - // Horizontal layout for QR code and instructions - QHBoxLayout *contentLayout = new QHBoxLayout(); - - // QR Code area - qrCodeLabel = new QLabel(this); - qrCodeLabel->setAlignment(Qt::AlignCenter); - qrCodeLabel->setMinimumSize(200, 200); - qrCodeLabel->setMaximumSize(200, 200); - qrCodeLabel->setText("QR Code will appear here after starting server"); - contentLayout->addWidget(qrCodeLabel); - - // Instructions container - QVBoxLayout *instructionContainer = new QVBoxLayout(); - - // Stacked widget for switchable instructions - m_instructionStack = new QStackedWidget(this); - m_instructionStack->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - - // Text instructions - m_instructionLabel = new QLabel("Loading", this); - m_instructionLabel->setWordWrap(true); - m_instructionStack->addWidget(m_instructionLabel); - - // Video instructions - m_instructionVideo = new QVideoWidget(this); - m_instructionVideo->setMinimumSize(300, 500); - m_instructionVideo->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - m_mediaPlayer = new QMediaPlayer(this); - m_mediaPlayer->setVideoOutput(m_instructionVideo); - m_instructionStack->addWidget(m_instructionVideo); - - m_instructionVideo->setAspectRatioMode( - Qt::AspectRatioMode::KeepAspectRatioByExpanding); - m_instructionVideo->setStyleSheet( - "QVideoWidget { background-color: transparent; }"); - - instructionContainer->addWidget(m_instructionStack); - - // Toggle button - m_toggleInstructionButton = - new QPushButton("Show Video Instructions", this); - connect(m_toggleInstructionButton, &QPushButton::clicked, this, - &PhotoImportDialog::toggleInstructionMode); - - instructionContainer->addSpacing(10); - - QHBoxLayout *buttonContainer = new QHBoxLayout(); - buttonContainer->addStretch(); - buttonContainer->addWidget(m_toggleInstructionButton); - buttonContainer->addStretch(); - instructionContainer->addLayout(buttonContainer); - - contentLayout->addLayout(instructionContainer); - mainLayout->addLayout(contentLayout); - - // Progress tracking - m_progressLabel = new QLabel("Download progress will appear here", this); - m_progressLabel->setVisible(false); - mainLayout->addWidget(m_progressLabel, Qt::AlignCenter); - - mainLayout->addSpacing(5); - - m_serverAddress = new QLabel("", this); - m_serverAddress->setVisible(false); - mainLayout->addWidget(m_serverAddress, Qt::AlignCenter); - - // Buttons - QHBoxLayout *buttonLayout = new QHBoxLayout(); - m_cancelButton = new QPushButton("Cancel", this); - - buttonLayout->addStretch(); - buttonLayout->addWidget(m_cancelButton); - - mainLayout->addLayout(buttonLayout); - - connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); - - // Setup video looping - connect(m_mediaPlayer, - QOverload::of( - &QMediaPlayer::mediaStatusChanged), - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::EndOfMedia) { - m_mediaPlayer->setPosition(0); - m_mediaPlayer->play(); - } - }); - - QTimer::singleShot(0, this, &PhotoImportDialog::init); -} - -void PhotoImportDialog::init() -{ - - // Create and start HTTP server - m_httpServer = new HttpServer(this); - connect(m_httpServer, &HttpServer::serverStarted, this, - &PhotoImportDialog::onServerStarted); - connect(m_httpServer, &HttpServer::serverError, this, - &PhotoImportDialog::onServerError); - connect(m_httpServer, &HttpServer::downloadProgress, this, - &PhotoImportDialog::onDownloadProgress); - - m_httpServer->start(selectedFiles); -} - -void PhotoImportDialog::onServerStarted() -{ - - QString localIP = HttpServer::getLocalIP(); - int port = m_httpServer->getPort(); - QString jsonFileName = m_httpServer->getJsonFileName(); - QString url = - QString("https://idescriptor.github.io/import?local=%1&port=%2&file=%3") - .arg(localIP) - .arg(port) - .arg(jsonFileName); - qDebug() << "Server url" << url; - - generateQRCode(url); - - m_instructionLabel->setText( - "Instructions on How to Import\n\n1.Scan the QR code to open the " - "web interface\n2.Click on \"Copy Server Address\"\n3.Click on " - "\"Import and Run Shortcut\" if you have not installed the " - "shortcut before or \"Run Shortcut\" if you have installed it " - "before. \n4.Run the shortcut in the Shortcuts app. Once the " - "shortcut imports to your device, it will automatically run " - "\"Photos app\" \n\n Switch to video tutorial if you want to see a " - "video tutorial."); - - m_mediaPlayer->setSource( - QUrl("qrc:/resources/wireless-gallery-import.mp4")); - - m_progressLabel->setText("Waiting for downloads..."); - m_progressLabel->setVisible(true); - - m_serverAddress->setText( - QString("Server started at %1:%2").arg(localIP).arg(port)); - m_serverAddress->setVisible(true); -} - -void PhotoImportDialog::onDownloadProgress(const QString &fileName, - int bytesDownloaded, int totalBytes) -{ - // TODO: bring in a progress bar each item - m_progressLabel->setText(QString("Downloaded: %1 (%2 KB)") - .arg(fileName) - .arg(bytesDownloaded / 1024)); -} - -void PhotoImportDialog::onServerError(const QString &error) -{ - m_cancelButton->setEnabled(true); - - QMessageBox::critical(this, "Server Error", - QString("Failed to start server: %1").arg(error)); - QDialog::reject(); -} - -void PhotoImportDialog::generateQRCode(const QString &url) -{ - QRcode *qrcode = QRcode_encodeString(url.toUtf8().constData(), 0, - QR_ECLEVEL_M, QR_MODE_8, 1); - if (!qrcode) { - qrCodeLabel->setText("Failed to generate QR code"); - return; - } - - int qrSize = 200; - int qrWidth = qrcode->width; - int scale = qrSize / qrWidth; - if (scale < 1) - scale = 1; - - QPixmap qrPixmap(qrWidth * scale, qrWidth * scale); - qrPixmap.fill(Qt::white); - - QPainter painter(&qrPixmap); - painter.setPen(Qt::NoPen); - painter.setBrush(Qt::black); - - for (int y = 0; y < qrWidth; y++) { - for (int x = 0; x < qrWidth; x++) { - if (qrcode->data[y * qrWidth + x] & 1) { - QRect rect(x * scale, y * scale, scale, scale); - painter.drawRect(rect); - } - } - } - - qrCodeLabel->setPixmap(qrPixmap); - QRcode_free(qrcode); -} - -void PhotoImportDialog::toggleInstructionMode() -{ - if (m_instructionStack->currentIndex() == 0) { - // Switch to video - m_instructionStack->setCurrentIndex(1); - m_toggleInstructionButton->setText("Show Text Instructions"); - m_mediaPlayer->play(); - } else { - // Switch to text - m_instructionStack->setCurrentIndex(0); - m_toggleInstructionButton->setText("Show Video Instructions"); - m_mediaPlayer->stop(); - } -} diff --git a/src/photoimportdialog.h b/src/photoimportdialog.h deleted file mode 100644 index 4109d93..0000000 --- a/src/photoimportdialog.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef PHOTOIMPORTDIALOG_H -#define PHOTOIMPORTDIALOG_H - -#include "httpserver.h" -#include "iDescriptor-ui.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class PhotoImportDialog : public QDialog -{ - Q_OBJECT - -public: - explicit PhotoImportDialog(const QStringList &files, - QWidget *parent = nullptr); - ~PhotoImportDialog(); - -private slots: - void init(); - void onServerStarted(); - void onServerError(const QString &error); - void onDownloadProgress(const QString &fileName, int bytesDownloaded, - int totalBytes); - void toggleInstructionMode(); - -private: - QStringList selectedFiles; - - QListWidget *fileList; - QLabel *qrCodeLabel; - QStackedWidget *m_instructionStack; - QLabel *m_instructionLabel; - QVideoWidget *m_instructionVideo; - QMediaPlayer *m_mediaPlayer; - QPushButton *m_toggleInstructionButton; - QPushButton *m_cancelButton; - QLabel *m_progressLabel; - QLabel *m_serverAddress; - - HttpServer *m_httpServer; - - void setupUI(); - void generateQRCode(const QString &url); -}; - -#endif // PHOTOIMPORTDIALOG_H diff --git a/src/photomodel.cpp b/src/photomodel.cpp deleted file mode 100644 index 2266f1f..0000000 --- a/src/photomodel.cpp +++ /dev/null @@ -1,343 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "photomodel.h" -#include "iDescriptor.h" -#include "imageloader.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -PhotoModel::PhotoModel(const std::shared_ptr device, - FilterType filterType, QObject *parent) - : QAbstractListModel(parent), m_device(device), m_sortOrder(NewestFirst), - m_filterType(filterType) -{ - // 350 MB cache for thumbnails - m_cache.setMaxCost(350 * 1024 * 1024); -} - -void PhotoModel::clear() -{ - QMutexLocker locker(&m_mutex); - disconnect(&ImageLoader::sharedInstance(), &ImageLoader::thumbnailReady, - this, &PhotoModel::onThumbnailReady); - - beginResetModel(); - m_photos.clear(); - m_allPhotos.clear(); - endResetModel(); - m_cache.clear(); - qDebug() << "Cleared PhotoModel data"; -} - -PhotoModel::~PhotoModel() -{ - qDebug() << "PhotoModel destructor called"; - clear(); -} - -int PhotoModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent) - return m_photos.size(); -} - -QVariant PhotoModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() >= m_photos.size()) - return QVariant(); - - const PhotoInfo &info = m_photos.at(index.row()); - - switch (role) { - case Qt::DisplayRole: - return info.fileName; - - case Qt::UserRole: - return info.filePath; - - case Qt::SizeHintRole: - return QSize(210, 260); - - case Qt::DecorationRole: { - ImageLoader &imgloader = ImageLoader::sharedInstance(); - // Check cache first - if (QPixmap *cached = m_cache.object(info.filePath)) { - return QIcon(*cached); - } - - if (imgloader.isLoading(info.filePath)) { - if (iDescriptor::Utils::isVideoFile(info.fileName)) { - return QIcon(":/resources/icons/video-x-generic.png"); - } else { - return QIcon(":/resources/icons/" - "MaterialSymbolsLightImageOutlineSharp.png"); - } - } - - imgloader.requestThumbnail(m_device, info.filePath, index.row()); - - if (iDescriptor::Utils::isVideoFile(info.fileName)) { - return QIcon(":/resources/icons/video-x-generic.png"); - } else { - return QIcon( - ":/resources/icons/MaterialSymbolsLightImageOutlineSharp.png"); - } - } - - case Qt::ToolTipRole: - return QString("Photo: %1").arg(info.fileName); - - default: - return QVariant(); - } -} - -void PhotoModel::onThumbnailReady(const QString &path, const QPixmap &pixmap, - unsigned int rowHint) -{ - Q_UNUSED(pixmap); - int cacheCost = pixmap.width() * pixmap.height() * pixmap.depth() / 8; - m_cache.insert(path, new QPixmap(pixmap), cacheCost); - QMutexLocker locker(&m_mutex); - - int row = -1; - - // if row hint still valid and matches this path - if (rowHint < static_cast(m_photos.size()) && - m_photos.at(static_cast(rowHint)).filePath == path) { - row = static_cast(rowHint); - } else { - // fallback: search by path in current model - for (int i = 0; i < m_photos.size(); ++i) { - if (m_photos.at(i).filePath == path) { - row = i; - break; - } - } - } - - if (row == -1) { - // Thumbnail arrived for an item that is no longer in the model - qDebug() << "PhotoModel::onThumbnailReady: path not in current model"; - return; - } - - QModelIndex idx = createIndex(row, 0); - locker.unlock(); // avoid holding mutex while emitting - emit dataChanged(idx, idx, {Qt::DecorationRole}); -} - -QPair> -PhotoModel::populatePhotoPaths(QString albumPath, - std::shared_ptr device) -{ - - if (albumPath.isEmpty()) { - qDebug() << "No album path set, skipping population"; - return {false, {}}; - } - - QMap photoPaths = - device->afc_backend->list_dir_with_creation_date(albumPath); - - QList photos; - for (auto it = photoPaths.constBegin(); it != photoPaths.constEnd(); ++it) { - const QString &fileName = it.key(); - const QVariant &creationDateVariant = it.value(); - - if (iDescriptor::Utils::isGalleryFile(fileName)) { - PhotoInfo info; - info.filePath = albumPath + "/" + fileName; - info.fileName = fileName; - info.thumbnailRequested = false; - info.dateTime = creationDateVariant.toDateTime(); - info.fileType = determineFileType(fileName); - photos.append(info); - } - } - - return {true, photos}; -} - -// Sorting and filtering methods -void PhotoModel::setSortOrder(SortOrder order) -{ - if (m_sortOrder != order) { - m_sortOrder = order; - applyFilterAndSort(); - } -} - -void PhotoModel::setFilterType(FilterType filter) -{ - if (m_filterType != filter) { - m_filterType = filter; - applyFilterAndSort(); - } -} - -void PhotoModel::applyFilterAndSort() -{ - QMutexLocker locker(&m_mutex); - beginResetModel(); - - // Filter photos - m_photos.clear(); - for (const PhotoInfo &info : m_allPhotos) { - if (matchesFilter(info)) { - m_photos.append(info); - } - } - - // Sort photos - sortPhotos(m_photos); - - endResetModel(); - - qDebug() << "Applied filter and sort - showing" << m_photos.size() << "of" - << m_allPhotos.size() << "items"; -} - -void PhotoModel::sortPhotos(QList &photos) const -{ - std::sort(photos.begin(), photos.end(), - [this](const PhotoInfo &a, const PhotoInfo &b) { - if (m_sortOrder == NewestFirst) { - return a.dateTime > b.dateTime; - } else { - return a.dateTime < b.dateTime; - } - }); -} - -bool PhotoModel::matchesFilter(const PhotoInfo &info) const -{ - switch (m_filterType) { - case All: - return true; - case ImagesOnly: - return info.fileType == PhotoInfo::Image; - case VideosOnly: - return info.fileType == PhotoInfo::Video; - default: - return true; - } -} - -// Export functionality -QStringList -PhotoModel::getSelectedFilePaths(const QModelIndexList &indexes) const -{ - QStringList paths; - for (const QModelIndex &index : indexes) { - if (index.isValid() && index.row() < m_photos.size()) { - paths.append(m_photos.at(index.row()).filePath); - } - } - return paths; -} - -QString PhotoModel::getFilePath(const QModelIndex &index) const -{ - if (index.isValid() && index.row() < m_photos.size()) { - return m_photos.at(index.row()).filePath; - } - return QString(); -} - -QStringList PhotoModel::getAllFilePaths() const -{ - QStringList paths; - for (const PhotoInfo &info : m_allPhotos) { - paths.append(info.filePath); - } - return paths; -} - -QList PhotoModel::getFilteredFilePaths() const -{ - QList paths; - for (const PhotoInfo &info : m_photos) { - paths.append(info.filePath); - } - return paths; -} - -PhotoInfo::FileType PhotoModel::determineFileType(const QString &fileName) -{ - if (iDescriptor::Utils::isVideoFile(fileName)) - return PhotoInfo::Video; - return PhotoInfo::Image; -} - -void PhotoModel::setAlbumPath(const QString &albumPath) -{ - qDebug() << "Setting new album path:" << albumPath; - - clear(); - connect(&ImageLoader::sharedInstance(), &ImageLoader::thumbnailReady, this, - &PhotoModel::onThumbnailReady, Qt::UniqueConnection); - - m_albumPath = albumPath; - - const auto device = m_device; - - auto *futureWatcher = - new QFutureWatcher>>(this); - - QFuture>> future = - QtConcurrent::run([albumPath, device]() { - return populatePhotoPaths(albumPath, device); - }); - - futureWatcher->setFuture(future); - - connect(futureWatcher, - &QFutureWatcher>>::finished, this, - [this, futureWatcher]() { - const auto result = futureWatcher->result(); - futureWatcher->deleteLater(); - - const bool success = result.first; - const QList photos = result.second; - - m_allPhotos = photos; - if (success) { - qDebug() << "Finished populating photo paths for album:" - << m_albumPath; - applyFilterAndSort(); - emit albumPathSet(); - } else { - qDebug() << "Failed to populate photo paths for album:" - << m_albumPath; - emit albumPathSetFailed(); - } - }); -} \ No newline at end of file diff --git a/src/photomodel.h b/src/photomodel.h deleted file mode 100644 index 680a20f..0000000 --- a/src/photomodel.h +++ /dev/null @@ -1,116 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef PHOTOMODEL_H -#define PHOTOMODEL_H - -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct PhotoInfo { - QString filePath; - QString fileName; - QDateTime dateTime; - bool thumbnailRequested = false; - - enum FileType { Image, Video }; - FileType fileType; -}; - -class PhotoModel : public QAbstractListModel -{ - Q_OBJECT - -public: - enum SortOrder { NewestFirst, OldestFirst }; - - enum FilterType { All, ImagesOnly, VideosOnly }; - - explicit PhotoModel(const std::shared_ptr device, - FilterType filterType, QObject *parent = nullptr); - ~PhotoModel(); - - // QAbstractItemModel interface - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, - int role = Qt::DisplayRole) const override; - - // Album management - void setAlbumPath(const QString &albumPath); - - // Sorting and filtering - void setSortOrder(SortOrder order); - SortOrder sortOrder() const { return m_sortOrder; } - - void setFilterType(FilterType filter); - FilterType filterType() const { return m_filterType; } - - // Export functionality - QStringList getSelectedFilePaths(const QModelIndexList &indexes) const; - QString getFilePath(const QModelIndex &index) const; - - // Get all items for export - QStringList getAllFilePaths() const; - QList getFilteredFilePaths() const; - - void clear(); - -private: - const std::shared_ptr m_device; - QString m_albumPath; - QList m_allPhotos; // All photos from device - QList m_photos; // Currently filtered/sorted photos - - // Sorting and filtering - SortOrder m_sortOrder; - FilterType m_filterType; - - QMutex m_mutex; - QCache m_cache; - - // Helper methods - static QPair> - populatePhotoPaths(QString albumPath, - std::shared_ptr device); - void applyFilterAndSort(); - void sortPhotos(QList &photos) const; - bool matchesFilter(const PhotoInfo &info) const; - - static PhotoInfo::FileType determineFileType(const QString &fileName); - -private slots: - void onThumbnailReady(const QString &path, const QPixmap &pixmap, - unsigned int row); - -signals: - void albumPathSet(); - void albumPathSetFailed(); -}; - -#endif // PHOTOMODEL_H \ No newline at end of file diff --git a/src/privateinfolabel.cpp b/src/privateinfolabel.cpp deleted file mode 100644 index 4a92719..0000000 --- a/src/privateinfolabel.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "privateinfolabel.h" - -PrivateInfoLabel::PrivateInfoLabel(const QString &fullText, QWidget *parent) - : QWidget(parent), m_fullText(fullText), m_isVisible(false) -{ - m_maskedText = getMaskedText(fullText); - - QHBoxLayout *layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(5); - - m_textLabel = new InfoLabel(m_maskedText, m_fullText, this); - layout->addWidget(m_textLabel); - - m_toggleButton = new ZIconWidget( - QIcon(":/resources/icons/ClarityEyeHideLine.png"), "Show", 1.0, this); - m_toggleButton->setIconSize(QSize(20, 20)); - connect(m_toggleButton, &ZIconWidget::clicked, this, - &PrivateInfoLabel::toggleVisibility); - layout->addWidget(m_toggleButton); - - layout->addStretch(); -} - -QString PrivateInfoLabel::getMaskedText(const QString &text) -{ - if (text.length() <= 4) { - return QString("*").repeated(text.length()); - } - // Show first 4 characters, hide the rest - return text.left(4) + QString("*").repeated(text.length() - 4); -} - -void PrivateInfoLabel::toggleVisibility() -{ - m_isVisible = !m_isVisible; - if (m_isVisible) { - m_textLabel->setText(m_fullText); - m_toggleButton->setIcon(QIcon(":/resources/icons/ClarityEyeLine.png")); - m_toggleButton->setToolTip("Hide"); - } else { - m_textLabel->setText(m_maskedText); - m_toggleButton->setIcon( - QIcon(":/resources/icons/ClarityEyeHideLine.png")); - m_toggleButton->setToolTip("Show"); - } -} diff --git a/src/privateinfolabel.h b/src/privateinfolabel.h deleted file mode 100644 index 3ef5f92..0000000 --- a/src/privateinfolabel.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef PRIVATEINFOLABEL_H -#define PRIVATEINFOLABEL_H - -#include "iDescriptor-ui.h" -#include "infolabel.h" -#include -#include - -class PrivateInfoLabel : public QWidget -{ - Q_OBJECT - -public: - explicit PrivateInfoLabel(const QString &fullText, - QWidget *parent = nullptr); - -private slots: - void toggleVisibility(); - -private: - QString m_fullText; - QString m_maskedText; - bool m_isVisible; - InfoLabel *m_textLabel; - ZIconWidget *m_toggleButton; - - QString getMaskedText(const QString &text); -}; - -#endif // PRIVATEINFOLABEL_H diff --git a/src/qballoontip.cpp b/src/qballoontip.cpp deleted file mode 100644 index 385c308..0000000 --- a/src/qballoontip.cpp +++ /dev/null @@ -1,270 +0,0 @@ -#include "qballoontip.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -void QBalloonTip::toggleBaloon(const QPoint &pos, int timeout, - bool forceVisible) -{ - if (m_visible && !forceVisible) { - hideBalloon(); - return; - } - - if (timeout < 0) - timeout = 10000; // 10 s default - balloon(pos, timeout); -} - -void QBalloonTip::hideBalloon() -{ - m_visible = false; - hide(); -} - -void QBalloonTip::updateBalloonPosition(const QPoint &pos) -{ - hideBalloon(); - balloon(pos, 0); -} - -bool QBalloonTip::isBalloonVisible() { return m_visible; } - -QBalloonTip::QBalloonTip(QWidget *widget) - : QWidget(widget ? widget->window() : QApplication::activeWindow(), - Qt::ToolTip), - widget(widget) -{ - setObjectName("balloonTip"); -#ifdef WIN32 - setAttribute(Qt::WA_TranslucentBackground); -#else - setAttribute(Qt::WA_StyledBackground, true); - setStyleSheet("QWidget#balloonTip { " - " background-color: transparent;" - "}"); -#endif - - if (widget) { - connect(widget, &QWidget::destroyed, this, &QBalloonTip::close); - } else if (QApplication::activeWindow()) { - connect(QApplication::activeWindow(), &QWidget::destroyed, this, - &QBalloonTip::close); - } - - // Add drop shadow effect - // QGraphicsDropShadowEffect *shadowEffect = - // new QGraphicsDropShadowEffect(this); - // shadowEffect->setBlurRadius(200); - // shadowEffect->setColor(QColor(0, 0, 0, 80)); - // shadowEffect->setOffset(0, 5); - // setGraphicsEffect(shadowEffect); - - // QLabel *titleLabel = new QLabel; - // titleLabel->installEventFilter(this); - // titleLabel->setText(title); - // QFont f = titleLabel->font(); - // f.setBold(true); - // titleLabel->setFont(f); - // titleLabel->setTextFormat(Qt::PlainText); // to maintain compat with - // windows - - // const int iconSize = 18; - // const int closeButtonSize = 15; - - // QPushButton *closeButton = new QPushButton; - // closeButton->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton)); - // closeButton->setIconSize(QSize(closeButtonSize, closeButtonSize)); - // closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - // closeButton->setFixedSize(closeButtonSize, closeButtonSize); - // QObject::connect(closeButton, SIGNAL(clicked()), this, SLOT(close())); - - // QLabel *msgLabel = new QLabel; - // msgLabel->installEventFilter(this); - // msgLabel->setText(message); - // msgLabel->setTextFormat(Qt::PlainText); - // msgLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); - - // QGridLayout *layout = new QGridLayout; - // if (!icon.isNull()) { - // QLabel *iconLabel = new QLabel; - // iconLabel->setPixmap( - // icon.pixmap(QSize(iconSize, iconSize), devicePixelRatio())); - // iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - // iconLabel->setMargin(2); - // layout->addWidget(iconLabel, 0, 0); - // layout->addWidget(titleLabel, 0, 1); - // } else { - // layout->addWidget(titleLabel, 0, 0, 1, 2); - // } - - // layout->addWidget(closeButton, 0, 2); - - // layout->addWidget(msgLabel, 1, 0, 1, 3); - // layout->setSizeConstraint(QLayout::SetFixedSize); - // layout->setContentsMargins(3, 3, 3, 3); - // setLayout(layout); -} - -QBalloonTip::~QBalloonTip() {} - -void QBalloonTip::paintEvent(QPaintEvent *ev) -{ - // QPainter painter(this); - // painter.drawPixmap(rect(), pixmap); - QWidget::paintEvent(ev); -} - -void QBalloonTip::resizeEvent(QResizeEvent *ev) { QWidget::resizeEvent(ev); } - -void QBalloonTip::balloon(const QPoint &pos, int msecs) -{ - m_visible = true; - qApp->installEventFilter(this); - - QScreen *screen = QGuiApplication::screenAt(pos); - if (!screen) { - screen = QGuiApplication::primaryScreen(); - } - QRect screenRect = screen->availableGeometry(); - - // Ensure layout is up to date so we get the correct size - ensurePolished(); - adjustSize(); - - QSize balloonSize = size(); - if (!balloonSize.isValid() || balloonSize.isEmpty()) { - balloonSize = sizeHint(); - } - - // Arrow dimensions - const int arrowGap = 40; // gap between balloon and anchor - const int margin = 5; // margin from screen edges - - // Calculate horizontal position - center on the anchor point - int balloonX = pos.x() - balloonSize.width() / 2; - - // Clamp to screen bounds with margin - balloonX = qBound(screenRect.left() + margin, balloonX, - screenRect.right() - balloonSize.width() - margin); - - // Calculate vertical position - prefer above the anchor point - int balloonY; - int spaceAbove = pos.y() - screenRect.top(); - - if (spaceAbove >= balloonSize.height() + arrowGap + margin) { - // Show above the anchor point (preferred) - balloonY = pos.y() - balloonSize.height() - arrowGap; - } else { - // Not enough space above, show below - balloonY = pos.y() + arrowGap; - } - - balloonY = qBound(screenRect.top() + margin, balloonY, - screenRect.bottom() - balloonSize.height() - margin); - - setGeometry(balloonX, balloonY, balloonSize.width(), balloonSize.height()); - - show(); - raise(); - activateWindow(); -} - -void QBalloonTip::mousePressEvent(QMouseEvent *e) -{ - if (e->button() == Qt::LeftButton) { - emit messageClicked(); - } -} - -void QBalloonTip::timerEvent(QTimerEvent *e) -{ - if (e->timerId() == timer.timerId()) { - timer.stop(); - if (!underMouse()) - close(); - return; - } - QWidget::timerEvent(e); -} - -bool QBalloonTip::eventFilter(QObject *obj, QEvent *event) -{ - if (event->type() == QEvent::MouseButtonPress) { - QMouseEvent *mouseEvent = static_cast(event); - - if (m_button) { - if (QWidget *clickedWidget = qobject_cast(obj)) { - if (clickedWidget == m_button || - m_button->isAncestorOf(clickedWidget)) { - return false; - } - } - } - - if (m_visible && !geometry().contains(mouseEvent->globalPos())) { - m_visible = false; - close(); - return true; - } - } else if (event->type() == QEvent::WindowDeactivate) { - // Close when window loses focus - if (obj == this) { - m_visible = false; - close(); - return false; - } - } else if (event->type() == QEvent::ApplicationDeactivate) { - // App went to background → hide balloon - if (m_visible) { - m_visible = false; - close(); - } - return false; // let others handle it too - } - - // handle macOS tab switch and Escape key handling - else if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); -#ifdef __APPLE__ - if (auto *ke = dynamic_cast(event)) { - const Qt::KeyboardModifiers mods = ke->modifiers(); - if ((mods & (Qt::MetaModifier | Qt::ControlModifier))) { - if (m_visible) { - m_visible = false; - close(); - return true; - } - } - } -#endif - if (keyEvent->key() == Qt::Key_Escape) { - if (m_visible) { - m_visible = false; - close(); - return true; - } - } - } - - return QWidget::eventFilter(obj, event); -} - -void QBalloonTip::hideEvent(QHideEvent *event) -{ - // Remove event filter when hiding - qApp->removeEventFilter(this); - QWidget::hideEvent(event); -} diff --git a/src/qballoontip.h b/src/qballoontip.h deleted file mode 100644 index a26f5fd..0000000 --- a/src/qballoontip.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef QBALLOONTIP_H -#define QBALLOONTIP_H - -#include "iDescriptor-ui.h" -#include -#include -#include -#include - -class ZStatusIconWidget : public ZIconWidget -{ - Q_OBJECT -public: - using ZIconWidget::ZIconWidget; - - void setIndicatorVisible(bool visible) - { - if (m_indicatorVisible == visible) - return; - m_indicatorVisible = visible; - update(); - } - - bool isIndicatorVisible() const { return m_indicatorVisible; } - -protected: - void paintEvent(QPaintEvent *event) override - { - ZIconWidget::paintEvent(event); - - if (!m_indicatorVisible) - return; - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing, true); - - const int radius = 5; - const int margin = 3; - - QPoint center(width() - radius - margin, radius + margin); - p.setBrush(COLOR_ACCENT_BLUE); - p.setPen(Qt::NoPen); - p.drawEllipse(center, radius, radius); - } - -private: - bool m_indicatorVisible = false; -}; - -class QBalloonTip : public QWidget -{ - Q_OBJECT -public: - explicit QBalloonTip(QWidget *widget); - void hideBalloon(); - bool isBalloonVisible(); - void updateBalloonPosition(const QPoint &pos); - void toggleBaloon(const QPoint &pos, int timeout, bool forceVisible); - void balloon(const QPoint &, int msecs); - ZStatusIconWidget *getButton() { return m_button; } - ZStatusIconWidget *m_button = new ZStatusIconWidget( - QIcon(":/resources/icons/UimProcess.png"), "Processes"); - -signals: - void messageClicked(); - -protected: - ~QBalloonTip(); - void paintEvent(QPaintEvent *) override; - void resizeEvent(QResizeEvent *) override; - void mousePressEvent(QMouseEvent *e) override; - void timerEvent(QTimerEvent *e) override; - bool eventFilter(QObject *obj, QEvent *event) override; - void hideEvent(QHideEvent *event) override; - -private: - QWidget *widget; - QBasicTimer timer; - bool m_visible = false; -}; -#endif // QBALLOONTIP_H \ No newline at end of file diff --git a/src/rust/src/qinput_get_text.cc b/src/qinput_get_text.cc similarity index 100% rename from src/rust/src/qinput_get_text.cc rename to src/qinput_get_text.cc diff --git a/src/qmetaobject_rust.hpp b/src/qmetaobject_rust.hpp new file mode 100644 index 0000000..f3979a5 --- /dev/null +++ b/src/qmetaobject_rust.hpp @@ -0,0 +1,237 @@ +/* Copyright (C) 2018 Olivier Goffart + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#pragma once + +#include +#include +#include + +/// Pointer to a method of QObject which takes no arguments and returns nothing. +/// Actually this is a "type-erased" method with various arguments and return +/// value, but it merely represents a generic pointer, and let other code +/// handle the types and memory safety. +using QObjectErasedMethod = void (QObject::*)(); + +/// This type represents signals defined both in C++ and Rust, and provides +/// handy conversions. +/// +/// Internally Qt signals are represented by some ID which must be unique +/// within class hierarchy (not to be confused with indices). So, things +/// like `&QObject::objectNameChanged` are only meaningful as far as they +/// can be converted to some kind of magic representation (`void **`), and +/// those representations can be compared for equality. +/// +/// In C++, signals are represented as pointers to member functions, but with +/// types erased down to `void (QObject::*)()`. +/// From `QMetaObject::Connection QObject::connectImpl(...)` documentation: +/// > signal is a pointer to a pointer to a member signal of the sender +/// +/// For classes defined in Rust, signals are represented as offsets of +/// corresponding `RustSignal` fields from the base address of their struct. +/// This provides an easy way to guarantee ID uniqueness at a low price of +/// having one bool field per signal. +/// +/// # Safety +/// +/// Users of `SignalInner` must ensure that they only ever use a +/// signal of one class with instances of that class or its subclasses. +/// +/// Erased `cpp_erased_method` is not directly used as a function pointer +/// anyway, so it is safe even if it contains garbage. +/// +/// # Further reading +/// +/// - http://itanium-cxx-abi.github.io/cxx-abi/abi.html#member-pointers +/// - https://docs.microsoft.com/en-us/cpp/cpp/pointers-to-members?view=vs-2019 +union SignalInner { + // No need to be public. Pointer to a signal is exposed via safe public + // getter. +private: + /// For signals derived from `RustSignal` Rust structs, e.g. + /// `greeter.name_changed`. + ptrdiff_t rust_field_offset; + /// For signals defined in C++ classes, e.g. `&QObject::objectNameChanged`. + QObjectErasedMethod cpp_erased_method; + +public: + /// Construct signal representation for an arbitrary Qt signal defined in + /// Rust as an offset of signal's field within Rust struct. + explicit SignalInner(ptrdiff_t field_offset) + : rust_field_offset(field_offset) + { + } + + /// Construct signal representation for an arbitrary Qt signal defined in + /// C++. + /// + /// Note: this is an implicit conversion. + template + SignalInner(R (Type::*qt_signal)(Args...)) + // (there is a double indirection in the reinterpret_cast to avoid + // -Wcast-function-type) + : cpp_erased_method( + *reinterpret_cast(&qt_signal)) + { + } + + /// Qt uses "pointer to a pointer to a member" signal representation inside + /// `QObject::connect(...)` functions. This little helper encapsulates the + /// required cast. + void **asRawSignal() + { + return reinterpret_cast(&cpp_erased_method); + // equivalently: + // return reinterpret_cast(&rust_field_offset); + } +}; + +/// Wrapper for Rust `std::raw::TraitObject` struct. +/// +/// Note: `std::raw` is marked unstable as of Rust 1.43.0, so for future +/// compatibility it would be better to box the trait object on the heap, +/// and never manipulate its content directly from C++. For the time being, +/// though, let it be. +struct TraitObject { + void *data; + void *vtable; + + /// Nullability check. + bool isValid() const { return data && vtable; } + + /// Forget about referenced object. + /// + /// If this TraitObject represented a `Box` (owned object) rather than a + /// `&dyn` reference (borrowed object) then it may cause memory leaks, + /// unless a copy was made for later proper destruction. + inline void invalidate() + { + data = nullptr; + vtable = nullptr; + } +}; + +extern "C" QMetaObject *RustObject_metaObject(TraitObject); +extern "C" void RustObject_destruct(TraitObject); + +/// "513 reserved for Qt Jambi's DeleteOnMainThread event" +/// We are just re-using this event type for our purposes. +/// +/// Source: +/// https://github.com/qtjambi/qtjambi/blob/8ef99da63315945e6ab540cc31d66e5b021b69e4/src/cpp/qtjambi/qtjambidebugevent.cpp#L857 +static constexpr int QtJambi_EventType_DeleteOnMainThread = 513; + +template struct RustObject : Base { + TraitObject rust_object; // A QObjectPinned where XXX is the base trait + TraitObject ptr_qobject; // a QObjectPinned + void (*extra_destruct)(QObject *); + const QMetaObject *metaObject() const override + { + return ptr_qobject.isValid() ? RustObject_metaObject(ptr_qobject) + : Base::metaObject(); + } + int qt_metacall(QMetaObject::Call _c, int _id, void **_a) override + { + _id = Base::qt_metacall(_c, _id, _a); + if (_id < 0) + return _id; + const QMetaObject *mo = metaObject(); + if (_c == QMetaObject::InvokeMetaMethod || + _c == QMetaObject::RegisterMethodArgumentMetaType) { + int methodCount = mo->methodCount(); + if (_id < methodCount) + mo->d.static_metacall(this, _c, _id, _a); + _id -= methodCount; + } else if ((_c >= QMetaObject::ReadProperty && + _c <= +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QMetaObject::QueryPropertyUser +#else + QMetaObject::ResetProperty +#endif + ) || + _c == QMetaObject::RegisterPropertyMetaType) { + int propertyCount = mo->propertyCount(); + if (_id < propertyCount) + mo->d.static_metacall(this, _c, _id, _a); + _id -= propertyCount; + } + return _id; + } + bool event(QEvent *event) override + { + if (ptr_qobject.isValid() && + event->type() == QtJambi_EventType_DeleteOnMainThread) { + // This event is sent by rust when we are deleted. + ptr_qobject.invalidate(); // so the destructor don't recurse + delete this; + return true; + } + return Base::event(event); + } + ~RustObject() + { + auto r = ptr_qobject; + ptr_qobject.invalidate(); + if (extra_destruct) + extra_destruct(this); + if (r.isValid()) + RustObject_destruct(r); + } +}; + +struct RustQObjectDescriptor { + size_t size; + const QMetaObject *baseMetaObject; + QObject *(*create)(const TraitObject *, const TraitObject *); + void (*qmlConstruct)(void *, const TraitObject *, const TraitObject *, + void (*extra_destruct)(QObject *)); + TraitObject (*get_rust_refcell)( + QObject *); // Possible optimisation: make this an offset + + /// Get singleton-per-type descriptor. + template static const RustQObjectDescriptor *instance(); +}; + +template +const RustQObjectDescriptor *RustQObjectDescriptor::instance() +{ + static RustQObjectDescriptor desc{ + /*size*/ sizeof(T), + /*baseMetaObject*/ &T::staticMetaObject, + /*create*/ + [](const TraitObject *self_pinned, + const TraitObject *self_ptr) -> QObject * { + auto q = new T(); + q->ptr_qobject = *self_ptr; + q->rust_object = *self_pinned; + return q; + }, + /*qmlConstruct*/ + [](void *data, const TraitObject *self_pinned, + const TraitObject *self_ptr, void (*extra_destruct)(QObject *)) { + auto *q = new (data) T(); + q->rust_object = *self_pinned; + q->ptr_qobject = *self_ptr; + q->extra_destruct = extra_destruct; + }, + /*get_rust_refcell*/ + [](QObject *q) { return static_cast(q)->ptr_qobject; }}; + return &desc; +} diff --git a/src/qprocessindicator.cpp b/src/qprocessindicator.cpp deleted file mode 100644 index b9d8a28..0000000 --- a/src/qprocessindicator.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -// https://github.com/raythorn/QProcessIndicator/blob/master/QProcessIndicator/QProcessIndicator.cpp -#include "qprocessindicator.h" -#include -#include -#include -#include - -#define SPIN_INTERVAL 60 - -QProcessIndicator::QProcessIndicator(QWidget *parent) - : QWidget(parent), m_type(line_rotate), m_interval(SPIN_INTERVAL), - m_angle(0), m_scale(0.0f), m_color(Qt::black) -{ - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - setFocusPolicy(Qt::NoFocus); - - m_timer = new QTimer(); - connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimeout())); -} -void QProcessIndicator::updateStyle() -{ - m_color = palette().color(QPalette::Window); - update(); -} - -QProcessIndicator::~QProcessIndicator() -{ - stop(); - - delete m_timer; -} - -void QProcessIndicator::paintEvent(QPaintEvent *e) -{ - Q_UNUSED(e) - - if (!m_timer->isActive()) { - return; - } - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - switch (m_type) { - case line_rotate: - drawRotateLine(&painter); - break; - case line_scale: - drawScaleLine((&painter)); - break; - case ball_rotate: - drawRotateBall(&painter); - break; - } -} - -void QProcessIndicator::start() { m_timer->start(m_interval); } - -void QProcessIndicator::stop() { m_timer->stop(); } - -void QProcessIndicator::onTimeout() -{ - switch (m_type) { - case line_rotate: - case ball_rotate: - m_angle = (m_angle + 45) % 360; - break; - case line_scale: - m_scale += 0.1f; - m_scale = m_scale > .5f ? 0.0f : m_scale; - break; - } - - update(); -} - -void QProcessIndicator::drawRotateLine(QPainter *painter) -{ - int width = qMin(this->width(), this->height()); - - int outerRadius = (width - 4) * 0.5f; - int innerRadius = outerRadius * 0.42f; - - int capsuleHeight = outerRadius - innerRadius; - int capsuleWidth = - (width > 32) ? capsuleHeight * .32f : capsuleHeight * .40f; - int capsuleRadius = capsuleWidth / 2; - - for (int i = 0; i < 8; i++) { - QColor color = m_color; - - color.setAlphaF(1.0f - (i / 8.0f)); - painter->setPen(Qt::NoPen); - painter->setBrush(color); - - painter->save(); - - painter->translate(rect().center()); - painter->rotate(m_angle - i * 45.0f); - - painter->drawRoundedRect(-capsuleWidth * 0.5, - -(innerRadius + capsuleHeight), capsuleWidth, - capsuleHeight, capsuleRadius, capsuleRadius); - - painter->restore(); - } -} - -void QProcessIndicator::drawScaleLine(QPainter *painter) -{ - int height = qMin(this->width(), this->height()); - - qreal lineWidth = height * 0.15f; - qreal lineHeight = height * 0.9f; - qreal lineRadius = lineWidth / 2.0f; - qreal lineGap = lineWidth; - qreal margin = (this->width() - lineWidth * 5 - lineGap * 4) / 2.0f; - - for (int i = 0; i < 5; i++) { - painter->setPen(Qt::NoPen); - painter->setBrush(m_color); - - int tmp = m_scale * 10 + i + 1; - if (tmp > 5) { - tmp = 5 - tmp % 5; - } - - qDebug() << tmp; - - qreal scale = 0.5f + tmp * 0.1f; - qreal h = lineHeight * scale; - - painter->save(); - - painter->translate( - QPointF(margin + (lineWidth + lineGap) * i, this->height() / 2)); - - painter->drawRoundedRect(0, -h / 2.0f, lineWidth, h, lineRadius, - lineRadius); - - painter->restore(); - } -} - -void QProcessIndicator::drawRotateBall(QPainter *painter) -{ - int width = qMin(this->width(), this->height()); - - int outerRadius = (width - 4) * 0.5f; - int innerRadius = outerRadius * 0.78f; - - int capsuleRadius = (outerRadius - innerRadius) / 2; - - for (int i = 0; i < 8; i++) { - QColor color = m_color; - - color.setAlphaF(1.0f - (i / 8.0f)); - - painter->setPen(Qt::NoPen); - painter->setBrush(color); - - qreal radius = capsuleRadius * (1.0f - (i / 16.0f)); - - painter->save(); - - painter->translate(rect().center()); - painter->rotate(m_angle - i * 45.0f); - - QPointF centre = - QPointF(-capsuleRadius, -(innerRadius + capsuleRadius)); - painter->drawEllipse(centre, radius * 2, radius * 2); - - painter->restore(); - } -} \ No newline at end of file diff --git a/src/qprocessindicator.h b/src/qprocessindicator.h deleted file mode 100644 index a159899..0000000 --- a/src/qprocessindicator.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -// https://github.com/raythorn/QProcessIndicator/blob/master/QProcessIndicator/QProcessIndicator.h -#ifndef QPROCESSINDICATOR_H -#define QPROCESSINDICATOR_H - -#include -#include -#include -#include -#include -#include - -class QProcessIndicator : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(int m_type READ type WRITE setType) - Q_PROPERTY(QColor m_color READ color WRITE setColor) - Q_PROPERTY(int m_interval READ interval WRITE setInterval) - -public: - QProcessIndicator(QWidget *parent = 0); - ~QProcessIndicator(); - - enum { - line_rotate, - line_scale, - ball_rotate, - }; - - void paintEvent(QPaintEvent *e) override; - - void start(); - void stop(); - - int type() { return m_type; } - void setType(int type) { m_type = type; } - - QColor &color() { return m_color; } - void setColor(QColor &color) { m_color = color; } - - int interval() { return m_interval; } - void setInterval(int interval) { m_interval = interval; } - -private slots: - void onTimeout(); - -private: - void drawRotateLine(QPainter *painter); - void drawScaleLine(QPainter *painter); - void drawRotateBall(QPainter *painter); - void updateStyle(); - -private: - int m_type; - int m_interval; - QColor m_color; - - int m_angle; - qreal m_scale; - - QTimer *m_timer; - -protected: - void changeEvent(QEvent *event) override - { - if (event->type() == QEvent::PaletteChange) { - updateStyle(); - } - QWidget::changeEvent(event); - }; -}; - -#endif // QPROCESSINDICATOR_H \ No newline at end of file diff --git a/src/qquickimageprovider_imp.rs b/src/qquickimageprovider_imp.rs new file mode 100644 index 0000000..9043355 --- /dev/null +++ b/src/qquickimageprovider_imp.rs @@ -0,0 +1,213 @@ +use cpp::*; +use qmetaobject::*; +use std::cell::RefCell; + +use std::collections::HashMap; + +/// Extension trait for adding a `QQuickImageProvider` to a `QmlEngine` +pub trait AddImageProvider { + /// Wrapper around [`void QQmlEngine::addImageProvider(const QString &providerId, QQmlImageProviderBase *provider)`][method] + /// + /// # Wrapper-specific + /// + /// Specialized to `ImageType::Image`. + /// + /// [method]: https://doc.qt.io/qt-5/qqmlengine.html#addImageProvider + fn add_image_provider(&mut self, provider_id: &str, provider: QObjectBox

); +} + +impl AddImageProvider

for QmlEngine { + fn add_image_provider(&mut self, provider_id: &str, provider: QObjectBox

) { + let qml_engine = self.cpp_ptr(); + let provider_id = QString::from(provider_id); + let provider_ptr = provider.pinned().get_or_create_cpp_object(); + cpp!(unsafe [ + qml_engine as "QQmlEngine *", + provider_id as "QString", + provider_ptr as "Rust_QImageProvider *" + ] { + qml_engine->addImageProvider(provider_id, provider_ptr); + }); + // FIXME: this or Box::leak(provider); ? + std::mem::forget(provider); + } +} + +/// Extension trait for adding a `QQuickPixmapProvider` to a `QmlEngine` +pub trait AddPixmapProvider { + /// Wrapper around [`void QQmlEngine::addImageProvider(const QString &providerId, QQmlImageProviderBase *provider)`][method] + /// + /// # Wrapper-specific + /// + /// Specialized to `ImageType::Pixmap`. + /// + /// [method]: https://doc.qt.io/qt-5/qqmlengine.html#addImageProvider + fn add_pixmap_provider(&mut self, provider_id: &str, provider: RefCell

); +} + +impl AddPixmapProvider

for QmlEngine { + fn add_pixmap_provider(&mut self, provider_id: &str, provider: RefCell

) { + let qml_engine = self.cpp_ptr(); + let provider_id = QString::from(provider_id); + let provider_ptr = provider.borrow().get_cpp_object(); + cpp!(unsafe [ + qml_engine as "QQmlEngine *", + provider_id as "QString", + provider_ptr as "Rust_QPixmapProvider *" + ] { + qml_engine->addImageProvider(provider_id, provider_ptr); + }); + std::mem::forget(provider); + } +} + +/// A simple, `HashMap`-based pixmap provider +#[derive(QObject, Default)] +pub struct SimplePixmapProvider { + base: qt_base_class!(trait QQuickPixmapProvider), + map: HashMap, +} + +impl QQuickPixmapProvider for SimplePixmapProvider { + fn request_pixmap(&self, id: &str, _requested_size: &QSize) -> (QSize, QPixmap) { + let pixmap = self.map.get(id).cloned().unwrap_or_default(); + (pixmap.size(), pixmap) + } +} + +/// [`QQuickImageProvider`][class] specialized to `ImageType::Pixmap` +/// +/// [class]: https://doc.qt.io/qt-5/qquickimageprovider.html +pub trait QQuickPixmapProvider: QObject { + /// Wrapper around [`QPixmap QQuickImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)`][method] + /// + /// # Wrapper-specific + /// + /// Returns a tuple of the original image size and the pixmap instead of providing a mutable size + /// parameter. + /// + /// [method]: https://doc.qt.io/qt-5/qquickimageprovider.html#requestPixmap + fn request_pixmap( + &self, + #[allow(unused_variables)] id: &str, + #[allow(unused_variables)] requested_size: &QSize, + ) -> (QSize, QPixmap) { + Default::default() + } + + /// Required for the implementation detail of the QObject custom derive + fn get_object_description() -> &'static QObjectDescriptor + where + Self: Sized, + { + unsafe { + &*cpp!([]-> *const QObjectDescriptor as "RustQObjectDescriptor const*" { + return RustQObjectDescriptor::instance(); + }) + } + } +} + +/// [`QQuickImageProvider`][class] specialized to `ImageType::Image` +/// +/// [class]: https://doc.qt.io/qt-5/qquickimageprovider.html +pub trait QQuickImageProvider: QObject { + /// Wrapper around [`QImage QQuickImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)`][method] + /// + /// # Wrapper-specific + /// + /// Returns a tuple of the original image size and the image instead of providing a mutable size + /// parameter. + /// + /// [method]: https://doc.qt.io/qt-5/qquickimageprovider.html#requestImage + fn request_image( + &self, + #[allow(unused_variables)] id: &str, + #[allow(unused_variables)] requested_size: &QSize, + ) -> (QSize, QImage) { + Default::default() + } + + /// Required for the implementation detail of the QObject custom derive + fn get_object_description() -> &'static QObjectDescriptor + where + Self: Sized, + { + unsafe { + &*cpp!([]-> *const QObjectDescriptor as "RustQObjectDescriptor const*" { + return RustQObjectDescriptor::instance(); + }) + } + } +} + +cpp! {{ + #include "src/qmetaobject_rust.hpp" + #include + + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + class QPixmapProvider : public QQuickImageProvider + { + public: + QPixmapProvider(): QQuickImageProvider(ImageType::Pixmap) {} + }; + #else + class QPixmapProvider : public QObject, public QQuickImageProvider + { + public: + QPixmapProvider(): QObject(), QQuickImageProvider(ImageType::Pixmap) {} + }; + #endif + + struct Rust_QPixmapProvider : public RustObject + { + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requested_size) override + { + return rust!(Rust_QPixmapProvider_request_pixmap [ + rust_object: QObjectPinned as "TraitObject", + id: &QString as "const QString &", + size: *mut QSize as "QSize *", + requested_size: &QSize as "const QSize &" + ] -> QPixmap as "QPixmap" { + let (orig_size, pixmap) = rust_object.borrow().request_pixmap(&id.to_string(), requested_size); + if !size.is_null() { + *size = orig_size; + } + pixmap + }); + } + }; + + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + class QImageProvider : public QQuickImageProvider + { + public: + QImageProvider(): QQuickImageProvider(ImageType::Image) {} + }; + #else + class QImageProvider : public QObject, public QQuickImageProvider + { + public: + QImageProvider(): QObject(), QQuickImageProvider(ImageType::Image) {} + }; + #endif + + struct Rust_QImageProvider : public RustObject + { + QImage requestImage(const QString &id, QSize *size, const QSize &requested_size) override + { + return rust!(Rust_QImageProvider_request_image [ + rust_object: QObjectPinned as "TraitObject", + id: &QString as "const QString &", + size: *mut QSize as "QSize *", + requested_size: &QSize as "const QSize &" + ] -> QImage as "QImage" { + let (orig_size, img) = rust_object.borrow().request_image(&id.to_string(), requested_size); + if !size.is_null() { + *size = orig_size; + } + img + }); + } + }; +}} diff --git a/src/qrc.rs b/src/qrc.rs new file mode 100644 index 0000000..e495a5d --- /dev/null +++ b/src/qrc.rs @@ -0,0 +1,85 @@ +use qmetaobject::qrc; + +qrc!(pub rsrc, + "/" { + "resources/icons/video-x-generic.png", + "resources/icons/MdiLightningBolt.png", + "resources/icons/MingcuteSettings7Line.png", + "resources/icons/ClarityHardDiskSolidAlerted.png", + "resources/icons/IcOutlinePowerSettingsNew.png", + "resources/icons/HugeiconsWrench01.png", + "resources/icons/IcTwotoneRestartAlt.png", + "resources/icons/MaterialSymbolsArrowUpwardAltRounded.png", + "resources/icons/LetsIconsImport.png", + "resources/icons/MaterialSymbolsArrowLeftAlt.png", + "resources/icons/MaterialSymbolsArrowRightAlt.png", + "resources/icons/PhExport.png", + "resources/icons/MaterialSymbolsLightKeyboardReturn.png", + "resources/icons/MaterialSymbolsFavorite.png", + "resources/icons/MdiLightMagnify.png", + "resources/icons/IcBaselineInsertDriveFile.png", + "resources/icons/MaterialSymbolsLightHome.png", + "resources/icons/MdiGithub.png", + "resources/icons/app-icon/icon.ico", + "resources/icons/MaterialSymbolsLightAirplayOutline.png", + "resources/icons/MaterialSymbolsLightCableRounded.png", + "resources/icons/MaterialSymbolsLocationOnOutline.png", + "resources/icons/MdiDisk.png", + "resources/icons/PepiconsPrintCellphoneEye.png", + "resources/icons/StreamlineProgrammingBrowserSearchSearchWindowGlassAppCodeProgrammingQueryFindMagnifyingApps.png", + "resources/icons/fuse.png", + "resources/icons/TablerDatabaseExport.png", + "resources/icons/StreamlineUltimateMultipleUsersNetwork.png", + "resources/icons/MaterialSymbolsAndroidWifi3BarPlus.png", + "resources/icons/IconParkTwotoneMoreTwo.png", + "resources/icons/BxBxsTerminal.png", + "resources/icons/ClarityEyeHideLine.png", + "resources/icons/ClarityEyeLine.png", + "resources/icons/MaterialSymbolsLightImageOutlineSharp.png", + "resources/icons/MaterialSymbolsFolder.png", + "resources/icons/QlementineIconsWireless116.png", + "resources/icons/UimProcess.png", + "resources/icons/LetsIconsHorizontalDownLeftMainLight.png", + "resources/icons/MaterialSymbolsDelete.png", + "resources/icons/MaterialSymbolsCloseRounded.png", + "resources/icons/MaterialSymbolsLightKeyboardArrowUp.png", + "resources/icons/MaterialSymbolsLightKeyboardArrowDown.png", + "resources/icons/IcOutlineRefresh.png", + "resources/icons/StreamlineFreehandChargingFlashWireless.png", + "qml/MapView.qml", + "resources/iphone.png", + "resources/ios-version.png", + "resources/ios-wallpapers/iphone-ios4.png", + "resources/ios-wallpapers/iphone-ios5.png", + "resources/ios-wallpapers/iphone-ios6.png", + "resources/ios-wallpapers/iphone-ios7.png", + "resources/ios-wallpapers/iphone-ios8.png", + "resources/ios-wallpapers/iphone-ios9.png", + "resources/ios-wallpapers/iphone-ios10.png", + "resources/ios-wallpapers/iphone-ios11.png", + "resources/ios-wallpapers/iphone-ios12.png", + "resources/ios-wallpapers/iphone-ios13.png", + "resources/ios-wallpapers/iphone-ios14.png", + "resources/ios-wallpapers/iphone-ios15.png", + "resources/ios-wallpapers/iphone-ios16.png", + "resources/ios-wallpapers/iphone-ios17.png", + "resources/ios-wallpapers/iphone-ios18.png", + "resources/ios-wallpapers/iphone-ios26.png", + "resources/iphone-mockups/iphone-3.png", + "resources/iphone-mockups/iphone-4.png", + "resources/iphone-mockups/iphone-5.png", + "resources/iphone-mockups/iphone-6.png", + "resources/iphone-mockups/iphone-x.png", + "resources/iphone-mockups/iphone-15.png", + "resources/iphone-mockups/iphone-16.png", + "resources/connect.png", + "resources/airplay-tutorial.mp4", + "resources/ipad-mockups/ipad.png", + "DeveloperDiskImages.json", + "resources/keychain.mp4", + "resources/wireless-gallery-import.mp4", + "resources/dev-mode.mp4", + "resources/unlock.mp4", + "resources/trust.png" + } +); diff --git a/src/qt_threading.rs b/src/qt_threading.rs new file mode 100644 index 0000000..29d5d4c --- /dev/null +++ b/src/qt_threading.rs @@ -0,0 +1,55 @@ +use qmetaobject::prelude::*; +use std::sync::Arc; + +struct SendableQPointer(QPointer); + +// https://doc.qt.io/qt-6/qmetaobject.html +// Only used in the main thread should be safe? +unsafe impl Send for SendableQPointer {} +unsafe impl Sync for SendableQPointer {} + +type Job = Box; + +pub struct QtThread { + invoke: Arc) + Send + Sync + 'static>, +} + +impl Clone for QtThread { + fn clone(&self) -> Self { + Self { + invoke: self.invoke.clone(), + } + } +} + +impl QtThread { + pub fn new(obj: &T) -> Self { + let ptr = SendableQPointer(QPointer::from(obj)); + + let cb = qmetaobject::queued_callback(move |job: Job| { + if let Some(obj) = ptr.0.as_pinned() { + let mut obj = obj.borrow_mut(); + job(&mut obj); + } else { + eprintln!("QtThread::queue: QObject is gone (QPointer is null)"); + } + }); + + Self { + invoke: Arc::new(move |job: Job| cb(job)), + } + } + + pub fn queue(&self, f: F) + where + F: FnOnce(&mut T) + Send + 'static, + { + (self.invoke)(Box::new(f)); + } +} + +pub trait QtThreading: QObject + 'static { + fn qt_thread(&self) -> QtThread + where + Self: Sized; +} diff --git a/src/rust/src/query_sqlite.rs b/src/query_sqlite.rs similarity index 72% rename from src/rust/src/query_sqlite.rs rename to src/query_sqlite.rs index 3ad694c..1792f34 100644 --- a/src/rust/src/query_sqlite.rs +++ b/src/query_sqlite.rs @@ -1,9 +1,9 @@ -use cxx_qt::{Constructor, CxxQtType, Threading}; -use cxx_qt_lib::{QByteArray, QList, QMap, QMapPair_QString_QVariant, QString, QVariant}; +use qmetaobject::prelude::*; +use qttypes::{QStringList, QVariantMap}; + +use crate::qt_threading::{QtThread, QtThreading}; +use crate::{APP_DEVICE_STATE, RUNTIME, run_sync}; -use crate::POSSIBLE_ROOT; -use crate::{APP_DEVICE_STATE, RUNTIME, afc, run_sync}; -use cxx_qt_lib::{QHash, QHashPair_QString_QVariant}; use idevice::afc::{AfcClient, opcode::AfcFopenMode}; use idevice::{ IdeviceError, IdeviceService, diagnostics_relay::DiagnosticsRelayClient, @@ -27,58 +27,56 @@ use tokio::sync::Mutex; use tokio::sync::oneshot; -#[cxx_qt::bridge] -mod qobject { - 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 QList_QString = cxx_qt_lib::QList; - type QByteArray = cxx_qt_lib::QByteArray; - type QMap_QString_QVariant = cxx_qt_lib::QMap; - } - - extern "RustQt" { - #[qobject] - #[qml_element] - #[qproperty(QMap_QString_QVariant, albums)] - #[qproperty(QString, error)] - #[qproperty(bool, is_init)] - #[qproperty(bool, is_err)] - type Query = super::RQuery; - - #[qinvokable] - fn init(self: Pin<&mut Self>, udid: &QString); - - #[qinvokable] - fn read_albums(self: Pin<&mut Self>); - - #[qinvokable] - fn query_album(self: Pin<&mut Self>, id: i32); - - #[qsignal] - fn album_queried(self: Pin<&mut Self>, id: i32, items: QList_QString); - - } - impl cxx_qt::Threading for Query {} -} - -#[derive(Default)] -pub struct RQuery { +#[derive(QObject)] +pub struct Query { + base: qt_base_class!(trait QObject), udid: QString, - albums: QMap, - error: QString, + albums: qt_property!(QVariantMap; NOTIFY albums_changed), + albums_changed: qt_signal!(), connection: Option>>, - is_init: bool, - is_err: bool, + state: qt_property!(QVariantMap; NOTIFY state_changed), + state_changed: qt_signal!(), + + init: qt_method!(fn(&mut self, udid: QString)), + read_albums: qt_method!(fn(&mut self)), + query_album: qt_method!(fn(&mut self, id: i32)), + album_queried: qt_signal!(id: i32, items: QStringList), } -impl qobject::Query { - fn init(mut self: Pin<&mut Self>, udid: &QString) { +impl QtThreading for Query { + fn qt_thread(&self) -> crate::qt_threading::QtThread + where + Self: Sized, + { + QtThread::new(self) + } +} + +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)); + state.insert(QString::from("err"), QVariant::from(QString::default())); + + Self { + base: Default::default(), + udid: Default::default(), + albums: Default::default(), + albums_changed: Default::default(), + connection: None, + state, + state_changed: Default::default(), + init: Default::default(), + read_albums: Default::default(), + query_album: Default::default(), + album_queried: Default::default(), + } + } +} + +impl Query { + fn init(&mut self, udid: QString) { let udid_clone = udid.clone(); let qt_thread = self.qt_thread(); @@ -133,30 +131,41 @@ impl qobject::Query { */ std::mem::forget(gallery_db_bytes); - qt_thread - .queue(|mut s| { - s.as_mut().rust_mut().connection = Some(Arc::new(Mutex::new(conn))); - }) - .ok(); + qt_thread.queue(|s| { + s.connection = Some(Arc::new(Mutex::new(conn))); + }); Ok(()) }) .await; match res { Ok(_) => { - qt_thread.queue(move |s| s.set_is_init(true)).ok(); - qt_thread.queue(move |s| s.set_is_err(false)).ok(); + qt_thread.queue(move |s| { + s.state[QString::from("err")] = QVariant::from(QString::default()); + s.state[QString::from("init")] = QVariant::from(true); + // FIXME: + // q_self.albums = albums; + + s.state_changed(); + }); } Err(e) => { // eprintln!("Failed to read sqlite db"); - qt_thread.queue(move |s| s.set_is_init(false)).ok(); - qt_thread.queue(move |s| s.set_is_err(true)).ok(); + qt_thread.queue(move |s| { + s.state[QString::from("err")] = + QVariant::from(QString::from(e.to_string())); + s.state[QString::from("init")] = QVariant::from(false); + // FIXME: + // q_self.albums = albums; + + s.state_changed(); + }); } }; }); } - fn read_albums(mut self: Pin<&mut Self>) { + fn read_albums(&mut self) { let q_thread = self.qt_thread(); let con_arc = match &self.connection { Some(c) => c.clone(), @@ -167,11 +176,11 @@ impl qobject::Query { }; RUNTIME.spawn(async move { - let mut albums = QMap::::default(); + println!("Runtime spawn for read_albums"); + let mut albums = QVariantMap::default(); let res: Result<(), Box> = (async { - //recents album - let conn = con_arc.lock().await; + //recents album let mut recents_stmt = conn.prepare( " SELECT @@ -289,34 +298,27 @@ impl qobject::Query { .await; if let Err(e) = res { - q_thread - .queue(move |q_self| { - q_self.set_error(QString::from(e.to_string())); - }) - .ok(); - - q_thread - .queue(move |q_self| { - q_self.set_albums(albums); - }) - .ok(); + q_thread.queue(move |q_self| { + q_self.state[QString::from("err")] = + QVariant::from(QString::from(e.to_string())); + q_self.albums = albums; + q_self.albums_changed(); + // q_self.state_changed() + }); } else { - q_thread - .queue(move |q_self| { - q_self.set_error(QString::default()); - }) - .ok(); + q_thread.queue(move |q_self| { + println!("Albums read fine firing events"); + q_self.state[QString::from("err")] = QVariant::from(QString::default()); + q_self.albums = albums; - q_thread - .queue(move |q_self| { - q_self.set_albums(albums); - }) - .ok(); + q_self.albums_changed(); + // q_self.state_changed() + }) } }); } - fn query_album(self: Pin<&mut Self>, id: i32) { + fn query_album(&mut self, id: i32) { let con_arc = match &self.connection { Some(c) => c.clone(), None => return, @@ -324,9 +326,9 @@ impl qobject::Query { let q_thread = self.qt_thread(); RUNTIME.spawn(async move { - let res: Result, Box> = (async { + let res: Result> = (async { let con = con_arc.lock().await; - let mut list: QList = QList::default(); + let mut list: QStringList = QStringList::default(); let mut stmt = con.prepare(&format!( " SELECT @@ -346,19 +348,18 @@ impl qobject::Query { for item in row_iter { let (fdir, fname) = item?; let full_path = format!("{}/{}", fdir, fname); - list.append(QString::from(full_path)); + list.push(QString::from(full_path)); } - Ok((list)) + Ok(list) }) .await; match res { Ok(list) => { - q_thread - .queue(move |q| { - q.album_queried(id, list); - }) - .ok(); + println!("Album loaded has length :{}", list.len()); + q_thread.queue(move |q| { + q.album_queried(id, list); + }); } Err(_) => { println!("Error querying album") diff --git a/src/querymobilegestaltwidget.cpp b/src/querymobilegestaltwidget.cpp deleted file mode 100644 index 6601b84..0000000 --- a/src/querymobilegestaltwidget.cpp +++ /dev/null @@ -1,1144 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "querymobilegestaltwidget.h" -#include -#include -#include -#include -#include -#include -#include - -QueryMobileGestaltWidget::QueryMobileGestaltWidget( - const std::shared_ptr device, QWidget *parent) - : Tool(parent), m_device(device) -{ - setupUI(); - populateKeys(); -} - -void QueryMobileGestaltWidget::setupUI() -{ - setWindowTitle("Query MobileGestalt - iDescriptor"); -#ifdef WIN32 - resize(600, 500); - setMaximumSize(800, 600); -#else - setMinimumSize(800, 600); -#endif - // Main layout - mainLayout = new QVBoxLayout(this); - - // Title - QLabel *desc = new QLabel("This tool lets you query MobileGestalt keys , " - "which provide various device information."); - desc->setStyleSheet("margin:5px;"); - mainLayout->addWidget(desc); - - // Selection group - selectionGroup = new QGroupBox("Select MobileGestalt Keys"); - mainLayout->addWidget(selectionGroup); - - QVBoxLayout *groupLayout = new QVBoxLayout(selectionGroup); - groupLayout->setContentsMargins(0, 0, 0, 0); - - // Select/Clear buttons - buttonLayout = new QHBoxLayout(); - selectAllButton = new QPushButton("Select All"); - clearAllButton = new QPushButton("Clear All"); - selectAllButton->setMaximumWidth(100); - clearAllButton->setMaximumWidth(100); - buttonLayout->addWidget(selectAllButton); - buttonLayout->addWidget(clearAllButton); - buttonLayout->addStretch(); - buttonLayout->setContentsMargins(5, 0, 5, 0); - groupLayout->addLayout(buttonLayout); - - // Scroll area for checkboxes - scrollArea = new QScrollArea(); - scrollArea->setWidgetResizable(true); - scrollArea->setMaximumHeight(200); - scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - checkboxWidget = new QWidget(); - checkboxLayout = new QVBoxLayout(checkboxWidget); - checkboxLayout->setContentsMargins(10, 5, 10, 5); - - scrollArea->setWidget(checkboxWidget); - groupLayout->addWidget(scrollArea); - - // Query button - queryButton = new QPushButton("Query MobileGestalt"); - queryButton->setProperty("primary", true); - queryButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - mainLayout->addWidget(queryButton, 0, Qt::AlignCenter); - - // Status label - statusLabel = new QLabel("Select keys and click Query to begin"); - statusLabel->setStyleSheet("color: #666; font-style: italic; margin: 5px;"); - mainLayout->addWidget(statusLabel); - - QGroupBox *outputGroup = new QGroupBox("Query Results"); - outputTextEdit = new QTextEdit(); - outputTextEdit->setReadOnly(true); - outputTextEdit->setPlaceholderText("results will appear here..."); - outputTextEdit->setStyleSheet("QTextEdit {" - "border : none;" - "}"); - outputGroup->setLayout(new QVBoxLayout()); - outputGroup->layout()->setContentsMargins(0, 0, 0, 0); - outputGroup->layout()->addWidget(outputTextEdit); - mainLayout->addWidget(outputGroup); - - // Connect signals - connect(queryButton, &QPushButton::clicked, this, - &QueryMobileGestaltWidget::onQueryButtonClicked); - connect(selectAllButton, &QPushButton::clicked, this, - &QueryMobileGestaltWidget::onSelectAllClicked); - connect(clearAllButton, &QPushButton::clicked, this, - &QueryMobileGestaltWidget::onClearAllClicked); - connect(m_device->service_manager, - &CXX::ServiceManager::mobilegestalt_info_retrieved, this, - &QueryMobileGestaltWidget::handleResults); -} - -void QueryMobileGestaltWidget::populateKeys() -{ - // Credits -> - // https://github.com/doronz88/pymobiledevice3/blob/master/pymobiledevice3/services/diagnostics.py - mobileGestaltKeys = { - "3GProximityCapability", - "3GVeniceCapability", - "3Gvenice", - "3d-imagery", - "3d-maps", - "64-bit", - "720p", - "720pPlaybackCapability", - "APNCapability", - "ARM64ExecutionCapability", - "ARMV6ExecutionCapability", - "ARMV7ExecutionCapability", - "ARMV7SExecutionCapability", - "ASTC", - "AWDID", - "AWDLCapability", - "AccelerometerCapability", - "AccessibilityCapability", - "AcousticID", - "ActivationProtocol", - "ActiveWirelessTechnology", - "ActuatorResonantFrequency", - "AdditionalTextTonesCapability", - "AggregateDevicePhotoZoomFactor", - "AggregateDeviceVideoZoomFactor", - "AirDropCapability", - "AirDropRestriction", - "AirplaneMode", - "AirplayMirroringCapability", - "AllDeviceCapabilities", - "Allow32BitApps", - "AllowOnlyATVCPSDKApps", - "AllowYouTube", - "AllowYouTubePlugin", - "AmbientLightSensorCapability", - "AmbientLightSensorSerialNumber", - "ApNonce", - "ApNonceRetrieve", - "AppCapacityTVOS", - "AppStore", - "AppStoreCapability", - "AppleInternalInstallCapability", - "AppleNeuralEngineSubtype", - "ApplicationInstallationCapability", - "ArcModuleSerialNumber", - "ArrowChipID", - "ArrowUniqueChipID", - "ArtworkTraits", - "AssistantCapability", - "AudioPlaybackCapability", - "AutoFocusCameraCapability", - "AvailableDisplayZoomSizes", - "BacklightCapability", - "BasebandAPTimeSync", - "BasebandBoardSnum", - "BasebandCertId", - "BasebandChipId", - "BasebandChipset", - "BasebandClass", - "BasebandFirmwareManifestData", - "BasebandFirmwareUpdateInfo", - "BasebandFirmwareVersion", - "BasebandKeyHashInformation", - "BasebandPostponementStatus", - "BasebandPostponementStatusBlob", - "BasebandRegionSKU", - "BasebandRegionSKURadioTechnology", - "BasebandSecurityInfoBlob", - "BasebandSerialNumber", - "BasebandSkeyId", - "BasebandStatus", - "BasebandUniqueId", - "BatteryCurrentCapacity", - "BatteryIsCharging", - "BatteryIsFullyCharged", - "BatterySerialNumber", - "BlueLightReductionSupported", - "BluetoothAddress", - "BluetoothAddressData", - "BluetoothCapability", - "BluetoothLE2Capability", - "BluetoothLECapability", - "BoardId", - "BoardRevision", - "BootManifestHash", - "BootNonce", - "BridgeBuild", - "BridgeRestoreVersion", - "BuddyLanguagesAnimationRequiresOptimization", - "BuildID", - "BuildVersion", - "C2KDeviceCapability", - "CPUArchitecture", - "CPUSubType", - "CPUType", - "CallForwardingCapability", - "CallWaitingCapability", - "CallerIDCapability", - "CameraAppUIVersion", - "CameraCapability", - "CameraFlashCapability", - "CameraFrontFlashCapability", - "CameraHDR2Capability", - "CameraHDRVersion", - "CameraLiveEffectsCapability", - "CameraMaxBurstLength", - "CameraRestriction", - "CarrierBundleInfoArray", - "CarrierInstallCapability", - "CellBroadcastCapability", - "CellularDataCapability", - "CellularTelephonyCapability", - "CertificateProductionStatus", - "CertificateSecurityMode", - "ChipID", - "CloudPhotoLibraryCapability", - "CoastlineGlowRenderingCapability", - "CompassCalibration", - "CompassCalibrationDictionary", - "CompassType", - "ComputerName", - "ConferenceCallType", - "ConfigNumber", - "ContainsCellularRadioCapability", - "ContinuityCapability", - "CoreRoutineCapability", - "CoverglassSerialNumber", - "DMin", - "DataPlanCapability", - "DebugBoardRevision", - "DelaySleepForHeadsetClickCapability", - "DesenseBuild", - "DeviceAlwaysPrewarmActuator", - "DeviceBackGlassMaterial", - "DeviceBackingColor", - "DeviceBrand", - "DeviceClass", - "DeviceClassNumber", - "DeviceColor", - "DeviceColorMapPolicy", - "DeviceCornerRadius", - "DeviceCoverGlassColor", - "DeviceCoverGlassMaterial", - "DeviceCoverMaterial", - "DeviceEnclosureColor", - "DeviceEnclosureMaterial", - "DeviceEnclosureRGBColor", - "DeviceHasAggregateCamera", - "DeviceHousingColor", - "DeviceIsMuseCapable", - "DeviceKeyboardCalibration", - "DeviceLaunchTimeLimitScale", - "DeviceName", - "DeviceNameString", - "DevicePrefers3DBuildingStrokes", - "DevicePrefersBuildingStrokes", - "DevicePrefersCheapTrafficShaders", - "DevicePrefersProceduralAntiAliasing", - "DevicePrefersTrafficAlpha", - "DeviceProximityCapability", - "DeviceRGBColor", - "DeviceRequiresPetalOptimization", - "DeviceRequiresProximityAmeliorations", - "DeviceRequiresSoftwareBrightnessCalculations", - "DeviceSceneUpdateTimeLimitScale", - "DeviceSubBrand", - "DeviceSupports1080p", - "DeviceSupports3DImagery", - "DeviceSupports3DMaps", - "DeviceSupports3rdPartyHaptics", - "DeviceSupports4G", - "DeviceSupports4k", - "DeviceSupports64Bit", - "DeviceSupports720p", - "DeviceSupports9Pin", - "DeviceSupportsAOP", - "DeviceSupportsARKit", - "DeviceSupportsASTC", - "DeviceSupportsAdaptiveMapsUI", - "DeviceSupportsAlwaysListening", - "DeviceSupportsAlwaysOnCompass", - "DeviceSupportsAlwaysOnTime", - "DeviceSupportsApplePencil", - "DeviceSupportsAutoLowLightVideo", - "DeviceSupportsAvatars", - "DeviceSupportsBatteryModuleAuthentication", - "DeviceSupportsBerkelium2", - "DeviceSupportsCCK", - "DeviceSupportsCameraCaptureOnTouchDown", - "DeviceSupportsCameraDeferredProcessing", - "DeviceSupportsCameraHaptics", - "DeviceSupportsCarIntegration", - "DeviceSupportsCinnamon", - "DeviceSupportsClosedLoopHaptics", - "DeviceSupportsCrudeProx", - "DeviceSupportsDClr", - "DeviceSupportsDoNotDisturbWhileDriving", - "DeviceSupportsELabel", - "DeviceSupportsEnhancedAC3", - "DeviceSupportsEnvironmentalDosimetry", - "DeviceSupportsExternalHDR", - "DeviceSupportsFloorCounting", - "DeviceSupportsHDRDeferredProcessing", - "DeviceSupportsHMEInARKit", - "DeviceSupportsHaptics", - "DeviceSupportsHardwareDetents", - "DeviceSupportsHeartHealthAlerts", - "DeviceSupportsHeartRateVariability", - "DeviceSupportsHiResBuildings", - "DeviceSupportsLineIn", - "DeviceSupportsLiquidDetection_CorrosionMitigation", - "DeviceSupportsLivePhotoAuto", - "DeviceSupportsLongFormAudio", - "DeviceSupportsMapsBlurredUI", - "DeviceSupportsMapsOpticalHeading", - "DeviceSupportsMomentCapture", - "DeviceSupportsNFC", - "DeviceSupportsNavigation", - "DeviceSupportsNewton", - "DeviceSupportsOnDemandPhotoAnalysis", - "DeviceSupportsP3ColorspaceVideoRecording", - "DeviceSupportsPeriodicALSUpdates", - "DeviceSupportsPhotosLocalLight", - "DeviceSupportsPortraitIntensityAdjustments", - "DeviceSupportsPortraitLightEffectFilters", - "DeviceSupportsRGB10", - "DeviceSupportsRaiseToSpeak", - "DeviceSupportsSiDP", - "DeviceSupportsSideButtonClickSpeed", - "DeviceSupportsSimplisticRoadMesh", - "DeviceSupportsSingleCameraPortrait", - "DeviceSupportsSiriBargeIn", - "DeviceSupportsSiriSpeaks", - "DeviceSupportsSiriSpokenMessages", - "DeviceSupportsSpatialOverCapture", - "DeviceSupportsStereoAudioRecording", - "DeviceSupportsStudioLightPortraitPreview", - "DeviceSupportsSwimmingWorkouts", - "DeviceSupportsTapToWake", - "DeviceSupportsTelephonyOverUSB", - "DeviceSupportsTethering", - "DeviceSupportsToneMapping", - "DeviceSupportsUSBTypeC", - "DeviceSupportsVSHCompensation", - "DeviceSupportsVoiceOverCanUseSiriVoice", - "DeviceSupportsWebkit", - "DeviceSupportsWirelessSplitting", - "DeviceSupportsYCbCr10", - "DeviceVariant", - "DeviceVariantGuess", - "DiagData", - "DictationCapability", - "DieId", - "DiskUsage", - "DisplayDriverICChipID", - "DisplayFCCLogosViaSoftwareCapability", - "DisplayMirroringCapability", - "DisplayPortCapability", - "DualSIMActivationPolicyCapable", - "EUICCChipID", - "EffectiveProductionStatus", - "EffectiveProductionStatusAp", - "EffectiveProductionStatusSEP", - "EffectiveSecurityMode", - "EffectiveSecurityModeAp", - "EffectiveSecurityModeSEP", - "EncodeAACCapability", - "EncryptedDataPartitionCapability", - "EnforceCameraShutterClick", - "EnforceGoogleMail", - "EthernetMacAddress", - "EthernetMacAddressData", - "ExplicitContentRestriction", - "ExternalChargeCapability", - "ExternalPowerSourceConnected", - "FDRSealingStatus", - "FMFAllowed", - "FaceTimeBackCameraTemporalNoiseReductionMode", - "FaceTimeBitRate2G", - "FaceTimeBitRate3G", - "FaceTimeBitRateLTE", - "FaceTimeBitRateWiFi", - "FaceTimeCameraRequiresFastSwitchOptions", - "FaceTimeCameraSupportsHardwareFaceDetection", - "FaceTimeDecodings", - "FaceTimeEncodings", - "FaceTimeFrontCameraTemporalNoiseReductionMode", - "FaceTimePhotosOptIn", - "FaceTimePreferredDecoding", - "FaceTimePreferredEncoding", - "FirmwareNonce", - "FirmwarePreflightInfo", - "FirmwareVersion", - "FirstPartyLaunchTimeLimitScale", - "ForwardCameraCapability", - "FrontCameraOffsetFromDisplayCenter", - "FrontCameraRotationFromDisplayNormal", - "FrontFacingCameraAutoHDRCapability", - "FrontFacingCameraBurstCapability", - "FrontFacingCameraCapability", - "FrontFacingCameraHDRCapability", - "FrontFacingCameraHDROnCapability", - "FrontFacingCameraHFRCapability", - "FrontFacingCameraHFRVideoCapture1080pMaxFPS", - "FrontFacingCameraHFRVideoCapture720pMaxFPS", - "FrontFacingCameraMaxVideoZoomFactor", - "FrontFacingCameraModuleSerialNumber", - "FrontFacingCameraStillDurationForBurst", - "FrontFacingCameraVideoCapture1080pMaxFPS", - "FrontFacingCameraVideoCapture4kMaxFPS", - "FrontFacingCameraVideoCapture720pMaxFPS", - "FrontFacingIRCameraModuleSerialNumber", - "FrontFacingIRStructuredLightProjectorModuleSerialNumber", - "Full6FeaturesCapability", - "GPSCapability", - "GSDeviceName", - "GameKitCapability", - "GasGaugeBatteryCapability", - "GreenTeaDeviceCapability", - "GyroscopeCapability", - "H264EncoderCapability", - "HDRImageCaptureCapability", - "HDVideoCaptureCapability", - "HEVCDecoder10bitSupported", - "HEVCDecoder12bitSupported", - "HEVCDecoder8bitSupported", - "HEVCEncodingCapability", - "HMERefreshRateInARKit", - "HWModelStr", - "HallEffectSensorCapability", - "HardwareEncodeSnapshotsCapability", - "HardwareKeyboardCapability", - "HardwarePlatform", - "HardwareSnapshotsRequirePurpleGfxCapability", - "HasAllFeaturesCapability", - "HasAppleNeuralEngine", - "HasBaseband", - "HasBattery", - "HasDaliMode", - "HasExtendedColorDisplay", - "HasIcefall", - "HasInternalSettingsBundle", - "HasMesa", - "HasPKA", - "HasSEP", - "HasSpringBoard", - "HasThinBezel", - "HealthKitCapability", - "HearingAidAudioEqualizationCapability", - "HearingAidLowEnergyAudioCapability", - "HearingAidPowerReductionCapability", - "HiDPICapability", - "HiccoughInterval", - "HideNonDefaultApplicationsCapability", - "HighestSupportedVideoMode", - "HomeButtonType", - "HomeScreenWallpaperCapability", - "IDAMCapability", - "IOSurfaceBackedImagesCapability", - "IOSurfaceFormatDictionary", - "IceFallID", - "IcefallInRestrictedMode", - "IcefallInfo", - "Image4CryptoHashMethod", - "Image4Supported", - "InDiagnosticsMode", - "IntegratedCircuitCardIdentifier", - "IntegratedCircuitCardIdentifier2", - "InternalBuild", - "InternationalMobileEquipmentIdentity", - "InternationalMobileEquipmentIdentity2", - "InternationalSettingsCapability", - "InverseDeviceID", - "IsEmulatedDevice", - "IsLargeFormatPhone", - "IsPwrOpposedVol", - "IsServicePart", - "IsSimulator", - "IsThereEnoughBatteryLevelForSoftwareUpdate", - "IsUIBuild", - "JasperSerialNumber", - "LTEDeviceCapability", - "LaunchTimeLimitScaleSupported", - "LisaCapability", - "LoadThumbnailsWhileScrollingCapability", - "LocalizedDeviceNameString", - "LocationRemindersCapability", - "LocationServicesCapability", - "LowPowerWalletMode", - "LunaFlexSerialNumber", - "LynxPublicKey", - "LynxSerialNumber", - "MLBSerialNumber", - "MLEHW", - "MMSCapability", - "MacBridgingKeys", - "MagnetometerCapability", - "MainDisplayRotation", - "MainScreenCanvasSizes", - "MainScreenClass", - "MainScreenHeight", - "MainScreenOrientation", - "MainScreenPitch", - "MainScreenScale", - "MainScreenStaticInfo", - "MainScreenWidth", - "MarketingNameString", - "MarketingProductName", - "MarketingVersion", - "MaxH264PlaybackLevel", - "MaximumScreenScale", - "MedusaFloatingLiveAppCapability", - "MedusaOverlayAppCapability", - "MedusaPIPCapability", - "MedusaPinnedAppCapability", - "MesaSerialNumber", - "MetalCapability", - "MicrophoneCapability", - "MicrophoneCount", - "MinimumSupportediTunesVersion", - "MixAndMatchPrevention", - "MobileDeviceMinimumVersion", - "MobileEquipmentIdentifier", - "MobileEquipmentInfoBaseId", - "MobileEquipmentInfoBaseProfile", - "MobileEquipmentInfoBaseVersion", - "MobileEquipmentInfoCSN", - "MobileEquipmentInfoDisplayCSN", - "MobileSubscriberCountryCode", - "MobileSubscriberNetworkCode", - "MobileWifi", - "ModelNumber", - "MonarchLowEndHardware", - "MultiLynxPublicKeyArray", - "MultiLynxSerialNumberArray", - "MultitaskingCapability", - "MultitaskingGesturesCapability", - "MusicStore", - "MusicStoreCapability", - "N78aHack", - "NFCRadio", - "NFCRadioCalibrationDataPresent", - "NFCUniqueChipID", - "NVRAMDictionary", - "NandControllerUID", - "NavajoFusingState", - "NikeIpodCapability", - "NotGreenTeaDeviceCapability", - "OLEDDisplay", - "OTAActivationCapability", - "OfflineDictationCapability", - "OpenGLES1Capability", - "OpenGLES2Capability", - "OpenGLES3Capability", - "OpenGLESVersion", - "PTPLargeFilesCapability", - "PanelSerialNumber", - "PanoramaCameraCapability", - "PartitionType", - "PasswordConfigured", - "PasswordProtected", - "PearlCameraCapability", - "PearlIDCapability", - "PeekUICapability", - "PeekUIWidth", - "Peer2PeerCapability", - "PersonalHotspotCapability", - "PhoneNumber", - "PhoneNumber2", - "PhosphorusCapability", - "PhotoAdjustmentsCapability", - "PhotoCapability", - "PhotoSharingCapability", - "PhotoStreamCapability", - "PhotosPostEffectsCapability", - "PiezoClickerCapability", - "PintoMacAddress", - "PintoMacAddressData", - "PipelinedStillImageProcessingCapability", - "PlatformStandAloneContactsCapability", - "PlatinumCapability", - "ProductHash", - "ProductName", - "ProductType", - "ProductVersion", - "ProximitySensorCalibration", - "ProximitySensorCalibrationDictionary", - "ProximitySensorCapability", - "RF-exposure-separation-distance", - "RFExposureSeparationDistance", - "RawPanelSerialNumber", - "RearCameraCapability", - "RearCameraOffsetFromDisplayCenter", - "RearFacingCamera60fpsVideoCaptureCapability", - "RearFacingCameraAutoHDRCapability", - "RearFacingCameraBurstCapability", - "RearFacingCameraCapability", - "RearFacingCameraHDRCapability", - "RearFacingCameraHDROnCapability", - "RearFacingCameraHFRCapability", - "RearFacingCameraHFRVideoCapture1080pMaxFPS", - "RearFacingCameraHFRVideoCapture720pMaxFPS", - "RearFacingCameraMaxVideoZoomFactor", - "RearFacingCameraModuleSerialNumber", - "RearFacingCameraStillDurationForBurst", - "RearFacingCameraSuperWideCameraCapability", - "RearFacingCameraTimeOfFlightCameraCapability", - "RearFacingCameraVideoCapture1080pMaxFPS", - "RearFacingCameraVideoCapture4kMaxFPS", - "RearFacingCameraVideoCapture720pMaxFPS", - "RearFacingCameraVideoCaptureFPS", - "RearFacingLowLightCameraCapability", - "RearFacingSuperWideCameraModuleSerialNumber", - "RearFacingTelephotoCameraCapability", - "RearFacingTelephotoCameraModuleSerialNumber", - "RecoveryOSVersion", - "RegionCode", - "RegionInfo", - "RegionSupportsCinnamon", - "RegionalBehaviorAll", - "RegionalBehaviorChinaBrick", - "RegionalBehaviorEUVolumeLimit", - "RegionalBehaviorGB18030", - "RegionalBehaviorGoogleMail", - "RegionalBehaviorNTSC", - "RegionalBehaviorNoPasscodeLocationTiles", - "RegionalBehaviorNoVOIP", - "RegionalBehaviorNoWiFi", - "RegionalBehaviorShutterClick", - "RegionalBehaviorValid", - "RegionalBehaviorVolumeLimit", - "RegulatoryModelNumber", - "ReleaseType", - "RemoteBluetoothAddress", - "RemoteBluetoothAddressData", - "RenderWideGamutImagesAtDisplayTime", - "RendersLetterPressSlowly", - "RequiredBatteryLevelForSoftwareUpdate", - "RestoreOSBuild", - "RestrictedCountryCodes", - "RingerSwitchCapability", - "RosalineSerialNumber", - "RoswellChipID", - "RotateToWakeStatus", - "SBAllowSensitiveUI", - "SBCanForceDebuggingInfo", - "SDIOManufacturerTuple", - "SDIOProductInfo", - "SEInfo", - "SEPNonce", - "SIMCapability", - "SIMPhonebookCapability", - "SIMStatus", - "SIMStatus2", - "SIMTrayStatus", - "SIMTrayStatus2", - "SMSCapability", - "SavageChipID", - "SavageInfo", - "SavageSerialNumber", - "SavageUID", - "ScreenDimensions", - "ScreenDimensionsCapability", - "ScreenRecorderCapability", - "ScreenSerialNumber", - "SecondaryBluetoothMacAddress", - "SecondaryBluetoothMacAddressData", - "SecondaryEthernetMacAddress", - "SecondaryEthernetMacAddressData", - "SecondaryWifiMacAddress", - "SecondaryWifiMacAddressData", - "SecureElement", - "SecureElementID", - "SecurityDomain", - "SensitiveUICapability", - "SerialNumber", - "ShoeboxCapability", - "ShouldHactivate", - "SiKACapability", - "SigningFuse", - "SiliconBringupBoard", - "SimultaneousCallAndDataCurrentlySupported", - "SimultaneousCallAndDataSupported", - "SiriGestureCapability", - "SiriOfflineCapability", - "Skey", - "SoftwareBehavior", - "SoftwareBundleVersion", - "SoftwareDimmingAlpha", - "SpeakerCalibrationMiGa", - "SpeakerCalibrationSpGa", - "SpeakerCalibrationSpTS", - "SphereCapability", - "StarkCapability", - "StockholmJcopInfo", - "StrictWakeKeyboardCases", - "SupportedDeviceFamilies", - "SupportedKeyboards", - "SupportsBurninMitigation", - "SupportsEDUMU", - "SupportsForceTouch", - "SupportsIrisCapture", - "SupportsLowPowerMode", - "SupportsPerseus", - "SupportsRotateToWake", - "SupportsSOS", - "SupportsSSHBButtonType", - "SupportsTouchRemote", - "SysCfg", - "SysCfgDict", - "SystemImageID", - "SystemTelephonyOfAnyKindCapability", - "TVOutCrossfadeCapability", - "TVOutSettingsCapability", - "TelephonyCapability", - "TelephonyMaximumGeneration", - "TimeSyncCapability", - "TopModuleAuthChipID", - "TouchDelivery120Hz", - "TouchIDCapability", - "TristarID", - "UIBackgroundQuality", - "UIParallaxCapability", - "UIProceduralWallpaperCapability", - "UIReachability", - "UMTSDeviceCapability", - "UnifiedIPodCapability", - "UniqueChipID", - "UniqueDeviceID", - "UniqueDeviceIDData", - "UserAssignedDeviceName", - "UserIntentPhysicalButtonCGRect", - "UserIntentPhysicalButtonCGRectString", - "UserIntentPhysicalButtonNormalizedCGRect", - "VOIPCapability", - "VeniceCapability", - "VibratorCapability", - "VideoCameraCapability", - "VideoStillsCapability", - "VoiceControlCapability", - "VolumeButtonCapability", - "WAGraphicQuality", - "WAPICapability", - "WLANBkgScanCache", - "WSKU", - "WatchCompanionCapability", - "WatchSupportsAutoPlaylistPlayback", - "WatchSupportsHighQualityClockFaceGraphics", - "WatchSupportsListeningOnGesture", - "WatchSupportsMusicStreaming", - "WatchSupportsSiriCommute", - "WiFiCallingCapability", - "WiFiCapability", - "WifiAddress", - "WifiAddressData", - "WifiAntennaSKUVersion", - "WifiCallingSecondaryDeviceCapability", - "WifiChipset", - "WifiFirmwareVersion", - "WifiVendor", - "WirelessBoardSnum", - "WirelessChargingCapability", - "YonkersChipID", - "YonkersSerialNumber", - "YonkersUID", - "YouTubeCapability", - "YouTubePluginCapability", - "accelerometer", - "accessibility", - "additional-text-tones", - "aggregate-cam-photo-zoom", - "aggregate-cam-video-zoom", - "airDropRestriction", - "airplay-mirroring", - "airplay-no-mirroring", - "all-features", - "allow-32bit-apps", - "ambient-light-sensor", - "ane", - "any-telephony", - "apn", - "apple-internal-install", - "applicationInstallation", - "arkit", - "arm64", - "armv6", - "armv7", - "armv7s", - "assistant", - "auto-focus", - "auto-focus-camera", - "baseband-chipset", - "bitrate-2g", - "bitrate-3g", - "bitrate-lte", - "bitrate-wifi", - "bluetooth", - "bluetooth-le", - "board-id", - "boot-manifest-hash", - "boot-nonce", - "builtin-mics", - "c2k-device", - "calibration", - "call-forwarding", - "call-waiting", - "caller-id", - "camera-flash", - "camera-front", - "camera-front-flash", - "camera-rear", - "cameraRestriction", - "car-integration", - "cell-broadcast", - "cellular-data", - "certificate-production-status", - "certificate-security-mode", - "chip-id", - "class", - "closed-loop", - "config-number", - "contains-cellular-radio", - "crypto-hash-method", - "dali-mode", - "data-plan", - "debug-board-revision", - "delay-sleep-for-headset-click", - "device-color-policy", - "device-colors", - "device-name", - "device-name-localized", - "dictation", - "die-id", - "display-mirroring", - "display-rotation", - "displayport", - "does-not-support-gamekit", - "effective-production-status", - "effective-production-status-ap", - "effective-production-status-sep", - "effective-security-mode", - "effective-security-mode-ap", - "effective-security-mode-sep", - "enc-top-type", - "encode-aac", - "encrypted-data-partition", - "enforce-googlemail", - "enforce-shutter-click", - "euicc-chip-id", - "explicitContentRestriction", - "face-detection-support", - "fast-switch-options", - "fcc-logos-via-software", - "fcm-type", - "firmware-version", - "flash", - "front-auto-hdr", - "front-burst", - "front-burst-image-duration", - "front-facing-camera", - "front-flash-capability", - "front-hdr", - "front-hdr-on", - "front-max-video-fps-1080p", - "front-max-video-fps-4k", - "front-max-video-fps-720p", - "front-max-video-zoom", - "front-slowmo", - "full-6", - "function-button_halleffect", - "function-button_ringerab", - "gamekit", - "gas-gauge-battery", - "gps", - "gps-capable", - "green-tea", - "gyroscope", - "h264-encoder", - "hall-effect-sensor", - "haptics", - "hardware-keyboard", - "has-sphere", - "hd-video-capture", - "hdr-image-capture", - "healthkit", - "hearingaid-audio-equalization", - "hearingaid-low-energy-audio", - "hearingaid-power-reduction", - "hiccough-interval", - "hide-non-default-apps", - "hidpi", - "home-button-type", - "homescreen-wallpaper", - "hw-encode-snapshots", - "hw-snapshots-need-purplegfx", - "iAP2Capability", - "iPadCapability", - "iTunesFamilyID", - "iap2-protocol-supported", - "image4-supported", - "international-settings", - "io-surface-backed-images", - "ipad", - "kConferenceCallType", - "kSimultaneousCallAndDataCurrentlySupported", - "kSimultaneousCallAndDataSupported", - "large-format-phone", - "live-effects", - "live-photo-capture", - "load-thumbnails-while-scrolling", - "location-reminders", - "location-services", - "low-power-wallet-mode", - "lte-device", - "magnetometer", - "main-screen-class", - "main-screen-height", - "main-screen-orientation", - "main-screen-pitch", - "main-screen-scale", - "main-screen-width", - "marketing-name", - "mesa", - "metal", - "microphone", - "mix-n-match-prevention-status", - "mms", - "modelIdentifier", - "multi-touch", - "multitasking", - "multitasking-gestures", - "n78a-mode", - "name", - "navigation", - "nfc", - "nfcWithRadio", - "nike-ipod", - "nike-support", - "no-coreroutine", - "no-hi-res-buildings", - "no-simplistic-road-mesh", - "not-green-tea", - "offline-dictation", - "opal", - "opengles-1", - "opengles-2", - "opengles-3", - "opposed-power-vol-buttons", - "ota-activation", - "panorama", - "peek-ui-width", - "peer-peer", - "personal-hotspot", - "photo-adjustments", - "photo-stream", - "piezo-clicker", - "pipelined-stillimage-capability", - "platinum", - "post-effects", - "pressure", - "prox-sensor", - "proximity-sensor", - "ptp-large-files", - "public-key-accelerator", - "rear-auto-hdr", - "rear-burst", - "rear-burst-image-duration", - "rear-cam-telephoto-capability", - "rear-facing-camera", - "rear-hdr", - "rear-hdr-on", - "rear-max-slomo-video-fps-1080p", - "rear-max-slomo-video-fps-720p", - "rear-max-video-fps-1080p", - "rear-max-video-fps-4k", - "rear-max-video-fps-720p", - "rear-max-video-frame_rate", - "rear-max-video-zoom", - "rear-slowmo", - "regulatory-model-number", - "ringer-switch", - "role", - "s8000\")", - "s8003\")", - "sandman-support", - "screen-dimensions", - "sensitive-ui", - "shoebox", - "sika-support", - "sim", - "sim-phonebook", - "siri-gesture", - "slow-letterpress-rendering", - "sms", - "software-bundle-version", - "software-dimming-alpha", - "stand-alone-contacts", - "still-camera", - "stockholm", - "supports-always-listening", - "t7000\")", - "telephony", - "telephony-maximum-generation", - "thin-bezel", - "tnr-mode-back", - "tnr-mode-front", - "touch-id", - "tv-out-crossfade", - "tv-out-settings", - "ui-background-quality", - "ui-no-parallax", - "ui-no-procedural-wallpaper", - "ui-pip", - "ui-reachability", - "ui-traffic-cheap-shaders", - "ui-weather-quality", - "umts-device", - "unified-ipod", - "unique-chip-id", - "venice", - "video-camera", - "video-cap", - "video-stills", - "voice-control", - "voip", - "volume-buttons", - "wapi", - "watch-companion", - "wi-fi", - "wifi", - "wifi-antenna-sku-info", - "wifi-chipset", - "wifi-module-sn", - "wlan", - "wlan.background-scan-cache", - "youtube", - "youtubePlugin"}; - - // Create checkboxes for each key - for (const QString &key : mobileGestaltKeys) { - QCheckBox *checkbox = new QCheckBox(key); - checkbox->setStyleSheet("QCheckBox { margin: 2px; }"); - keyCheckboxes.append(checkbox); - checkboxLayout->addWidget(checkbox); - } -} - -QStringList QueryMobileGestaltWidget::getSelectedKeys() -{ - QStringList selectedKeys; - for (QCheckBox *checkbox : keyCheckboxes) { - if (checkbox->isChecked()) { - selectedKeys.append(checkbox->text()); - } - } - return selectedKeys; -} - -void QueryMobileGestaltWidget::onQueryButtonClicked() -{ - QStringList selectedKeys = getSelectedKeys(); - - if (selectedKeys.isEmpty()) { - statusLabel->setText("Please select at least one key to query."); - statusLabel->setStyleSheet("color: #ff6b6b; font-style: italic;"); - return; - } - - statusLabel->setText( - QString("Querying %1 key(s)...").arg(selectedKeys.size())); - statusLabel->setStyleSheet("color: #4CAF50; font-style: italic;"); - - m_device->service_manager->query_mobilegestalt(selectedKeys); -} - -void QueryMobileGestaltWidget::handleResults( - const QMap &results) -{ - displayResults(results); - - statusLabel->setText( - QString("Query completed. Found %1 result(s).").arg(results.size())); -} - -void QueryMobileGestaltWidget::onSelectAllClicked() -{ - for (QCheckBox *checkbox : keyCheckboxes) { - checkbox->setChecked(true); - } -} - -void QueryMobileGestaltWidget::onClearAllClicked() -{ - for (QCheckBox *checkbox : keyCheckboxes) { - checkbox->setChecked(false); - } -} - -void QueryMobileGestaltWidget::displayResults( - const QMap &results) -{ - QString output; - output += "MobileGestalt Query Results\n"; - output += "=" + QString("=").repeated(50) + "\n\n"; - - if (results.isEmpty()) { - output += "No results found.\n"; - } else { - for (auto it = results.begin(); it != results.end(); ++it) { - output += QString("Key: %1\n").arg(it.key()); - output += QString("Value: %1\n").arg(it.value().toString()); - output += QString("-").repeated(30) + "\n"; - } - } - - outputTextEdit->setPlainText(output); -} \ No newline at end of file diff --git a/src/querymobilegestaltwidget.h b/src/querymobilegestaltwidget.h deleted file mode 100644 index e9ba7bf..0000000 --- a/src/querymobilegestaltwidget.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef QUERYMOBILEGESTALTWIDGET_H -#define QUERYMOBILEGESTALTWIDGET_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class QueryMobileGestaltWidget : public Tool -{ - Q_OBJECT - -public: - QueryMobileGestaltWidget(const std::shared_ptr device, - QWidget *parent = nullptr); - -private slots: - void onQueryButtonClicked(); - void onSelectAllClicked(); - void onClearAllClicked(); - -public: - static bool - canOpenForDevice(const std::shared_ptr device) - { - // FIXME: not tested on iOS 17,18 but it's deprecated on iOS 26 - // assuming it won't work - if (device->deviceInfo.parsedDeviceVersion.major > 16) { - return false; - } - return true; - } - -private: - void setupUI(); - void populateKeys(); - QStringList getSelectedKeys(); - void displayResults(const QMap &results); - void handleResults(const QMap &results); - - // UI Components - QVBoxLayout *mainLayout; - QGroupBox *selectionGroup; - QScrollArea *scrollArea; - QWidget *checkboxWidget; - QVBoxLayout *checkboxLayout; - QHBoxLayout *buttonLayout; - QPushButton *selectAllButton; - QPushButton *clearAllButton; - QPushButton *queryButton; - QTextEdit *outputTextEdit; - QLabel *statusLabel; - const std::shared_ptr m_device; - - // Data - QStringList mobileGestaltKeys; - QList keyCheckboxes; -}; - -#endif // QUERYMOBILEGESTALTWIDGET_H \ No newline at end of file diff --git a/src/recoverydeviceinfowidget.cpp b/src/recoverydeviceinfowidget.cpp deleted file mode 100644 index b4ce007..0000000 --- a/src/recoverydeviceinfowidget.cpp +++ /dev/null @@ -1,184 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "recoverydeviceinfowidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include - -std::string parse_recovery_mode(irecv_mode productType) -{ - switch (productType) { - case irecv_mode::IRECV_K_RECOVERY_MODE_1: - case irecv_mode::IRECV_K_RECOVERY_MODE_2: - case irecv_mode::IRECV_K_RECOVERY_MODE_3: - case irecv_mode::IRECV_K_RECOVERY_MODE_4: - return "Recovery Mode"; - case irecv_mode::IRECV_K_WTF_MODE: - return "WTF Mode"; - case irecv_mode::IRECV_K_DFU_MODE: - case irecv_mode::IRECV_K_PORT_DFU_MODE: - return "DFU Mode"; - default: - return "Unknown Mode"; - } -} - -RecoveryDeviceInfoWidget::RecoveryDeviceInfoWidget( - const iDescriptorRecoveryDevice *info, QWidget *parent) - : QWidget{parent} -{ - ecid = info->ecid; // Assuming ecid is unique for each device - - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(20, 20, 20, 20); - mainLayout->setSpacing(16); - - // Device Information Group - QGroupBox *deviceInfoGroup = new QGroupBox("Device Information"); - QVBoxLayout *infoLayout = new QVBoxLayout(deviceInfoGroup); - infoLayout->setSpacing(8); - infoLayout->setContentsMargins(16, 16, 16, 16); - - // Device name with larger font - QLabel *deviceNameLabel = - new QLabel(QString::fromStdString(info->displayName)); - QFont nameFont = deviceNameLabel->font(); - nameFont.setPointSize(nameFont.pointSize() + 2); - nameFont.setWeight(QFont::DemiBold); - deviceNameLabel->setFont(nameFont); - infoLayout->addWidget(deviceNameLabel); - - // Add spacing - infoLayout->addSpacing(8); - - // Mode info - QString modeText = QString::fromStdString(parse_recovery_mode(info->mode)); - QLabel *modeLabel = new QLabel("Mode: " + modeText); - infoLayout->addWidget(modeLabel); - - // ECID info - QLabel *ecidLabel = new QLabel("ECID: " + QString::number(info->ecid)); - infoLayout->addWidget(ecidLabel); - - // CPID info - QLabel *cpidLabel = new QLabel("CPID: " + QString::number(info->cpid)); - infoLayout->addWidget(cpidLabel); - - mainLayout->addWidget(deviceInfoGroup); - - // Actions Group - QGroupBox *actionsGroup = new QGroupBox("Actions"); - QVBoxLayout *actionsLayout = new QVBoxLayout(actionsGroup); - actionsLayout->setSpacing(12); - actionsLayout->setContentsMargins(16, 16, 16, 16); - - // Info label - QLabel *infoLabel = new QLabel( - "Exit recovery mode and restart the device into normal mode."); - infoLabel->setWordWrap(true); - QFont infoFont = infoLabel->font(); - infoFont.setPointSize(infoFont.pointSize() - 1); - infoLabel->setFont(infoFont); - QPalette infoPalette = infoLabel->palette(); - infoPalette.setColor(QPalette::WindowText, - infoPalette.color(QPalette::WindowText).lighter(120)); - infoLabel->setPalette(infoPalette); - actionsLayout->addWidget(infoLabel); - - // Button layout - QHBoxLayout *buttonLayout = new QHBoxLayout(); - buttonLayout->addStretch(); - - QPushButton *exitRecoveryMode = new QPushButton("Exit Recovery Mode"); - exitRecoveryMode->setMinimumWidth(160); - exitRecoveryMode->setMinimumHeight(32); - - connect(exitRecoveryMode, &QPushButton::clicked, this, [this, info]() { - irecv_client_t client = NULL; - irecv_error_t ierr = irecv_open_with_ecid_and_attempts( - &client, info->ecid, RECOVERY_CLIENT_CONNECTION_TRIES); - irecv_error_t error = IRECV_E_SUCCESS; - if (ierr != IRECV_E_SUCCESS) { - QMessageBox::critical( - this, "Connection Error", - QString("Failed to open device with ECID %1:\n%2") - .arg(info->ecid) - .arg(irecv_strerror(ierr))); - return; - } - - if (client == NULL) { - QMessageBox::critical(this, "Error", - "Client is NULL after successful open"); - return; - } - - error = irecv_setenv(client, "auto-boot", "true"); - if (error != IRECV_E_SUCCESS) { - QMessageBox::critical( - this, "Error", - QString("Failed to set environment variable 'auto-boot':\n%1") - .arg(irecv_strerror(error))); - irecv_close(client); - return; - } - - error = irecv_saveenv(client); - if (error != IRECV_E_SUCCESS) { - QMessageBox::critical( - this, "Error", - QString("Failed to save environment variables:\n%1") - .arg(irecv_strerror(error))); - irecv_close(client); - return; - } - - error = irecv_reboot(client); - if (error != IRECV_E_SUCCESS) { - QMessageBox::critical(this, "Error", - QString("Failed to send reboot command:\n%1") - .arg(irecv_strerror(error))); - irecv_close(client); - return; - } - - irecv_close(client); - - auto *msgBox = new QMessageBox( - QMessageBox::Information, "Success", - "Device is exiting recovery mode and restarting... ", - QMessageBox::Ok, QApplication::activeWindow()); - msgBox->setAttribute(Qt::WA_DeleteOnClose); - msgBox->open(); - }); - - buttonLayout->addWidget(exitRecoveryMode); - buttonLayout->addStretch(); - actionsLayout->addLayout(buttonLayout); - - mainLayout->addWidget(actionsGroup); - mainLayout->addStretch(); -} diff --git a/src/recoverydeviceinfowidget.h b/src/recoverydeviceinfowidget.h deleted file mode 100644 index 67920ce..0000000 --- a/src/recoverydeviceinfowidget.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef RECOVERYDEVICEINFOWIDGET_H -#define RECOVERYDEVICEINFOWIDGET_H -#include "iDescriptor.h" -#include - -class RecoveryDeviceInfoWidget : public QWidget -{ - - Q_OBJECT -public: - explicit RecoveryDeviceInfoWidget(const iDescriptorRecoveryDevice *info, - QWidget *parent = nullptr); - uint64_t ecid; // Assuming ecid is unique for each device -signals: -}; - -#endif // RECOVERYDEVICEINFOWIDGET_H diff --git a/src/releasechangelogdialog.cpp b/src/releasechangelogdialog.cpp deleted file mode 100644 index da09075..0000000 --- a/src/releasechangelogdialog.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "releasechangelogdialog.h" -#include "iDescriptor.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -ReleaseChangelogDialog::ReleaseChangelogDialog(QJsonDocument data, - QWidget *parent) - : QDialog(parent) -{ - setupUI(data); -#ifdef WIN32 - setupWinWindow(this); -#endif -} - -ReleaseChangelogDialog::~ReleaseChangelogDialog() {} - -void ReleaseChangelogDialog::setupUI(const QJsonDocument &data) -{ - setWindowTitle("Release Changelog"); - setModal(true); - setMinimumSize(400, 250); - resize(500, 400); - - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(20, 20, 20, 20); - m_mainLayout->setSpacing(15); - - m_titleLabel = - new QLabel(QString("iDescriptor has been updated to v") + APP_VERSION); - m_titleLabel->setAlignment(Qt::AlignCenter); - m_titleLabel->setStyleSheet( - "font-size: 18px; font-weight: bold; margin-bottom: 10px;"); - m_mainLayout->addWidget(m_titleLabel); - - QString description = "Failed to load changelog data."; - QJsonArray dataArr = data.array(); - if (!dataArr.isEmpty()) { - for (const QJsonValue &releaseVal : dataArr) { - QJsonObject releaseObj = releaseVal.toObject(); - if (!releaseObj.isEmpty()) { - QString tagName = releaseObj.value("tag_name").toString(); - - if (tagName.isEmpty()) { - continue; - } - if (tagName == QString("v") + APP_VERSION) { - if (releaseObj.value("body").isUndefined()) - break; - description = releaseObj.value("body").toString(); - break; - } - } - } - } - - m_descriptionLabel = new QLabel(description); - m_descriptionLabel->setAlignment(Qt::AlignCenter); - m_descriptionLabel->setWordWrap(true); - m_descriptionLabel->setStyleSheet("font-size: 14px; margin: 10px;"); - m_mainLayout->addWidget(m_descriptionLabel); - - m_mainLayout->addStretch(); - - QHBoxLayout *buttonsLayout = new QHBoxLayout(); - m_skipButton = new QPushButton("Ok, Thanks!"); - m_skipButton->setFixedHeight(40); - - m_donateButton = new QPushButton("Donate"); - m_donateButton->setDefault(true); - m_donateButton->setFixedHeight(40); - - buttonsLayout->addWidget(m_skipButton); - buttonsLayout->addWidget(m_donateButton); - - m_mainLayout->addLayout(buttonsLayout, Qt::AlignCenter); - - connect(m_donateButton, &QPushButton::clicked, this, - &ReleaseChangelogDialog::onDonateClicked); - connect(m_skipButton, &QPushButton::clicked, this, - &ReleaseChangelogDialog::onSkipButtonClicked); -} - -void ReleaseChangelogDialog::onDonateClicked() -{ - QDesktopServices::openUrl(QUrl(DONATE_URL)); - accept(); -} - -void ReleaseChangelogDialog::onSkipButtonClicked() { accept(); } diff --git a/src/releasechangelogdialog.h b/src/releasechangelogdialog.h deleted file mode 100644 index c6acb15..0000000 --- a/src/releasechangelogdialog.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RELEASECHANGELOG_H -#define RELEASECHANGELOG_H - -#include -#include -#include -#include -#include - -class ReleaseChangelogDialog : public QDialog -{ - Q_OBJECT -public: - explicit ReleaseChangelogDialog(QJsonDocument data, - QWidget *parent = nullptr); - - ~ReleaseChangelogDialog(); -signals: - -private: - void setupUI(const QJsonDocument &data); - - QVBoxLayout *m_mainLayout = nullptr; - QPushButton *m_skipButton = nullptr; - QPushButton *m_donateButton = nullptr; - QLabel *m_titleLabel = nullptr; - QLabel *m_descriptionLabel = nullptr; - - void onDonateClicked(); - void onSkipButtonClicked(); -}; - -#endif // RELEASECHANGELOG_H diff --git a/src/responsiveqlabel.cpp b/src/responsiveqlabel.cpp deleted file mode 100644 index 2c7ae20..0000000 --- a/src/responsiveqlabel.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "responsiveqlabel.h" -#include -#include - -ResponsiveQLabel::ResponsiveQLabel(QWidget *parent) : QLabel(parent) -{ - setAlignment(Qt::AlignCenter); - setScaledContents(false); - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - setMinimumSize(100, 100); -} - -void ResponsiveQLabel::setPixmap(const QPixmap &pixmap) -{ - m_originalPixmap = pixmap; - updateScaledPixmap(); -} - -void ResponsiveQLabel::resizeEvent(QResizeEvent *event) -{ - QLabel::resizeEvent(event); - if (!m_originalPixmap.isNull()) { - updateScaledPixmap(); - } -} - -void ResponsiveQLabel::paintEvent(QPaintEvent *event) -{ - if (m_scaledPixmap.isNull()) { - QLabel::paintEvent(event); - return; - } - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - - // Calculate position to center the pixmap - int x = (width() - m_scaledPixmap.width()) / 2; - int y = (height() - m_scaledPixmap.height()) / 2; - - painter.drawPixmap(x, y, m_scaledPixmap); -} - -void ResponsiveQLabel::updateScaledPixmap() -{ - if (m_originalPixmap.isNull()) { - return; - } - - // Use the minimum width as the constraint for scaling - int targetWidth = qMax(minimumWidth(), width()); - - // Scale the pixmap while maintaining aspect ratio - m_scaledPixmap = - m_originalPixmap.scaled(targetWidth, QWIDGETSIZE_MAX, - Qt::KeepAspectRatio, Qt::SmoothTransformation); - - // Resize the widget to match the scaled pixmap size - // This prevents the widget from taking up more space than the actual image - if (!m_scaledPixmap.isNull()) { - setFixedSize(m_scaledPixmap.size()); - } - - update(); -} \ No newline at end of file diff --git a/src/responsiveqlabel.h b/src/responsiveqlabel.h deleted file mode 100644 index e2efc33..0000000 --- a/src/responsiveqlabel.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef RESPONSIVEQLABEL_H -#define RESPONSIVEQLABEL_H - -#include -#include -#include - -class ResponsiveQLabel : public QLabel -{ - Q_OBJECT - -public: - explicit ResponsiveQLabel(QWidget *parent = nullptr); - void setPixmap(const QPixmap &pixmap); - -protected: - void resizeEvent(QResizeEvent *event) override; - void paintEvent(QPaintEvent *event) override; - -private: - void updateScaledPixmap(); - - QPixmap m_originalPixmap; - QPixmap m_scaledPixmap; -}; - -#endif // RESPONSIVEQLABEL_H \ No newline at end of file diff --git a/src/rust/build.rs b/src/rust/build.rs deleted file mode 100644 index efe7d3c..0000000 --- a/src/rust/build.rs +++ /dev/null @@ -1,31 +0,0 @@ -use cxx_qt_build::{CxxQtBuilder, QmlModule}; - -fn main() { - let builder = CxxQtBuilder::new_qml_module( - QmlModule::new("com.kdab.cxx_qt.demo").qml_file("../qml/Tabs.qml"), - ) - .qt_module("Qml") - .files([ - "src/lib.rs", - "src/afc_services.rs", - "src/afc2_services.rs", - "src/service_manager.rs", - "src/screenshot.rs", - "src/hause_arrest.rs", - "src/io_manager.rs", - "src/query_sqlite.rs", - "src/image_loader.rs", - "src/bridge.rs", - "src/apps.rs", - ]) - .include_dir("include"); - - let builder = unsafe { - builder.cc_builder(|cc| { - cc.file("src/thumbnail.cc"); - cc.file("src/heic_to_image.cc"); - cc.file("src/qinput_get_text.cc"); - }) - }; - builder.build(); -} diff --git a/src/rust/include/bridge.h b/src/rust/include/bridge.h deleted file mode 100644 index 074e9f2..0000000 --- a/src/rust/include/bridge.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "rust/cxx.h" -#include - -QImage heic_to_image(rust::Slice buf); - -class AfcReader; - -QImage generate_thumbnail_with_reader(const AfcReader &reader, - int32_t file_size, int32_t requested_w, - int32_t requested_h); - -QString qinput_get_text(bool ok); \ No newline at end of file diff --git a/src/rust/src/afc.rs b/src/rust/src/afc.rs deleted file mode 100644 index a56c8ee..0000000 --- a/src/rust/src/afc.rs +++ /dev/null @@ -1,315 +0,0 @@ -use cxx_qt_lib::{QMap, QMapPair_QString_QVariant, QString, QVariant}; -use idevice::services::afc::{AfcClient, opcode::AfcFopenMode}; -use std::io::SeekFrom; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufWriter}; - -// FIXME: resolve symlinks -pub async fn check_is_dir_and_list( - afc: &mut AfcClient, - path_str: String, -) -> QMap { - let mut map = QMap::::default(); - - match afc.list_dir(&path_str).await { - Ok(list) => { - for name in list { - // ui already has up/down buttons maybe unnecessary - if name == "." || name == ".." { - continue; - } - let full_path = format!("{}/{}", path_str, name); - let is_dir = match afc.get_file_info(&full_path).await { - Ok(info) => info.st_ifmt == "S_IFDIR", - Err(e) => { - eprintln!("Failed to get file info for {full_path}: {e}"); - false - } - }; - map.insert(QString::from(name), QVariant::from(&is_dir)); - } - } - Err(e) => { - eprintln!("Failed to read directory {path_str}: {e}"); - } - } - - map -} - -pub async fn get_file_size(afc: &mut AfcClient, path_str: String) -> Option { - match afc.get_file_info(&path_str).await { - Ok(info) => Some(info.size), - Err(e) => { - eprintln!("Failed to get file info for {path_str}: {e}"); - None - } - } -} - -pub async fn handle_http_connection( - afc: &mut AfcClient, - path: String, - mut socket: tokio::net::TcpStream, -) { - use std::cmp::min; - - let mut buf = vec![0u8; 4096]; - let n = match socket.read(&mut buf).await { - Ok(n) if n > 0 => n, - _ => { - eprintln!( - "handle_http_connection: failed to read initial request for {}", - path - ); - return; - } - }; - - let req_str = String::from_utf8_lossy(&buf[..n]).to_string(); - eprintln!( - "handle_http_connection: received request head for {}: {}", - path, - req_str.lines().take(10).collect::>().join("\\n") - ); - let lines: Vec<&str> = req_str.split("\r\n").collect(); - if lines.is_empty() { - eprintln!("handle_http_connection: empty request lines for {}", path); - let _ = socket.shutdown().await; - return; - } - - // request line: "GET /... HTTP/1.1" - let mut method = "GET".to_string(); - if let Some(first) = lines.first() { - let parts: Vec<&str> = first.split_whitespace().collect(); - if parts.len() >= 1 { - method = parts[0].to_string(); - } - } - eprintln!("handle_http_connection: method={} for {}", method, path); - - if method != "GET" && method != "HEAD" { - eprintln!( - "handle_http_connection: unsupported method {} for {}", - method, path - ); - //FIXME:FLUSH? - let _ = socket - .write_all( - b"HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - ) - .await; - let _ = socket.shutdown().await; - return; - } - - // parse Range header - let mut has_range = false; - let mut range_start: i64 = 0; - let mut range_end: i64 = -1; - for line in &lines[1..] { - if line.is_empty() { - break; - } - if let Some(rest) = line - .strip_prefix("Range: bytes=") - .or_else(|| line.strip_prefix("range: bytes=")) - { - let parts: Vec<&str> = rest.trim().split('-').collect(); - if parts.len() == 2 { - has_range = true; - if let Ok(s) = parts[0].trim().parse::() { - range_start = s; - } - if !parts[1].trim().is_empty() { - if let Ok(e) = parts[1].trim().parse::() { - range_end = e; - } - } - } - } - } - eprintln!( - "handle_http_connection: range parsed has_range={} start={} end={} for {}", - has_range, range_start, range_end, path - ); - - let file_size = { - let info = match afc.get_file_info(path.clone()).await { - Ok(i) => i, - Err(e) => { - eprintln!( - "handle_http_connection: get_file_info({}) failed: {}", - path, e - ); - let _ = socket - .write_all( - b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - ) - .await; - let _ = socket.shutdown().await; - return; - } - }; - info.size as i64 - }; - - eprintln!( - "handle_http_connection: file_size={} for {}", - file_size, path - ); - if file_size <= 0 { - eprintln!("handle_http_connection: invalid file_size for {}", path); - let _ = socket - .write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") - .await; - let _ = socket.shutdown().await; - return; - } - - let mut start = 0_i64; - let mut end = file_size - 1; - - if has_range { - if range_start >= 0 { - start = range_start; - } - if range_end >= 0 && range_end < file_size { - end = range_end; - } - if start < 0 || start >= file_size || start > end { - eprintln!( - "handle_http_connection: range not satisfiable for {} (start={}, end={}, file_size={})", - path, start, end, file_size - ); - let _ = socket - .write_all( - b"HTTP/1.1 416 Range Not Satisfiable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - ) - .await; - let _ = socket.shutdown().await; - return; - } - } - - let content_len = (end - start + 1).max(0) as u64; - let path_lower = path.to_lowercase(); - let mime_type = if path_lower.ends_with(".mp4") || path_lower.ends_with(".m4v") { - "video/mp4" - } else if path_lower.ends_with(".mov") { - "video/quicktime" - } else if path_lower.ends_with(".avi") { - "video/x-msvideo" - } else if path_lower.ends_with(".mkv") { - "video/x-matroska" - } else { - "application/octet-stream" - }; - - let mut headers = String::new(); - if has_range { - headers.push_str("HTTP/1.1 206 Partial Content\r\n"); - headers.push_str(&format!( - "Content-Range: bytes {}-{}/{}\r\n", - start, end, file_size - )); - } else { - headers.push_str("HTTP/1.1 200 OK\r\n"); - } - - headers.push_str(&format!("Content-Length: {}\r\n", content_len)); - headers.push_str("Accept-Ranges: bytes\r\n"); - headers.push_str(&format!("Content-Type: {}\r\n", mime_type)); - headers.push_str("Connection: close\r\n"); - headers.push_str("Cache-Control: no-cache\r\n\r\n"); - - eprintln!( - "handle_http_connection: sending headers for {}: {}", - path, - headers.lines().next().unwrap_or_default() - ); - if socket.write_all(headers.as_bytes()).await.is_err() { - eprintln!("handle_http_connection: write headers failed for {}", path); - let _ = socket.shutdown().await; - return; - } - if socket.flush().await.is_err() { - eprintln!( - "handle_http_connection: flush failed after headers for {}", - path - ); - let _ = socket.shutdown().await; - return; - } - - if method == "HEAD" { - eprintln!( - "handle_http_connection: HEAD request completed for {}", - path - ); - let _ = socket.shutdown().await; - return; - } - - let mut fd = match afc.open(path.clone(), AfcFopenMode::RdOnly).await { - Ok(f) => f, - Err(e) => { - eprintln!("handle_http_connection: open({}) failed: {}", path, e); - let _ = socket.shutdown().await; - return; - } - }; - - if start > 0 { - if let Err(e) = fd.seek(SeekFrom::Start(start as u64)).await { - eprintln!( - "handle_http_connection: seek({}, {}) failed: {}", - path, start, e - ); - let _ = fd.close().await; - let _ = socket.shutdown().await; - return; - } - } - - eprintln!( - "handle_http_connection: streaming {} bytes ({}-{}) for {}", - content_len, start, end, path - ); - - let mut remaining = content_len; - // let mut chunk = vec![0u8; 64 * 1024]; - let mut writer = BufWriter::with_capacity(256 * 1024, &mut socket); - let mut chunk = vec![0u8; 256 * 1024]; - - while remaining > 0 { - let to_read = min(chunk.len() as u64, remaining) as usize; - let n = match fd.read(&mut chunk[..to_read]).await { - Ok(n) => n, - Err(e) => { - eprintln!("handle_http_connection: read({}) failed: {}", path, e); - break; - } - }; - if n == 0 { - eprintln!("handle_http_connection: EOF while streaming {}", path); - break; - } - if let Err(e) = writer.write_all(&chunk[..n]).await { - eprintln!("handle_http_connection: write({}) failed: {}", path, e); - break; - } - remaining -= n as u64; - } - - writer.flush().await.ok(); - //drop(writer) is explicit so the borrow is released before we touch socket again. - drop(writer); - - let _ = fd.close().await; - let _ = socket.shutdown().await; - eprintln!( - "handle_http_connection: finished/closed connection for {}", - path - ); -} diff --git a/src/rust/src/afc2_services.rs b/src/rust/src/afc2_services.rs deleted file mode 100644 index 8560960..0000000 --- a/src/rust/src/afc2_services.rs +++ /dev/null @@ -1,839 +0,0 @@ -use cxx_qt::Threading; -use cxx_qt_lib::{QByteArray, QList, QMap, QMapPair_QString_QVariant, QString}; - -use crate::{APP_DEVICE_STATE, RUNTIME, afc, run_sync}; -use idevice::afc::{AfcClient, opcode::AfcFopenMode}; -use once_cell::sync::Lazy; -use regex::Regex; -use std::{io::SeekFrom, pin::Pin}; -use tokio::io::{AsyncReadExt, AsyncSeekExt}; -use tokio::net::TcpListener; -use tokio::sync::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 QList_QString = cxx_qt_lib::QList; - type QByteArray = cxx_qt_lib::QByteArray; - type QMap_QString_QVariant = cxx_qt_lib::QMap; - } - - extern "RustQt" { - #[qobject] - type Afc2Backend = super::RAfc2Backend; - - #[qinvokable] - fn set_udid(self: Pin<&mut Afc2Backend>, udid: &QString); - - #[qinvokable] - fn load_album_list(self: Pin<&mut Afc2Backend>); - - #[qinvokable] - fn list_dir(self: &Afc2Backend, path: &QString) -> QList_QString; - - #[qinvokable] - fn file_to_buffer(self: &Afc2Backend, file_path: &QString) -> QByteArray; - - #[qinvokable] - fn is_directory(self: &Afc2Backend, path: &QString) -> bool; - - #[qinvokable] - fn get_file_size(self: &Afc2Backend, path: &QString) -> i64; - - #[qinvokable] - fn read_file_range(self: &Afc2Backend, path: &QString, offset: i64, len: i64) - -> QByteArray; - - #[qinvokable] - fn check_is_dir_and_list(self: &Afc2Backend, path: &QString); - #[qsignal] - fn check_is_dir_and_list_finished( - self: Pin<&mut Afc2Backend>, - success: bool, - entries: &QMap_QString_QVariant, - ); - - #[qsignal] - fn album_list_loaded(self: Pin<&mut Afc2Backend>, udid: QString, album_list: QList_QString); - - #[qinvokable] - fn get_dirs_item_count(self: &Afc2Backend, dir: &QList_QString) -> i64; - - #[qinvokable] - fn list_files_flat(self: &Afc2Backend, dir: &QString) -> QList_QString; - - #[qinvokable] - fn start_video_stream(self: &Afc2Backend, file_path: &QString) -> QString; - - #[qinvokable] - fn is_available(self: &Afc2Backend) -> bool; - - #[qinvokable] - fn delete_path(self: &Afc2Backend, path: &QString) -> bool; - } - - impl cxx_qt::Threading for Afc2Backend {} - impl cxx_qt::Constructor<(QString,), NewArguments = (QString,)> for Afc2Backend {} -} - -#[derive(Default)] -pub struct RAfc2Backend { - udid: QString, -} -impl cxx_qt::Constructor<(QString,)> for qobject::Afc2Backend { - type BaseArguments = (); - type InitializeArguments = (); - type NewArguments = (QString,); - - fn route_arguments( - args: (QString,), - ) -> ( - Self::NewArguments, - Self::BaseArguments, - Self::InitializeArguments, - ) { - (args, (), ()) - } - - fn new(args: (QString,)) -> RAfc2Backend { - RAfc2Backend { udid: args.0 } - } -} - -impl qobject::Afc2Backend { - fn get_udid(&self) -> &QString { - use cxx_qt::CxxQtType; - &self.rust().udid - } - - fn set_udid(mut self: Pin<&mut Self>, udid: &QString) { - use cxx_qt::CxxQtType; - self.as_mut().rust_mut().udid = udid.clone(); - } - - fn is_available(self: &Self) -> bool { - let udid_string = self.get_udid().to_string(); - - run_sync(async move { - let device = APP_DEVICE_STATE - .lock() - .await - .get(udid_string.as_str()) - .cloned(); - if let Some(d) = device { - d.afc2.is_some() - } else { - eprintln!("Device with UDID {} not found", udid_string); - false - } - }) - } - - fn is_directory(self: &Self, path: &QString) -> bool { - let udid_string = self.get_udid().to_string(); - let path_string = path.to_string(); - - run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid_string.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid_string); - return false; - } - }; - - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid_string); - return false; - } - } - }; - - let mut afc = afc_arc.lock().await; - match afc.get_file_info(path_string.clone()).await { - Ok(info) => info.st_ifmt == "S_IFDIR", - Err(e) => { - eprintln!("Failed to get file info for {path_string}: {e}"); - false - } - } - }) - } - - fn list_dir(self: &Self, path: &QString) -> QList { - let udid = self.get_udid().to_string(); - let path_str = path.to_string(); - let list = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid); - return Vec::new(); - } - }; - - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid); - return Vec::new(); - } - } - }; - - let mut afc = afc_arc.lock().await; - match afc.list_dir(&path_str).await { - Ok(list) => list, - Err(e) => { - eprintln!("Failed to read directory {path_str}: {e}"); - Vec::new() - } - } - }); - - let mut qlist: QList = QList::default(); - for name in list { - // ui already has up/down buttons maybe unnecessary - if name == "." || name == ".." { - continue; - } - qlist.append(QString::from(name)); - } - qlist - } - - fn check_is_dir_and_list(self: &Self, path: &QString) { - let udid = self.get_udid().to_string(); - let path_str = path.to_string(); - let qt_t = self.qt_thread(); - - RUNTIME.spawn(async move { - let qt_thread = qt_t.clone(); - let afc_arc = { - let device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - let device = match device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid); - qt_thread - .queue(move |q| { - q.check_is_dir_and_list_finished( - false, - &QMap::::default(), - ); - }) - .ok(); - return; - } - }; - - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid); - qt_thread - .queue(move |q| { - q.check_is_dir_and_list_finished( - false, - &QMap::::default(), - ); - }) - .ok(); - return; - } - } - }; - - let mut afc = afc_arc.lock().await; - let map_result = afc::check_is_dir_and_list(&mut afc, path_str).await; - - qt_thread - .queue(move |q| { - q.check_is_dir_and_list_finished(true, &map_result); - }) - .ok(); - }); - } - - fn load_album_list(self: Pin<&mut Self>) { - let qt_t = self.qt_thread(); - let udid_owned = self.get_udid().clone(); - - RUNTIME.spawn(async move { - let udid_str = udid_owned.to_string(); - let afc_arc = { - let device = APP_DEVICE_STATE - .lock() - .await - .get(udid_str.as_str()) - .cloned(); - let device = match device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid_str); - return; - } - }; - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid_str); - return; - } - } - }; - - println!("Device found: {:?}", udid_str); - - // list entries in /DCIM - let mut afc = afc_arc.lock().await; - let album_names = match afc.list_dir("/DCIM").await { - Ok(list) => list, - Err(e) => { - eprintln!("Failed to load /DCIM directory: {e}"); - return; - } - }; - - // Regexes: ^\d{3}APPLE$ and ^\d{8}$ - static RE_3DIGIT_APPLE: Lazy = - Lazy::new(|| Regex::new(r"^\d{3}APPLE$").unwrap()); - static RE_DATE_YYYYMMDD: Lazy = Lazy::new(|| Regex::new(r"^\d{8}$").unwrap()); - - let mut qlist_album: QList = QList::default(); - - for name in album_names { - // skip . and .. - if name == "." || name == ".." { - continue; - } - - // name filter - let matches_name = name.contains("APPLE") - || RE_3DIGIT_APPLE.is_match(&name) - || RE_DATE_YYYYMMDD.is_match(&name); - - if !matches_name { - continue; - } - - // check it's a directory - let full_path = format!("/DCIM/{name}"); - match afc.get_file_info(full_path).await { - Ok(info) => { - if info.st_ifmt != "S_IFDIR" { - continue; - } - } - Err(_) => continue, - }; - - qlist_album.append(QString::from(name)); - } - let qt_thread = qt_t.clone(); - qt_thread - .queue(move |backend_qobj| { - backend_qobj.album_list_loaded(udid_owned.clone(), qlist_album); - }) - .unwrap(); - }); - } - - fn file_to_buffer(&self, album_path: &QString) -> QByteArray { - let udid = self.get_udid().to_string(); - let album_path_string = album_path.to_string(); - - let data: Vec = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("file_to_buffer: device {udid} not found"); - return Vec::new(); - } - }; - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid); - return Vec::new(); - } - } - }; - - let mut afc = afc_arc.lock().await; - - let mut fd = match afc - .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 get_file_size(self: &Self, path: &QString) -> i64 { - let udid = self.get_udid().to_string(); - let path_string = path.to_string(); - - run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("file_to_buffer: device {udid} not found"); - return -1; - } - }; - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid); - return -1; - } - } - }; - - let mut afc = afc_arc.lock().await; - - afc::get_file_size(&mut afc, path_string) - .await - .map(|v| v as i64) - .unwrap_or(-1) - }) - } - - fn read_file_range(&self, path: &QString, offset: i64, len: i64) -> QByteArray { - if offset < 0 || len <= 0 { - return QByteArray::default(); - } - - let udid = self.get_udid().to_string(); - let path_string = path.to_string(); - - let data: Vec = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("read_file_range: device {udid} not found"); - return Vec::new(); - } - }; - - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid); - return Vec::new(); - } - } - }; - - let mut afc = afc_arc.lock().await; - - let mut fd = match afc.open(path_string.clone(), AfcFopenMode::RdOnly).await { - Ok(f) => f, - Err(e) => { - eprintln!("read_file_range: open({path_string}) failed: {e}"); - return Vec::new(); - } - }; - - if offset > 0 { - if let Err(e) = fd.seek(SeekFrom::Start(offset as u64)).await { - eprintln!("read_file_range: seek({path_string}, {offset}) failed: {e}"); - let _ = fd.close().await; - return Vec::new(); - } - } - - let mut buf = Vec::new(); - let mut remaining = len as usize; - let mut chunk = vec![0u8; 8192]; - - while remaining > 0 { - let to_read = remaining.min(chunk.len()); - let n = match fd.read(&mut chunk[..to_read]).await { - Ok(n) => n, - Err(e) => { - eprintln!("read_file_range: read({path_string}) failed: {e}"); - buf.clear(); - break; - } - }; - if n == 0 { - break; - } - buf.extend_from_slice(&chunk[..n]); - remaining -= n; - } - - let _ = fd.close().await; - buf - }); - - if data.is_empty() { - QByteArray::default() - } else { - QByteArray::from(&data[..]) - } - } - - fn get_dirs_item_count(self: &Self, dirs: &QList) -> i64 { - let udid = self.get_udid().to_string(); - - let mut dir_vec: Vec = Vec::new(); - for i in 0..dirs.len() { - if let Some(qdir) = dirs.get(i) { - dir_vec.push(qdir.to_string()); - } - } - - run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("get_dirs_item_count: device {udid} not found"); - return -1; - } - }; - - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid); - return -1; - } - } - }; - - let mut afc = afc_arc.lock().await; - let mut total: i64 = 0; - - for dir_str in dir_vec { - let names = match afc.list_dir(&dir_str).await { - Ok(list) => list, - Err(e) => { - eprintln!("get_dirs_item_count: list_dir({dir_str}) failed: {e}"); - continue; - } - }; - - let count = names - .into_iter() - .filter(|name| name != "." && name != "..") - .count() as i64; - - total += count; - } - - total - }) - } - - fn list_files_flat(self: &Self, dir: &QString) -> QList { - let udid = self.get_udid().to_string(); - let dir_str = dir.to_string(); - - let entries = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("list_files_flat: device {udid} not found"); - return Vec::new(); - } - }; - - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid); - return Vec::new(); - } - } - }; - - let mut afc = afc_arc.lock().await; - - let names = match afc.list_dir(&dir_str).await { - Ok(list) => list, - Err(e) => { - eprintln!("list_files_flat: list_dir({dir_str}) failed: {e}"); - return Vec::new(); - } - }; - - let mut files = Vec::new(); - for name in names { - if name == "." || name == ".." { - continue; - } - let full_path = format!("{}/{}", dir_str, name); - - match afc.get_file_info(full_path.clone()).await { - Ok(info) => { - if info.st_ifmt != "S_IFDIR" { - files.push(full_path); - } - } - Err(e) => { - eprintln!("list_files_flat: get_file_info({full_path}) failed: {e}"); - continue; - } - } - } - files - }); - - let mut qlist: QList = QList::default(); - for path in entries { - qlist.append(QString::from(path)); - } - qlist - } - - fn start_video_stream(&self, file_path: &QString) -> QString { - let udid_str = self.get_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 udid_clone = udid_str.clone(); - let path_clone = path_str.clone(); - - let mut afc_client = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(&udid_clone).cloned(); - let device = match maybe_device { - Some(d) => d, - None => { - // FIXME - // eprintln!( - // "handle_http_connection: device {} not found for {}", - // udid, path - // ); - // let _ = socket - // .write_all( - // b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - // ) - // .await; - // let _ = socket.shutdown().await; - return; - } - }; - let provider = device.provider.lock().await; - match AfcClient::new_afc2(provider.as_ref()).await { - Ok(c) => c, - Err(_) => { - //FIXME - // eprintln!( - // "handle_http_connection: AfcClient::connect failed for {}: {:?}", - // path, e - // ); - // let _ = socket - // .write_all( - // b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - // ) - // .await; - // let _ = socket.shutdown().await; - return; - } - } - }; - - - tokio::spawn(async move { - afc::handle_http_connection(&mut afc_client, path_clone, socket).await; - }); - } - } - } - eprintln!("start_video_stream: accept-loop exiting for {}", url_clone); - }); - - QString::from(url_clone_for_log) - } - - fn delete_path(self: &Self, path: &QString) -> bool { - let udid = self.get_udid().to_string(); - let path_str = path.to_string(); - - run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("delete_path: device {udid} not found"); - return false; - } - }; - - match device.afc2 { - Some(afc) => afc.clone(), - None => { - eprintln!("AFC2 service not available for device {}", udid); - return false; - } - } - }; - - let mut afc = afc_arc.lock().await; - - match afc.remove(path_str).await { - Ok(_) => true, - Err(e) => { - eprintln!("delete_path: remove_path failed: {e}"); - false - } - } - }) - } -} diff --git a/src/rust/src/afc_services.rs b/src/rust/src/afc_services.rs deleted file mode 100644 index bd6e236..0000000 --- a/src/rust/src/afc_services.rs +++ /dev/null @@ -1,824 +0,0 @@ -use cxx_qt::Threading; -use cxx_qt_lib::{ - QByteArray, QDateTime, QList, QMap, QMapPair_QString_QVariant, QString, QTimeZone, QVariant, -}; - -use crate::{APP_DEVICE_STATE, RUNTIME, afc, run_sync}; -use idevice::{ - IdeviceService, - afc::{AfcClient, opcode::AfcFopenMode}, -}; -use once_cell::sync::Lazy; -use regex::Regex; -use std::{io::SeekFrom, pin::Pin}; -use tokio::io::{AsyncReadExt, AsyncSeekExt}; -use tokio::net::TcpListener; -use tokio::sync::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"); - include!("cxx-qt-lib/qdatetime.h"); - - type QString = cxx_qt_lib::QString; - type QList_QString = cxx_qt_lib::QList; - type QByteArray = cxx_qt_lib::QByteArray; - type QMap_QString_QVariant = cxx_qt_lib::QMap; - } - - extern "RustQt" { - #[qobject] - type AfcBackend = super::RAfcBackend; - - #[qinvokable] - fn set_udid(self: Pin<&mut AfcBackend>, udid: &QString); - - #[qinvokable] - fn load_album_list(self: Pin<&mut AfcBackend>); - - #[qinvokable] - fn list_dir(self: &AfcBackend, path: &QString) -> QList_QString; - - #[qinvokable] - fn file_to_buffer(self: &AfcBackend, file_path: &QString) -> QByteArray; - - #[qinvokable] - fn is_directory(self: &AfcBackend, path: &QString) -> bool; - - #[qinvokable] - fn get_file_size(self: &AfcBackend, path: &QString) -> i64; - - #[qinvokable] - fn read_file_range(self: &AfcBackend, path: &QString, offset: i64, len: i64) -> QByteArray; - - #[qinvokable] - fn check_is_dir_and_list(self: &AfcBackend, path: &QString); - - #[qsignal] - fn check_is_dir_and_list_finished( - self: Pin<&mut AfcBackend>, - success: bool, - entries: &QMap_QString_QVariant, - ); - - #[qsignal] - fn album_list_loaded(self: Pin<&mut AfcBackend>, udid: QString, album_list: QList_QString); - - #[qinvokable] - fn get_dirs_item_count(self: &AfcBackend, dir: &QList_QString) -> i64; - - #[qinvokable] - fn list_files_flat(self: &AfcBackend, dir: &QString) -> QList_QString; - - #[qinvokable] - fn start_video_stream(self: &AfcBackend, file_path: &QString) -> QString; - - #[qinvokable] - fn list_dir_with_creation_date(self: &AfcBackend, path: &QString) -> QMap_QString_QVariant; - - #[qinvokable] - fn delete_path(self: &AfcBackend, path: &QString) -> bool; - } - - impl cxx_qt::Threading for AfcBackend {} - impl cxx_qt::Constructor<(QString,), NewArguments = (QString,)> for AfcBackend {} -} - -#[derive(Default)] -pub struct RAfcBackend { - udid: QString, -} -impl cxx_qt::Constructor<(QString,)> for qobject::AfcBackend { - type BaseArguments = (); - type InitializeArguments = (); - type NewArguments = (QString,); - - fn route_arguments( - args: (QString,), - ) -> ( - Self::NewArguments, - Self::BaseArguments, - Self::InitializeArguments, - ) { - (args, (), ()) - } - - fn new(args: (QString,)) -> RAfcBackend { - RAfcBackend { udid: args.0 } - } -} - -impl qobject::AfcBackend { - fn get_udid(&self) -> &QString { - use cxx_qt::CxxQtType; - &self.rust().udid - } - - fn set_udid(mut self: Pin<&mut Self>, udid: &QString) { - use cxx_qt::CxxQtType; - self.as_mut().rust_mut().udid = udid.clone(); - } - - fn is_directory(self: &Self, path: &QString) -> bool { - let udid_string = self.get_udid().to_string(); - let path_string = path.to_string(); - - run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid_string.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid_string); - return false; - } - }; - - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - match afc.get_file_info(path_string.clone()).await { - Ok(info) => info.st_ifmt == "S_IFDIR", - Err(e) => { - eprintln!("Failed to get file info for {path_string}: {e}"); - false - } - } - }) - } - - fn list_dir(self: &Self, path: &QString) -> QList { - let udid = self.get_udid().to_string(); - let path_str = path.to_string(); - let list = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid); - return Vec::new(); - } - }; - - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - match afc.list_dir(&path_str).await { - Ok(list) => list, - Err(e) => { - eprintln!("Failed to read directory {path_str}: {e}"); - Vec::new() - } - } - }); - - let mut qlist: QList = QList::default(); - for name in list { - // ui already has up/down buttons maybe unnecessary - if name == "." || name == ".." { - continue; - } - - qlist.append(QString::from(name)); - } - qlist - } - - fn check_is_dir_and_list(self: &Self, path: &QString) { - let udid = self.get_udid().to_string(); - let path_str = path.to_string(); - let qt_t = self.qt_thread(); - - RUNTIME.spawn(async move { - let qt_thread = qt_t.clone(); - let afc = { - let device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - let device = match device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid); - qt_thread - .queue(move |q| { - q.check_is_dir_and_list_finished( - false, - &QMap::::default(), - ); - }) - .ok(); - return; - } - }; - - device.afc.clone() - }; - - let mut afc = afc.lock().await; - let map_result = afc::check_is_dir_and_list(&mut afc, path_str).await; - - qt_thread - .queue(move |q| { - q.check_is_dir_and_list_finished(true, &map_result); - }) - .ok(); - }); - } - - fn load_album_list(self: Pin<&mut Self>) { - let qt_t = self.qt_thread(); - let udid_owned = self.get_udid().clone(); - - RUNTIME.spawn(async move { - let udid_str = udid_owned.to_string(); - let afc_arc = { - let device = APP_DEVICE_STATE - .lock() - .await - .get(udid_str.as_str()) - .cloned(); - let device = match device { - Some(d) => d, - None => { - eprintln!("Device with UDID {} not found", udid_str); - return; - } - }; - device.afc.clone() - }; - - println!("Device found: {:?}", udid_str); - - // list entries in /DCIM - let mut afc = afc_arc.lock().await; - let album_names = match afc.list_dir("/DCIM").await { - Ok(list) => list, - Err(e) => { - eprintln!("Failed to load /DCIM directory: {e}"); - return; - } - }; - - // Regexes: ^\d{3}APPLE$ and ^\d{8}$ - static RE_3DIGIT_APPLE: Lazy = - Lazy::new(|| Regex::new(r"^\d{3}APPLE$").unwrap()); - static RE_DATE_YYYYMMDD: Lazy = Lazy::new(|| Regex::new(r"^\d{8}$").unwrap()); - - let mut qlist_album: QList = QList::default(); - - for name in album_names { - // skip . and .. - if name == "." || name == ".." { - continue; - } - - // name filter - let matches_name = name.contains("APPLE") - || RE_3DIGIT_APPLE.is_match(&name) - || RE_DATE_YYYYMMDD.is_match(&name); - - if !matches_name { - continue; - } - - // check it's a directory - let full_path = format!("/DCIM/{name}"); - match afc.get_file_info(full_path).await { - Ok(info) => { - if info.st_ifmt != "S_IFDIR" { - continue; - } - } - Err(_) => continue, - }; - - qlist_album.append(QString::from(name)); - } - let qt_thread = qt_t.clone(); - qt_thread - .queue(move |backend_qobj| { - backend_qobj.album_list_loaded(udid_owned.clone(), qlist_album); - }) - .unwrap(); - }); - } - - fn file_to_buffer(&self, album_path: &QString) -> QByteArray { - let udid = self.get_udid().to_string(); - let album_path_string = album_path.to_string(); - - let data: Vec = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("file_to_buffer: device {udid} not found"); - return Vec::new(); - } - }; - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - - let mut fd = match afc - .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 get_file_size(self: &Self, path: &QString) -> i64 { - let udid = self.get_udid().to_string(); - let path_string = path.to_string(); - - run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("file_to_buffer: device {udid} not found"); - return -1; - } - }; - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - - afc::get_file_size(&mut afc, path_string) - .await - .map(|v| v as i64) - .unwrap_or(-1) - }) - } - - fn read_file_range(&self, path: &QString, offset: i64, len: i64) -> QByteArray { - if offset < 0 || len <= 0 { - return QByteArray::default(); - } - - let udid = self.get_udid().to_string(); - let path_string = path.to_string(); - - let data: Vec = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("read_file_range: device {udid} not found"); - return Vec::new(); - } - }; - - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - - let mut fd = match afc.open(path_string.clone(), AfcFopenMode::RdOnly).await { - Ok(f) => f, - Err(e) => { - eprintln!("read_file_range: open({path_string}) failed: {e}"); - return Vec::new(); - } - }; - - if offset > 0 { - if let Err(e) = fd.seek(SeekFrom::Start(offset as u64)).await { - eprintln!("read_file_range: seek({path_string}, {offset}) failed: {e}"); - let _ = fd.close().await; - return Vec::new(); - } - } - - let mut buf = Vec::new(); - let mut remaining = len as usize; - let mut chunk = vec![0u8; 8192]; - - while remaining > 0 { - let to_read = remaining.min(chunk.len()); - let n = match fd.read(&mut chunk[..to_read]).await { - Ok(n) => n, - Err(e) => { - eprintln!("read_file_range: read({path_string}) failed: {e}"); - buf.clear(); - break; - } - }; - if n == 0 { - break; - } - buf.extend_from_slice(&chunk[..n]); - remaining -= n; - } - - let _ = fd.close().await; - buf - }); - - if data.is_empty() { - QByteArray::default() - } else { - QByteArray::from(&data[..]) - } - } - - fn get_dirs_item_count(self: &Self, dirs: &QList) -> i64 { - let udid = self.get_udid().to_string(); - - let mut dir_vec: Vec = Vec::new(); - for i in 0..dirs.len() { - if let Some(qdir) = dirs.get(i) { - dir_vec.push(qdir.to_string()); - } - } - - run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("get_dirs_item_count: device {udid} not found"); - return -1; - } - }; - - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - let mut total: i64 = 0; - - for dir_str in dir_vec { - let names = match afc.list_dir(&dir_str).await { - Ok(list) => list, - Err(e) => { - eprintln!("get_dirs_item_count: list_dir({dir_str}) failed: {e}"); - continue; - } - }; - - let count = names - .into_iter() - .filter(|name| name != "." && name != "..") - .count() as i64; - - total += count; - } - - total - }) - } - - fn list_files_flat(self: &Self, dir: &QString) -> QList { - let udid = self.get_udid().to_string(); - let dir_str = dir.to_string(); - - let entries = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("list_files_flat: device {udid} not found"); - return Vec::new(); - } - }; - - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - - let names = match afc.list_dir(&dir_str).await { - Ok(list) => list, - Err(e) => { - eprintln!("list_files_flat: list_dir({dir_str}) failed: {e}"); - return Vec::new(); - } - }; - - let mut files = Vec::new(); - for name in names { - if name == "." || name == ".." { - continue; - } - let full_path = format!("{}/{}", dir_str, name); - - match afc.get_file_info(full_path.clone()).await { - Ok(info) => { - if info.st_ifmt != "S_IFDIR" { - files.push(full_path); - } - } - Err(e) => { - eprintln!("list_files_flat: get_file_info({full_path}) failed: {e}"); - continue; - } - } - } - files - }); - - let mut qlist: QList = QList::default(); - for path in entries { - qlist.append(QString::from(path)); - } - qlist - } - - fn start_video_stream(&self, file_path: &QString) -> QString { - let udid_str = self.get_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 udid_clone = udid_str.clone(); - let path_clone = path_str.clone(); - - let mut afc_client = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(&udid_clone).cloned(); - let device =match maybe_device { - Some(d) => d, - None => { - // FIXME - // eprintln!( - // "handle_http_connection: device {} not found for {}", - // udid, path - // ); - // let _ = socket - // .write_all( - // b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - // ) - // .await; - // let _ = socket.shutdown().await; - return; - } - }; - let provider = device.provider.lock().await; - match AfcClient::connect(provider.as_ref()).await { - Ok(c) => c, - Err(_) => { - //FIXME - // eprintln!( - // "handle_http_connection: AfcClient::connect failed for {}: {:?}", - // path, e - // ); - // let _ = socket - // .write_all( - // b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - // ) - // .await; - // let _ = socket.shutdown().await; - return; - } - } - }; - - - tokio::spawn(async move { - afc::handle_http_connection(&mut afc_client, path_clone, socket).await; - }); - } - } - } - eprintln!("start_video_stream: accept-loop exiting for {}", url_clone); - }); - - QString::from(url_clone_for_log) - } - - fn list_dir_with_creation_date(self: &Self, path: &QString) -> QMap { - let udid = self.get_udid().to_string(); - let dir_str = path.to_string(); - - let entries: Vec<(String, i64)> = run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("list_dir_with_creation_date: device {udid} not found"); - return Vec::new(); - } - }; - - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - - let names = match afc.list_dir(&dir_str).await { - Ok(list) => list, - Err(e) => { - eprintln!("list_dir_with_creation_date: list_dir({dir_str}) failed: {e}"); - return Vec::new(); - } - }; - - let mut result = Vec::new(); - for name in names { - if name == "." || name == ".." { - continue; - } - - let full_path = format!("{}/{}", dir_str, name); - match afc.get_file_info(full_path.clone()).await { - Ok(info) => { - // use creation time; could also choose info.modified - let creation_utc = info.creation.and_utc(); - let msecs = creation_utc.timestamp_millis(); - result.push((name, msecs)); - } - Err(e) => { - eprintln!( - "list_dir_with_creation_date: get_file_info({full_path}) failed: {e}" - ); - continue; - } - } - } - result - }); - - // Build QMap - let mut map: QMap = QMap::default(); - for (full_path, msecs) in entries { - let dt = QDateTime::from_msecs_since_epoch(msecs, &QTimeZone::utc()); - let var = QVariant::from(&dt); - map.insert(QString::from(full_path), var); - } - map - } - - fn delete_path(self: &Self, path: &QString) -> bool { - let udid = self.get_udid().to_string(); - let path_str = path.to_string(); - - run_sync(async move { - let afc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("delete_path: device {udid} not found"); - return false; - } - }; - - device.afc.clone() - }; - - let mut afc = afc_arc.lock().await; - - match afc.remove(&path_str).await { - Ok(_) => true, - Err(e) => { - eprintln!("delete_path: delete({path_str}) failed: {e}"); - false - } - } - }) - } -} diff --git a/src/rust/src/apps.rs b/src/rust/src/apps.rs deleted file mode 100644 index c79c32b..0000000 --- a/src/rust/src/apps.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::RUNTIME; - -use crate::qmap_insert; -use cxx_qt::Threading; -use cxx_qt_lib::{QMap, QMapPair_QString_QVariant, QString, QVariant}; -use ipatool::Account; -use ipatool::IpaTool; -use std::pin::Pin; - -#[cxx_qt::bridge] -mod qobject { - unsafe extern "C++" { - include!("cxx-qt-lib/qlist.h"); - include!("cxx-qt-lib/qmap.h"); - include!("cxx-qt-lib/qstring.h"); - - type QString = cxx_qt_lib::QString; - type QMap_QString_QVariant = cxx_qt_lib::QMap; - } - - extern "RustQt" { - #[qobject] - #[qml_element] - #[qproperty(QMap_QString_QVariant, state)] - type Apps = super::RApps; - - #[qinvokable] - fn init(self: Pin<&mut Apps>); - - #[qinvokable] - fn sign_in(self: Pin<&mut Apps>, email: &QString, password: &QString); - } - impl cxx_qt::Threading for Apps {} -} -pub struct RApps { - state: QMap, -} - -impl Default for RApps { - fn default() -> Self { - let mut state = QMap::::default(); - qmap_insert!(state, "init", false); - qmap_insert!(state, "error", QString::default()); - qmap_insert!(state, "email", QString::default()); - Self { state } - } -} - -impl qobject::Apps { - fn init(self: Pin<&mut Self>) { - let q_thread = self.qt_thread(); - RUNTIME.spawn(async move { - let res: anyhow::Result> = async { - let tool = IpaTool::new_default().await?; - Ok(tool.account_info().await?) - } - .await; - - match res { - Ok(maybe_acc) => { - let acc = maybe_acc.unwrap_or_default(); - println!("email :{}", acc.email); - - let mut state = QMap::::default(); - qmap_insert!(state, "init", true); - qmap_insert!(state, "error", QString::default()); - qmap_insert!(state, "email", QString::from(acc.email)); - - q_thread.queue(|t| t.set_state(state)).ok(); - } - Err(err) => { - let mut state = QMap::::default(); - qmap_insert!(state, "init", true); - qmap_insert!(state, "error", QString::from(format!("{}", err))); - qmap_insert!(state, "email", QString::default()); - - q_thread.queue(|t| t.set_state(state)).ok(); - } - } - }); - } - - fn sign_in(self: Pin<&mut Self>, email: &QString, password: &QString) { - // FIXME: implement - // RUNTIME.spawn(async move { - // let res: anyhow::Result<()> = async { - // let tool = IpaTool::new_default().await?; - - // let auth_code_cb: Box ipatool::Result + Send + Sync> = - // Box::new(|| { - // }); - // tool.login( - // &email.to_string(), - // &password.to_string(), - // Some(auth_code_cb), - // None, - // ) - // .await; - - // Ok(()) - // } - // .await; - - // Ok(()) - // }); - } -} diff --git a/src/rust/src/utils.rs b/src/rust/src/utils.rs deleted file mode 100644 index 8bc1e37..0000000 --- a/src/rust/src/utils.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::POSSIBLE_ROOT; -use cxx_qt_lib::QString; -use idevice::{ - IdeviceError, IdeviceService, afc::AfcClient, diagnostics_relay::DiagnosticsRelayClient, - house_arrest::HouseArrestClient, installation_proxy::InstallationProxyClient, - provider::IdeviceProvider, -}; -use plist::Dictionary as PlistDictionary; -use plist_macro::plist; -use rusqlite::Connection; -use serde_json::json; -use std::path::PathBuf; - -pub const PUBLIC_STAGING: &str = "PublicStaging"; - -pub async fn get_battery_info(diag: &mut DiagnosticsRelayClient) -> Option { - match diag.ioregistry(None, None, Some("IOPMPowerSource")).await { - Ok(Some(dict)) => Some(dict), - _ => None, - } -} - -pub async fn get_cable_info(diag: &mut DiagnosticsRelayClient) -> Option { - match diag - .ioregistry(None, None, Some("AppleTriStarBuiltIn")) - .await - { - Ok(Some(dict)) => Some(dict), - _ => None, - } -} - -pub async fn detect_jailbroken(afc: &mut AfcClient) -> bool { - match afc.list_dir(format!("{}/bin", POSSIBLE_ROOT)).await { - Ok(vec) => vec.len() > 0, - Err(_) => false, - } -} - -pub fn qstring_to_f64(qstring: &QString) -> Result { - let rust_string: String = qstring.into(); - rust_string.parse::() -} - -pub async fn calculate_apps_usage( - instproxy: &mut InstallationProxyClient, -) -> Result> { - let options = plist!({ - "ApplicationType": "User", - "ReturnAttributes": [ - "StaticDiskUsage", - "DynamicDiskUsage" - ] - }); - let apps = instproxy.browse(Some(options)).await?; - let mut total_apps_space = 0u64; - - for app_info in apps { - if let Some(app_dict) = app_info.as_dictionary() { - if let Some(static_usage) = app_dict - .get("StaticDiskUsage") - .and_then(|v| v.as_unsigned_integer()) - { - total_apps_space += static_usage; - } - - if let Some(dynamic_usage) = app_dict - .get("DynamicDiskUsage") - .and_then(|v| v.as_unsigned_integer()) - { - total_apps_space += dynamic_usage; - } - } - } - - Ok(total_apps_space) -} - -pub async fn vend_app_documents( - provider: &dyn IdeviceProvider, - bundle_id: &str, -) -> Result { - let house_arrest_client = HouseArrestClient::connect(provider).await?; - let afc_client = house_arrest_client.vend_documents(bundle_id).await?; - Ok(afc_client) -} - -pub fn query_gallery_usage(db_bytes: &mut Vec) -> Result { - // HACK: WAL -> legacy mode patch - if db_bytes.len() > 20 && db_bytes[18] == 0x02 { - db_bytes[18] = 0x01; - db_bytes[19] = 0x01; - } - println!("Querying gallery usage for disk usage calculation..."); - - // Open in-memory DB - let conn = Connection::open_in_memory()?; - - unsafe { - let db_ptr = rusqlite::ffi::sqlite3_deserialize( - conn.handle(), - b"main\0".as_ptr() as *const std::os::raw::c_char, - db_bytes.as_mut_ptr(), - db_bytes.len() as i64, - db_bytes.len() as i64, - rusqlite::ffi::SQLITE_DESERIALIZE_READONLY as u32, - ); - if db_ptr != rusqlite::ffi::SQLITE_OK { - return Err(rusqlite::Error::SqliteFailure( - rusqlite::ffi::Error::new(db_ptr), - None, - )); - } - } - - let size: i64 = conn.query_row( - "SELECT COALESCE(SUM(ZORIGINALFILESIZE), 0) FROM ZADDITIONALASSETATTRIBUTES", - [], - |row| row.get(0), - )?; - - println!("Gallery usage calculated: {} bytes", size); - Ok(size as u64) -} - -pub fn get_lockdown_path() -> PathBuf { - if let Ok(val) = std::env::var("USBMUXD_PAIRING_FILES_LOCATION") { - if !val.is_empty() { - eprintln!("Pulling pairing files from USBMUXD_PAIRING_FILES_LOCATION: {val}"); - return PathBuf::from(val); - } - } - - #[cfg(target_os = "linux")] - { - PathBuf::from("/var/lib/lockdown") - } - - #[cfg(target_os = "macos")] - { - PathBuf::from("/var/db/lockdown") - } - - #[cfg(target_os = "windows")] - { - let base = std::env::var_os("PROGRAMDATA") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(r"C:\ProgramData")); - base.join("Apple").join("Lockdown") - } -} - -/// Ensure `PublicStaging` exists on device via AFC -pub async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { - // Try to stat and if it fails, create directory - match afc.get_file_info(PUBLIC_STAGING).await { - Ok(_) => Ok(()), - Err(_) => afc.mk_dir(PUBLIC_STAGING).await, - } -} - -// converts album info to json -pub fn create_album_info( - album_id: i32, - item_count: i32, - asset_dir: String, - asset_file_name: String, -) -> String { - json!({"album_id" : album_id, "item_count" : item_count,"file_path" : format!("{}/{}",asset_dir,asset_file_name)}) - .to_string() -} - -// pub fn emit(thread: cxx_qt::CxxQtThread) {} -use cxx_qt_lib::{QMap, QMapPair_QString_QVariant, QVariant}; - -pub fn qmap_insert(map: &mut QMap, key: &str, value: &T) -where - QVariant: for<'a> From<&'a T>, -{ - map.insert(QString::from(key), QVariant::from(value)); -} - -#[macro_export] -macro_rules! qmap_insert { - ($map:expr, $key:expr , $value:expr) => { - $crate::utils::qmap_insert(&mut $map, $key, &$value) - }; -} diff --git a/src/rust/src/screenshot.rs b/src/screenshot.rs similarity index 100% rename from src/rust/src/screenshot.rs rename to src/screenshot.rs diff --git a/src/service_factory.rs b/src/service_factory.rs new file mode 100644 index 0000000..55f224c --- /dev/null +++ b/src/service_factory.rs @@ -0,0 +1,71 @@ +use crate::utils::{empty_qjsvalue, engine_ptr_new_object}; +use crate::{APP_DEVICE_STATE, afc_services::AfcServices, run_sync}; +use qmetaobject::{QJSValue, prelude::*}; +use std::ffi::c_void; + +/* + we need this because qml side + cannot pass constructor args + https://forum.qt.io/topic/155986/qt6-qabstractlistmodel-constructor-with-two-arguments-qml-element-is-not-creatable/2 +*/ +#[derive(QObject)] +pub struct ServiceFactory { + base: qt_base_class!(trait QObject), + /* SAFETY: check if engine_ptr.is_null() */ + engine_ptr: *mut c_void, + create_afc_client: qt_method!(fn(&self, udid: QString, afc2: bool) -> QJSValue), +} + +impl ServiceFactory { + pub fn new(engine_ptr: *mut c_void) -> Self { + Self { + base: Default::default(), + engine_ptr, + create_afc_client: Default::default(), + } + } + + fn create_afc_client(&self, udid: QString, afc2: bool) -> QJSValue { + let udid_str = udid.to_string(); + let engine_ptr: *mut c_void = self.engine_ptr; + + if engine_ptr.is_null() { + eprintln!("ServiceFactory: engine_ptr is null"); + return empty_qjsvalue(); + } + + 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 device = match maybe_device { + Some(d) => d, + None => { + eprintln!("ServiceFactory: device with UDID {udid_str} not found"); + return None; + } + }; + + if afc2 { + device.afc2.clone().or(None) + } else { + Some(device.afc.clone()) + } + } + }); + + let Some(afc) = afc_arc else { + eprintln!("ServiceFactory: no AFC client available for {udid_str} (afc2={afc2})"); + return empty_qjsvalue(); + }; + + let obj = AfcServices::from_afc_client(afc, udid_str); + let obj_ptr = qmetaobject::into_leaked_cpp_ptr(obj); + + engine_ptr_new_object(engine_ptr, obj_ptr) + } +} diff --git a/src/rust/src/service_manager.rs b/src/service_manager.rs similarity index 59% rename from src/rust/src/service_manager.rs rename to src/service_manager.rs index b49c8c9..e89dcb9 100644 --- a/src/rust/src/service_manager.rs +++ b/src/service_manager.rs @@ -1,7 +1,9 @@ -use crate::{APP_DEVICE_STATE, RUNTIME, run_sync, utils}; -use cxx_qt::Threading; -use cxx_qt_lib::{QByteArray, QList, QMap, QMapPair_QString_QVariant, QString, QVariant}; -use idevice::afc::opcode::AfcFopenMode; +use qmetaobject::prelude::*; +use qttypes::{QVariantMap,QStringList}; + +use crate::{APP_DEVICE_STATE, RUNTIME, run_sync, utils,qt_threading::{QtThread,QtThreading},DeviceServices}; +use macros::QtThreading; +use idevice::{afc::opcode::AfcFopenMode, provider}; use idevice::services::core_device_proxy::CoreDeviceProxy; use idevice::{ IdeviceService, RsdService, amfi, @@ -19,168 +21,65 @@ use plist_macro::plist; use serde_json; use std::{io::Read, pin::Pin, time::Duration}; -#[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"); +#[derive(Default, QObject, QtThreading)] +pub struct ServiceManager { + base: qt_base_class!(trait QObject), - type QString = cxx_qt_lib::QString; - type QList_QString = cxx_qt_lib::QList; - type QByteArray = cxx_qt_lib::QByteArray; - type QMap_QString_QVariant = cxx_qt_lib::QMap; - } + get_cable_info: qt_method!(fn(&self)), + reveal_developer_mode_option_in_ui: qt_method!(fn(&self)), + query_mobilegestalt: qt_method!(fn(&self, keys: QStringList)), + mount_dev_image: qt_method!(fn(&self, image_path: QString, sig: QString)), + get_mounted_image: qt_method!(fn(&self)), + fetch_installed_apps: qt_method!(fn(&self)), + set_location: qt_method!(fn(&self, latitude: QString, longitude: QString) -> i32), + fetch_app_icon: qt_method!(fn(&self, bundle_id: QString)), + fetch_disk_usage: qt_method!(fn(&self)), + restart: qt_method!(fn(&self) -> bool), + shutdown: qt_method!(fn(&self) -> bool), + enter_recovery_mode: qt_method!(fn(&self) -> bool), + install_ipa: qt_method!(fn(&self, ipa_path: QString)), + enable_wifi_connections: qt_method!(fn(&self)), - extern "RustQt" { - #[qobject] - type ServiceManager = super::RServiceManager; + // Signals + cable_info_retrieved: qt_signal!(info: QString), + mobilegestalt_info_retrieved: qt_signal!(info: QVariantMap), + dev_image_mounted: qt_signal!(success: bool, is_locked: bool), + developer_mode_option_revealed: qt_signal!(success: bool), + mounted_image_retrieved: qt_signal!( + success: bool, + is_locked: bool, + sig: QByteArray, + sig_length: u64 + ), + installed_apps_retrieved: qt_signal!(apps: QVariantMap), + app_icon_loaded: qt_signal!(bundle_id: QString, icon_data: QByteArray), + battery_info_updated: qt_signal!(info: QString), + disk_usage_retrieved: qt_signal!(success: bool, apps_usage: u64), + install_ipa_init: qt_signal!(started: bool, state: QString), + install_ipa_progress: qt_signal!(progress: f64, state: QString), + enable_wifi_connections_result: qt_signal!(success: bool), - #[qinvokable] - fn set_udid(self: Pin<&mut ServiceManager>, udid: &QString); - - #[qinvokable] - fn get_cable_info(&self); - - #[qinvokable] - fn reveal_developer_mode_option_in_ui(&self); - - #[qinvokable] - fn query_mobilegestalt(&self, keys: &QList_QString); - - #[qinvokable] - fn mount_dev_image(&self, image_path: &QString, sig: &QString); - - #[qinvokable] - fn get_mounted_image(&self); - - #[qinvokable] - fn fetch_installed_apps(&self); - - #[qinvokable] - fn set_location(&self, latitude: &QString, longitude: &QString) -> i32; - - #[qinvokable] - fn fetch_app_icon(&self, bundle_id: QString); - - #[qsignal] - fn cable_info_retrieved(self: Pin<&mut ServiceManager>, info: QString); - - #[qsignal] - fn mobilegestalt_info_retrieved( - self: Pin<&mut ServiceManager>, - info: QMap_QString_QVariant, - ); - - #[qsignal] - fn dev_image_mounted(self: Pin<&mut ServiceManager>, success: bool, is_locked: bool); - - #[qsignal] - fn developer_mode_option_revealed(self: Pin<&mut ServiceManager>, success: bool); - - #[qsignal] - fn mounted_image_retrieved( - self: Pin<&mut ServiceManager>, - success: bool, - is_locked: bool, - sig: QByteArray, - sig_length: u64, - ); - - #[qsignal] - fn installed_apps_retrieved(self: Pin<&mut ServiceManager>, apps: &QMap_QString_QVariant); - - #[qsignal] - fn app_icon_loaded( - self: Pin<&mut ServiceManager>, - bundle_id: QString, - icon_data: QByteArray, - ); - - #[qsignal] - fn battery_info_updated(self: Pin<&mut ServiceManager>, info: &QString); - - #[qinvokable] - fn fetch_disk_usage(&self, skip_gallery_usage: bool); - - #[qsignal] - fn disk_usage_retrieved( - self: Pin<&mut ServiceManager>, - success: bool, - apps_usage: u64, - gallery_usage: u64, - ); - - #[qinvokable] - fn restart(&self) -> bool; - - #[qinvokable] - fn shutdown(&self) -> bool; - - #[qinvokable] - fn enter_recovery_mode(&self) -> bool; - - #[qinvokable] - fn install_ipa(&self, ipa_path: &QString); - - #[qsignal] - fn install_ipa_init(self: Pin<&mut ServiceManager>, started: bool, state: QString); - - #[qsignal] - fn install_ipa_progress(self: Pin<&mut ServiceManager>, progress: f64, state: QString); - - #[qinvokable] - fn enable_wifi_connections(&self); - - #[qsignal] - fn enable_wifi_connections_result(self: Pin<&mut ServiceManager>, success: bool); - } - - impl cxx_qt::Threading for ServiceManager {} - impl cxx_qt::Constructor<(QString, u32), NewArguments = (QString, u32)> for ServiceManager {} -} - -#[derive(Default)] -pub struct RServiceManager { - udid: QString, + udid: String, ios_version: u32, + /* + * TODO: default cannot be implemented for + * DeviceServices, so we have to wrap inside Option + */ + device : Option } -impl cxx_qt::Constructor<(QString, u32)> for qobject::ServiceManager { - type BaseArguments = (); - type InitializeArguments = (); - type NewArguments = (QString, u32); +impl ServiceManager { + + /* unwrap on self.device must + * be safe as from_device literally + * receives a device not an option */ + pub fn from_device(device : DeviceServices, udid : String, ios_version : u32) -> Self { + Self { device: Some(device) , udid : udid, ios_version : ios_version ,..Default::default() } + } - fn route_arguments( - args: (QString, u32), - ) -> ( - Self::NewArguments, - Self::BaseArguments, - Self::InitializeArguments, - ) { - (args, (), ()) - } - - fn new(args: (QString, u32)) -> RServiceManager { - RServiceManager { - udid: args.0, - ios_version: args.1, - } - } - - fn initialize(self: Pin<&mut Self>, _arguments: Self::InitializeArguments) { - let udid_for_log = self.get_udid().to_string(); - println!("ServiceManager::initialize called for UDID: {udid_for_log}"); - self.start_update_battery_info_interval(); - } -} - -impl qobject::ServiceManager { - pub fn start_update_battery_info_interval(self: Pin<&mut Self>) { + pub fn start_update_battery_info_interval(&self) { + let udid = self.udid.clone(); let qt_thread = self.qt_thread(); - let udid = self.get_udid().to_string(); println!("Starting battery info update interval for device {udid}"); RUNTIME.spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(30)); @@ -208,9 +107,8 @@ impl qobject::ServiceManager { if let Ok(s) = String::from_utf8(buf) { qt_thread .queue(move |t| { - t.battery_info_updated(&QString::from(s)); - }) - .ok(); + t.battery_info_updated(QString::from(s)); + }); } } }); @@ -218,47 +116,20 @@ impl qobject::ServiceManager { }); } - fn get_udid(&self) -> &QString { - use cxx_qt::CxxQtType; - &self.rust().udid - } - - fn get_ios_version(&self) -> u32 { - use cxx_qt::CxxQtType; - self.rust().ios_version - } - - fn set_udid(mut self: Pin<&mut Self>, udid: &QString) { - use cxx_qt::CxxQtType; - self.as_mut().rust_mut().udid = udid.clone(); - } - - fn query_mobilegestalt(&self, keys: &QList) { - let udid = self.get_udid().to_string(); + fn query_mobilegestalt(&self, keys: QStringList) { + let udid = self.udid.clone(); let qt_t = self.qt_thread(); let keys_owned = keys.clone(); + let diag = self.device.as_ref().unwrap().clone().diag; RUNTIME.spawn(async move { - let mut map = QMap::::default(); + let mut map = QVariantMap::default(); let keys_vec: Vec = keys_owned .into_iter() .map(|qstr| qstr.to_string()) .collect(); let qt_thread = qt_t.clone(); - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("query_mobilegestalt: device {udid} not found"); - let _ = qt_thread.queue(move |t| { - t.mobilegestalt_info_retrieved(map); - }); - return; - } - }; - - let result = device.diag.lock().await.mobilegestalt(Some(keys_vec)).await; + let result = diag.lock().await.mobilegestalt(Some(keys_vec)).await; match result { Ok(opt_dict) => { @@ -300,33 +171,18 @@ impl qobject::ServiceManager { } fn get_cable_info(&self) { - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); let qt_t = self.qt_thread(); + let diag = self.device.as_ref().unwrap().clone().diag; RUNTIME.spawn(async move { let qt_thread = qt_t.clone(); - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("get_cable_info: device {udid} not found"); - let _ = qt_thread.queue(|t| { - t.cable_info_retrieved(QString::from("")); - }); - return; - } - }; - - let res = utils::get_cable_info(&mut *device.diag.lock().await).await; + let res = utils::get_cable_info(&mut *diag.lock().await).await; match res { Some(dict) => { let mut buf = Vec::new(); + // ditch this, and return map instead if Value::Dictionary(dict).to_writer_xml(&mut buf).is_err() { eprintln!( "get_cable_info: Failed to serialize ioregistry values to XML for device {udid}." @@ -362,34 +218,19 @@ impl qobject::ServiceManager { } }); } - fn mount_dev_image(&self, image_path: &QString, sig: &QString) { - let udid = self.get_udid().to_string(); + fn mount_dev_image(&self, image_path: QString, sig: QString) { + let udid = self.udid.clone(); let qt_t = self.qt_thread(); let image = image_path.to_string(); let signature = sig.to_string(); + let provider_guard = self.device.as_ref().unwrap().clone().provider; RUNTIME.spawn(async move { let qt_thread = qt_t.clone(); - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("get_cable_info: device {udid} not found"); - let _ = qt_thread.queue(|t| { - t.dev_image_mounted(false,false); - }); - return; - } - }; + let provider = provider_guard.lock().await; let mut mounter = match { - let provider_guard = device.provider.lock().await; - let provider_ref: &dyn IdeviceProvider = provider_guard.as_ref(); + let provider_ref: &dyn IdeviceProvider = provider.as_ref(); ImageMounter::connect(provider_ref).await } { Ok(m) => m, @@ -449,15 +290,15 @@ impl qobject::ServiceManager { } Err(idevice::IdeviceError::DeviceLocked) => { eprintln!("mount_dev_image: Failed to mount developer image for device {udid}: device locked"); - let _ = qt_thread.queue(|t| { + qt_thread.queue(|t| { t.dev_image_mounted(false,true); - }).ok(); + }); } Err(e) => { eprintln!("mount_dev_image: Failed to mount developer image for device {udid}: {e}"); - let _ = qt_thread.queue(|t| { + qt_thread.queue(|t| { t.dev_image_mounted(false,false); - }).ok(); + }); } }; @@ -465,66 +306,53 @@ impl qobject::ServiceManager { } fn get_mounted_image(&self) { - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); let qt_t = self.qt_thread(); + let provider_guard = self.device.as_ref().unwrap().clone().provider; + RUNTIME.spawn(async move { let qt_thread = qt_t.clone(); - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("get_mounted_image: device {udid} not found"); - let _ = qt_thread.queue(|t| { - t.mounted_image_retrieved(false,false,QByteArray::default(), 0); - }).ok(); - return; - } - }; let mut mounter = match { - let provider_guard = device.provider.lock().await; - let provider_ref: &dyn IdeviceProvider = provider_guard.as_ref(); + let provider = provider_guard.lock().await; + + let provider_ref: &dyn IdeviceProvider = provider.as_ref(); ImageMounter::connect(provider_ref).await } { Ok(m) => m, Err(e) => { eprintln!("get_mounted_image: Failed to connect to ImageMounter for device {udid}: {e}"); - let _ = qt_thread.queue(|t| { + qt_thread.queue(|t| { t.mounted_image_retrieved(false,false,QByteArray::default(), 0); - }).ok(); + }); return; } }; match mounter.lookup_image("Developer").await { Ok(res) => { - let _ = qt_thread.queue(move|t| { + qt_thread.queue(move|t| { t.mounted_image_retrieved(true,false,QByteArray::from(&res[..]), res.len() as u64); - }).ok(); + }); } Err(idevice::IdeviceError::DeviceLocked) => { eprintln!("get_mounted_image: Failed to lookup mounted developer image for device {udid}: device locked"); - let _ = qt_thread.queue(|t| { + qt_thread.queue(|t| { t.mounted_image_retrieved(false,true,QByteArray::default(), 0); - }).ok(); + }); } Err(idevice::IdeviceError::NotFound) => { eprintln!("get_mounted_image: No mounted developer image found for device {udid}"); let _ = qt_thread.queue(|t| { t.mounted_image_retrieved(true,false,QByteArray::default(), 0); - }).ok(); + }); } Err(e) => { eprintln!("get_mounted_image: Failed to lookup mounted developer image for device {udid}: {e}"); let _ = qt_thread.queue(|t| { t.mounted_image_retrieved(false,false,QByteArray::default(), 0); - }).ok(); + }); } }; @@ -532,30 +360,17 @@ impl qobject::ServiceManager { } fn reveal_developer_mode_option_in_ui(&self) { - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); let qt_t = self.qt_thread(); + + let provider_guard = self.device.as_ref().unwrap().clone().provider; + RUNTIME.spawn(async move { let qt_thread = qt_t.clone(); let mut amfi_client = match { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("get_cable_info: device {udid} not found"); - let _ = qt_thread.queue(|t| { - t.developer_mode_option_revealed(false); - }).ok(); - return; - } - }; - let provider_guard = device.provider.lock().await; + let provider_guard = provider_guard.lock().await; let provider_ref: &dyn IdeviceProvider = provider_guard.as_ref(); amfi::AmfiClient::connect(provider_ref).await } { @@ -564,7 +379,7 @@ impl qobject::ServiceManager { eprintln!("reveal_developer_mode_option_in_ui: Failed to connect to AMFI service for device {udid}: {e}"); let _ = qt_thread.queue(|t| { t.developer_mode_option_revealed(false); - }).ok(); + }); return; } }; @@ -574,13 +389,13 @@ impl qobject::ServiceManager { Ok(_) => { let _ = qt_thread.queue(|t| { t.developer_mode_option_revealed(true); - }).ok(); + }); } Err(e) => { eprintln!("reveal_developer_mode_option_in_ui: Failed to reveal developer mode option in UI for device {udid}: {e}"); let _ = qt_thread.queue(|t| { t.developer_mode_option_revealed(false); - }).ok(); + }); } } @@ -588,32 +403,19 @@ impl qobject::ServiceManager { } fn fetch_installed_apps(&self) { - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); let qt_t = self.qt_thread(); + let provider_guard = self.device.as_ref().unwrap().clone().provider; + RUNTIME.spawn(async move { let qt_thread = qt_t.clone(); - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let mut all_apps = QMap::::default(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("fetch_installed_apps: device {udid} not found"); - let _ = qt_thread.queue(move |t| { - t.installed_apps_retrieved(&all_apps); - }); - return; - } - }; + + let mut all_apps = QVariantMap::default(); + let mut ins_client = match { - let provider_guard = device.provider.lock().await; + let provider_guard = provider_guard.lock().await; let provider_ref: &dyn IdeviceProvider = provider_guard.as_ref(); InstallationProxyClient::connect(provider_ref).await } { @@ -621,7 +423,7 @@ impl qobject::ServiceManager { Err(e) => { eprintln!("fetch_installed_apps: Failed to connect to InstallationProxy service for device {udid}: {e}"); let _ = qt_thread.queue( move |t| { - t.installed_apps_retrieved(&all_apps); + t.installed_apps_retrieved(all_apps); }); return; } @@ -690,33 +492,23 @@ impl qobject::ServiceManager { } let _ = qt_thread.queue(move |t| { - t.installed_apps_retrieved(&all_apps); + t.installed_apps_retrieved(all_apps); }); }); } fn fetch_app_icon(&self, bundle_id: QString) { - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); let qt_t = self.qt_thread(); let bundle_id_owned = bundle_id.clone(); + + let provider_guard = self.device.as_ref().unwrap().clone().provider; + RUNTIME.spawn(async move { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("fetch_app_icon: device {udid} not found"); - return; - } - }; - + let mut springboard_client = match { - let provider_guard = device.provider.lock().await; + let provider_guard = provider_guard.lock().await; let provider_ref: &dyn IdeviceProvider = provider_guard.as_ref(); SpringBoardServicesClient::connect(provider_ref).await } { @@ -731,7 +523,7 @@ impl qobject::ServiceManager { Ok(icon_data) => { qt_t.queue(move |t| { t.app_icon_loaded(bundle_id_owned, QByteArray::from(&icon_data[..])); - }).ok(); + }); } Err(e) => { eprintln!("fetch_app_icon: Failed to fetch app icon for bundle ID {} on device {udid}: {e}", bundle_id_owned); @@ -740,32 +532,20 @@ impl qobject::ServiceManager { }); } - fn set_location(&self, latitude: &QString, longitude: &QString) -> i32 { - let udid = self.get_udid().to_string(); - let ios_version = self.get_ios_version(); + fn set_location(&self, latitude: QString, longitude: QString) -> i32 { + let udid = self.udid.clone(); + let ios_version = self.ios_version; + let provider_guard = self.device.as_ref().unwrap().clone().provider; /* FIXME: use RUNTIME.spawn in the future */ RUNTIME.block_on(async move { tokio::select! { res = async { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("set_location: device {udid} not found"); - return -1; - } - }; - let result = { - let provider_guard = device.provider.lock().await; + let provider_guard = provider_guard.lock().await; + if ios_version < 17 { set_device_location_lockdown( provider_guard.as_ref(), latitude.to_string().as_str(), longitude.to_string().as_str()).await @@ -805,36 +585,25 @@ impl qobject::ServiceManager { }) } - fn fetch_disk_usage(&self, skip_gallery_usage: bool) { - let udid = self.get_udid().to_string(); + fn fetch_disk_usage(&self) { + let udid = self.udid.clone(); let qt_t = self.qt_thread(); + let provider_guard = self.device.as_ref().unwrap().clone().provider; + RUNTIME.spawn(async move { let qt_thread = qt_t.clone(); let mut instproxy = { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("fetch_disk_usage: device {udid} not found"); - return; - } - }; + let provider_guard = provider_guard.lock().await; - - match InstallationProxyClient::connect(device.provider.lock().await.as_ref()).await { + match InstallationProxyClient::connect(provider_guard.as_ref()).await { Ok(c) => c, Err(e) => { eprintln!("fetch_disk_usage: Failed to connect to InstallationProxy service for device {udid}: {e}"); qt_thread.queue(move |t| { - t.disk_usage_retrieved(false, 0, 0); - }).ok(); + t.disk_usage_retrieved(false, 0); + }); return; } } @@ -842,89 +611,16 @@ impl qobject::ServiceManager { match utils::calculate_apps_usage(&mut instproxy).await { - Ok(apps_usage) => { - - if skip_gallery_usage { - qt_thread.queue(move |t| { - t.disk_usage_retrieved(true, apps_usage, 0); - }).ok(); - return; - } - - let afc_arc = { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("fetch_disk_usage: device {udid} not found"); - return; - } - }; - - device.afc.clone() - }; - let mut afc = afc_arc.lock().await; - - let mut fd = match afc - .open("/PhotoData/Photos.sqlite", AfcFopenMode::RdOnly) - .await - { - Ok(fd) => fd, - Err(e) => { - eprintln!( - "fetch_disk_usage: Failed to open Photos.sqlite for device {udid}: {e}" - ); - qt_thread - .queue(move |t| { - // apps_usage is u64 (Copy), so safe to capture - t.disk_usage_retrieved(true, apps_usage, 0); - }) - .ok(); - return; - } - }; - - let mut gallery_db_bytes = match fd.read_entire().await { - Ok(bytes) => bytes, - Err(e) => { - eprintln!( - "fetch_disk_usage: Failed to read Photos.sqlite for device {udid}: {e}" - ); - qt_thread - .queue(move |t| { - t.disk_usage_retrieved(true, apps_usage, 0); - }) - .ok(); - return; - } - }; - - match utils::query_gallery_usage(&mut gallery_db_bytes) { - Ok(gallery_usage) => { - qt_thread.queue(move |t| { - t.disk_usage_retrieved(true, apps_usage, gallery_usage); - }).ok(); - } - Err(e) => { - eprintln!("fetch_disk_usage: Failed to calculate gallery disk usage for device {udid}: {e}"); - qt_thread.queue(move |t| { - t.disk_usage_retrieved(true, apps_usage, 0); - }).ok(); - } - } - - + Ok(apps_usage) => { + qt_thread.queue(move |t| { + t.disk_usage_retrieved(true, apps_usage); + }); } Err(e) => { eprintln!("fetch_disk_usage: Failed to calculate apps disk usage for device {udid}: {e}"); qt_thread.queue(move |t| { - t.disk_usage_retrieved(false, 0, 0); - }).ok(); + t.disk_usage_retrieved(false, 0); + }); } }; @@ -933,20 +629,14 @@ impl qobject::ServiceManager { } fn restart(&self) -> bool { - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); + + let diag_guard = self.device.as_ref().unwrap().clone().diag; run_sync(async move { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let mut diag = diag_guard.lock().await; - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("restart: device {udid} not found"); - return false; - } - }; - - if let Err(e) = device.diag.lock().await.restart().await { + if let Err(e) = diag.restart().await { eprintln!("restart: Failed to restart device {udid}: {e}"); return false; } @@ -955,20 +645,14 @@ impl qobject::ServiceManager { } fn shutdown(&self) -> bool { - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); + let diag_guard = self.device.as_ref().unwrap().clone().diag; run_sync(async move { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let mut diag = diag_guard.lock().await; + - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("shutdown: device {udid} not found"); - return false; - } - }; - - if let Err(e) = device.diag.lock().await.shutdown().await { + if let Err(e) = diag.shutdown().await { eprintln!("shutdown: Failed to shutdown device {udid}: {e}"); return false; } @@ -976,20 +660,13 @@ impl qobject::ServiceManager { }) } fn enter_recovery_mode(&self) -> bool { - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); + let lc_guard = self.device.as_ref().unwrap().clone().lockdown; run_sync(async move { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); + let mut lc = lc_guard.lock().await; - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("enter_recovery_mode: device {udid} not found"); - return false; - } - }; - - if let Err(e) = device.lockdown.lock().await.enter_recovery().await { + if let Err(e) = lc.enter_recovery().await { eprintln!( "enter_recovery_mode: Failed to enter recovery mode for device {udid}: {e}" ); @@ -999,8 +676,8 @@ impl qobject::ServiceManager { }) } - fn install_ipa(&self, local_ipa_path: &QString) { - let udid = self.get_udid().to_string(); + fn install_ipa(&self, local_ipa_path: QString) { + let udid = self.udid.clone(); let qt_t = self.qt_thread(); let local_ipa_path_owned = local_ipa_path.clone().to_string(); // FIXME: this is a bit hacky @@ -1012,28 +689,16 @@ impl qobject::ServiceManager { .last() .unwrap_or("app.ipa") ); + let cloned = self.device.as_ref().unwrap().clone(); + let afc_guard = cloned.afc; + let provider_guard = cloned.provider; + RUNTIME.spawn(async move { let qt_thread = qt_t.clone(); let mut ins_client = match { - let maybe_device = APP_DEVICE_STATE - .lock() - .await - .get(udid.as_str()) - .cloned(); - - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("install_ipa: device {udid} not found"); - qt_thread.queue(move |t| { - t.install_ipa_init(false, QString::from("Device not found")); - }).ok(); - return; - } - }; - let mut afc = device.afc.lock().await; + let mut afc = afc_guard.lock().await; // Create the staging directory match utils::ensure_public_staging(&mut afc).await { @@ -1042,7 +707,7 @@ impl qobject::ServiceManager { eprintln!("install_ipa: Failed to ensure /PublicStaging directory exists on device {udid}: {e}"); qt_thread.queue(move |t| { t.install_ipa_init(false, QString::from("Failed to prepare device for IPA upload")); - }).ok(); + }); return; } }; @@ -1053,14 +718,14 @@ impl qobject::ServiceManager { eprintln!("install_ipa: IPA file not found at path {local_ipa_path_owned}"); qt_thread.queue(move |t| { t.install_ipa_init(false, QString::from("IPA file not found")); - }).ok(); + }); return; } Err(e) => { eprintln!("install_ipa: Failed to check if IPA file exists at path {local_ipa_path_owned}: {e}"); qt_thread.queue(move |t| { t.install_ipa_init(false, QString::from("Failed to access IPA file")); - }).ok(); + }); return; } } @@ -1075,7 +740,7 @@ impl qobject::ServiceManager { eprintln!("install_ipa: Failed to open local IPA file for device {udid}: {e}"); qt_thread.queue(move |t| { t.install_ipa_init(false, QString::from("Failed to open local IPA file")); - }).ok(); + }); return; } }; @@ -1087,7 +752,7 @@ impl qobject::ServiceManager { eprintln!("install_ipa: Failed to read local IPA file for device {udid}: {e}"); qt_thread.queue(move |t| { t.install_ipa_init(false, QString::from("Failed to read local IPA file")); - }).ok(); + }); return; } }; @@ -1096,7 +761,7 @@ impl qobject::ServiceManager { eprintln!("install_ipa: Failed to upload IPA to device {udid}: {e}"); qt_thread.queue(move |t| { t.install_ipa_init(false, QString::from("Failed to upload IPA to device")); - }).ok(); + }); return; } } @@ -1104,14 +769,13 @@ impl qobject::ServiceManager { eprintln!("install_ipa: Failed to create file on device {udid} for IPA upload: {e}"); qt_thread.queue(move |t| { t.install_ipa_init(false, QString::from("Failed to create file on device for IPA upload")); - }).ok(); + }); return; } } - - let provider_guard = device.provider.lock().await; + let provider_guard = provider_guard.lock().await; let provider_ref: &dyn IdeviceProvider = provider_guard.as_ref(); InstallationProxyClient::connect(provider_ref).await } { @@ -1120,14 +784,14 @@ impl qobject::ServiceManager { eprintln!("install_ipa: Failed to connect to InstallationProxy service for device {udid}: {e}"); qt_thread.queue(move |t| { t.install_ipa_init(false, QString::from("Failed to connect to Installation Proxy")); - }).ok(); + }); return; } }; qt_thread.queue(move |t| { t.install_ipa_init(true, QString::from("Connected to Installation Proxy")); - }).ok(); + }); let state = String::from("Installing IPA"); @@ -1144,10 +808,9 @@ impl qobject::ServiceManager { .queue(move |t| { t.install_ipa_progress( progress, - QString::from(&state), + QString::from(state), ); - }) - .ok(); + }); println!( "Installation progress: {percent}%" @@ -1168,31 +831,14 @@ impl qobject::ServiceManager { fn enable_wifi_connections(&self) { let qt_t = self.qt_thread(); - let udid = self.get_udid().to_string(); + let udid = self.udid.clone(); + let lc_guard = self.device.as_ref().unwrap().clone().lockdown; RUNTIME.spawn(async move { let qt_thread = qt_t.clone(); - let lc_arc = { - let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned(); - let device = match maybe_device { - Some(d) => d, - None => { - eprintln!("enable_wifi_connections: device {udid} not found"); - let _ = qt_thread - .queue(|t| { - t.enable_wifi_connections_result(false); - }) - .ok(); - return; - } - }; - - device.lockdown.clone() - }; - - let mut lc = lc_arc.lock().await; + let mut lc = lc_guard.lock().await; let value = Value::Boolean(true); match lc @@ -1207,16 +853,14 @@ impl qobject::ServiceManager { let _ = qt_thread .queue(|t| { t.enable_wifi_connections_result(true); - }) - .ok(); + }); } Err(e) => { - eprintln!("wireless: LockdownClient::set_value failed: {e:?}"); + eprintln!("wireless: LockdownClient::set_value failed: {e:?} udid: {udid}"); let _ = qt_thread .queue(|t| { t.enable_wifi_connections_result(false); - }) - .ok(); + }); } } }); diff --git a/src/settingswidget.cpp b/src/settingswidget.cpp deleted file mode 100644 index 647b48d..0000000 --- a/src/settingswidget.cpp +++ /dev/null @@ -1,574 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "settingswidget.h" -#include "mainwindow.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include "platform/windows/win_common.h" -#include -#endif - -SettingsWidget::SettingsWidget(QWidget *parent) : QDialog{parent} -{ -#ifdef WIN32 - m_backDropTypeCombo = nullptr; - m_disableMicaCheckBox = nullptr; -#endif - setupUI(); - loadSettings(); - connectSignals(); - // due to scrollbar add 10px on windows -#ifdef WIN32 - resize(sizeHint().width() + 10, sizeHint().height()); - setupWinWindow(this); -#endif -} - -void SettingsWidget::setupUI() -{ - auto *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(15); - - // Create scroll area for the settings - auto *scrollArea = new QScrollArea(); - auto *scrollWidget = new QWidget(); - auto *scrollLayout = new QVBoxLayout(scrollWidget); - scrollLayout->setSpacing(35); - scrollLayout->setContentsMargins(10, 10, 10, 10); - - // === GENERAL SETTINGS === - auto *generalGroup = new QGroupBox("General"); - auto *generalLayout = new QVBoxLayout(generalGroup); - - // Download path - auto *downloadLayout = new QHBoxLayout(); - downloadLayout->addWidget(new QLabel("Download Path:")); - m_downloadPathEdit = new QLineEdit(); - m_downloadPathEdit->setReadOnly(true); - m_downloadPathEdit->setMaximumWidth(300); - downloadLayout->addWidget(m_downloadPathEdit); - auto *browseButton = new QPushButton("Browse..."); - downloadLayout->addWidget(browseButton); - generalLayout->addLayout(downloadLayout); - - // Wireless file server port - auto *portLayout = new QHBoxLayout(); - portLayout->addWidget(new QLabel("Wireless File Server Port:")); - m_wirelessFileServerPort = new QSpinBox(); - m_wirelessFileServerPort->setRange(1024, 65535); - m_wirelessFileServerPort->setToolTip( - "The starting port for the wireless file server. If this port is " - "unavailable, it will try the next 10 ports."); - portLayout->addWidget(m_wirelessFileServerPort); - portLayout->addStretch(); - generalLayout->addLayout(portLayout); - - // Unmount iFuse drives on exit (not implemented on macOS) - // TODO: Implement -#ifndef __APPLE__ - m_unmount_iFuseDrives = new QCheckBox("Unmount iFuse drives on exit"); - generalLayout->addWidget(m_unmount_iFuseDrives); -#endif - - connect(browseButton, &QPushButton::clicked, this, - &SettingsWidget::onBrowseButtonClicked); - - // Auto-check for updates - m_autoUpdateCheck = new QCheckBox("Automatically check for updates"); - generalLayout->addWidget(m_autoUpdateCheck); - - m_autoEnableWifiConnections = - new QCheckBox("Automatically enable Wi-Fi connections"); - generalLayout->addWidget(m_autoEnableWifiConnections); - - // Theme selection - auto *themeLayout = new QHBoxLayout(); - themeLayout->addWidget(new QLabel("Theme:")); - m_themeCombo = new QComboBox(); - - /* FIXME: Theme control needs to be implemented */ - m_themeCombo->addItems({"System Default"}); - // m_themeCombo->addItems({"System Default", "Light", "Dark"}); - - themeLayout->addWidget(m_themeCombo); - themeLayout->addStretch(); - -#ifdef WIN32 - QOperatingSystemVersion osVersion = QOperatingSystemVersion::current(); - if (osVersion >= QOperatingSystemVersion::Windows11) { - auto *backDropTypeLayout = new QHBoxLayout(); - backDropTypeLayout->addWidget(new QLabel("Backdrop Type:")); - m_backDropTypeCombo = new QComboBox(); - - // "Auto" => no userData => means "no override" - m_backDropTypeCombo->addItem("Auto"); - m_backDropTypeCombo->addItem("Mica", static_cast(MICA)); - m_backDropTypeCombo->addItem("Mica Alt", static_cast(MICA_ALT)); - m_backDropTypeCombo->addItem("Acrylic", static_cast(ACRYLIC)); - - backDropTypeLayout->addWidget(m_backDropTypeCombo); - backDropTypeLayout->addStretch(); - - generalLayout->addLayout(backDropTypeLayout); - - m_disableMicaCheckBox = - new QCheckBox("Disable Mica effects (also disables WinUI styles)"); - generalLayout->addWidget(m_disableMicaCheckBox); - } -#endif - - scrollLayout->addWidget(generalGroup); - - // === DEVICE CONNECTION SETTINGS === - auto *deviceGroup = new QGroupBox("Device Connection"); - auto *deviceLayout = new QVBoxLayout(deviceGroup); - - m_autoRaiseWindow = - new QCheckBox("Auto-raise main window on device connection"); - deviceLayout->addWidget(m_autoRaiseWindow); - - m_switchToNewDevice = new QCheckBox("Switch to newly connected device"); - deviceLayout->addWidget(m_switchToNewDevice); - - m_autoConnectWirelessDevices = - new QCheckBox("Automatically connect to wireless devices"); - deviceLayout->addWidget(m_autoConnectWirelessDevices); - - // Connection timeout - auto *timeoutLayout = new QHBoxLayout(); - timeoutLayout->addWidget(new QLabel("Connection Timeout:")); - m_connectionTimeout = new QSpinBox(); - m_connectionTimeout->setRange(5, 60); - m_connectionTimeout->setSuffix(" seconds"); - timeoutLayout->addWidget(m_connectionTimeout); - timeoutLayout->addStretch(); - deviceLayout->addLayout(timeoutLayout); - - scrollLayout->addWidget(deviceGroup); - - // === SECURITY SETTINGS === - auto *securityGroup = new QGroupBox("Security"); - auto *securityLayout = new QVBoxLayout(securityGroup); - - m_useUnsecureBackend = - new QCheckBox("Use unsecure backend for app store (ipatool)"); - m_useUnsecureBackend->setToolTip( - "Enabling this may put your Apple account at risk but you don't have " - "to deal with Apple keychain."); - securityLayout->addWidget(m_useUnsecureBackend); - scrollLayout->addWidget(securityGroup); - - // === JAILBROKEN SETTINGS === - auto *jailbrokenGroup = new QGroupBox("Jailbroken"); - auto *jailbrokenLayout = new QVBoxLayout(jailbrokenGroup); - - // Default jailbroken root password - auto *passwordLayout = new QHBoxLayout(); - passwordLayout->addWidget(new QLabel("Default Jailbroken Root Password:")); - m_defaultJailbrokenRootPassword = new QLineEdit(); - m_defaultJailbrokenRootPassword->setEchoMode(QLineEdit::PasswordEchoOnEdit); - m_defaultJailbrokenRootPassword->setMaximumWidth(200); - m_defaultJailbrokenRootPassword->setToolTip( - "Default password used for SSH root authentication on jailbroken " - "devices: Default is 'alpine'."); - passwordLayout->addWidget(m_defaultJailbrokenRootPassword); - passwordLayout->addStretch(); - jailbrokenLayout->addLayout(passwordLayout); - - scrollLayout->addWidget(jailbrokenGroup); - - // === AirPlay SETTINGS === - auto *airplayGroup = new QGroupBox("AirPlay"); - auto *airplayLayout = new QVBoxLayout(airplayGroup); - - auto *fpsLayout = new QHBoxLayout(); - - auto *fpsLabel = new QLabel("Fps:"); - m_fpsComboBox = new QComboBox(); - m_fpsComboBox->addItems({"24", "30", "60", "120"}); - m_fpsComboBox->setToolTip( - "Set the fps for AirPlay. Go with 30 fps if have an older device."); - - fpsLayout->addWidget(fpsLabel); - fpsLayout->addWidget(m_fpsComboBox); - fpsLayout->addStretch(); - airplayLayout->addLayout(fpsLayout); - - m_noHoldCheckbox = new QCheckBox("Allow New Connections to Take Over"); - airplayLayout->addWidget(m_noHoldCheckbox); - -#ifdef __linux__ - m_useLegacyPortsCheckbox = new QCheckBox("Use legacy ports"); - m_useLegacyPortsCheckbox->setToolTip( - "Use legacy ports, refer to AIRPLAY.md for more information."); - airplayLayout->addWidget(m_useLegacyPortsCheckbox); - - m_showV4L2CheckBox = new QCheckBox("Show V4L2 Button on AirPlay Widget"); - airplayLayout->addWidget(m_showV4L2CheckBox); -#endif - - scrollLayout->addWidget(airplayGroup); - - // === MISCELLANEOUS SETTINGS === - auto *miscGroup = new QGroupBox("Miscellaneous"); - auto *miscLayout = new QVBoxLayout(miscGroup); - - auto *iconSizeBaseMultiplierLayout = new QHBoxLayout(); - m_iconSizeBaseMultiplier = new QDoubleSpinBox(); - m_iconSizeBaseMultiplier->setRange(1.0, 5.0); - m_iconSizeBaseMultiplier->setSingleStep(0.1); - m_iconSizeBaseMultiplier->setDecimals(1); - m_iconSizeBaseMultiplier->setSuffix("x"); - m_iconSizeBaseMultiplier->setToolTip( - "Adjust the base multiplier for icon sizes. This affects how large " - "icons appear throughout the application. Requires restart to take " - "effect."); - - iconSizeBaseMultiplierLayout->addWidget( - new QLabel("Icon Size Base Multiplier:")); - iconSizeBaseMultiplierLayout->addWidget(m_iconSizeBaseMultiplier); - iconSizeBaseMultiplierLayout->addStretch(); - miscLayout->addLayout(iconSizeBaseMultiplierLayout); - - scrollLayout->addWidget(miscGroup); - - scrollLayout->addSpacing(30); - - // Add a footer Author & Version & app info & app description - auto *footerLabel = new QLabel( - QString( - "iDescriptor v%1\n" - "A free, open-source, and cross-platform iDevice management tool.\n" - "© 2026 See AUTHORS for details. Licensed under AGPLv3.") - .arg(APP_VERSION)); - footerLabel->setAlignment(Qt::AlignCenter); - footerLabel->setStyleSheet("color: gray; font-size: 8pt;"); - scrollLayout->addWidget(footerLabel); - - // Add stretch to push everything to the top - scrollLayout->addStretch(); - - scrollArea->setWidget(scrollWidget); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameStyle(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - // == BUTTONS === - auto *buttonLayout = new QHBoxLayout(); - - m_checkUpdatesButton = new QPushButton("Check for Updates"); - m_resetButton = new QPushButton("Reset Settings"); - m_applyButton = new QPushButton("Apply"); - - buttonLayout->addWidget(m_checkUpdatesButton); - buttonLayout->addWidget(m_resetButton); - buttonLayout->addWidget(m_applyButton); - buttonLayout->setContentsMargins(10, 10, 10, 10); - - mainLayout->addWidget(scrollArea); - mainLayout->addLayout(buttonLayout); - - // Connect button signals - connect(m_checkUpdatesButton, &QPushButton::clicked, this, - &SettingsWidget::onCheckUpdatesClicked); - connect(m_resetButton, &QPushButton::clicked, this, - &SettingsWidget::onResetToDefaultsClicked); - connect(m_applyButton, &QPushButton::clicked, this, - &SettingsWidget::onApplyClicked); -} - -void SettingsWidget::loadSettings() -{ - SettingsManager *sm = SettingsManager::sharedInstance(); - - m_downloadPathEdit->setText(sm->devdiskimgpath()); - m_autoUpdateCheck->setChecked(sm->autoCheckUpdates()); - m_autoRaiseWindow->setChecked(sm->autoRaiseWindow()); - m_switchToNewDevice->setChecked(sm->switchToNewDevice()); - m_autoEnableWifiConnections->setChecked(sm->autoEnableWifiConnections()); - m_autoConnectWirelessDevices->setChecked(sm->autoConnectWirelessDevices()); - m_wirelessFileServerPort->setValue(sm->wirelessFileServerPort()); - -#ifndef __APPLE__ - m_unmount_iFuseDrives->setChecked(sm->unmountiFuseOnExit()); -#endif - - // Set theme combo box - QString currentTheme = sm->theme(); - int themeIndex = m_themeCombo->findText(currentTheme); - if (themeIndex != -1) { - m_themeCombo->setCurrentIndex(themeIndex); - } - - m_connectionTimeout->setValue(sm->connectionTimeout()); - m_useUnsecureBackend->setChecked(sm->useUnsecureBackend()); - m_defaultJailbrokenRootPassword->setText( - sm->defaultJailbrokenRootPassword()); - - // Disable apply button initially - m_applyButton->setEnabled(false); - - m_iconSizeBaseMultiplier->setValue(sm->iconSizeBaseMultiplier()); - m_fpsComboBox->setCurrentText(QString::number(sm->airplayFps())); - m_noHoldCheckbox->setChecked(sm->airplayNoHold()); -#ifdef __linux__ - m_useLegacyPortsCheckbox->setChecked(sm->airplayUseLegacyPorts()); - m_showV4L2CheckBox->setChecked(sm->showV4L2()); -#endif - -#ifdef WIN32 - if (m_backDropTypeCombo) { - const int typeValue = static_cast(sm->winBackdropType()); - const int index = m_backDropTypeCombo->findData(typeValue); - if (index != -1) { - m_backDropTypeCombo->setCurrentIndex(index); - } else { - m_backDropTypeCombo->setCurrentIndex(0); - } - } - if (m_disableMicaCheckBox) { - m_disableMicaCheckBox->setChecked(sm->disableMica()); - } -#endif -} - -void SettingsWidget::connectSignals() -{ - // Connect all checkboxes and combos for immediate feedback - connect(m_autoUpdateCheck, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); - connect(m_autoRaiseWindow, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); - connect(m_switchToNewDevice, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); - connect(m_autoEnableWifiConnections, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); - connect(m_autoConnectWirelessDevices, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); -#ifndef __APPLE__ - connect(m_unmount_iFuseDrives, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); -#endif - connect(m_themeCombo, QOverload::of(&QComboBox::currentIndexChanged), - this, &SettingsWidget::onSettingChanged); - connect(m_connectionTimeout, QOverload::of(&QSpinBox::valueChanged), - this, &SettingsWidget::onSettingChanged); - connect(m_wirelessFileServerPort, - QOverload::of(&QSpinBox::valueChanged), this, - &SettingsWidget::onSettingChanged); - - connect(m_iconSizeBaseMultiplier, - QOverload::of(&QDoubleSpinBox::valueChanged), this, - [this]() { - m_restartRequired = true; - onSettingChanged(); - }); - - connect(m_useUnsecureBackend, &QCheckBox::toggled, this, [this]() { - // since this is unsafe if its being enabled, show a warning - if (m_useUnsecureBackend->isChecked()) { - auto reply = QMessageBox::warning( - this, "Warning", - "Enabling this will not encrypt your Apple account which " - "is a " - "security risk. Are you sure you want to enable this?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (reply == QMessageBox::Yes) { - m_restartRequired = true; - onSettingChanged(); - } else { - m_useUnsecureBackend->setChecked(false); - } - } else { - m_restartRequired = true; - onSettingChanged(); - } - }); - - connect(m_defaultJailbrokenRootPassword, &QLineEdit::textChanged, this, - &SettingsWidget::onSettingChanged); - connect(m_fpsComboBox, QOverload::of(&QComboBox::currentIndexChanged), - this, &SettingsWidget::onSettingChanged); - connect(m_noHoldCheckbox, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); -#ifdef __linux__ - connect(m_useLegacyPortsCheckbox, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); - connect(m_showV4L2CheckBox, &QCheckBox::toggled, this, - &SettingsWidget::onSettingChanged); -#endif -#ifdef WIN32 - if (m_backDropTypeCombo) { - connect(m_backDropTypeCombo, - QOverload::of(&QComboBox::currentIndexChanged), this, - [this]() { - m_restartRequired = true; - onSettingChanged(); - }); - } - if (m_disableMicaCheckBox) { - connect(m_disableMicaCheckBox, &QCheckBox::toggled, this, [this]() { - m_restartRequired = true; - onSettingChanged(); - }); - } -#endif -} - -void SettingsWidget::onBrowseButtonClicked() -{ - QString dir = QFileDialog::getExistingDirectory( - this, "Select Download Directory", m_downloadPathEdit->text(), - QFileDialog::ShowDirsOnly); - - if (!dir.isEmpty()) { - m_downloadPathEdit->setText(dir); - onSettingChanged(); - } -} - -void SettingsWidget::onCheckUpdatesClicked() -{ - m_checkUpdatesButton->setText("Checking..."); - m_checkUpdatesButton->setEnabled(false); - - connect( - MainWindow::sharedInstance()->m_updater, &ZUpdater::dataAvailable, this, - [this](const QJsonDocument data, bool isUpdateAvailable) { - if (!isUpdateAvailable) { - QMessageBox::information(this, "No Updates", - "You are using the latest version of " - "iDescriptor."); - } - m_checkUpdatesButton->setText("Check for Updates"); - m_checkUpdatesButton->setEnabled(true); - }, - Qt::SingleShotConnection); - - MainWindow::sharedInstance()->m_updater->checkForUpdates(); -} - -void SettingsWidget::onResetToDefaultsClicked() -{ - auto reply = QMessageBox::question( - this, "Reset Settings", - "Are you sure you want to reset all settings to their default values?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (reply == QMessageBox::Yes) { - resetToDefaults(); - } -} - -void SettingsWidget::onApplyClicked() -{ - saveSettings(); - QMessageBox::information(this, "Settings", - m_restartRequired - ? "Settings applied. Please restart " - "the application for changes to " - "take effect." - : "Settings applied."); - m_restartRequired = false; -} - -void SettingsWidget::onSettingChanged() -{ - // Enable apply button when settings change - m_applyButton->setEnabled(true); -} - -void SettingsWidget::saveSettings() -{ - SettingsManager *sm = SettingsManager::sharedInstance(); - - sm->setDevDiskImgPath(m_downloadPathEdit->text()); - sm->setAutoCheckUpdates(m_autoUpdateCheck->isChecked()); - sm->setAutoRaiseWindow(m_autoRaiseWindow->isChecked()); - sm->setSwitchToNewDevice(m_switchToNewDevice->isChecked()); - sm->setAutoEnableWifiConnections(m_autoEnableWifiConnections->isChecked()); - sm->setAutoConnectWirelessDevices( - m_autoConnectWirelessDevices->isChecked()); - sm->setWirelessFileServerPort(m_wirelessFileServerPort->value()); - -#ifndef __APPLE__ - sm->setUnmountiFuseOnExit(m_unmount_iFuseDrives->isChecked()); -#endif - sm->setUseUnsecureBackend(m_useUnsecureBackend->isChecked()); - - sm->setTheme(m_themeCombo->currentText()); - sm->setConnectionTimeout(m_connectionTimeout->value()); - sm->setDefaultJailbrokenRootPassword( - m_defaultJailbrokenRootPassword->text()); - - sm->setIconSizeBaseMultiplier(m_iconSizeBaseMultiplier->value()); - - sm->setAirplayFps(m_fpsComboBox->currentText().toInt()); - sm->setAirplayNoHold(m_noHoldCheckbox->isChecked()); -#ifdef __linux__ - sm->setAirplayUseLegacyPorts(m_useLegacyPortsCheckbox->isChecked()); - sm->setShowV4L2(m_showV4L2CheckBox->isChecked()); -#endif - m_applyButton->setEnabled(false); - -#ifdef WIN32 - if (m_backDropTypeCombo) { - const QVariant data = m_backDropTypeCombo->currentData(); - if (!data.isValid()) { - // AUTO = ACRYLIC - sm->setWinBackdropType(static_cast(ACRYLIC)); - } else { - sm->setWinBackdropType(static_cast(data.toInt())); - } - } - if (m_disableMicaCheckBox) { - sm->setDisableMica(m_disableMicaCheckBox->isChecked()); - } -#endif -} - -void SettingsWidget::resetToDefaults() -{ - SettingsManager::sharedInstance()->resetToDefaults(); - - // Reload UI with default values - loadSettings(); - - onSettingChanged(); -} diff --git a/src/settingswidget.h b/src/settingswidget.h deleted file mode 100644 index 2488950..0000000 --- a/src/settingswidget.h +++ /dev/null @@ -1,96 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef SETTINGSWIDGET_H -#define SETTINGSWIDGET_H - -#include -#include -#include -#include -#include -#include -#include - -class SettingsWidget : public QDialog -{ - Q_OBJECT - -public: - explicit SettingsWidget(QWidget *parent = nullptr); - -private slots: - void onBrowseButtonClicked(); - void onCheckUpdatesClicked(); - void onResetToDefaultsClicked(); - void onApplyClicked(); - void onSettingChanged(); - -private: - void setupUI(); - void loadSettings(); - void saveSettings(); - void connectSignals(); - void resetToDefaults(); - - // UI Elements - // General - QLineEdit *m_downloadPathEdit; - QSpinBox *m_wirelessFileServerPort; - QCheckBox *m_autoUpdateCheck; - QComboBox *m_themeCombo; - QCheckBox *m_autoRaiseWindow; - QCheckBox *m_switchToNewDevice; - QCheckBox *m_autoEnableWifiConnections; -#ifndef __APPLE__ - QCheckBox *m_unmount_iFuseDrives; -#endif - QCheckBox *m_useUnsecureBackend; - // Device Connection - QCheckBox *m_autoConnectWirelessDevices; - QSpinBox *m_connectionTimeout; - - // Jailbroken - QLineEdit *m_defaultJailbrokenRootPassword; - - QDoubleSpinBox *m_iconSizeBaseMultiplier; - - // Airplay - QComboBox *m_fpsComboBox; - QCheckBox *m_noHoldCheckbox; - -#ifdef __linux__ - QCheckBox *m_useLegacyPortsCheckbox; - QCheckBox *m_showV4L2CheckBox; -#endif - -#ifdef WIN32 - QComboBox *m_backDropTypeCombo; - QCheckBox *m_disableMicaCheckBox; -#endif - - // Buttons - QPushButton *m_checkUpdatesButton; - QPushButton *m_resetButton; - QPushButton *m_applyButton; - - bool m_restartRequired = false; -}; - -#endif // SETTINGSWIDGET_H diff --git a/src/sponsorappcard.cpp b/src/sponsorappcard.cpp deleted file mode 100644 index fe6b77e..0000000 --- a/src/sponsorappcard.cpp +++ /dev/null @@ -1,134 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "sponsorappcard.h" -#include "appswidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include - -SponsorAppCard::SponsorAppCard(QWidget *parent) : QWidget{parent} -{ - - QHBoxLayout *layout = new QHBoxLayout(this); - setMaximumHeight(200); - setMaximumWidth(500); - setObjectName("SponsorAppCard"); - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - setStyleSheet("QWidget#SponsorAppCard { border: 1px solid #ddd; " - "border-radius: 8px; }"); - layout->setContentsMargins(15, 15, 15, 15); - layout->setSpacing(15); - - // App icon - QLabel *iconLabel = new QLabel(); - QPixmap placeholderIcon = QApplication::style() - ->standardIcon(QStyle::SP_ComputerIcon) - .pixmap(64, 64); - iconLabel->setPixmap(placeholderIcon); - iconLabel->setAlignment(Qt::AlignCenter); - layout->addWidget(iconLabel); - - QString name = "Shopify"; - QString bundleId = "com.jadedpixel.shopify"; - QString description = - "Create an online store within minutes and start selling."; - QString websiteUrl = "https://www.shopify.com"; - - ::fetchAppIconFromApple( - m_networkManager, bundleId, [iconLabel](const QPixmap &pixmap, const QJsonObject &appInfo) { - if (!pixmap.isNull()) { - QPixmap scaled = - pixmap.scaled(64, 64, Qt::KeepAspectRatioByExpanding, - Qt::SmoothTransformation); - QPixmap rounded(64, 64); - rounded.fill(Qt::transparent); - - QPainter painter(&rounded); - painter.setRenderHint(QPainter::Antialiasing); - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, 64, 64), 16, 16); - painter.setClipPath(path); - painter.drawPixmap(0, 0, scaled); - painter.end(); - - iconLabel->setPixmap(rounded); - } - }); - - // Vertical layout for name and description - QVBoxLayout *textLayout = new QVBoxLayout(); - - // App name - QLabel *nameLabel = new QLabel(name); - nameLabel->setStyleSheet("font-size: 16px;"); - nameLabel->setAlignment(Qt::AlignCenter); - nameLabel->setWordWrap(true); - textLayout->addWidget(nameLabel); - - // App description - QLabel *descLabel = new QLabel(description); - descLabel->setStyleSheet("font-size: 12px; color: #666;"); - descLabel->setAlignment(Qt::AlignCenter); - descLabel->setWordWrap(true); - textLayout->addWidget(descLabel); - - layout->addLayout(textLayout); - - QVBoxLayout *buttonsLayout = new QVBoxLayout(); - - // Install button placeholder - ZLabel *installLabel = new ZLabel("Install"); - installLabel->setAlignment(Qt::AlignCenter); - installLabel->setStyleSheet("font-size: 12px; color: #007AFF; font-weight: " - "bold; background-color: transparent;"); - installLabel->setCursor(Qt::PointingHandCursor); - installLabel->setFixedHeight(30); - - ZLabel *websiteLabel = new ZLabel("Website"); - websiteLabel->setStyleSheet("font-size: 12px; font-weight: " - "bold; background-color: transparent;"); - websiteLabel->setAlignment(Qt::AlignCenter); - websiteLabel->setCursor(Qt::PointingHandCursor); - - connect(installLabel, &ZLabel::clicked, this, - [this, name, bundleId, description]() { - AppsWidget::sharedInstance()->onAppCardClicked(name, bundleId, - description); - }); - - connect(websiteLabel, &ZLabel::clicked, this, [this, websiteUrl]() { - QDesktopServices::openUrl(QUrl(websiteUrl)); - }); - - buttonsLayout->addStretch(); - buttonsLayout->addWidget(installLabel); - buttonsLayout->addWidget(websiteLabel); - buttonsLayout->addStretch(); - - layout->addLayout(buttonsLayout); - // gridLayout->addWidget(cardWidget, row, col); -} diff --git a/src/sponsorappcard.h b/src/sponsorappcard.h deleted file mode 100644 index 2eec7a7..0000000 --- a/src/sponsorappcard.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef SPONSORAPPCARD_H -#define SPONSORAPPCARD_H -#include -#include - -class SponsorAppCard : public QWidget -{ - Q_OBJECT -public: - explicit SponsorAppCard(QWidget *parent = nullptr); - -private: - QNetworkAccessManager *m_networkManager = new QNetworkAccessManager(this); - -signals: -}; - -#endif // SPONSORAPPCARD_H diff --git a/src/sponsorwidget.cpp b/src/sponsorwidget.cpp deleted file mode 100644 index 5389c01..0000000 --- a/src/sponsorwidget.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "sponsorwidget.h" - -SponsorWidget::SponsorWidget(QWidget *parent) : Tool(parent) -{ - setMaximumSize(600, 400); - setLayout(new QVBoxLayout(this)); - setWindowTitle("Sponsor Us - iDescriptor"); - QLabel *sponsorTitle = new QLabel("Would you like to sponsor us?"); - sponsorTitle->setStyleSheet("font-weight: bold; font-size: 16pt;"); - sponsorTitle->setAlignment(Qt::AlignCenter); - - QLabel *sponsorDesc = - new QLabel("This app is open-source and free to use. " - "And in order to keep it that way, we rely on donations. " - "Consider becoming a sponsor to support " - "and promote your app/brand here"); - - QLabel *contactDesc = new QLabel(); - contactDesc->setTextFormat(Qt::RichText); - contactDesc->setOpenExternalLinks(true); - contactDesc->setText( - QString( - "You can read more about sponsorships on our " - "github repository.") - .arg(REPO_URL)); - - sponsorDesc->setStyleSheet("font-size: 10pt;"); - sponsorDesc->setWordWrap(true); - layout()->addWidget(sponsorTitle); - layout()->addWidget(sponsorDesc); - layout()->addWidget(contactDesc); - QLabel *sponsorIconLabel = new QLabel("Example:"); - layout()->addWidget(sponsorIconLabel); - SponsorAppCard *card = new SponsorAppCard(this); - layout()->addWidget(card); - layout()->setAlignment(card, Qt::AlignCenter); -} diff --git a/src/sponsorwidget.h b/src/sponsorwidget.h deleted file mode 100644 index cc0767c..0000000 --- a/src/sponsorwidget.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef SPONSORWIDGET_H -#define SPONSORWIDGET_H - -#include "base/tool.h" -#include "iDescriptor.h" -#include "sponsorappcard.h" -#include -#include -#include -#include - -class SponsorWidget : public Tool -{ - Q_OBJECT -public: - SponsorWidget(QWidget *parent = nullptr); -}; - -#endif // SPONSORWIDGET_H diff --git a/src/statusballoon.cpp b/src/statusballoon.cpp deleted file mode 100644 index 49b6322..0000000 --- a/src/statusballoon.cpp +++ /dev/null @@ -1,685 +0,0 @@ -#include "statusballoon.h" -#include "appcontext.h" -#include "iDescriptor.h" -#include "qballoontip.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include "platform/windows/win_common.h" -#endif - -BalloonProcess::BalloonProcess(std::shared_ptr item, - QWidget *parent) - : QWidget(parent), m_item(std::move(item)) -{ - auto *layout = new QVBoxLayout(this); - layout->setSpacing(5); - layout->setContentsMargins(15, 5, 5, 15); - - m_lastBytesTransferred = 0; - m_lastUpdateTime = QDateTime::currentDateTime(); - - // Title - m_titleLabel = new QLabel(m_item->title); - QFont titleFont = m_titleLabel->font(); - m_titleLabel->setWordWrap(true); - titleFont.setBold(true); - m_titleLabel->setFont(titleFont); - - QHBoxLayout *titleLayout = new QHBoxLayout(); - titleLayout->addWidget(m_titleLabel); - titleLayout->addStretch(); - - m_removeBtn = new ZIconWidget( - QIcon(":/resources/icons/MaterialSymbolsCloseRounded.png"), "Remove"); - auto *opacity = new QGraphicsOpacityEffect(m_removeBtn); - opacity->setOpacity(0.0); - m_removeBtn->setGraphicsEffect(opacity); - - m_removeBtn->setEnabled(false); - - connect(m_removeBtn, &ZIconWidget::clicked, this, [this]() { - StatusBalloon::sharedInstance()->removeProcess(m_item->processId); - }); - titleLayout->addWidget(m_removeBtn); - - layout->addLayout(titleLayout); - - // Status - m_statusLabel = new QLabel("Starting..."); - layout->addWidget(m_statusLabel); - - // Progress bar - m_progressBar = new QProgressBar(); -#ifdef __APPLE__ - m_progressBar->setStyleSheet(QString("QProgressBar {" - " border-radius: 4px;" - " background: #eee;" - "}" - "QProgressBar::chunk {" - " background-color: %1;" - " border-radius: 4px;" - "}") - .arg(COLOR_ACCENT_BLUE.name())); -#endif - m_progressBar->setRange(0, 100); - m_progressBar->setValue(0); - m_progressBar->setTextVisible(false); - m_progressBar->setFixedHeight(12); - layout->addWidget(m_progressBar); - - // Current file - m_currentFileLabel = new QLabel(); - m_currentFileLabel->setWordWrap(true); - QFont currentFileFont = m_currentFileLabel->font(); - currentFileFont.setPointSize(currentFileFont.pointSize() - 1); - m_currentFileLabel->setFont(currentFileFont); - layout->addWidget(m_currentFileLabel); - - // Stats - m_statsLabel = new QLabel(); - QFont statsFont = m_statsLabel->font(); - statsFont.setPointSize(statsFont.pointSize() - 2); - m_statsLabel->setFont(statsFont); - layout->addWidget(m_statsLabel); - - // Buttons layout - auto *buttonsLayout = new QHBoxLayout(); - buttonsLayout->setSpacing(6); - - // Action button - m_actionButton = new QPushButton(); - m_actionButton->setVisible(false); - if (m_item->type == ProcessType::Export) { - m_actionButton->setText("Open Folder"); - connect(m_actionButton, &QPushButton::clicked, this, - &BalloonProcess::onOpenFolderClicked); - } - buttonsLayout->addWidget(m_actionButton); - - buttonsLayout->addStretch(); - - // Cancel button - m_cancelButton = new QPushButton("Cancel"); - connect(m_cancelButton, &QPushButton::clicked, this, - &BalloonProcess::onCancelClicked); - buttonsLayout->addWidget(m_cancelButton); - - layout->addLayout(buttonsLayout); - layout->addStretch(); - - setObjectName("BalloonProcess"); - setAttribute(Qt::WA_StyledBackground, true); - updateStyles(); -} - -void BalloonProcess::updateStyles() -{ - QString style; - const bool dark = isDarkMode(); - - if (!dark) { -#ifdef WIN32 - style = "QWidget#BalloonProcess { background-color: " - "rgba(0, 0, 0, 10); border-radius: 5px; }"; -#else - style = "QWidget#BalloonProcess { background-color: rgba(0,0,0,10); " - "border-radius: 5px; }" - "QWidget#BalloonProcess QPushButton {" - " background-color: palette(Button);" - " color: palette(ButtonText);" - " border: 1px solid palette(Mid);" - " border-radius: 4px;" - " padding: 4px 8px;" - "}" - "QWidget#BalloonProcess QPushButton:hover {" - "background-color: palette(Dark);" - "}"; -#endif - } else { -#ifdef WIN32 - style = "QWidget#BalloonProcess { background-color: rgba(255, " - "255, 255, 16); border-radius: 5px; }"; -#else - style = "QWidget#BalloonProcess { background-color: " - "rgba(255,255,255,16); border-radius: 5px; }" - "QWidget#BalloonProcess QPushButton {" - " background-color: palette(Button);" - " color: palette(ButtonText);" - " border: 1px solid palette(Mid);" - " border-radius: 4px;" - " padding: 4px 8px;" - "}" - "QWidget#BalloonProcess QPushButton:hover {" - "background-color: palette(Light);" - "}"; -#endif - } - - if (style != styleSheet()) - setStyleSheet(style); -} - -void BalloonProcess::onCancelClicked() -{ - m_cancelButton->setEnabled(false); - m_cancelButton->setText("Cancelling..."); - IOManagerClient::sharedInstance()->cancel(m_item->processId); -} - -void BalloonProcess::updateUI() -{ - QString statusText; - if (m_item->status == ProcessStatus::Running) { - statusText = m_item->currentFile.isEmpty() ? "Starting..." : "Running"; - } else if (m_item->status == ProcessStatus::Completed) { - statusText = "Completed successfully"; - - QTimer::singleShot(1000, this, [this]() { - if (m_item->onComplete.has_value() && m_item->onComplete.value()) { - m_item->onComplete.value()(); - } - }); - - } else if (m_item->status == ProcessStatus::Failed) { - statusText = "Failed"; - } else if (m_item->status == ProcessStatus::Cancelled) { - statusText = "Cancelled"; - } - m_statusLabel->setText(statusText); - - if (m_item->totalBytes > 0 && m_item->transferredBytes > 0) { - int progress = (m_item->transferredBytes * 100) / m_item->totalBytes; - m_progressBar->setValue(progress); - } - - m_currentFileLabel->setText(m_item->currentFile); - - QString statsText = QString("%1 of %2 items") - .arg(m_item->completedItems) - .arg(m_item->totalItems); - if (m_item->failedItems > 0) { - statsText += QString(" • %1 failed").arg(m_item->failedItems); - } - - if (m_item->status == ProcessStatus::Running && - m_item->transferredBytes > 0) { - - QDateTime now = QDateTime::currentDateTime(); - const int minIntervalMs = 750; - - qint64 elapsed = m_lastUpdateTime.msecsTo(now); - // debounced transfer rate - if (!m_lastUpdateTime.isValid() || elapsed >= minIntervalMs) { - if (elapsed > 0) { - qint64 bytesDiff = - m_item->transferredBytes - m_lastBytesTransferred; - qint64 bytesPerSecond = (bytesDiff * 1000) / elapsed; - if (bytesPerSecond > 0) { - m_lastSpeedText = - " • " + - iDescriptor::Utils::formatTransferRate(bytesPerSecond); - } - } - m_lastBytesTransferred = m_item->transferredBytes; - m_lastUpdateTime = now; - } - - if (!m_lastSpeedText.isEmpty()) { - statsText += m_lastSpeedText; // reuse last speed until next update - } - } - - m_statsLabel->setText(statsText); - - if (m_item->status == ProcessStatus::Running) { - m_cancelButton->setVisible(true); - m_actionButton->setVisible(false); - } else { - m_cancelButton->setVisible(false); - if (m_item->type == ProcessType::Export && - m_item->status == ProcessStatus::Completed) { - m_actionButton->setVisible(true); - } - } -} - -void BalloonProcess::onOpenFolderClicked() -{ - if (!m_item->destinationPath.isEmpty() && - m_item->type == ProcessType::Export) { - QDesktopServices::openUrl(QUrl::fromLocalFile(m_item->destinationPath)); - } -} - -void BalloonProcess::enterEvent(QEnterEvent *event) -{ - QWidget::enterEvent(event); - if (m_item->status == ProcessStatus::Completed || - m_item->status == ProcessStatus::Failed || - m_item->status == ProcessStatus::Cancelled) { - if (auto *eff = qobject_cast( - m_removeBtn->graphicsEffect())) { - eff->setOpacity(1.0); - } - m_removeBtn->setEnabled(true); - } -} - -void BalloonProcess::leaveEvent(QEvent *event) -{ - QWidget::leaveEvent(event); - if (auto *eff = qobject_cast( - m_removeBtn->graphicsEffect())) { - eff->setOpacity(0.0); - } -} - -StatusBalloon *StatusBalloon::sharedInstance() -{ - static StatusBalloon instance; - return &instance; -} - -StatusBalloon::StatusBalloon(QWidget *parent) : QBalloonTip(parent) -{ - setMinimumHeight(300); - setMinimumWidth(300); - - auto *outerLayout = new QVBoxLayout(this); - outerLayout->setContentsMargins(0, 0, 0, 0); - outerLayout->setSpacing(0); - - QWidget *container = new QWidget; -#ifndef WIN32 - container->setObjectName("StatusBalloon"); - container->setStyleSheet(QString("QWidget#StatusBalloon { " - " background-color: %1;" - " border-radius: 8px;" - "border: 1px solid #ccc;" - "}") - .arg(QApplication::palette() - .color(QPalette::Window) - .name(QColor::HexArgb))); -#endif - outerLayout->addWidget(container); - - m_mainLayout = new QVBoxLayout(container); - m_mainLayout->setSpacing(8); - m_mainLayout->setContentsMargins(15, 15, 15, 15); - - m_noProcesesLabel = - new QLabel("Export & Import processes will appear here", this); - m_noProcesesLabel->setAlignment(Qt::AlignCenter); - m_noProcesesLabel->setWordWrap(true); - - // Header label - m_headerLabel = new QLabel("Processes"); - m_headerLabel->hide(); - m_headerLabel->setWordWrap(true); - QFont headerFont = m_headerLabel->font(); - headerFont.setPointSize(headerFont.pointSize() + 2); - headerFont.setBold(true); - m_headerLabel->setFont(headerFont); - m_mainLayout->addWidget(m_headerLabel); - - // Container for processes - m_processesContainer = new QWidget(); - m_processesLayout = new QVBoxLayout(m_processesContainer); - - QScrollArea *scrollArea = new QScrollArea(); - scrollArea->setWidget(m_processesContainer); - scrollArea->setWidgetResizable(true); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - scrollArea->viewport()->setStyleSheet("background: transparent;"); - - m_processesLayout->setSpacing(12); - m_processesLayout->setContentsMargins(10, 10, 10, 10); - m_mainLayout->addWidget(scrollArea); - - setLayout(m_mainLayout); - connect(m_button, &ZIconWidget::clicked, this, &StatusBalloon::handleShow); - connectExportThreadSignals(); -} - -void StatusBalloon::connectExportThreadSignals() -{ - auto *ioManager = AppContext::sharedInstance()->ioManager; - - connect(ioManager, &CXX::IOManager::export_job_finished, this, - &StatusBalloon::onExportJobFinished); - - connect(ioManager, &CXX::IOManager::export_item_finished, this, - &StatusBalloon::onItemExported); - - connect(ioManager, &CXX::IOManager::import_job_finished, this, - &StatusBalloon::onImportJobFinished); - - connect(ioManager, &CXX::IOManager::import_item_finished, this, - &StatusBalloon::onItemImported); - - connect(ioManager, &CXX::IOManager::file_transfer_progress, this, - &StatusBalloon::onFileTransferProgress); - // QTimer::singleShot(3000, this, [this]() { - // // test - // startProcess("Test Export Process", 10, "/path/to/destination", - // ProcessType::Export, QUuid()); - // }); -} - -void StatusBalloon::onFileTransferProgress(const QUuid &processId, - const QString ¤tFile, - qint64 bytesTransferred, - qint64 totalBytes) -{ - QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(processId)) - return; - - auto item = m_processes[processId]; - item->currentFile = currentFile; - item->transferredBytes = bytesTransferred; - item->totalBytes = totalBytes; - - handleJobUpdate(item); -} - -void StatusBalloon::onExportJobFinished(const QUuid &job_id, bool cancelled, - qint64 successful_items, - qint64 failed_items, qint64 total_bytes) -{ - QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(job_id)) { - qDebug() << "Received export job finished signal for unknown job_id:" - << job_id; - return; - } - - auto item = m_processes[job_id]; - if (cancelled) { - item->status = ProcessStatus::Cancelled; - } else { - if (item->failedItems > 0) { - item->status = ProcessStatus::Failed; - } else { - item->status = ProcessStatus::Completed; - } - } - item->endTime = QDateTime::currentDateTime(); - - handleJobUpdate(item); - updateHeader(); -} - -void StatusBalloon::onItemExported(const QUuid &job_id, - const QString &file_name, - const QString &destination_path, - bool success, int bytes_transferred, - const QString &error_message) -{ - QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(job_id)) { - qDebug() << "Received export item finished signal for unknown job_id:" - << job_id; - return; - } - - auto item = m_processes[job_id]; - if (success) - item->completedItems++; - else { - item->failedItems++; - qDebug() << "Export of" << file_name << "failed:" << error_message; - } - - handleJobUpdate(item); - updateHeader(); -} - -void StatusBalloon::onImportJobFinished(const QUuid &job_id, bool cancelled, - qint64 successful_items, - qint64 failed_items, qint64 total_bytes) -{ - QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(job_id)) { - qDebug() << "Received import job finished signal for unknown job_id:" - << job_id; - return; - } - - auto item = m_processes[job_id]; - if (cancelled) { - item->status = ProcessStatus::Cancelled; - } else { - if (item->failedItems > 0) { - item->status = ProcessStatus::Failed; - } else { - item->status = ProcessStatus::Completed; - } - } - item->endTime = QDateTime::currentDateTime(); - - handleJobUpdate(item); - updateHeader(); -} - -void StatusBalloon::onItemImported(const QUuid &job_id, - const QString &file_name, - const QString &destination_path, - bool success, int bytes_transferred, - const QString &error_message) -{ - QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(job_id)) - return; - - auto item = m_processes[job_id]; - if (success) - item->completedItems++; - else { - item->failedItems++; - qDebug() << "Import of" << file_name << "failed:" << error_message; - } - - handleJobUpdate(item); - updateHeader(); -} - -QUuid StatusBalloon::startProcess( - const QString &title, int totalItems, const QString &destinationPath, - ProcessType type, const QUuid &jobId, - std::optional> onComplete) -{ - handleShow(true); - - auto item = std::make_shared(); - item->processId = jobId; - item->type = type; - item->status = ProcessStatus::Running; - item->title = title; - item->totalItems = totalItems; - item->startTime = QDateTime::currentDateTime(); - item->destinationPath = destinationPath; - item->onComplete = std::move(onComplete); - - { - QMutexLocker locker(&m_processesMutex); - m_processes[jobId] = item; - } - - createProcessWidget(item); - updateHeader(); - - if (m_button) - m_button->setIndicatorVisible(true); - return item->processId; -} - -void StatusBalloon::createProcessWidget(std::shared_ptr item) -{ - // Pass shared_ptr to widget - BalloonProcess *processWidget = new BalloonProcess(item); - item->processWidget = processWidget; - m_processesLayout->addWidget(processWidget); - m_processesLayout->addStretch(); -} - -void StatusBalloon::updateHeader() -{ - // QMutexLocker locker(&m_processesMutex); - - int running = 0, completed = 0, failed = 0, canceled = 0; - for (const auto &item : m_processes) { - if (item->status == ProcessStatus::Running) - running++; - else if (item->status == ProcessStatus::Completed) - completed++; - else if (item->status == ProcessStatus::Failed) - failed++; - else if (item->status == ProcessStatus::Cancelled) - canceled++; - } - int total = running + completed + failed + canceled; - - QString headerText = QString("Processes:\n %1 running").arg(running); - if (completed > 0 || failed > 0 || canceled > 0) { - headerText += QString(" • %1 completed").arg(completed); - if (failed > 0) { - headerText += QString(" • %1 failed").arg(failed); - } - if (canceled > 0) { - headerText += QString(" • %1 cancelled").arg(canceled); - } - } - m_headerLabel->setText(headerText); - - if (total == 0) { - m_headerLabel->hide(); - m_noProcesesLabel->show(); - return; - } else { - m_headerLabel->show(); - m_noProcesesLabel->hide(); - } -} - -void StatusBalloon::handleShow(bool forceVisible) -{ - /* required on Wayland */ - QWidget *anchorWindow = - m_button ? m_button->window() : QApplication::activeWindow(); - if (!anchorWindow) { - if (m_button) - m_button->setIndicatorVisible(true); - return; - } - - // ensure popup has a real QWidget parent. - if (parentWidget() != anchorWindow) { - setParent(anchorWindow, Qt::ToolTip); - } - /**/ - - QPoint pos = m_button->mapToGlobal( - QPoint(m_button->width() / 2, m_button->height())); - - toggleBaloon(pos, -1, forceVisible); -} - -bool StatusBalloon::hasActiveProcesses() const -{ - QMutexLocker locker(&m_processesMutex); - for (const auto &item : m_processes) { - if (item->status == ProcessStatus::Running) { - return true; - } - } - return false; -} - -void StatusBalloon::removeProcess(const QUuid &processId) -{ - std::shared_ptr item; - { - QMutexLocker locker(&m_processesMutex); - if (!m_processes.contains(processId)) - return; - - item = m_processes[processId]; - m_processes.remove(processId); - } - - if (item->processWidget) { - m_processesLayout->removeWidget(item->processWidget); - item->processWidget->deleteLater(); - item->processWidget = nullptr; - } - - // hide dot if no active processes left - if (m_button && !hasActiveProcesses()) - m_button->setIndicatorVisible(false); - - updateHeader(); -} - -void StatusBalloon::handleJobUpdate(const std::shared_ptr &item) -{ - if (item->processWidget) { - item->processWidget->updateUI(); - } -} - -#ifdef WIN32 -void StatusBalloon::showEvent(QShowEvent *event) -{ - QBalloonTip::showEvent(event); - // HWND changes after hide/show, have to reapply acrylic here - enableMica((HWND)winId()); - SetCorner((HWND)winId(), CornerPreference::Corner_Round); -} -#endif - -void StatusBalloon::resizeEvent(QResizeEvent *event) -{ - QBalloonTip::resizeEvent(event); - - if (!m_noProcesesLabel) - return; - - const int margin = 10; - int maxWidth = qMax(0, width() - 2 * margin); - m_noProcesesLabel->setMaximumWidth(maxWidth); - m_noProcesesLabel->adjustSize(); - - int x = (width() - m_noProcesesLabel->width()) / 2; - int y = (height() - m_noProcesesLabel->height()) / 2; - x = qMax(margin, x); - y = qMax(margin, y); - - m_noProcesesLabel->move(x, y); -} diff --git a/src/statusballoon.h b/src/statusballoon.h deleted file mode 100644 index 1ec16f9..0000000 --- a/src/statusballoon.h +++ /dev/null @@ -1,141 +0,0 @@ -#ifndef STATUSBALLOON_H -#define STATUSBALLOON_H - -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "iomanagerclient.h" -#include "qballoontip.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class BalloonProcess; - -enum class ProcessType { Export, Import }; - -enum class ProcessStatus { Queued, Running, Completed, Failed, Cancelled }; - -struct ProcessItem { - QUuid processId; - ProcessType type; - ProcessStatus status; - QString title; - QString currentFile; - int totalItems = 0; - int completedItems = 0; - int failedItems = 0; - qint64 totalBytes = 0; - qint64 transferredBytes = 0; - QDateTime startTime; - QDateTime endTime; - QString destinationPath; - // QUuid jobId; - std::optional> onComplete; - BalloonProcess *processWidget = nullptr; -}; - -class BalloonProcess : public QWidget -{ - Q_OBJECT -public: - explicit BalloonProcess(std::shared_ptr item, - QWidget *parent = nullptr); - - void updateUI(); - -private: - void onCancelClicked(); - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - void onOpenFolderClicked(); - void updateStyles(); - - std::shared_ptr m_item; - QDateTime m_lastUpdateTime; - QString m_lastSpeedText; - qint64 m_lastBytesTransferred; - - QLabel *m_titleLabel; - QLabel *m_statusLabel; - QLabel *m_statsLabel; - QLabel *m_currentFileLabel; - QProgressBar *m_progressBar; - QPushButton *m_actionButton; - QPushButton *m_cancelButton; - ZIconWidget *m_removeBtn; - -protected: - void changeEvent(QEvent *event) override - { - if (event->type() == QEvent::PaletteChange) { - updateStyles(); - } - QWidget::changeEvent(event); - } -}; - -class StatusBalloon : public QBalloonTip -{ - Q_OBJECT -public: - explicit StatusBalloon(QWidget *parent = nullptr); - static StatusBalloon *sharedInstance(); - - // Process management - QUuid startProcess( - const QString &title, int totalItems, const QString &destinationPath, - ProcessType type, const QUuid &jobId, - std::optional> onComplete = std::nullopt); - - void onFileTransferProgress(const QUuid &processId, - const QString ¤tFile, - qint64 bytesTransferred, qint64 totalBytes); - - bool hasActiveProcesses() const; - void removeProcess(const QUuid &processId); - -protected: -#ifdef WIN32 - void showEvent(QShowEvent *event) override; -#endif - void resizeEvent(QResizeEvent *event) override; - -private: - void updateHeader(); - void handleShow(bool forceVisible = false); - void createProcessWidget(std::shared_ptr item); - void connectExportThreadSignals(); - void onExportJobFinished(const QUuid &job_id, bool cancelled, - qint64 successful_items, qint64 failed_items, - qint64 total_bytes); - void onItemExported(const QUuid &job_id, const QString &file_name, - const QString &destination_path, bool success, - int bytes_transferred, const QString &error_message); - void onImportJobFinished(const QUuid &job_id, bool cancelled, - qint64 successful_items, qint64 failed_items, - qint64 total_bytes); - void onItemImported(const QUuid &job_id, const QString &file_name, - const QString &destination_path, bool success, - int bytes_transferred, const QString &error_message); - void handleJobUpdate(const std::shared_ptr &item); - - QVBoxLayout *m_mainLayout; - QLabel *m_headerLabel; - QWidget *m_processesContainer; - QVBoxLayout *m_processesLayout; - - QMap> m_processes; - mutable QMutex m_processesMutex; - QLabel *m_noProcesesLabel; -}; -#endif // STATUSBALLOON_H diff --git a/src/thumbnailprovider.h b/src/thumbnailprovider.h deleted file mode 100644 index 3af1d51..0000000 --- a/src/thumbnailprovider.h +++ /dev/null @@ -1,79 +0,0 @@ -#include "idescriptor_rust_codebase/src/image_loader.cxxqt.h" -#include -#include -#include - -class ThumbnailProvider : public QQuickImageProvider -{ - Q_OBJECT -public: - ThumbnailProvider() : QQuickImageProvider(Image) - { - // 350 MB - m_cache.setMaxCost(350 * 1024 * 1024); - connect(&m_imageLoader, &ImageBackend::thumbnail_ready, this, - [this](const QString &path, const QImage &img, - unsigned int rowHint) { - insert(path, img); - qDebug() << "thumb ready in provider" << path << "Row" - << rowHint; - emit thumbnailReady(path, img, rowHint); - }); - } - - static ThumbnailProvider *sharedInstance() - { - static auto *instance = new ThumbnailProvider(); - return instance; - } - - QImage requestImage(const QString &id, QSize *size, - const QSize &requestedSize) override - { - const QUrl url(QStringLiteral("image://thumb/") + id); - const QString path = url.path().mid(1); // strip leading '/' - const QUrlQuery query(url); - const QString udid = query.queryItemValue("udid"); - const qint32 index = query.queryItemValue("index").toInt(); - - // FIXME: dont use path for key - if (m_cache.contains(path)) { - qDebug() << "Serving from provider cache"; - const QImage img = *m_cache.object(path); - if (size) - *size = img.size(); - return img; - } - - qDebug() << "path" << path << "udid" << udid; - - m_imageLoader.request_thumbnail(udid, path, index); - - const QString resPath = QStringLiteral( - ":/resources/icons/MaterialSymbolsLightImageOutlineSharp.png"); - qDebug() << "ThumbnailProvider: requestImage id=" << id - << " requestedSize=" << requestedSize; - - QImage placeholder(resPath); - - if (size) - *size = placeholder.size(); - return placeholder; - } - - void insert(const QString &id, const QImage &img) - { - QImage *heapImg = new QImage(img); - - int cacheCost = - heapImg->width() * heapImg->height() * heapImg->depth() / 8; - m_cache.insert(id, heapImg, cacheCost); - } -signals: - void thumbnailReady(const QString &path, const QImage &data, - unsigned int rowHint); - -private: - QCache m_cache; - ImageBackend m_imageLoader; -}; \ No newline at end of file diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp deleted file mode 100644 index b749f61..0000000 --- a/src/toolboxwidget.cpp +++ /dev/null @@ -1,645 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "toolboxwidget.h" -#include "airplaywidget.h" -#include "appcontext.h" -#include "cableinfowidget.h" -#include "devdiskimageswidget.h" -#include "devdiskmanager.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#ifndef __APPLE__ -#include "ifusewidget.h" -#endif -#include "livescreenwidget.h" -#include "querymobilegestaltwidget.h" -#include "virtuallocationwidget.h" -#include "wirelessgalleryimportwidget.h" -#include -#include -#include -#include - -#ifdef WIN32 -#include -#endif - -struct iDescriptorToolWidget { - iDescriptorTool tool; - QString description; - bool requiresDevice; - QString iconName; -}; - -ToolboxWidget *ToolboxWidget::sharedInstance() -{ - static ToolboxWidget *instance = new ToolboxWidget(); - return instance; -} - -ToolboxWidget::ToolboxWidget(QWidget *parent) : QWidget{parent} -{ - setupUI(); - updateDeviceList(); - updateToolboxStates(); - - connect(AppContext::sharedInstance(), &AppContext::deviceChange, this, - &ToolboxWidget::updateUI); - connect(AppContext::sharedInstance(), - &AppContext::currentDeviceSelectionChanged, this, - &ToolboxWidget::onCurrentDeviceChanged); -} - -void ToolboxWidget::setupUI() -{ - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - - // Device selection section - QHBoxLayout *deviceLayout = new QHBoxLayout(); - m_deviceLabel = new QLabel("Device:"); - m_deviceCombo = new QComboBox(); - m_deviceCombo->setMinimumWidth(200); - - deviceLayout->addWidget(m_deviceLabel); - deviceLayout->addWidget(m_deviceCombo); - deviceLayout->setContentsMargins(15, 5, 15, 5); - deviceLayout->addStretch(); - - mainLayout->addLayout(deviceLayout); - - // Scroll area for toolboxes - m_scrollArea = new QScrollArea(); - m_scrollArea->setWidgetResizable(true); - m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_scrollArea->setStyleSheet( - "QScrollArea { background: transparent; border: none; }"); - m_scrollArea->viewport()->setStyleSheet("background: transparent;"); - - m_contentWidget = new QWidget(); - QVBoxLayout *contentLayout = new QVBoxLayout(m_contentWidget); - contentLayout->setSpacing(20); - contentLayout->setContentsMargins(0, 0, 0, 0); - - // Main Tools Section - QLabel *mainToolsLabel = new QLabel("Tools"); - mainToolsLabel->setStyleSheet( - "font-weight: bold; font-size: 14px; margin-left: 10px"); - contentLayout->addWidget(mainToolsLabel); - - QWidget *mainToolsWidget = new QWidget(); - m_gridLayout = new QGridLayout(mainToolsWidget); - m_gridLayout->setSpacing(10); - - QList mainToolWidgets; - mainToolWidgets.append( - {iDescriptorTool::Airplayer, "Cast your device screen ", false, ""}); - mainToolWidgets.append({iDescriptorTool::VirtualLocation, - "Simulate GPS location on your device", true, ""}); - mainToolWidgets.append({iDescriptorTool::LiveScreen, - "View device screen in real-time", true, ""}); - mainToolWidgets.append({iDescriptorTool::QueryMobileGestalt, - "Query device hardware information", true, ""}); - mainToolWidgets.append({iDescriptorTool::DeveloperDiskImages, - "Manage developer disk images", false, ""}); - mainToolWidgets.append( - {iDescriptorTool::WirelessGalleryImport, - "Import photos wirelessly to your iDevice (requires Shortcuts app)", - false, ""}); -#ifndef __APPLE__ - mainToolWidgets.append({iDescriptorTool::iFuse, - "Mount your iPhone's filesystem on your PC", true, - ""}); -#endif - mainToolWidgets.append({iDescriptorTool::CableInfoWidget, - "View detailed cable and connection info", true, - ""}); - mainToolWidgets.append({iDescriptorTool::NetworkDevices, - "Discover and monitor devices on your network", - false, ""}); - - for (int i = 0; i < mainToolWidgets.size(); ++i) { - const auto &tool = mainToolWidgets[i]; - ClickableWidget *toolbox = - createToolbox(tool.tool, tool.description, tool.requiresDevice); - int row = i / 3; - int col = i % 3; - m_gridLayout->addWidget(toolbox, row, col); - } - - contentLayout->addWidget(mainToolsWidget); - - // More Tools Section - QLabel *moreToolsLabel = new QLabel("More Tools"); - moreToolsLabel->setStyleSheet( - "font-weight: bold; font-size: 14px; margin-left: 10px"); - contentLayout->addWidget(moreToolsLabel); - - QWidget *moreToolsWidget = new QWidget(); - QGridLayout *moreGridLayout = new QGridLayout(moreToolsWidget); - moreGridLayout->setSpacing(10); - - QList moreToolWidgets; - moreToolWidgets.append( - {iDescriptorTool::MountDevImage, - "Mount a compatible device image with a single click", true, ""}); - moreToolWidgets.append( - {iDescriptorTool::Restart, "Restart device services", true, ""}); - moreToolWidgets.append( - {iDescriptorTool::Shutdown, "Shut down the device", true, ""}); - moreToolWidgets.append({iDescriptorTool::RecoveryMode, - "Enter device recovery mode", true, ""}); - moreToolWidgets.append({iDescriptorTool::EnableWifiConnections, - "Make device connectable via Wi-Fi", true, ""}); - - for (int i = 0; i < moreToolWidgets.size(); ++i) { - const auto &tool = moreToolWidgets[i]; - ClickableWidget *toolbox = - createToolbox(tool.tool, tool.description, tool.requiresDevice); - int row = i / 3; - int col = i % 3; - moreGridLayout->addWidget(toolbox, row, col); - } - - contentLayout->addWidget(moreToolsWidget); - contentLayout->addStretch(); - - m_scrollArea->setWidget(m_contentWidget); - mainLayout->addWidget(m_scrollArea); - - connect(m_deviceCombo, QOverload::of(&QComboBox::currentIndexChanged), - this, &ToolboxWidget::onDeviceSelectionChanged); -} - -ToolboxItemWidget *ToolboxWidget::createToolbox(iDescriptorTool tool, - const QString &description, - bool requiresDevice) -{ - QString title; - QString iconName; - bool iconThemable = true; - switch (tool) { - case iDescriptorTool::Airplayer: - title = "Airplayer"; - iconName = ":/resources/icons/MaterialSymbolsLightAirplayOutline.png"; - break; - case iDescriptorTool::LiveScreen: - title = "Live Screen"; - iconName = ":/resources/icons/PepiconsPrintCellphoneEye.png"; - break; - case iDescriptorTool::MountDevImage: - title = "Mount Dev Image"; - iconName = ":/resources/icons/MdiDisk.png"; - break; - case iDescriptorTool::VirtualLocation: - title = "Virtual Location"; - iconName = ":/resources/icons/MaterialSymbolsLocationOnOutline.png"; - break; - case iDescriptorTool::Restart: - title = "Restart"; - iconName = ":/resources/icons/IcTwotoneRestartAlt.png"; - break; - case iDescriptorTool::Shutdown: - title = "Shutdown"; - iconName = ":/resources/icons/IcOutlinePowerSettingsNew.png"; - break; - case iDescriptorTool::RecoveryMode: - title = "Recovery Mode"; - iconName = ":/resources/icons/HugeiconsWrench01.png"; - break; - case iDescriptorTool::QueryMobileGestalt: - title = "Query Mobile Gestalt"; - iconName = ":/resources/icons/" - "StreamlineProgrammingBrowserSearchSearchWindowGlassAppCod" - "eProgrammingQueryFindMagnifyingApps.png"; - break; - case iDescriptorTool::DeveloperDiskImages: - title = "Dev Disk Images"; - iconName = ":/resources/icons/TablerDatabaseExport.png"; - break; - case iDescriptorTool::WirelessGalleryImport: - title = "Wireless Gallery Import"; - iconName = ":/resources/icons/MaterialSymbolsAndroidWifi3BarPlus.png"; - break; - case iDescriptorTool::iFuse: - title = "iFuse Mount"; - iconName = ":/resources/icons/fuse.png"; - iconThemable = false; - break; - case iDescriptorTool::CableInfoWidget: - title = "Cable Info"; - iconName = ":/resources/icons/MaterialSymbolsLightCableRounded.png"; - break; - case iDescriptorTool::NetworkDevices: - title = "Network Devices"; - iconName = - ":/resources/icons/StreamlineUltimateMultipleUsersNetwork.png"; - break; - case iDescriptorTool::EnableWifiConnections: - title = "Enable Wi-Fi Connections"; - iconName = - ":/resources/icons/StreamlineFreehandChargingFlashWireless.png"; - break; - default: - title = "Unknown Tool"; - break; - } - - ToolboxItemWidget *b = new ToolboxItemWidget( - tool, description, iconName, title, requiresDevice, iconThemable); - - m_toolboxes.append(b); - b->setProperty("requiresDevice", requiresDevice); - connect(b, &ToolboxItemWidget::clicked, [this, tool, requiresDevice]() { - onToolboxClicked(tool, requiresDevice); - }); - return b; -} - -void ToolboxWidget::updateDeviceList() -{ - m_deviceCombo->blockSignals(true); - m_deviceCombo->clear(); - - QList> devices = - AppContext::sharedInstance()->getAllDevices(); - - if (devices.isEmpty()) { - m_deviceCombo->addItem("No device connected"); - m_deviceCombo->setEnabled(false); - m_uuid.clear(); - } else { - m_deviceCombo->setEnabled(true); - for (const std::shared_ptr device : devices) { - QString shortUdid = device->udid.left(8) + "..."; - m_deviceCombo->addItem( - QString::fromStdString(device->deviceInfo.productType) + " / " + - shortUdid + - (device->deviceInfo.isWireless ? " (Wi-Fi)" : ""), - device->udid); - } - } - - onCurrentDeviceChanged( - AppContext::sharedInstance()->getCurrentDeviceSelection()); - - m_deviceCombo->blockSignals(false); - - if (m_deviceCombo->count() > 0 && m_deviceCombo->currentIndex() >= 0) { - QString currentUdid = m_deviceCombo->currentData().toString(); - if (!currentUdid.isEmpty()) { - m_uuid = currentUdid; - qDebug() << "[toolboxwidget] Initialized m_uuid to:" << currentUdid; - } - } -} - -void ToolboxWidget::updateToolboxStates() -{ - bool hasDevice = !AppContext::sharedInstance()->getAllDevices().isEmpty(); - - for (int i = 0; i < m_toolboxes.size(); ++i) { - ToolboxItemWidget *toolbox = m_toolboxes[i]; - bool requiresDevice = toolbox->property("requiresDevice").toBool(); - - bool enabled = !requiresDevice || hasDevice; - toolbox->updateStyles(enabled); - } -} - -void ToolboxWidget::updateUI() -{ - updateDeviceList(); - updateToolboxStates(); -} - -void ToolboxWidget::onDeviceSelectionChanged() -{ - QString selectedUdid = m_deviceCombo->currentData().toString(); - - // Clear m_uuid if no valid selection - if (selectedUdid.isEmpty()) { - m_uuid.clear(); - return; - } - - if (AppContext::sharedInstance()->getDevice(selectedUdid) == nullptr) { - QMessageBox::warning(this, "Device Not Found", - "The selected device is no longer connected."); - m_uuid.clear(); // Clear stale UUID - updateDeviceList(); - return; - } - - m_uuid = selectedUdid; - qDebug() << "[toolboxwidget] Selected device UDID:" << selectedUdid; - // Update the selected device in main menu - AppContext::sharedInstance()->setCurrentDeviceSelection( - DeviceSelection(selectedUdid)); -} - -void ToolboxWidget::onCurrentDeviceChanged(const DeviceSelection &selection) -{ - if (selection.valid() && selection.type == DeviceSelection::Normal) { - int index = m_deviceCombo->findData(selection.udid); - if (index != -1) { - // Block signals to prevent recursive calls when we update the UI - m_deviceCombo->blockSignals(true); - m_deviceCombo->setCurrentIndex(index); - m_deviceCombo->blockSignals(false); - - m_uuid = selection.udid; - } - } else { - // Clear m_uuid when selection is invalid - m_uuid.clear(); - } -} - -void ToolboxWidget::onToolboxClicked(iDescriptorTool tool, bool requiresDevice) -{ - // final check to make sure device is connected if required - auto device = AppContext::sharedInstance()->getDevice(m_uuid); - if (!device && requiresDevice) { - QMessageBox::warning( - this, "Device Disconnected ?", - "Device just disconnected, please select a device."); - return; - } - - qDebug() << "idevice exists:" << (device != nullptr) << m_uuid; - switch (tool) { - case iDescriptorTool::Airplayer: { - if (!m_airplayWidget) { - m_airplayWidget = new AirPlayWidget(); - connect(m_airplayWidget, &QObject::destroyed, this, - [this]() { m_airplayWidget = nullptr; }); - m_airplayWidget->setAttribute(Qt::WA_DeleteOnClose); - m_airplayWidget->setWindowFlag(Qt::Window); - m_airplayWidget->resize(400, 300); - m_airplayWidget->show(); - } else { - m_airplayWidget->raise(); - m_airplayWidget->activateWindow(); - } - } break; - - case iDescriptorTool::LiveScreen: { - LiveScreenWidget *liveScreen = new LiveScreenWidget(device); - liveScreen->show(); - } break; - case iDescriptorTool::RecoveryMode: { - enterRecoveryMode(device); - } break; - case iDescriptorTool::MountDevImage: { - - if (!DevDiskImageHelper::canMountForDevice(device)) { - QMessageBox::warning(this, "Unsupported iOS Version", - "You don't need to mount a developer disk " - "image on devices running iOS 17 or later"); - return; - } - DevDiskImageHelper *devDiskImageHelper = - new DevDiskImageHelper(device, this); - - connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted, - this, [this, devDiskImageHelper](bool success) { - if (success) { - QMessageBox::information( - this, "Success", - "Developer image mounted successfully."); - } else { - QMessageBox::warning( - this, "Failure", - "Failed to mount developer image."); - } - }); - devDiskImageHelper->start(); - } break; - case iDescriptorTool::VirtualLocation: { - VirtualLocation *virtualLocation = new VirtualLocation(device); - virtualLocation->setAttribute(Qt::WA_DeleteOnClose); - virtualLocation->show(); - } break; - case iDescriptorTool::Restart: { - restartDevice(device); - } break; - case iDescriptorTool::Shutdown: { - shutdownDevice(device); - } break; - case iDescriptorTool::QueryMobileGestalt: { - if (!QueryMobileGestaltWidget::canOpenForDevice(device)) { - QMessageBox::warning(this, "Unsupported iOS Version", - "Apple deprecated this protocol for Devices " - "running iOS 17 or later"); - return; - } - - QueryMobileGestaltWidget *queryMobileGestaltWidget = - new QueryMobileGestaltWidget(device); - queryMobileGestaltWidget->setAttribute(Qt::WA_DeleteOnClose); - queryMobileGestaltWidget->show(); - } break; - case iDescriptorTool::DeveloperDiskImages: { - if (!m_devDiskImagesWidget) { - m_devDiskImagesWidget = - new DevDiskImagesWidget(device ? device->udid : QString()); - m_devDiskImagesWidget->setAttribute(Qt::WA_DeleteOnClose); - connect(m_devDiskImagesWidget, &QObject::destroyed, this, - [this]() { m_devDiskImagesWidget = nullptr; }); - m_devDiskImagesWidget->show(); - } else { - m_devDiskImagesWidget->raise(); - m_devDiskImagesWidget->activateWindow(); - } - } break; - case iDescriptorTool::WirelessGalleryImport: { - if (!m_wirelessGalleryImportWidget) { - m_wirelessGalleryImportWidget = new WirelessGalleryImportWidget(); - connect(m_wirelessGalleryImportWidget, &QObject::destroyed, this, - [this]() { m_wirelessGalleryImportWidget = nullptr; }); - m_wirelessGalleryImportWidget->setAttribute(Qt::WA_DeleteOnClose); - m_wirelessGalleryImportWidget->show(); - } else { - m_wirelessGalleryImportWidget->raise(); - m_wirelessGalleryImportWidget->activateWindow(); - } - } break; -#ifndef __APPLE__ - case iDescriptorTool::iFuse: { - bool canOpen = iFuseWidget::canOpenForDevice(device); - if (!canOpen) { - QMessageBox::warning(this, "Unsupported Device", - "iFuse mounting is not supported for wireless " - "devices."); - return; - } - - if (!m_ifuseWidget) { - m_ifuseWidget = new iFuseWidget(device); - m_ifuseWidget->setAttribute(Qt::WA_DeleteOnClose); - connect(m_ifuseWidget, &QObject::destroyed, this, - [this]() { m_ifuseWidget = nullptr; }); - m_ifuseWidget->setWindowFlag(Qt::Window); - m_ifuseWidget->resize(600, 400); - m_ifuseWidget->show(); - } else { - m_ifuseWidget->raise(); - m_ifuseWidget->activateWindow(); - } - } break; -#endif - case iDescriptorTool::CableInfoWidget: { - CableInfoWidget *cableInfoWidget = new CableInfoWidget(device); - cableInfoWidget->show(); - } break; - case iDescriptorTool::NetworkDevices: { - if (!m_networkDevicesWidget) { - m_networkDevicesWidget = new NetworkDevicesWidget(); - m_networkDevicesWidget->setAttribute(Qt::WA_DeleteOnClose); - connect(m_networkDevicesWidget, &QObject::destroyed, this, - [this]() { m_networkDevicesWidget = nullptr; }); - m_networkDevicesWidget->show(); - } else { - m_networkDevicesWidget->raise(); - m_networkDevicesWidget->activateWindow(); - } - } break; - - case iDescriptorTool::EnableWifiConnections: { - connect( - device->service_manager, - &CXX::ServiceManager::enable_wifi_connections_result, this, - [this](bool success) { - if (success) { - QMessageBox::information( - this, "Success", - "Wi-Fi connections enabled successfully. You can now " - "connect to this device wirelessly."); - } else { - QMessageBox::warning(this, "Failure", - "Failed to enable Wi-Fi connections."); - } - }, - Qt::SingleShotConnection); - - device->service_manager->enable_wifi_connections(); - } break; - default: - qDebug() << "Clicked on unimplemented tool"; - break; - } -} - -void ToolboxWidget::restartDevice( - const std::shared_ptr device) -{ - QMessageBox msgBox; - msgBox.setWindowTitle("Restart Device"); - msgBox.setText("Are you sure you want to restart the device?"); - msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - msgBox.setDefaultButton(QMessageBox::No); - int ret = msgBox.exec(); - if (ret != QMessageBox::Yes) - return; - - bool res = device->service_manager->restart(); - - if (res) { - QMessageBox::information(nullptr, "Restart Initiated", - "Device will restart shortly..."); - qDebug() << "Restarting device"; - } else { - QMessageBox::warning(nullptr, "Restart Failed", - "Failed to restart device."); - } -} - -void ToolboxWidget::shutdownDevice( - const std::shared_ptr device) -{ - - QMessageBox msgBox; - msgBox.setWindowTitle("Shutdown Device"); - msgBox.setText("Are you sure you want to shutdown the device?"); - msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - msgBox.setDefaultButton(QMessageBox::No); - - int ret = msgBox.exec(); - if (ret != QMessageBox::Yes) - return; - - bool res = device->service_manager->shutdown(); - - if (res) { - QMessageBox::information(nullptr, "Shutdown Initiated", - "Device will shutdown shortly.."); - qDebug() << "Shutting down device"; - } else { - QMessageBox::warning(nullptr, "Shutdown Failed", - "Failed to shutdown device."); - } -} - -void ToolboxWidget::enterRecoveryMode( - const std::shared_ptr device) -{ - QMessageBox msgBox; - msgBox.setWindowTitle("Enter Recovery Mode"); - msgBox.setText("Are you sure you want to enter recovery mode?"); - msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - msgBox.setDefaultButton(QMessageBox::No); - - int ret = msgBox.exec(); - if (ret != QMessageBox::Yes) - return; - - bool res = device->service_manager->enter_recovery_mode(); - - if (res) { - QMessageBox::information(nullptr, "Enter Recovery Mode Initiated", - "Device will enter recovery mode."); - qDebug() << "Entering recovery mode"; - } else { - QMessageBox::warning(nullptr, "Enter Recovery Mode Failed", - "Failed to enter recovery mode."); - } -} - -void ToolboxWidget::restartAirPlayWidget() -{ - if (!m_airplayWidget) { - onToolboxClicked(iDescriptorTool::Airplayer, false); - return; - } - - connect( - m_airplayWidget, &QObject::destroyed, this, - [this]() { - m_airplayWidget = nullptr; - // give some time for cleanup - QTimer::singleShot(100, this, [this]() { - onToolboxClicked(iDescriptorTool::Airplayer, false); - }); - }, - Qt::SingleShotConnection); - - m_airplayWidget->close(); -} \ No newline at end of file diff --git a/src/toolboxwidget.h b/src/toolboxwidget.h deleted file mode 100644 index 7c6466f..0000000 --- a/src/toolboxwidget.h +++ /dev/null @@ -1,148 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef TOOLBOXWIDGET_H -#define TOOLBOXWIDGET_H - -#include "airplaywidget.h" -#include "devdiskimageswidget.h" -#include "devicesidebarwidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "networkdeviceswidget.h" -#include "wirelessgalleryimportwidget.h" -#include -#include -#include -#include -#include -#include -#include -#include -#ifndef __APPLE__ -#include "ifusewidget.h" -#endif - -class ToolboxItemWidget : public ClickableWidget -{ - Q_OBJECT -public: - ToolboxItemWidget(iDescriptorTool tool, const QString &description, - const QString &iconName, const QString &title, - bool requiresDevice, bool iconThemable, - QWidget *parent = nullptr) - : ClickableWidget(parent) - { - setCursor(Qt::PointingHandCursor); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - setStyleSheet("padding: 5px; border: none; outline: none;"); - QVBoxLayout *layout = new QVBoxLayout(this); - ZIconLabel *icon = new ZIconLabel(QIcon(iconName), nullptr, 1.5, this); - if (!iconThemable) { - icon->setIconThemable(false); - } - QLabel *titleLabel = new QLabel(title); - titleLabel->setAlignment(Qt::AlignCenter); - - // Description - QLabel *descLabel = new QLabel(description); - descLabel->setWordWrap(true); - descLabel->setAlignment(Qt::AlignCenter); - descLabel->setStyleSheet("color: #666; font-size: 12px;"); - icon->setIconSizeMultiplier(1.90); - - layout->addWidget(icon, 0, Qt::AlignCenter); - layout->addWidget(titleLabel); - layout->addWidget(descLabel); - } - - void updateStyles(bool enabled) - { - // FIXME: Opacity does not work because of the stylesheet on Windows -#ifndef WIN32 - if (enabled) { - setStyleSheet("QWidget#toolboxFrame { " - "padding: 5px; }"); - setEnabled(true); - } else { - setStyleSheet("QWidget#toolboxFrame { " - "padding: 5px;" - "opacity: 0.45; }"); - setEnabled(false); - } -#else - if (enabled) { - setStyleSheet("QWidget#toolboxFrame { padding: 5px; " - "border: none; outline: none; }"); - setCursor(Qt::PointingHandCursor); - setEnabled(true); - } else { - setStyleSheet("padding: 5px;" - "border-radius: 8px;" - "color: #666;"); - setCursor(Qt::ArrowCursor); - setEnabled(false); - } -#endif - } -}; - -class ToolboxWidget : public QWidget -{ - Q_OBJECT -public: - explicit ToolboxWidget(QWidget *parent = nullptr); - static void restartDevice(const std::shared_ptr device); - static void shutdownDevice(const std::shared_ptr device); - static void - enterRecoveryMode(const std::shared_ptr device); - static ToolboxWidget *sharedInstance(); - void restartAirPlayWidget(); -private slots: - void onDeviceSelectionChanged(); - void onToolboxClicked(iDescriptorTool tool, bool requiresDevice); - void onCurrentDeviceChanged(const DeviceSelection &selection); - -private: - void setupUI(); - void updateDeviceList(); - void updateToolboxStates(); - void updateUI(); - ToolboxItemWidget *createToolbox(iDescriptorTool tool, - const QString &description, - bool requiresDevice); - QComboBox *m_deviceCombo; - QLabel *m_deviceLabel; - QScrollArea *m_scrollArea; - QWidget *m_contentWidget; - QGridLayout *m_gridLayout; - QList m_toolboxes; - QString m_uuid; - DevDiskImagesWidget *m_devDiskImagesWidget = nullptr; - NetworkDevicesWidget *m_networkDevicesWidget = nullptr; - AirPlayWidget *m_airplayWidget = nullptr; -#ifndef __APPLE__ - iFuseWidget *m_ifuseWidget = nullptr; -#endif - WirelessGalleryImportWidget *m_wirelessGalleryImportWidget = nullptr; - -signals: -}; - -#endif // TOOLBOXWIDGET_H diff --git a/src/qml/AlbumContents.qml b/src/ui/AlbumContents.qml similarity index 97% rename from src/qml/AlbumContents.qml rename to src/ui/AlbumContents.qml index d4f7527..2d9a0bf 100644 --- a/src/qml/AlbumContents.qml +++ b/src/ui/AlbumContents.qml @@ -1,8 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -// import iDescriptor 1.0 -import com.kdab.cxx_qt.demo 1.0 +import iDescriptor 1.0 Item { @@ -69,9 +68,9 @@ Item { } Connections { - target : ThumbnailProvider + target : imageLoader - function onThumbnailReady(path, data, rowHint) { + function onThumbnailReady(path, rowHint) { console.log(path, rowHint, "!!!!!!!!! album contents thumb ready") const item = albumContentsModel.get(rowHint) diff --git a/src/ui/App.qml b/src/ui/App.qml new file mode 100644 index 0000000..4316413 --- /dev/null +++ b/src/ui/App.qml @@ -0,0 +1,8 @@ +import QtQuick 2.6 +import QtQuick.Window 2.0 +import QtQuick.Controls 2.0 +import "." + +Item { + Main {} +} \ No newline at end of file diff --git a/src/qml/AppsTab.qml b/src/ui/AppsTab.qml similarity index 94% rename from src/qml/AppsTab.qml rename to src/ui/AppsTab.qml index bf03068..908263f 100644 --- a/src/qml/AppsTab.qml +++ b/src/ui/AppsTab.qml @@ -1,13 +1,14 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -import com.kdab.cxx_qt.demo 1.0 +import Qt5Compat.GraphicalEffects 1.15 +import QtQuick.Controls.impl Item { id: root anchors.fill: parent - readonly property Apps apps: Apps {} + // readonly property Apps apps readonly property string sponsorsUrl: "https://raw.githubusercontent.com/iDescriptor/iDescriptor/refs/heads/main/sponsors.json" property bool loading: true @@ -70,6 +71,7 @@ Item { if (xhr.status === 200) { try { var rootObj = JSON.parse(xhr.responseText); + // FIXME: don't pick the last version var key = pickLastVersionKey(rootObj); var versioned = key ? rootObj[key] : null; var sponsors = versioned && versioned.sponsors ? versioned.sponsors : null; @@ -113,7 +115,7 @@ Item { } Component.onCompleted: { - // FIXME: show keychain/cred dialog if required. + // FIXME: show keychain/cred dialog. apps.init(); } @@ -139,7 +141,7 @@ Item { TextField { Layout.preferredWidth: 240 - enabled: false + enabled: true placeholderText: isLoggedIn ? "Search for apps..." : "Sign in to search" } @@ -181,12 +183,13 @@ Item { id: grid width: parent.width height: parent.height - cellWidth: Math.max(320, parent.width / 3) + cellWidth: Math.max(250, parent.width / 3) cellHeight: 140 model: appModel interactive: true delegate: Rectangle { + id : rec width: grid.cellWidth - 20 height: grid.cellHeight - 20 radius: 8 @@ -208,11 +211,8 @@ Item { anchors.margins: 12 spacing: 10 - Image { - width: 48 - height: 48 - source: iconSource - fillMode: Image.PreserveAspectFit + IconLoader { + iconSource: rec.iconSource } ColumnLayout { @@ -257,15 +257,15 @@ Item { ColumnLayout { spacing: 6 - - Label { + // FIXME: wire up click handling + Button { text: "Install" color: "#007AFF" font.pixelSize: 12 font.bold: true } - Label { + Button { text: (model.websiteUrl && model.websiteUrl.length) ? "Website" : "Download IPA" font.pixelSize: 12 font.bold: true @@ -273,7 +273,6 @@ Item { } } - // FIXME: wire click handling and install flow later. } } } diff --git a/src/qml/Device.qml b/src/ui/Device.qml similarity index 76% rename from src/qml/Device.qml rename to src/ui/Device.qml index b002f3a..bd2806f 100644 --- a/src/qml/Device.qml +++ b/src/ui/Device.qml @@ -16,9 +16,14 @@ Item { } DeviceGallery { - visible : currentSection == 1 + visible : currentSection == 2 anchors.fill: parent udid: root.udid // info: root.info } + + FilesSection { + visible : currentSection == 3 + udid : root.udid + } } \ No newline at end of file diff --git a/src/ui/DeviceContext.qml b/src/ui/DeviceContext.qml new file mode 100644 index 0000000..a37c779 --- /dev/null +++ b/src/ui/DeviceContext.qml @@ -0,0 +1,52 @@ +pragma Singleton +import QtQml 2.15 +import QtQml.Models 2.15 +import QtQuick 2.15 +import iDescriptor 1.0 + +QtObject { + id: root + property ListModel devices: ListModel {} + property string currentDeviceUdid : "" + // default info section + property int currentSection : 0 + + property bool showWelcomePage : true + readonly property Core core: Core {} + + function init() { + root.core.init() + + } + + // workaround to use connections inside a QtObject + property var _connections : Connections { + target: root.core + + function onDevice_event(eventType, udid, info) { + console.log("Device event:", eventType, udid, JSON.stringify(info)) + + switch (eventType) { + case 1: + // FIXME: text should be `$device_market_name / $udid ` + devices.set(udid, { udid: udid, info: info , text: `TODO` }) + root.showWelcomePage = false + root.currentDeviceUdid = udid + break; + case 2: + devices.remove(udid) + root.showWelcomePage = !!devices.count + root.currentDeviceUdid = "" + + break; + case 3: + break; + case 4: + break; + default: + + } + } + } + +} \ No newline at end of file diff --git a/src/qml/DeviceGallery.qml b/src/ui/DeviceGallery.qml similarity index 96% rename from src/qml/DeviceGallery.qml rename to src/ui/DeviceGallery.qml index 0b64a0f..75696f0 100644 --- a/src/qml/DeviceGallery.qml +++ b/src/ui/DeviceGallery.qml @@ -1,8 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -// import iDescriptor 1.0 -import com.kdab.cxx_qt.demo 1.0 +import iDescriptor 1.0 Item { @@ -16,6 +15,7 @@ Item { Component.onCompleted: { + console.log("Calling init query") query.init(root.udid); } @@ -52,7 +52,8 @@ Item { Connections { target: query - function onIs_initChanged() { + function onStateChanged() { + console.log("state changed") query.read_albums() } @@ -80,9 +81,10 @@ Item { } Connections { - target : ThumbnailProvider + target : imageLoader - function onThumbnailReady(path, data, rowHint) { + function onThumbnailReady(path, rowHint) { + console.log("thumb ready") const item = albumModel.get(rowHint) if (item && item.filePath == path) { albumModel.setProperty(rowHint, "thumbVersion", item.thumbVersion + 1) @@ -175,7 +177,6 @@ Item { border.width: 1 visible: false - // Optional: semi-transparent fill like standard rubber bands opacity: 0.3 Rectangle { anchors.fill: parent; color: "blue"; opacity: 0.2 } } diff --git a/src/qml/DeviceImage.qml b/src/ui/DeviceImage.qml similarity index 100% rename from src/qml/DeviceImage.qml rename to src/ui/DeviceImage.qml diff --git a/src/qml/DeviceInfo.qml b/src/ui/DeviceInfo.qml similarity index 100% rename from src/qml/DeviceInfo.qml rename to src/ui/DeviceInfo.qml diff --git a/src/qml/DeviceTab.qml b/src/ui/DeviceTab.qml similarity index 65% rename from src/qml/DeviceTab.qml rename to src/ui/DeviceTab.qml index 81bf6e5..11b9272 100644 --- a/src/qml/DeviceTab.qml +++ b/src/ui/DeviceTab.qml @@ -1,37 +1,18 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -import com.kdab.cxx_qt.demo 1.0 +import iDescriptor 1.0 +import "." as App Item { id: root - property ListModel devices: ListModel {} - property string currentDeviceUdid : "" // default info section - property int currentSection : 0 - - property bool showWelcomePage : true - readonly property Core core: Core {} + property int currentSection : 2 Component.onCompleted: { - root.core.init() + App.DeviceContext.init() } - Connections { - target: root.core - - function onDevice_event(eventType, udid, info) { - console.log("Device event:", eventType, udid, JSON.stringify(info)) - if (eventType === 1) { - devices.append({ udid: udid, info: info }) - root.showWelcomePage = false - root.currentDeviceUdid = udid - } - } - } - - // Connections { - // target : NetworkDeviceProvider RowLayout { anchors.fill: parent @@ -44,7 +25,7 @@ Item { Layout.fillHeight : true Layout.preferredWidth: 220 Repeater { - model: devices + model: App.DeviceContext.devices delegate: Item { SidebarTabButton { Layout.fillWidth: true @@ -70,11 +51,11 @@ Item { Repeater { - model: devices + model: App.DeviceContext.devices delegate:Item { Layout.fillWidth : true Layout.fillHeight : true - visible : model.udid === root.currentDeviceUdid + visible : model.udid === App.DeviceContext.currentDeviceUdid Device { udid: model.udid anchors.fill: parent @@ -88,7 +69,7 @@ Item { Welcome { id: welcomePage - visible : showWelcomePage + visible : App.DeviceContext.showWelcomePage Layout.fillWidth: true Layout.fillHeight: true } diff --git a/src/ui/FileExplorer.qml b/src/ui/FileExplorer.qml new file mode 100644 index 0000000..c035801 --- /dev/null +++ b/src/ui/FileExplorer.qml @@ -0,0 +1,660 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls.impl + +// FIXME: wire up export logic +Item { + id: root + anchors.fill: parent + + property var afcClient: null + + property bool favEnabled: true + property string rootPath: "/" + + property string currentPath: "/" + property bool loading: false + property string errorMessage: "" + + property var backStack: [] + property var forwardStack: [] + + function normalizePath(p) { + var path = (p || "").trim() + if (path.length === 0) path = "/" + if (path[0] !== "/") path = "/" + path + // collapse repeated slashes + path = path.replace(/\/+/g, "/") + if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1) + return path + } + + function _setLoading(on) { + loading = on + if (on) errorMessage = "" + } + + function refresh() { + if (!afcClient) { + errorMessage = qsTr("AFC client is not available.") + return + } + _setLoading(true) + afcClient.check_is_dir_and_list(currentPath) + } + + function navigateToPath(path, pushHistory) { + var next = normalizePath(path) + if (pushHistory === undefined) pushHistory = true + + if (pushHistory) { + if (currentPath !== next) { + backStack = backStack.concat([currentPath]) + forwardStack = [] + } + } + + currentPath = next + addressBar.text = next + refresh() + _updateNavEnabled() + } + + function goHome() { navigateToPath(rootPath, true) } + + function goBack() { + if (backStack.length === 0) return + var prev = backStack[backStack.length - 1] + backStack = backStack.slice(0, -1) + forwardStack = forwardStack.concat([currentPath]) + navigateToPath(prev, false) + } + + function goForward() { + if (forwardStack.length === 0) return + var next = forwardStack[forwardStack.length - 1] + forwardStack = forwardStack.slice(0, -1) + backStack = backStack.concat([currentPath]) + navigateToPath(next, false) + } + + function goUp() { + var p = normalizePath(currentPath) + if (p === "/") return + var idx = p.lastIndexOf("/") + var parentPath = (idx <= 0) ? "/" : p.slice(0, idx) + navigateToPath(parentPath, true) + } + + function _updateNavEnabled() { + backBtn.enabled = backStack.length > 0 + forwardBtn.enabled = forwardStack.length > 0 + upBtn.enabled = normalizePath(currentPath) !== "/" + } + + function _fullPath(name) { + var base = normalizePath(currentPath) + if (base === "/") return "/" + name + return base + "/" + name + } + + // TODO: maybe call from rust + function _isPreviewable(name) { + var lower = (name || "").toLowerCase() + return lower.endsWith(".mp4") || lower.endsWith(".m4v") || lower.endsWith(".mov") || + lower.endsWith(".avi") || lower.endsWith(".mkv") + } + + function _openFileOnDevice(name) { + if (!afcClient) return + var path = _fullPath(name) + + if (_isPreviewable(name)) { + var url = afcClient.start_video_stream(path) + if (url && url.length > 0) { + Qt.openUrlExternally(url) + return + } + // fallthrough to error + errorMessage = qsTr("Failed to start stream.") + return + } + + errorMessage = qsTr("Open is not implemented for this file type yet.") + } + + function _deleteSelected() { + if (!afcClient) return + if (selectedPaths.length === 0) return + + _setLoading(true) + var ok = true + for (var i = 0; i < selectedPaths.length; i++) { + var p = selectedPaths[i] + var r = afcClient.delete_path(p) + if (!r) ok = false + } + + selectedPaths = [] + if (!ok) { + _setLoading(false) + errorMessage = qsTr("Failed to delete one or more items.") + return + } + refresh() + } + + ListModel { id: entriesModel } + + property var selectedPaths: [] + function _isSelected(path) { return selectedPaths.indexOf(path) !== -1 } + function _toggleSelected(path, isDir) { + // FIXME: dir + if (isDir) return + var idx = selectedPaths.indexOf(path) + if (idx === -1) selectedPaths = selectedPaths.concat([path]) + else selectedPaths = selectedPaths.slice(0, idx).concat(selectedPaths.slice(idx + 1)) + _updateActionEnabled() + } + + function _updateActionEnabled() { + exportBtn.enabled = selectedPaths.length > 0 + deleteBtn.enabled = selectedPaths.length > 0 + } + + Connections { + target: afcClient + enabled: !!afcClient + + function onCheck_is_dir_and_list_finished(success, entries) { + entriesModel.clear() + + if (!success) { + root.loading = false + root.errorMessage = qsTr("Failed to load directory.") + return + } + + // entries is QVariantMap: name -> isDir(bool) + var names = [] + for (var k in entries) names.push(k) + names.sort() + + // dirs first, then files (approx) + var dirs = [] + var files = [] + for (var i = 0; i < names.length; i++) { + var name = names[i] + var isDir = !!entries[name] + var iconSource = isDir + ? "qrc:/resources/icons/MaterialSymbolsFolder.png" + : "qrc:/resources/icons/IcBaselineInsertDriveFile.png" + var item = { "name": name, "isDir": isDir, "iconSource": iconSource } + if (isDir) dirs.push(item); else files.push(item) + } + + for (var d = 0; d < dirs.length; d++) entriesModel.append(dirs[d]) + for (var f = 0; f < files.length; f++) entriesModel.append(files[f]) + + root.loading = false + root.errorMessage = "" + root.selectedPaths = [] + root._updateActionEnabled() + root._updateNavEnabled() + } + } + + onAfcClientChanged: { + backStack = [] + forwardStack = [] + selectedPaths = [] + currentPath = normalizePath(rootPath) + addressBar.text = currentPath + if (afcClient) refresh() + else errorMessage = qsTr("AFC client is not available.") + _updateNavEnabled() + _updateActionEnabled() + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 58 + + RowLayout { + anchors.fill: parent + + Item { Layout.fillWidth: true } + + Rectangle { + id: navWidget + Layout.preferredWidth: 700 + Layout.preferredHeight: 44 + radius: 10 + color: "transparent" + border.width: 1 + border.color: "#22000000" + + RowLayout { + anchors.fill: parent + anchors.margins: 6 + spacing: 6 + + ToolButton { + id: backBtn + enabled: false + onClicked: root.goBack() + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/MaterialSymbolsArrowLeftAlt.png" + // FIXME:theming + color: "black" + opacity: backBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Go Back") + } + + ToolButton { + id: forwardBtn + enabled: false + onClicked: root.goForward() + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/MaterialSymbolsArrowRightAlt.png" + // FIXME:theming + color: "black" + opacity: forwardBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Go Forward") + } + + ToolButton { + id: homeBtn + onClicked: root.goHome() + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/MaterialSymbolsLightHome.png" + // FIXME:theming + color: "black" + opacity: homeBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Go Home") + } + + ToolButton { + id: upBtn + enabled: false + onClicked: root.goUp() + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/MaterialSymbolsArrowUpwardAltRounded.png" + // FIXME:theming + color: "black" + opacity: upBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Go Up") + } + + TextField { + id: addressBar + Layout.fillWidth: true + placeholderText: qsTr("Enter path...") + text: root.currentPath + selectByMouse: true + onAccepted: root.navigateToPath(text, true) + } + + ToolButton { + id: importBtn + enabled: false + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/LetsIconsImport.png" + // FIXME:theming + color: "black" + opacity: importBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Import (FIXME)") + } + + ToolButton { + id: exportBtn + enabled: false + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/PhExport.png" + // FIXME:theming + color: "black" + opacity: exportBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Export (FIXME)") + } + + ToolButton { + id: deleteBtn + enabled: false + onClicked: confirmDelete.open() + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/MaterialSymbolsDelete.png" + // FIXME:theming + color: "black" + opacity: deleteBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Delete") + } + + ToolButton { + id: favBtn + visible: root.favEnabled + enabled: true + onClicked: favDialog.open() + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/MaterialSymbolsFavorite.png" + // FIXME:theming + color: "black" + opacity: favBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Add to Favorites") + } + + ToolButton { + id: enterBtn + onClicked: root.navigateToPath(addressBar.text, true) + + padding: 0 + implicitHeight: 44 + implicitWidth: 44 + + contentItem: Item { + anchors.fill: parent + IconImage { + anchors.fill: parent + source: "qrc:/resources/icons/MaterialSymbolsLightKeyboardReturn.png" + // FIXME:theming + color: "black" + opacity: enterBtn.enabled ? 1.0 : 0.7 + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Navigate to path") + } + } + } + + Item { Layout.fillWidth: true } + } + } + + // Content states + StackLayout { + id: contentStack + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: root.loading ? 1 : (root.errorMessage.length > 0 ? 2 : 0) + + // File list + Item { + anchors.fill: parent + + ListView { + id: listView + anchors.fill: parent + clip: true + model: entriesModel + + delegate: Rectangle { + id: row + width: ListView.view.width + height: 44 + color: (mouseArea.containsMouse ? "#0f000000" : "transparent") + + property string entryName: model.name + property bool entryIsDir: model.isDir + property string entryPath: root._fullPath(entryName) + property bool entrySelected: root._isSelected(entryPath) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 10 + + CheckBox { + visible: !row.entryIsDir + checked: row.entrySelected + onClicked: root._toggleSelected(row.entryPath, row.entryIsDir) + } + + IconImage { + source: model.iconSource + Layout.preferredHeight: 34 + Layout.preferredWidth: 34 + // FIXME:theming + color: "black" + opacity: 1.0 + } + + Text { + Layout.fillWidth: true + text: row.entryName + elide: Text.ElideRight + color: "#111" + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onDoubleClicked: { + if (row.entryIsDir) { + root.navigateToPath(row.entryPath, true) + } else { + root._openFileOnDevice(row.entryName) + } + } + + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) { + contextMenu._name = row.entryName + contextMenu._isDir = row.entryIsDir + contextMenu.open() + return + } + // single click: toggle selection for files, navigate for dirs (QWidget parity: approximate) + if (row.entryIsDir) { + // single-click doesn't navigate in the QWidget; keep it inert. + return + } + root._toggleSelected(row.entryPath, row.entryIsDir) + } + } + } + } + + Menu { + id: contextMenu + property string _name: "" + property bool _isDir: false + + MenuItem { + text: qsTr("Open") + enabled: !contextMenu._isDir + onTriggered: root._openFileOnDevice(contextMenu._name) + } + + MenuItem { + text: qsTr("Open Externally") + enabled: !contextMenu._isDir + onTriggered: root._openFileOnDevice(contextMenu._name) // uses stream + Qt.openUrlExternally for previewables + } + + MenuItem { + text: qsTr("Export") + enabled: false + } + } + } + + // Loading + ColumnLayout { + anchors.centerIn: parent + spacing: 10 + BusyIndicator { running: true } + Text { text: qsTr("Loading..."); color: "#444" } + } + + // Error + ColumnLayout { + anchors.centerIn: parent + spacing: 10 + width: Math.min(parent.width * 0.8, 520) + + Text { + text: root.errorMessage + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: "#444" + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + Button { text: qsTr("Try Again"); onClicked: root.refresh() } + } + } + } + } + + Dialog { + id: favDialog + modal: true + title: qsTr("Add to Favorites") + standardButtons: Dialog.Ok | Dialog.Cancel + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + Text { text: qsTr("Enter alias for this location:") } + TextField { id: favAlias; placeholderText: qsTr("Alias here") } + Text { text: qsTr("Path: ") + root.currentPath; color: "#666" } + } + + onAccepted: { + var alias = (favAlias.text || "").trim() + if (alias.length === 0) return + // FIXME: persist + // favoritePlaceAdded(alias, root.currentPath) + favAlias.text = "" + } + onRejected: favAlias.text = "" + } + + Dialog { + id: confirmDelete + modal: true + title: qsTr("Confirm Deletion") + standardButtons: Dialog.Yes | Dialog.No + + ColumnLayout { + anchors.fill: parent + spacing: 10 + Text { + text: qsTr("Are you sure you want to delete the selected item(s)?") + wrapMode: Text.WordWrap + } + Text { text: qsTr("Count: ") + root.selectedPaths.length; color: "#666" } + } + + onAccepted: root._deleteSelected() + } +} diff --git a/src/ui/FilesSection.qml b/src/ui/FilesSection.qml new file mode 100644 index 0000000..c0c4aec --- /dev/null +++ b/src/ui/FilesSection.qml @@ -0,0 +1,339 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls.impl + +Item { + id: root + anchors.fill: parent + + required property string udid + visible: (parent && parent.currentSection !== undefined) ? (parent.currentSection === 2) : true + + /* clients are created from Rust side */ + property var afcClient: null + property var afc2Client: null + property bool afc2Available: false + + property bool loading: true + property string errorMessage: "" + + /* 0 = default, 1 = afc2 */ + property int currentExplorerIndex: 0 + + function _resetUi() { + loading = true + errorMessage = "" + afcClient = null + afc2Client = null + afc2Available = false + currentExplorerIndex = 0 + } + + function loadClients() { + _resetUi() + + if (!root.udid || root.udid.length === 0) { + console.log + loading = false + console.log("wtf no device") + errorMessage = qsTr("No device selected.") + return + } + + if (typeof serviceFactory === "undefined" || !serviceFactory) { + loading = false + errorMessage = qsTr("serviceFactory is not available in QML scope.") + return + } + + afcClient = serviceFactory.create_afc_client(root.udid, false) + afc2Client = serviceFactory.create_afc_client(root.udid, true) + + if (afcClient === null) { + console.log("No AFC client in FilesSection.qml") + loading = false + errorMessage = qsTr("Failed to create AFC client.") + return + } + console.log("AFC client ready") + + afc2Available = (afc2Client !== null) + loading = false + } + + Component.onCompleted: loadClients() + + // FIXME: wire up settings ) + ListModel { id: favoritesModel } + + RowLayout { + anchors.fill: parent + spacing: 0 + + // Sidebar + Rectangle { + id: sidebar + Layout.preferredWidth: 250 + Layout.maximumWidth: 250 + Layout.fillHeight: true + color: "transparent" + border.color: "#1f000000" + border.width: 1 + + ListView { + id: sidebarList + anchors.fill: parent + clip: true + + model: ListModel { + id: sidebarModel + + ListElement { kind: "header"; title: "Explorer"; icon: "qrc:/resources/icons/MaterialSymbolsFolder.png" } + ListElement { kind: "explorer"; title: "Default"; icon: "qrc:/resources/icons/MaterialSymbolsFolder.png"; afc2: false } + ListElement { kind: "explorer"; title: "Jailbroken (AFC2)"; icon: "qrc:/resources/icons/MaterialSymbolsFolder.png"; afc2: true } + + ListElement { kind: "header"; title: "Common Places"; icon: "qrc:/resources/icons/MaterialSymbolsFavorite.png" } + ListElement { kind: "place"; title: "Pictures"; icon: "qrc:/resources/icons/MaterialSymbolsFolder.png"; path: "/DCIM"; afc2: false } + + ListElement { kind: "header"; title: "Favorite Places"; icon: "qrc:/resources/icons/MaterialSymbolsFavorite.png" } + } + + delegate: Item { + id: row + width: ListView.view.width + height: (model.kind === "header") ? 44 : 40 + + property bool isHeader: model.kind === "header" + property bool isExplorer: model.kind === "explorer" + property bool isPlace: model.kind === "place" + + Rectangle { + anchors.fill: parent + color: "transparent" + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 10 + + IconImage { + id: icon + source: model.icon + + Layout.preferredHeight: 34 + Layout.preferredWidth: 34 + + // FIXME:theming + color: "black" + opacity: 1.0 + } + + Text { + Layout.fillWidth: true + text: qsTr(model.title) + font.bold: row.isHeader + elide: Text.ElideRight + color: "#111" + } + + Text { + visible: row.isExplorer && model.afc2 && !root.afc2Available + text: qsTr("(unavailable)") + color: "#666" + font.pixelSize: 12 + } + } + + MouseArea { + anchors.fill: parent + enabled: !row.isHeader && !(row.isExplorer && model.afc2 && !root.afc2Available) + onClicked: { + if (row.isExplorer) { + root.currentExplorerIndex = model.afc2 ? 1 : 0 + if (root.currentExplorerIndex === 0) explorerDefault.goHome() + else explorerAfc2.goHome() + return + } + if (row.isPlace) { + root.currentExplorerIndex = model.afc2 ? 1 : 0 + if (root.currentExplorerIndex === 0) explorerDefault.navigateToPath(model.path) + else explorerAfc2.navigateToPath(model.path) + return + } + } + } + } + + footer: Item { + width: sidebarList.width + height: favCol.implicitHeight + + Column { + id: favCol + width: parent.width + + Repeater { + model: favoritesModel + + delegate: Item { + id: favRow + width: favCol.width + height: 40 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 10 + + IconImage { + source: "qrc:/resources/icons/MaterialSymbolsFolder.png" + Layout.preferredHeight: 34 + Layout.preferredWidth: 34 + // FIXME:theming + color: "black" + opacity: 1.0 + } + + Text { + Layout.fillWidth: true + text: alias + elide: Text.ElideRight + color: "#111" + } + + Text { + visible: afc2 === true + text: qsTr("AFC2") + color: "#666" + font.pixelSize: 12 + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) { + favMenu._index = index + favMenu.open() + return + } + root.currentExplorerIndex = afc2 ? 1 : 0 + if (root.currentExplorerIndex === 0) explorerDefault.navigateToPath(path) + else explorerAfc2.navigateToPath(path) + } + } + } + } + } + } + } + + Menu { + id: favMenu + property int _index: -1 + + MenuItem { + text: qsTr("Remove from Favorites") + onTriggered: { + if (favMenu._index >= 0) favoritesModel.remove(favMenu._index) + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + StackLayout { + id: mainStack + anchors.fill: parent + currentIndex: root.loading ? 1 : (root.errorMessage.length > 0 ? 2 : 0) + + // Content + Item { + id: contentView + anchors.fill: parent + + StackLayout { + anchors.fill: parent + currentIndex: root.currentExplorerIndex + + FileExplorer { + id: explorerDefault + afcClient: root.afcClient + favEnabled: true + + onFavoritePlaceAdded: (alias, path) => { + // FIXME + favoritesModel.append({ "alias": alias, "path": path, "afc2": false }) + } + } + + Item { + anchors.fill: parent + + FileExplorer { + id: explorerAfc2 + anchors.fill: parent + afcClient: root.afc2Client + favEnabled: true + + visible: root.afc2Available + onFavoritePlaceAdded: (alias, path) => { + favoritesModel.append({ "alias": alias, "path": path, "afc2": true }) + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 10 + visible: !root.afc2Available + + Text { text: qsTr("AFC2 is not available on this device."); color: "#444" } + Button { + text: qsTr("Switch to Default") + onClicked: root.currentExplorerIndex = 0 + } + } + } + } + } + + // Loading + ColumnLayout { + anchors.centerIn: parent + spacing: 10 + + BusyIndicator { running: true } + Text { text: qsTr("Loading file explorer..."); color: "#444" } + } + + // Error + ColumnLayout { + anchors.centerIn: parent + spacing: 10 + width: Math.min(parent.width * 0.8, 520) + + Text { + text: root.errorMessage + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: "#444" + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + Button { text: qsTr("Try Again"); onClicked: root.loadClients() } + } + } + } + } + } +} diff --git a/src/qml/HowToConnect.qml b/src/ui/HowToConnect.qml similarity index 100% rename from src/qml/HowToConnect.qml rename to src/ui/HowToConnect.qml diff --git a/src/ui/IconLoader.qml b/src/ui/IconLoader.qml new file mode 100644 index 0000000..4f9b9ba --- /dev/null +++ b/src/ui/IconLoader.qml @@ -0,0 +1,94 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import Qt5Compat.GraphicalEffects 1.15 + +Rectangle { + id: root + required property string iconSource + property int radius: 12 + clip : true + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + + // FIXME: hardcoded + readonly property color shimmerBase: "#e5e7eb" + readonly property color shimmerHighlight: "#f4f4f5" + readonly property bool loading: iconImg.status === Image.Loading || iconImg.status === Image.Null + + // Actual icon + Image { + id: iconImg + anchors.fill: parent + source: root.iconSource + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + visible: loading + } + + Rectangle { + id: mask + anchors.fill: parent + radius: root.radius + visible: false + } + + + // base for placeholder + Rectangle { + id: placeholder + anchors.fill: parent + color: root.shimmerBase + visible: loading + radius: root.radius + + } + + // shimmer + Rectangle { + id: shimmerBand + visible: loading + height: parent.height + width: Math.round(parent.width * 0.65) + y: 0 + x: -width + + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(1, 1, 1, 0.0) } + GradientStop { position: 0.5; color: root.shimmerHighlight } + GradientStop { position: 1.0; color: Qt.rgba(1, 1, 1, 0.0) } + } + opacity: 0.9 + + // angle + rotation: -12 + transformOrigin: Item.Center + + NumberAnimation on x { + running: loading + loops: Animation.Infinite + duration: 1200 + from: -shimmerBand.width + to: iconImg.width + shimmerBand.width + } + } + + + Label { + text : "iD" + anchors.fill : parent + horizontalAlignment : Text.AlignHCenter + verticalAlignment : Text.AlignVCenter + + } + + + OpacityMask { + anchors.fill: parent + source: iconImg + maskSource: mask + } + + +} \ No newline at end of file diff --git a/src/ui/InstalledApps.qml b/src/ui/InstalledApps.qml new file mode 100644 index 0000000..e69de29 diff --git a/src/qml/LoginDialog.qml b/src/ui/LoginDialog.qml similarity index 98% rename from src/qml/LoginDialog.qml rename to src/ui/LoginDialog.qml index 05c5b4a..756826b 100644 --- a/src/qml/LoginDialog.qml +++ b/src/ui/LoginDialog.qml @@ -1,7 +1,6 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -import com.kdab.cxx_qt.demo 1.0 Dialog { id: dialog diff --git a/src/qml/Main.qml b/src/ui/Main.qml similarity index 92% rename from src/qml/Main.qml rename to src/ui/Main.qml index 3ac9ed9..4de1673 100644 --- a/src/qml/Main.qml +++ b/src/ui/Main.qml @@ -23,7 +23,7 @@ ApplicationWindow { spacing: 0 TabButton { text: qsTr("iDevice") - onClicked: currentIndex = 0 + onClicked: currentIndex = 0 active: currentIndex == 0 } diff --git a/src/qml/PreviewWindow.qml b/src/ui/PreviewWindow.qml similarity index 100% rename from src/qml/PreviewWindow.qml rename to src/ui/PreviewWindow.qml diff --git a/src/qml/SidebarTabButton.qml b/src/ui/SidebarTabButton.qml similarity index 95% rename from src/qml/SidebarTabButton.qml rename to src/ui/SidebarTabButton.qml index d1dcfec..6d3074e 100644 --- a/src/qml/SidebarTabButton.qml +++ b/src/ui/SidebarTabButton.qml @@ -1,7 +1,6 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -import com.kdab.cxx_qt.demo 1.0 ColumnLayout { id:root @@ -9,7 +8,7 @@ ColumnLayout { Button { Layout.preferredWidth: 220 - Layout.fillHeight: true + Layout.fillHeight: true contentItem : Text { text: "Info" color: "Red" @@ -26,7 +25,7 @@ ColumnLayout { Layout.preferredWidth: 220 Layout.fillHeight: true contentItem : Text { - text: "Gallery" + text: "Apps" color: "Red" } background : Rectangle { @@ -41,7 +40,7 @@ ColumnLayout { Layout.preferredWidth: 220 Layout.fillHeight: true contentItem : Text { - text: "Apps" + text: "Gallery" color: "Red" } background : Rectangle { @@ -52,7 +51,6 @@ ColumnLayout { } } - Button { Layout.preferredWidth: 220 Layout.fillHeight: true diff --git a/src/qml/TabButton.qml b/src/ui/TabButton.qml similarity index 100% rename from src/qml/TabButton.qml rename to src/ui/TabButton.qml diff --git a/src/qml/Tabs.qml b/src/ui/Tabs.qml similarity index 81% rename from src/qml/Tabs.qml rename to src/ui/Tabs.qml index 0dd15d9..779a981 100644 --- a/src/qml/Tabs.qml +++ b/src/ui/Tabs.qml @@ -1,11 +1,10 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -import com.kdab.cxx_qt.demo 1.0 Item { id: root - property int currentIndex : 0 + required property int currentIndex DeviceTab { @@ -29,4 +28,9 @@ Item { visible : currentIndex == 1 } + + Toolbox { + anchors.fill: parent + visible : currentIndex == 2 + } } diff --git a/src/ui/Toolbox.qml b/src/ui/Toolbox.qml new file mode 100644 index 0000000..2d9abe1 --- /dev/null +++ b/src/ui/Toolbox.qml @@ -0,0 +1,340 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Controls.impl +import "." as App + +// FIXME: handle window creation logic +Item { + id: root + anchors.fill: parent + + + property string currentDeviceUdid: "" + readonly property bool hasDevice: App.DeviceContext.devices && App.DeviceContext.devices.count > 0 + + // 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 deviceSelectionChanged(string udid) + + readonly property var mainToolsModel: ([ + { + toolId: 0, + title: "Airplayer", + description: "Cast your device screen", + requiresDevice: false, + iconSource: "qrc:/resources/icons/MaterialSymbolsLightAirplayOutline.png", + visible: true + }, + { + toolId: 1, + title: "Virtual Location", + description: "Simulate GPS location on your device", + requiresDevice: true, + iconSource: "qrc:/resources/icons/MaterialSymbolsLocationOnOutline.png", + visible: true + }, + { + toolId: 2, + title: "Live Screen", + description: "View device screen in real-time", + requiresDevice: true, + iconSource: "qrc:/resources/icons/PepiconsPrintCellphoneEye.png", + visible: true + }, + { + toolId: 3, + title: "Query Mobile Gestalt", + description: "Query device hardware information", + requiresDevice: true, + iconSource: "qrc:/resources/icons/StreamlineProgrammingBrowserSearchSearchWindowGlassAppCodeProgrammingQueryFindMagnifyingApps.png", + visible: true + }, + { + toolId: 4, + title: "Dev Disk Images", + description: "Manage developer disk images", + requiresDevice: false, + iconSource: "qrc:/resources/icons/TablerDatabaseExport.png", + visible: true + }, + { + toolId: 5, + title: "Wireless Gallery Import", + description: "Import photos wirelessly to your iDevice (requires Shortcuts app)", + requiresDevice: false, + iconSource: "qrc:/resources/icons/MaterialSymbolsAndroidWifi3BarPlus.png", + visible: true + }, + { + toolId: 6, + title: "iFuse Mount", + description: "Mount your iPhone's filesystem on your PC", + requiresDevice: true, + iconSource: "qrc:/resources/icons/fuse.png", + visible: (Qt.platform.os !== "osx" && Qt.platform.os !== "darwin") + }, + { + toolId: 7, + title: "Cable Info", + description: "View detailed cable and connection info", + requiresDevice: true, + iconSource: "qrc:/resources/icons/MaterialSymbolsLightCableRounded.png", + visible: true + }, + { + toolId: 8, + title: "Network Devices", + description: "Discover and monitor devices on your network", + requiresDevice: false, + iconSource: "qrc:/resources/icons/StreamlineUltimateMultipleUsersNetwork.png", + visible: true + } + ]) + + readonly property var moreToolsModel: ([ + { + toolId: 9, + title: "Mount Dev Image", + description: "Mount a compatible device image with a single click", + requiresDevice: true, + iconSource: "qrc:/resources/icons/MdiDisk.png", + visible: true + }, + { + toolId: 10, + title: "Restart", + description: "Restart device services", + requiresDevice: true, + iconSource: "qrc:/resources/icons/IcTwotoneRestartAlt.png", + visible: true + }, + { + toolId: 11, + title: "Shutdown", + description: "Shut down the device", + requiresDevice: true, + iconSource: "qrc:/resources/icons/IcOutlinePowerSettingsNew.png", + visible: true + }, + { + toolId: 12, + title: "Recovery Mode", + description: "Enter device recovery mode", + requiresDevice: true, + iconSource: "qrc:/resources/icons/HugeiconsWrench01.png", + visible: true + }, + { + toolId: 13, + title: "Enable Wi-Fi Connections", + description: "Make device connectable via Wi-Fi", + requiresDevice: true, + iconSource: "qrc:/resources/icons/StreamlineFreehandChargingFlashWireless.png", + visible: true + } + ]) + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Device selection row + RowLayout { + Layout.fillWidth: true + Layout.margins: 12 + spacing: 10 + + Label { + text: "Device:" + Layout.alignment: Qt.AlignVCenter + } + + ComboBox { + id: deviceCombo + Layout.minimumWidth: 260 + Layout.preferredWidth: 320 + enabled: root.hasDevice + + model: root.hasDevice ? App.DeviceContext.devices : [{ text: "No device connected", udid: "" }] + textRole: "text" + valueRole: "udid" + + onActivated: (index) => { + const udid = deviceCombo.currentValue || "" + root.currentDeviceUdid = udid + root.deviceSelectionChanged(udid) + } + } + + Item { Layout.fillWidth: true } + } + + ScrollView { + id: scroll + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + width: scroll.availableWidth + spacing: 14 + Layout.margins: 0 + + // Section: Tools + Label { + text: "Tools" + font.bold: true + font.pixelSize: 14 + leftPadding: 10 + } + + GridLayout { + id: mainGrid + Layout.fillWidth: true + columns: 3 + columnSpacing: 10 + rowSpacing: 10 + + Repeater { + model: root.mainToolsModel + delegate: ToolTile { + Layout.fillWidth: true + visible: modelData.visible + + toolId: modelData.toolId + title: modelData.title + description: modelData.description + requiresDevice: modelData.requiresDevice + iconSource: modelData.iconSource + + // Disabled state (dull look) + enabled: !requiresDevice || root.hasDevice + + onClicked: { + // FIXME(connection-logic): implement actual routing / navigation. + root.toolClicked(toolId, requiresDevice) + } + } + } + } + + // Section: More Tools + Label { + text: "More Tools" + font.bold: true + font.pixelSize: 14 + leftPadding: 10 + topPadding: 6 + } + + GridLayout { + id: moreGrid + Layout.fillWidth: true + columns: 3 + columnSpacing: 10 + rowSpacing: 10 + + Repeater { + model: root.moreToolsModel + delegate: ToolTile { + Layout.fillWidth: true + visible: modelData.visible + + toolId: modelData.toolId + title: modelData.title + description: modelData.description + requiresDevice: modelData.requiresDevice + iconSource: modelData.iconSource + + enabled: !requiresDevice || root.hasDevice + + onClicked: { + // FIXME(connection-logic): implement actual routing / navigation. + root.toolClicked(toolId, requiresDevice) + } + } + } + } + + Item { Layout.fillHeight: true } + } + } + } + + component ToolTile: Rectangle { + id: tile + + property int toolId: -1 + property string title: "" + property string description: "" + property bool requiresDevice: false + property url iconSource: "" + + signal clicked() + + radius: 8 + color: "transparent" + + implicitHeight: 92 + + opacity: enabled ? 1.0 : 0.45 + + // Rectangle { + // // subtle hover overlay + // anchors.fill: parent + // radius: tile.radius + // color: mouse.containsMouse && tile.enabled ? "#ffffff" : "transparent" + // opacity: 0.05 + // } + + MouseArea { + id: mouse + anchors.fill: parent + hoverEnabled: true + enabled: tile.enabled + cursorShape: tile.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: tile.clicked() + } + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 12 + + IconImage { + id: icon + source: tile.iconSource + + Layout.preferredHeight: 34 + Layout.preferredWidth: 34 + + // FIXME:theming + color: "black" + opacity: tile.enabled ? 1.0 : 0.7 + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Label { + text: tile.title + font.bold: true + elide: Text.ElideRight + Layout.fillWidth: true + } + + Label { + text: tile.description + wrapMode: Text.WordWrap + elide: Text.ElideRight + maximumLineCount: 2 + Layout.fillWidth: true + opacity: 0.85 + } + } + } + } +} \ No newline at end of file diff --git a/src/qml/Welcome.qml b/src/ui/Welcome.qml similarity index 100% rename from src/qml/Welcome.qml rename to src/ui/Welcome.qml diff --git a/src/ui/qmldir b/src/ui/qmldir new file mode 100644 index 0000000..4f755d5 --- /dev/null +++ b/src/ui/qmldir @@ -0,0 +1 @@ +singleton DeviceContext 1.0 DeviceContext.qml \ No newline at end of file diff --git a/src/qml/wIndows/Index.qml b/src/ui/wIndows/Index.qml similarity index 100% rename from src/qml/wIndows/Index.qml rename to src/ui/wIndows/Index.qml diff --git a/src/qml/wIndows/Main.qml b/src/ui/wIndows/Main.qml similarity index 100% rename from src/qml/wIndows/Main.qml rename to src/ui/wIndows/Main.qml diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..064a3aa --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,612 @@ +use crate::POSSIBLE_ROOT; +use crate::run_sync; +use cpp::*; +use idevice::{ + IdeviceError, IdeviceService, + afc::{AfcClient, opcode::AfcFopenMode}, + diagnostics_relay::DiagnosticsRelayClient, + house_arrest::HouseArrestClient, + installation_proxy::InstallationProxyClient, + provider::IdeviceProvider, +}; +use plist::Dictionary as PlistDictionary; +use plist_macro::plist; +use qmetaobject::prelude::*; +use qmetaobject::*; +use rusqlite::Connection; +use serde_json::json; +use std::ffi::c_void; +use std::io::SeekFrom; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncSeekExt}; +use tokio::sync::Mutex; + +cpp! {{ + struct TraitObject2 { void *data; void *vtable; }; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include "src/include/bridge.h" + + QCoreApplication *globalApp = nullptr; +}} + +pub const PUBLIC_STAGING: &str = "PublicStaging"; + +pub async fn get_battery_info(diag: &mut DiagnosticsRelayClient) -> Option { + match diag.ioregistry(None, None, Some("IOPMPowerSource")).await { + Ok(Some(dict)) => Some(dict), + _ => None, + } +} + +pub async fn get_cable_info(diag: &mut DiagnosticsRelayClient) -> Option { + match diag + .ioregistry(None, None, Some("AppleTriStarBuiltIn")) + .await + { + Ok(Some(dict)) => Some(dict), + _ => None, + } +} + +pub async fn detect_jailbroken(afc: &mut AfcClient) -> bool { + match afc.list_dir(format!("{}/bin", POSSIBLE_ROOT)).await { + Ok(vec) => vec.len() > 0, + Err(_) => false, + } +} + +pub fn qstring_to_f64(qstring: QString) -> Result { + let rust_string: String = qstring.into(); + rust_string.parse::() +} + +pub async fn calculate_apps_usage( + instproxy: &mut InstallationProxyClient, +) -> Result> { + let options = plist!({ + "ApplicationType": "User", + "ReturnAttributes": [ + "StaticDiskUsage", + "DynamicDiskUsage" + ] + }); + let apps = instproxy.browse(Some(options)).await?; + let mut total_apps_space = 0u64; + + for app_info in apps { + if let Some(app_dict) = app_info.as_dictionary() { + if let Some(static_usage) = app_dict + .get("StaticDiskUsage") + .and_then(|v| v.as_unsigned_integer()) + { + total_apps_space += static_usage; + } + + if let Some(dynamic_usage) = app_dict + .get("DynamicDiskUsage") + .and_then(|v| v.as_unsigned_integer()) + { + total_apps_space += dynamic_usage; + } + } + } + + Ok(total_apps_space) +} + +pub async fn vend_app_documents( + provider: &dyn IdeviceProvider, + bundle_id: &str, +) -> Result { + let house_arrest_client = HouseArrestClient::connect(provider).await?; + let afc_client = house_arrest_client.vend_documents(bundle_id).await?; + Ok(afc_client) +} + +pub fn query_gallery_usage(db_bytes: &mut Vec) -> Result { + // HACK: WAL -> legacy mode patch + if db_bytes.len() > 20 && db_bytes[18] == 0x02 { + db_bytes[18] = 0x01; + db_bytes[19] = 0x01; + } + println!("Querying gallery usage for disk usage calculation..."); + + // Open in-memory DB + let conn = Connection::open_in_memory()?; + + unsafe { + let db_ptr = rusqlite::ffi::sqlite3_deserialize( + conn.handle(), + b"main\0".as_ptr() as *const std::os::raw::c_char, + db_bytes.as_mut_ptr(), + db_bytes.len() as i64, + db_bytes.len() as i64, + rusqlite::ffi::SQLITE_DESERIALIZE_READONLY as u32, + ); + if db_ptr != rusqlite::ffi::SQLITE_OK { + return Err(rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(db_ptr), + None, + )); + } + } + + let size: i64 = conn.query_row( + "SELECT COALESCE(SUM(ZORIGINALFILESIZE), 0) FROM ZADDITIONALASSETATTRIBUTES", + [], + |row| row.get(0), + )?; + + println!("Gallery usage calculated: {} bytes", size); + Ok(size as u64) +} + +pub fn get_lockdown_path() -> PathBuf { + if let Ok(val) = std::env::var("USBMUXD_PAIRING_FILES_LOCATION") { + if !val.is_empty() { + eprintln!("Pulling pairing files from USBMUXD_PAIRING_FILES_LOCATION: {val}"); + return PathBuf::from(val); + } + } + + #[cfg(target_os = "linux")] + { + PathBuf::from("/var/lib/lockdown") + } + + #[cfg(target_os = "macos")] + { + PathBuf::from("/var/db/lockdown") + } + + #[cfg(target_os = "windows")] + { + let base = std::env::var_os("PROGRAMDATA") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(r"C:\ProgramData")); + base.join("Apple").join("Lockdown") + } +} + +/// Ensure `PublicStaging` exists on device via AFC +pub async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { + // Try to stat and if it fails, create directory + match afc.get_file_info(PUBLIC_STAGING).await { + Ok(_) => Ok(()), + Err(_) => afc.mk_dir(PUBLIC_STAGING).await, + } +} + +// converts album info to json +pub fn create_album_info( + album_id: i32, + item_count: i32, + asset_dir: String, + asset_file_name: String, +) -> String { + json!({"album_id" : album_id, "item_count" : item_count,"file_path" : format!("{}/{}",asset_dir,asset_file_name)}) + .to_string() +} + +use qttypes::QVariantMap; + +pub fn qvariantmap_insert(map: &mut QVariantMap, key: &str, value: &T) +where + QVariant: for<'a> From<&'a T>, +{ + map.insert(QString::from(key), QVariant::from(value)); +} + +#[macro_export] +macro_rules! qvariantmap_insert { + ($map:expr, $key:expr , $value:expr) => { + $crate::utils::qvariantmap_insert(&mut $map, $key, &$value) + }; +} + +pub fn is_video_file(path: &str) -> bool { + let ext = path + .rsplit_once('.') + .map(|(_, e)| e.to_ascii_lowercase()) + .unwrap_or_default(); + + matches!( + ext.as_str(), + "mp4" + | "mov" + | "m4v" + | "avi" + | "mkv" + | "webm" + | "flv" + | "wmv" + | "3gp" + | "mpeg" + | "mpg" + | "ts" + | "mts" + | "m2ts" + ) +} + +pub fn serde_json_to_qt_array(v: &serde_json::Value) -> QJsonArray { + let mut ret = QJsonArray::default(); + if let Some(arr) = v.as_array() { + for param in arr { + match param { + serde_json::Value::Number(v) => { + ret.push(QJsonValue::from(v.as_f64().unwrap())); + } + serde_json::Value::Bool(v) => { + ret.push(QJsonValue::from(*v)); + } + serde_json::Value::String(v) => { + ret.push(QJsonValue::from(QString::from(v.clone()))); + } + serde_json::Value::Array(v) => { + ret.push(QJsonValue::from(serde_json_to_qt_array( + &serde_json::Value::Array(v.to_vec()), + ))); + } + serde_json::Value::Object(_) => { + ret.push(QJsonValue::from(serde_json_to_qt_object(param))); + } + serde_json::Value::Null => { /* ::log::warn!("null unimplemented");*/ } + }; + } + } + ret +} +pub fn serde_json_to_qt_object(v: &serde_json::Value) -> QJsonObject { + let mut map = QJsonObject::default(); + if let Some(obj) = v.as_object() { + for (k, v) in obj { + match v { + serde_json::Value::Number(v) => { + map.insert(k, QJsonValue::from(v.as_f64().unwrap())); + } + serde_json::Value::Bool(v) => { + map.insert(k, QJsonValue::from(*v)); + } + serde_json::Value::String(v) => { + map.insert(k, QJsonValue::from(QString::from(v.clone()))); + } + serde_json::Value::Array(v) => { + map.insert( + k, + QJsonValue::from(serde_json_to_qt_array(&serde_json::Value::Array( + v.to_vec(), + ))), + ); + } + serde_json::Value::Object(_) => { + map.insert(k, QJsonValue::from(serde_json_to_qt_object(v))); + } + serde_json::Value::Null => { /* ::log::warn!("null unimplemented");*/ } + }; + } + } + map +} + +pub fn create_image_from_buffer(buf: &[u8]) -> QImage { + if buf.is_empty() { + return QImage::default(); + } + + let buf_ptr = buf.as_ptr(); + let buf_len: i32 = buf.len().try_into().unwrap_or(i32::MAX); + + cpp!(unsafe [buf_ptr as "const uchar*", buf_len as "int"] -> QImage as "QImage" { + return QImage::fromData(buf_ptr, buf_len, nullptr); + }) +} + +pub fn qt_queued_callback( + qptr: QPointer, + mut cb: F, +) -> impl Fn(T2) + Send + Sync + Clone + 'static { + qmetaobject::queued_callback(move |arg| { + if let Some(this) = qptr.as_pinned() { + let this = this.borrow(); + cb(this, arg); + } + }) +} +pub fn qt_queued_callback_mut< + T: QObject + 'static, + T2: Send + 'static, + F: FnMut(&mut T, T2) + 'static, +>( + qptr: QPointer, + mut cb: F, +) -> impl Fn(T2) + Send + Sync + Clone + 'static { + qmetaobject::queued_callback(move |arg| { + if let Some(this) = qptr.as_pinned() { + let mut this = this.borrow_mut(); + cb(&mut this, arg); + } + }) +} + +// pub fn catch_qt_file_open(cb: F) { +// let func: Box = Box::new(cb); +// let cb_ptr = Box::into_raw(func); +// cpp!(unsafe [cb_ptr as "TraitObject2"] { +// qGuiApp->installEventFilter(new QtEventFilter([cb_ptr](QUrl url) { +// rust!(Rust_catch_qt_file_open [cb_ptr: *mut dyn FnMut(QUrl) as "TraitObject2", url: QUrl as "QUrl"] { +// let mut cb = unsafe { Box::from_raw(cb_ptr) }; +// cb(url.clone()); +// let _ = Box::into_raw(cb); // leak again so it doesn't get deleted here +// }); +// })); +// }); +// } + +// pub fn get_data_location() -> String { +// cpp!(unsafe [] -> QString as "QString" { +// return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); +// }) +// .into() +// } + +// pub fn install_crash_handler() -> std::io::Result<()> { +// let cur_dir = std::env::current_dir()?; + +// #[cfg(not(any(target_os = "android", target_os = "ios")))] +// { +// let os_str = cur_dir.as_os_str(); +// let path: Vec = { +// #[cfg(windows)] +// { +// use std::os::windows::ffi::OsStrExt; +// os_str.encode_wide().collect() +// } +// #[cfg(unix)] +// { +// use std::os::unix::ffi::OsStrExt; +// Vec::from(os_str.as_bytes()) +// } +// }; + +// unsafe { +// extern "C" fn callback( +// path: *const breakpad_sys::PathChar, +// path_len: usize, +// _ctx: *mut c_void, +// ) { +// let path_slice = unsafe { std::slice::from_raw_parts(path, path_len) }; + +// let path = { +// #[cfg(windows)] +// { +// use std::os::windows::ffi::OsStringExt; +// std::path::PathBuf::from(std::ffi::OsString::from_wide(path_slice)) +// } +// #[cfg(unix)] +// { +// use std::os::unix::ffi::OsStrExt; +// std::path::PathBuf::from(std::ffi::OsStr::from_bytes(path_slice).to_owned()) +// } +// }; + +// println!("Crashdump written to {}", path.display()); +// } + +// breakpad_sys::attach_exception_handler( +// path.as_ptr(), +// path.len(), +// callback, +// std::ptr::null_mut(), +// breakpad_sys::INSTALL_BOTH_HANDLERS, +// ); +// } +// } + +// // Upload crash dumps +// crate::core::run_threaded(move || { +// if let Ok(files) = std::fs::read_dir(cur_dir) { +// for path in files.flatten() { +// let path = path.path(); +// if path.to_string_lossy().ends_with(".dmp") { +// if let Ok(content) = std::fs::read(&path) { +// if let Ok(Ok(body)) = ureq::post("https://api.gyroflow.xyz/upload_dump") +// .header("Content-Type", "application/octet-stream") +// .send(&content) +// .map(|x| x.into_body().read_to_string()) +// { +// ::log::debug!("Minidump uploaded: {}", body.as_str()); +// let _ = std::fs::remove_file(path); +// } +// } +// } +// } +// } +// }); +// Ok(()) +// } + +pub fn tr(context: &str, text: &str) -> String { + let context = QString::from(context); + let text = QString::from(text); + cpp!(unsafe [context as "QString", text as "QString"] -> QString as "QString" { + return QCoreApplication::translate(qUtf8Printable(context), qUtf8Printable(text)); + }) + .to_string() +} + +pub fn get_version() -> String { + let ver = env!("CARGO_PKG_VERSION"); + if option_env!("GITHUB_REF").map_or(false, |x| x.contains("tags")) { + ver.to_string() // Official, tagged version + } else if let Some(gh_run) = option_env!("GITHUB_RUN_NUMBER") { + format!("{} (gh{})", ver, gh_run) + } else if let Some(time) = option_env!("BUILD_TIME") { + format!("{} (dev{})", ver, time) + } else { + ver.to_string() + } +} + +pub fn copy_to_clipboard(text: QString) { + cpp!(unsafe [text as "QString"] { QGuiApplication::clipboard()->setText(text); }) +} + +pub fn image_data_to_base64(w: u32, h: u32, s: u32, data: &[u8]) -> QString { + let ptr = data.as_ptr(); + cpp!(unsafe [w as "uint32_t", h as "uint32_t", s as "uint32_t", ptr as "const uint8_t *"] -> QString as "QString" { + QImage img(ptr, w, h, s, QImage::Format_RGBA8888_Premultiplied); + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + img.save(&buffer, "JPEG", 50); + QString b64("data:image/jpg;base64,"); + b64.append(QString::fromLatin1(byteArray.toBase64().data())); + return b64; + }) +} + +pub fn image_to_b64(img: QImage) -> QString { + cpp!(unsafe [img as "QImage"] -> QString as "QString" { + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + img.save(&buffer, "JPEG", 50); + QString b64("data:image/jpg;base64,"); + b64.append(QString::fromLatin1(byteArray.toBase64().data())); + return b64; + }) +} + +pub struct AfcReader { + udid: String, + path: String, + afc_arc: Arc>, +} + +impl AfcReader { + pub fn new(udid: String, path: String, afc_arc: Arc>) -> Self { + Self { + udid, + path, + afc_arc, + } + } + + pub async fn get_size(&self) -> i32 { + let mut afc = self.afc_arc.lock().await; + + match afc.get_file_info(self.path.clone()).await { + Ok(info) => info.size as i32, + Err(_) => 0, + } + } + + pub fn read_at(&self, offset: i64, size: i32) -> Vec { + if size <= 0 || offset < 0 { + return Vec::new(); + } + + let udid = self.udid.clone(); + let path = self.path.clone(); + let afc_arc = self.afc_arc.clone(); + // FIXME: is run_sync safe in this context? + run_sync(async move { + let mut afc = afc_arc.lock().await; + + let mut fd = match afc.open(path.clone(), AfcFopenMode::RdOnly).await { + Ok(f) => f, + Err(e) => { + eprintln!("read_at: open({}) failed: {}", path, e); + return Vec::new(); + } + }; + + if offset > 0 { + if let Err(e) = fd.seek(SeekFrom::Start(offset as u64)).await { + eprintln!("read_at: seek({}, {}) failed: {}", path, offset, e); + let _ = fd.close().await; + return Vec::new(); + } + } + + let mut buf = vec![0u8; size as usize]; + let n = match fd.read(&mut buf).await { + Ok(n) => n, + Err(e) => { + eprintln!("read_at: read({}, {}) failed: {}", path, offset, e); + let _ = fd.close().await; + return Vec::new(); + } + }; + buf.truncate(n); + let _ = fd.close().await; + buf + }) + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn afc_reader_read_at( + reader_ptr: *const c_void, + offset: i64, + size: i32, + out_buf: *mut u8, + out_len: *mut i32, +) { + let reader = unsafe { &*(reader_ptr as *const AfcReader) }; + let data = reader.read_at(offset, size); + let n = data.len().min(size as usize); + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr(), out_buf, n); + *out_len = n as i32; + } +} + +pub fn generate_thumbnail( + reader: &AfcReader, + file_size: i32, + requested_w: i32, + requested_h: i32, +) -> QImage { + let reader_ptr = reader as *const AfcReader as *const c_void; + + cpp!(unsafe [ + reader_ptr as "const void*", + file_size as "int32_t", + requested_w as "int32_t", + requested_h as "int32_t" + ] -> QImage as "QImage" { + return generate_thumbnail_with_reader_ffi( + reader_ptr, file_size, requested_w, requested_h + ); + }) +} + +pub fn empty_qjsvalue() -> QJSValue { + cpp!(unsafe [] -> QJSValue as "QJSValue" { return QJSValue(); }) +} + +pub fn engine_ptr_new_object(engine_ptr: *mut c_void, obj_ptr: *mut c_void) -> QJSValue { + cpp!(unsafe [ + engine_ptr as "QQmlEngine *", + obj_ptr as "QObject *" + ] -> QJSValue as "QJSValue" { + return engine_ptr->newQObject(obj_ptr); + }) +} +//TODO: implement +// pub fn heic_to_qimage() diff --git a/src/virtuallocationwidget.cpp b/src/virtuallocationwidget.cpp deleted file mode 100644 index e7ae667..0000000 --- a/src/virtuallocationwidget.cpp +++ /dev/null @@ -1,482 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "virtuallocationwidget.h" - -// FIXME: on macOS setupToolFrame in Tool widget does nothing -// probably because we are using a QQuickWidget -VirtualLocation::VirtualLocation( - const std::shared_ptr device, QWidget *parent) - : QWidget(parent), m_device(device) -{ - setWindowTitle("Virtual Location - iDescriptor"); - setMinimumSize(600, 400); - resize(800, 600); - -#ifdef WIN32 - setObjectName("VirtualLocationWidget"); - setAttribute(Qt::WA_StyledBackground, true); -#endif - - // Create the main layout - QHBoxLayout *mainLayout = new QHBoxLayout(this); - mainLayout->setContentsMargins(10, 10, 10, 10); - mainLayout->setSpacing(10); - - // Create left panel for controls - QWidget *rightPanel = new QWidget(); - rightPanel->setFixedWidth(250); - - m_rightLayout = new QVBoxLayout(rightPanel); - m_rightLayout->setContentsMargins(15, 15, 15, 15); - m_rightLayout->setSpacing(10); - - // Title - QLabel *titleLabel = new QLabel("Virtual Location Settings"); - titleLabel->setStyleSheet("margin-bottom: 10px;"); - m_rightLayout->addWidget(titleLabel); - - QGroupBox *coordGroup = new QGroupBox("Coordinates"); - m_rightLayout->addWidget(coordGroup); - - QVBoxLayout *coordLayout = new QVBoxLayout(coordGroup); - - // Latitude input - QLabel *latLabel = new QLabel("Latitude:"); - coordLayout->addWidget(latLabel); - - m_latitudeEdit = new QLineEdit(); - m_latitudeEdit->setPlaceholderText("e.g., 59.9139"); - m_latitudeEdit->setText("59.9139"); - m_latitudeEdit->setValidator(new QDoubleValidator(-90.0, 90.0, 6, this)); - coordLayout->addWidget(m_latitudeEdit); - - // Longitude input - QLabel *lonLabel = new QLabel("Longitude:"); - coordLayout->addWidget(lonLabel); - - m_longitudeEdit = new QLineEdit(); - m_longitudeEdit->setPlaceholderText("e.g., 10.7522"); - m_longitudeEdit->setText("10.7522"); - m_longitudeEdit->setValidator(new QDoubleValidator(-180.0, 180.0, 6, this)); - coordLayout->addWidget(m_longitudeEdit); - - // Add some spacing - m_rightLayout->addItem( - new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Fixed)); - - // Apply button - m_applyButton = new QPushButton("Apply Location"); - m_applyButton->setDefault(true); - m_rightLayout->addWidget(m_applyButton); - - // Recent locations section - loadRecentLocations(m_rightLayout); - - // Add stretch to push everything to the top - m_rightLayout->addStretch(); - - // Connect to recent locations changes - connect(SettingsManager::sharedInstance(), - &SettingsManager::recentLocationsChanged, this, - &VirtualLocation::refreshRecentLocations); - - // Create map widget - m_quickWidget = new QQuickWidget(this); - m_quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); - m_quickWidget->setSource(QUrl(QStringLiteral("qrc:/qml/MapView.qml"))); - - // Enable input handling - m_quickWidget->setFocusPolicy(Qt::StrongFocus); - m_quickWidget->setAttribute(Qt::WA_AcceptTouchEvents, true); - - // Add widgets to main layout - mainLayout->addWidget(m_quickWidget, - 1); // Give map widget stretch factor of 1 - mainLayout->addWidget(rightPanel); - - setLayout(mainLayout); - - // Connect signals - connect(m_latitudeEdit, &QLineEdit::textChanged, this, - &VirtualLocation::onInputChanged); - connect(m_longitudeEdit, &QLineEdit::textChanged, this, - &VirtualLocation::onInputChanged); - connect(m_applyButton, &QPushButton::clicked, this, - &VirtualLocation::onApplyClicked); - - // Connect to QML map - connect(m_quickWidget, &QQuickWidget::statusChanged, this, - &VirtualLocation::onQuickWidgetStatusChanged); - - // Register this object with QML context so QML can call our slots - m_quickWidget->rootContext()->setContextProperty("cppHandler", this); - - connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, - [this](const QString &udid) { - if (m_device->udid == udid) { - this->close(); - this->deleteLater(); - } - }); -} - -void VirtualLocation::onQuickWidgetStatusChanged(QQuickWidget::Status status) -{ - if (status == QQuickWidget::Ready) { - qDebug() << "QuickWidget is ready"; - - // Set initial map position - updateMapFromInputs(); - } else if (status == QQuickWidget::Error) { - qDebug() << "QuickWidget errors:" << m_quickWidget->errors(); - } -} - -void VirtualLocation::onInputChanged() -{ - // Update map when input changes - m_updateTimer.setSingleShot(true); - m_updateTimer.setInterval(500); // 500ms delay - - disconnect(&m_updateTimer, &QTimer::timeout, this, - &VirtualLocation::updateMapFromInputs); - connect(&m_updateTimer, &QTimer::timeout, this, - &VirtualLocation::updateMapFromInputs); - - m_updateTimer.start(); -} - -void VirtualLocation::updateMapFromInputs() -{ - bool latOk, lonOk; - double latitude = m_latitudeEdit->text().toDouble(&latOk); - double longitude = m_longitudeEdit->text().toDouble(&lonOk); - - // FIXME: warn if not valid - if (latOk && lonOk && latitude >= -90 && latitude <= 90 && - longitude >= -180 && longitude <= 180) { - QQuickItem *rootObject = m_quickWidget->rootObject(); - if (rootObject) { - QQuickItem *mapItem = rootObject->findChild("map"); - if (mapItem) { - // Block signals to prevent feedback loop - m_updatingFromInput = true; - - // Call QML function to update map center - QMetaObject::invokeMethod(mapItem, "updateCenter", - Q_ARG(QVariant, latitude), - Q_ARG(QVariant, longitude)); - - m_updatingFromInput = false; - - qDebug() << "Updated map center to:" << latitude << "," - << longitude; - } - } - } -} - -void VirtualLocation::onMapCenterChanged() -{ - if (m_updatingFromInput) { - return; // Prevent feedback loop - } - - qDebug() << "onMapCenterChanged called!"; - - QQuickItem *rootObject = m_quickWidget->rootObject(); - if (rootObject) { - QQuickItem *mapItem = rootObject->findChild("map"); - if (mapItem) { - // Get map center using QMetaObject::invokeMethod for more reliable - // access - QVariant centerVar = mapItem->property("center"); - - if (centerVar.isValid()) { - // Try to get the coordinate directly - QGeoCoordinate coord = centerVar.value(); - - if (coord.isValid()) { - double latitude = coord.latitude(); - double longitude = coord.longitude(); - - // Block signals temporarily to prevent feedback - m_latitudeEdit->blockSignals(true); - m_longitudeEdit->blockSignals(true); - - // Update input fields - m_latitudeEdit->setText(QString::number(latitude, 'f', 6)); - m_longitudeEdit->setText( - QString::number(longitude, 'f', 6)); - - // Restore signals - m_latitudeEdit->blockSignals(false); - m_longitudeEdit->blockSignals(false); - - qDebug() << "Updated inputs from map:" << latitude << "," - << longitude; - } else { - qDebug() << "Invalid coordinate from map"; - } - } else { - qDebug() << "Could not get center property from map"; - } - } - } -} - -// called from QML -void VirtualLocation::updateInputsFromMap(const QString &latitude, - const QString &longitude) -{ - if (m_updatingFromInput) { - return; // Prevent feedback loop - } - - qDebug() << "updateInputsFromMap called with:" << latitude << "," - << longitude; - - // Block signals temporarily to prevent feedback - m_latitudeEdit->blockSignals(true); - m_longitudeEdit->blockSignals(true); - - // Update input fields - m_latitudeEdit->setText(latitude); - m_longitudeEdit->setText(longitude); - - // Restore signals - m_latitudeEdit->blockSignals(false); - m_longitudeEdit->blockSignals(false); - - qDebug() << "Updated inputs from map:" << latitude << "," << longitude; -} - -void VirtualLocation::restoreButtons() -{ - QTimer::singleShot(300, this, [this]() { - m_applyButton->setText("Apply Location"); - m_applyButton->setEnabled(true); - }); -} - -void VirtualLocation::onApplyClicked() -{ - m_applyButton->setEnabled(false); - QString latitude = m_latitudeEdit->text(); - QString longitude = m_longitudeEdit->text(); - - if (longitude.isEmpty() || latitude.isEmpty()) { - QMessageBox::warning( - this, "Invalid Input", - "Please enter valid latitude and longitude values."); - m_applyButton->setEnabled(true); - return; - } - - int major = m_device->deviceInfo.parsedDeviceVersion.major; - - qDebug() << "Setting location to:" << latitude << "," << longitude; - - if (major < 17) { - DevDiskImageHelper *devDiskImageHelper = - new DevDiskImageHelper(m_device, this); - connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted, - this, - [this, latitude, longitude, devDiskImageHelper](bool success) { - if (!success) { - // mounter will show its own error message - return; - } - - u_int32_t set_location_success = - m_device->service_manager->set_location(latitude, - longitude); - - if (set_location_success != 0) { - qDebug() << "Failed to set location simulation"; - QMessageBox::warning( - this, "Error", - QString("Failed to set location simulation")); - - } else { - - QMessageBox::information( - this, "Success", "Location applied successfully!"); - - SettingsManager::sharedInstance()->saveRecentLocation( - latitude, longitude); - } - }); - connect(devDiskImageHelper, &DevDiskImageHelper::destroyed, this, - &VirtualLocation::restoreButtons, Qt::SingleShotConnection); - return devDiskImageHelper->start(); - } - - /* iOS 17 and above */ - int32_t set_location_res = - m_device->service_manager->set_location(latitude, longitude); - - switch (set_location_res) { - case 0: - QMessageBox::information(this, "Success", - "Location applied successfully"); - break; - case ServiceNotFoundErrorCode: - DevModeWidget(m_device, this).exec(); - break; - case TimeoutErrorCode: - qDebug() << "Failed to set location simulation: timed out"; - QMessageBox::warning(this, "Error", - "Failed to set location simulation: timed out"); - break; - default: - qDebug() << "Failed to set location simulation: device not found"; - QMessageBox::warning(this, "Error", - "Failed to set location simulation: Error code: " + - QString::number(set_location_res)); - } - - restoreButtons(); -} - -void VirtualLocation::loadRecentLocations(QVBoxLayout *layout) -{ - QList recentLocations = - SettingsManager::sharedInstance()->getRecentLocations(); - - if (recentLocations.isEmpty()) { - return; // Don't render anything if no recent locations - } - - layout->addItem( - new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Fixed)); - - m_recentGroup = new QGroupBox("Recent Locations"); - layout->addWidget(m_recentGroup); - - // A group box needs a layout to contain its children - QVBoxLayout *groupBoxLayout = new QVBoxLayout(m_recentGroup); - - QScrollArea *scrollArea = new QScrollArea(); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - groupBoxLayout->addWidget(scrollArea); - - QWidget *scrollContent = new QWidget(); - scrollArea->setWidget(scrollContent); - - // This layout is for the content widget - QVBoxLayout *recentLayout = new QVBoxLayout(scrollContent); - - addLocationButtons(recentLayout, recentLocations); -} - -void VirtualLocation::onRecentLocationClicked(const QString &latitude, - const QString &longitude) -{ - // Update input fields - m_latitudeEdit->setText(latitude); - m_longitudeEdit->setText(longitude); - // Update map - updateMapFromInputs(); - - qDebug() << "Recent location clicked:" << latitude << "," << longitude; -} - -void VirtualLocation::refreshRecentLocations() -{ - if (!m_recentGroup) { - return; - } - - // Get the group box's layout - QVBoxLayout *groupBoxLayout = - qobject_cast(m_recentGroup->layout()); - if (!groupBoxLayout) { - return; - } - - // Get the scroll area from the group box layout - QScrollArea *scrollArea = nullptr; - if (groupBoxLayout->count() > 0) { - scrollArea = - qobject_cast(groupBoxLayout->itemAt(0)->widget()); - } - - if (!scrollArea) { - return; - } - - // Get the scroll content widget - QWidget *scrollContent = scrollArea->widget(); - if (!scrollContent) { - return; - } - - // Get the content layout - QVBoxLayout *recentLayout = - qobject_cast(scrollContent->layout()); - if (!recentLayout) { - return; - } - - // Clear all existing buttons - QLayoutItem *item; - while ((item = recentLayout->takeAt(0)) != nullptr) { - if (item->widget()) { - item->widget()->deleteLater(); - } - delete item; - } - - // Reload recent locations - QList recentLocations = - SettingsManager::sharedInstance()->getRecentLocations(); - - if (recentLocations.isEmpty()) { - // Hide the group if no locations - m_recentGroup->hide(); - return; - } - - // Show the group if it was hidden - m_recentGroup->show(); - - addLocationButtons(recentLayout, recentLocations); -} - -void VirtualLocation::addLocationButtons(QLayout *layout, - QList recentLocations) -{ - for (const QVariantMap &location : recentLocations) { - QString latitude = location["latitude"].toString(); - QString longitude = location["longitude"].toString(); - - QPushButton *locationBtn = new QPushButton( - QString("Lat: %1\nLon: %2").arg(latitude).arg(longitude)); - - connect(locationBtn, &QPushButton::clicked, this, - [this, latitude, longitude]() { - onRecentLocationClicked(latitude, longitude); - }); - - layout->addWidget(locationBtn); - } -} diff --git a/src/virtuallocationwidget.h b/src/virtuallocationwidget.h deleted file mode 100644 index d4aa36f..0000000 --- a/src/virtuallocationwidget.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef VIRTUAL_LOCATION_H -#define VIRTUAL_LOCATION_H - -#include "appcontext.h" -#include "devdiskimagehelper.h" -#include "devdiskmanager.h" -#include "devmodewidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "settingsmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class VirtualLocation : public QWidget -{ - Q_OBJECT - -public: - explicit VirtualLocation(const std::shared_ptr device, - QWidget *parent = nullptr); - -public slots: - void updateInputsFromMap(const QString &latitude, const QString &longitude); - -private slots: - void onQuickWidgetStatusChanged(QQuickWidget::Status status); - void onInputChanged(); - void onMapCenterChanged(); - void onApplyClicked(); - void updateMapFromInputs(); - void onRecentLocationClicked(const QString &latitude, - const QString &longitude); - -private: - void loadRecentLocations(QVBoxLayout *layout); - void refreshRecentLocations(); - void addLocationButtons(QLayout *layout, - QList recentLocations); - - void restoreButtons(); - - QQuickWidget *m_quickWidget; - QLineEdit *m_latitudeEdit; - QLineEdit *m_longitudeEdit; - QPushButton *m_applyButton; - QTimer m_updateTimer; - bool m_updatingFromInput = false; - const std::shared_ptr m_device; - QVBoxLayout *m_rightLayout = nullptr; - QGroupBox *m_recentGroup = nullptr; -}; - -#endif // VIRTUAL_LOCATION_H \ No newline at end of file diff --git a/src/welcomewidget.cpp b/src/welcomewidget.cpp deleted file mode 100644 index 078a14e..0000000 --- a/src/welcomewidget.cpp +++ /dev/null @@ -1,161 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "welcomewidget.h" -#include "diagnosewidget.h" -#include "iDescriptor-ui.h" -#include "iDescriptor.h" -#include "networkdevicestoconnectwidget.h" -#include "responsiveqlabel.h" -#include -#include -#include -#include -#include -#include -#include - -WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) { setupUI(); } - -void WelcomeWidget::setupUI() -{ - // Main layout with proper spacing and margins - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(0, 10, 0, 0); - m_mainLayout->setSpacing(0); - - // Add top stretch - m_mainLayout->addStretch(1); - - // Welcome title - m_titleLabel = createStyledLabel("Welcome to iDescriptor", 28, true); - m_titleLabel->setAlignment(Qt::AlignCenter); - m_mainLayout->addWidget(m_titleLabel); - - // Subtitle - m_subtitleLabel = createStyledLabel("Open-Source & Free", 10, false); - m_subtitleLabel->setAlignment(Qt::AlignCenter); - QPalette palette = m_subtitleLabel->palette(); - m_mainLayout->addWidget(m_subtitleLabel); - - QHBoxLayout *imageAndWirelessDevicesLayout = new QHBoxLayout(); - - m_imageLabel = new QLabel(); - - const QPixmap pixmap(":/resources/connect.png"); - m_imageLabel->setPixmap(pixmap); - m_imageLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - m_imageLabel->setFixedSize(pixmap.size()); - m_imageLabel->setStyleSheet("background: transparent; border: none;"); - m_imageLabel->setAlignment(Qt::AlignCenter); - - imageAndWirelessDevicesLayout->addSpacing(75); - imageAndWirelessDevicesLayout->addWidget(m_imageLabel); - imageAndWirelessDevicesLayout->addSpacing(75); - - QVBoxLayout *explorerWithIntructionLayout = new QVBoxLayout(); - NetworkDevicesToConnectWidget *networkDevicesWidget = - new NetworkDevicesToConnectWidget(); - m_howToConnectLabel = createStyledLabel("How to connect a wireless device?", - 12, true, COLOR_HYPERLINK); - m_howToConnectLabel->setWordWrap(false); - QPalette howToConnectLabelPalette = m_howToConnectLabel->palette(); - howToConnectLabelPalette.setColor(QPalette::WindowText, COLOR_HYPERLINK); - m_howToConnectLabel->setPalette(howToConnectLabelPalette); - m_howToConnectLabel->setCursor(Qt::PointingHandCursor); - - connect(m_howToConnectLabel, &ZLabel::clicked, this, - &WelcomeWidget::showHowToConnectDialog); - explorerWithIntructionLayout->addWidget(networkDevicesWidget); - explorerWithIntructionLayout->addWidget(m_howToConnectLabel, 0, - Qt::AlignCenter); - explorerWithIntructionLayout->addSpacing(20); - imageAndWirelessDevicesLayout->addLayout(explorerWithIntructionLayout); - - m_mainLayout->addLayout(imageAndWirelessDevicesLayout); - m_mainLayout->addSpacing(10); - - m_instructionLabel = - createStyledLabel("Connect an iDevice to get started", 14, false); - m_instructionLabel->setAlignment(Qt::AlignCenter); - m_mainLayout->addWidget(m_instructionLabel); - m_mainLayout->addSpacing(10); - - // GitHub link - m_githubLabel = createStyledLabel("Found an issue? Report it on GitHub", 12, - true, COLOR_HYPERLINK); - m_githubLabel->setWordWrap(false); - m_githubLabel->setCursor(Qt::PointingHandCursor); - connect(m_githubLabel, &ZLabel::clicked, this, - []() { QDesktopServices::openUrl(QUrl(REPO_URL)); }); - - QPalette githubPalette = m_githubLabel->palette(); - githubPalette.setColor(QPalette::WindowText, COLOR_HYPERLINK); - m_githubLabel->setPalette(githubPalette); - - m_mainLayout->addWidget(m_githubLabel, 0, Qt::AlignCenter); - - // no additional deps needed on macOS -#ifndef __APPLE__ - DiagnoseWidget *diagnoseWidget = new DiagnoseWidget(); - m_mainLayout->addWidget(diagnoseWidget); -#endif - - m_mainLayout->addStretch(1); -} - -// FIXME: color param is only respected in Windows build -ZLabel *WelcomeWidget::createStyledLabel(const QString &text, int fontSize, - bool isBold, QColor color) -{ - ZLabel *label = new ZLabel(text); - -#ifndef WIN32 - QFont font = label->font(); - if (fontSize > 0) { - font.setPointSize(fontSize); - } - if (isBold) { - font.setWeight(QFont::Medium); - } - - label->setFont(font); - label->setWordWrap(true); -#else - label->setStyleSheet(mergeStyles( - label, - QString("QLabel {" - " font-size: %1px;" - " font-weight: %2;" - "%3" - "}") - .arg(fontSize > 0 ? QString::number(fontSize) : "inherit") - .arg(isBold ? "bold" : "normal") - // FIXME: handle this better - .arg(color != Qt::black ? QString("color: %1;").arg(color.name()) - : ""))); -#endif - - return label; -} - -void WelcomeWidget::showHowToConnectDialog() -{ - HowToConnectDialog(this).exec(); -} \ No newline at end of file diff --git a/src/welcomewidget.h b/src/welcomewidget.h deleted file mode 100644 index 4cdd783..0000000 --- a/src/welcomewidget.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef WELCOMEWIDGET_H -#define WELCOMEWIDGET_H - -#include "howtoconnectdialog.h" -#include "iDescriptor-ui.h" -#include -#include -#include -#include -#include - -class WelcomeWidget : public QWidget -{ - Q_OBJECT - -public: - explicit WelcomeWidget(QWidget *parent = nullptr); - -private: - void setupUI(); - ZLabel *createStyledLabel(const QString &text, int fontSize = 0, - bool isBold = false, QColor color = Qt::black); - - QVBoxLayout *m_mainLayout; - ZLabel *m_titleLabel; - ZLabel *m_subtitleLabel; - QLabel *m_imageLabel; - ZLabel *m_instructionLabel; - ZLabel *m_githubLabel; - ZLabel *m_howToConnectLabel; - - void showHowToConnectDialog(); -}; - -#endif // WELCOMEWIDGET_H \ No newline at end of file diff --git a/src/wirelessgalleryimportwidget.cpp b/src/wirelessgalleryimportwidget.cpp deleted file mode 100644 index 5fc3cb0..0000000 --- a/src/wirelessgalleryimportwidget.cpp +++ /dev/null @@ -1,200 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "wirelessgalleryimportwidget.h" -#include "photoimportdialog.h" -#include -#include -#include -#include -#include -#include -#include -#include - -WirelessGalleryImportWidget::WirelessGalleryImportWidget(QWidget *parent) - : Tool(parent), m_scrollArea(nullptr), m_scrollContent(nullptr), - m_fileListLayout(nullptr), m_browseButton(nullptr), - m_importButton(nullptr), m_statusLabel(nullptr) -{ - setupUI(); - setMinimumSize(400, 400); - setWindowTitle("Wireless Gallery Import - iDescriptor"); -} - -void WirelessGalleryImportWidget::setupUI() -{ - QHBoxLayout *mainLayout = new QHBoxLayout(this); - mainLayout->setContentsMargins(10, 10, 10, 10); - mainLayout->setSpacing(10); - - QVBoxLayout *layout = new QVBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(10); - - // Browse button - m_browseButton = new QPushButton("Select Files"); - connect(m_browseButton, &QPushButton::clicked, this, - &WirelessGalleryImportWidget::onBrowseFiles); - layout->addWidget(m_browseButton); - - // Status label - m_statusLabel = new QLabel("No files selected"); - m_statusLabel->setWordWrap(true); - layout->addWidget(m_statusLabel); - - // Scroll area for file list - m_scrollArea = new QScrollArea(); - m_scrollArea->setStyleSheet("QScrollArea { border: none; }"); - m_scrollArea->setWidgetResizable(true); - m_scrollArea->setMinimumWidth(300); - - m_scrollContent = new QWidget(); - m_fileListLayout = new QVBoxLayout(m_scrollContent); - m_fileListLayout->setContentsMargins(5, 5, 5, 5); - m_fileListLayout->setSpacing(5); - m_fileListLayout->addStretch(); - - m_scrollArea->setWidget(m_scrollContent); - layout->addWidget(m_scrollArea, 1); - - // Import button - m_importButton = new QPushButton("Import to Gallery"); - m_importButton->setEnabled(false); - connect(m_importButton, &QPushButton::clicked, this, - &WirelessGalleryImportWidget::onImportPhotos); - layout->addWidget(m_importButton); - - mainLayout->addLayout(layout); -} - -void WirelessGalleryImportWidget::onBrowseFiles() -{ - QStringList files = QFileDialog::getOpenFileNames( - this, "Select Photos/Videos to Import", - QStandardPaths::writableLocation(QStandardPaths::PicturesLocation), - "Media Files (*.jpg *.jpeg *.png *.gif *.bmp *.tiff *.tif *.webp " - "*.heic *.heif *.mp4 *.mov *.avi *.mkv *.m4v *.3gp *.webm);;All Files " - "(*)"); - - if (files.isEmpty()) { - return; - } - - // Filter out non-compatible files - QStringList compatibleFiles; - for (const QString &file : files) { - if (isGalleryCompatible(file)) { - compatibleFiles.append(file); - } - } - - m_selectedFiles = compatibleFiles; - updateFileList(); - updateStatusLabel(); -} - -void WirelessGalleryImportWidget::updateFileList() -{ - // Clear existing file list - QLayoutItem *child; - while ((child = m_fileListLayout->takeAt(0)) != nullptr) { - if (child->widget()) { - child->widget()->deleteLater(); - } - delete child; - } - - // Add files to the list - for (int i = 0; i < m_selectedFiles.size(); ++i) { - QFileInfo fileInfo(m_selectedFiles[i]); - - QWidget *fileItem = new QWidget(); - QHBoxLayout *fileLayout = new QHBoxLayout(fileItem); - fileLayout->setContentsMargins(5, 5, 5, 5); - fileLayout->setSpacing(0); - - QLabel *fileLabel = new QLabel(fileInfo.fileName()); - fileLabel->setWordWrap(true); - - QPushButton *removeButton = new QPushButton("Remove"); - removeButton->setMaximumWidth(80); - - int index = i; - connect(removeButton, &QPushButton::clicked, this, - [this, index]() { onRemoveFile(index); }); - - fileLayout->addWidget(fileLabel, 1); - fileLayout->addWidget(removeButton); - - m_fileListLayout->insertWidget(m_fileListLayout->count() - 1, fileItem); - } - - m_importButton->setEnabled(!m_selectedFiles.isEmpty()); -} - -void WirelessGalleryImportWidget::updateStatusLabel() -{ - if (m_selectedFiles.isEmpty()) { - m_statusLabel->setText("No files selected"); - } else { - m_statusLabel->setText( - QString("Selected %1 file(s)").arg(m_selectedFiles.size())); - } -} - -void WirelessGalleryImportWidget::onRemoveFile(int index) -{ - if (index >= 0 && index < m_selectedFiles.size()) { - m_selectedFiles.removeAt(index); - updateFileList(); - updateStatusLabel(); - } -} - -QStringList WirelessGalleryImportWidget::getGalleryCompatibleExtensions() const -{ - return {"jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp", "heic", - "heif", "mp4", "mov", "avi", "mkv", "m4v", "3gp", "webm"}; -} - -bool WirelessGalleryImportWidget::isGalleryCompatible( - const QString &filePath) const -{ - QFileInfo info(filePath); - QString ext = info.suffix().toLower(); - return getGalleryCompatibleExtensions().contains(ext); -} - -void WirelessGalleryImportWidget::onImportPhotos() -{ - if (m_selectedFiles.isEmpty()) { - QMessageBox::warning(this, "No Files", - "No gallery-compatible files selected."); - return; - } - - PhotoImportDialog dialog(m_selectedFiles, this); - dialog.exec(); -} - -QStringList WirelessGalleryImportWidget::getSelectedFiles() const -{ - return m_selectedFiles; -} diff --git a/src/wirelessgalleryimportwidget.h b/src/wirelessgalleryimportwidget.h deleted file mode 100644 index 50cd045..0000000 --- a/src/wirelessgalleryimportwidget.h +++ /dev/null @@ -1,67 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef WIRELESSGALLERYIMPORTWIDGET_H -#define WIRELESSGALLERYIMPORTWIDGET_H - -#include "iDescriptor-ui.h" -#include "qprocessindicator.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class WirelessGalleryImportWidget : public Tool -{ - Q_OBJECT - -public: - explicit WirelessGalleryImportWidget(QWidget *parent = nullptr); - - QStringList getSelectedFiles() const; - -private slots: - void onBrowseFiles(); - void onImportPhotos(); - void onRemoveFile(int index); - -private: - QScrollArea *m_scrollArea; - QWidget *m_scrollContent; - QVBoxLayout *m_fileListLayout; - QPushButton *m_browseButton; - QPushButton *m_importButton; - QLabel *m_statusLabel; - - QStringList m_selectedFiles; - - void setupUI(); - void updateFileList(); - void updateStatusLabel(); - bool isGalleryCompatible(const QString &filePath) const; - QStringList getGalleryCompatibleExtensions() const; -}; - -#endif // WIRELESSGALLERYIMPORTWIDGET_H diff --git a/src/zlineedit.cpp b/src/zlineedit.cpp deleted file mode 100644 index f5e9fed..0000000 --- a/src/zlineedit.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "zlineedit.h" -#include "iDescriptor-ui.h" - -ZLineEdit::ZLineEdit(QWidget *parent) : QLineEdit(parent) { setupStyles(); } - -ZLineEdit::ZLineEdit(const QString &text, QWidget *parent) - : QLineEdit(text, parent) -{ - setupStyles(); -} - -void ZLineEdit::setupStyles() { updateStyles(); } - -void ZLineEdit::updateStyles() -{ - // FIMXE: seg faults if Qt decides to change pallets due to applied - // stylesheets - // setStyleSheet("QLineEdit { " - // " border: 2px solid " + - // qApp->palette().color(QPalette::Midlight).name() + - // "; " - // " border-radius: 6px; " - // " padding: 8px 12px; " - // " font-size: 14px; " - // "} " - // "QLineEdit:focus { " - // " border: 2px solid " + - // COLOR_ACCENT_BLUE.name() + - // "; " - // " outline: none; " - // "}"); -} \ No newline at end of file diff --git a/src/zlineedit.h b/src/zlineedit.h deleted file mode 100644 index a5699fe..0000000 --- a/src/zlineedit.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include - -class ZLineEdit : public QLineEdit -{ - Q_OBJECT - -public: - explicit ZLineEdit(QWidget *parent = nullptr); - explicit ZLineEdit(const QString &text, QWidget *parent = nullptr); - -private slots: - void updateStyles(); - -private: - void setupStyles(); - -protected: - void changeEvent(QEvent *event) override - { - if (event->type() == QEvent::PaletteChange) { - updateStyles(); - } - QLineEdit::changeEvent(event); - } -}; \ No newline at end of file diff --git a/src/zloadingwidget.cpp b/src/zloadingwidget.cpp deleted file mode 100644 index 10659b5..0000000 --- a/src/zloadingwidget.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include "zloadingwidget.h" - -#include "qprocessindicator.h" -#include -#include -#include - -ZLoadingWidget::ZLoadingWidget(bool retryEnabled, QWidget *parent) - : QStackedWidget{parent}, m_retryEnabled(retryEnabled) -{ - QWidget *loadingWidget = new QWidget(this); - - m_loadingIndicator = new QProcessIndicator(loadingWidget); - m_loadingIndicator->setType(QProcessIndicator::line_rotate); - m_loadingIndicator->setFixedSize(64, 32); - m_loadingIndicator->start(); - - QHBoxLayout *loadingLayout = new QHBoxLayout(loadingWidget); - loadingLayout->setSpacing(1); - loadingLayout->addStretch(); - loadingLayout->addWidget(m_loadingIndicator); - loadingLayout->addStretch(); - - m_errorWidget = new ZLoadingErrorWidget(m_retryEnabled, this); - connect(static_cast(m_errorWidget), - &ZLoadingErrorWidget::retryClicked, this, - [this]() { emit retryClicked(); }); - - addWidget(loadingWidget); - addWidget(m_errorWidget); -} - -void ZLoadingWidget::setupContentWidget(QWidget *contentWidget) -{ - m_contentWidget = contentWidget; - addWidget(m_contentWidget); -} - -void ZLoadingWidget::setupContentWidget(QLayout *contentLayout) -{ - m_contentWidget = new QWidget(); - m_contentWidget->setLayout(contentLayout); - - addWidget(m_contentWidget); -} - -void ZLoadingWidget::setupErrorWidget(QWidget *errorWidget) -{ - if (m_errorWidget) { - m_errorWidget->deleteLater(); - } - m_errorWidget = errorWidget; - addWidget(m_errorWidget); -} - -void ZLoadingWidget::setupErrorWidget(QLayout *errorLayout) -{ - if (m_errorWidget) { - m_errorWidget->deleteLater(); - } - m_errorWidget = new QWidget(); - m_errorWidget->setLayout(errorLayout); - - addWidget(m_errorWidget); -} - -int ZLoadingWidget::setupAditionalWidget(QWidget *customWidget) -{ - return addWidget(customWidget); -} - -void ZLoadingWidget::switchToWidget(QWidget *widget) -{ - int index = indexOf(widget); - if (index != -1) { - if (m_loadingIndicator) { - m_loadingIndicator->stop(); - } - setCurrentIndex(index); - } -} - -void ZLoadingWidget::stop(bool showContent) -{ - if (m_loadingIndicator) { - m_loadingIndicator->stop(); - } - if (showContent && m_contentWidget) { - switchToWidget(m_contentWidget); - } -} - -void ZLoadingWidget::showError() -{ - m_loadingIndicator->stop(); - if (m_errorWidget) { - setCurrentWidget(m_errorWidget); - } -} - -void ZLoadingWidget::showError(const QString &errorMessage) -{ - m_loadingIndicator->stop(); - if (m_errorWidget) { - // FIXME: can be handled better - // maybe subclass ZLoadingWidget for custom error widget? - if (auto errorWidget = - qobject_cast(m_errorWidget)) { - errorWidget->setText(errorMessage); - } - setCurrentWidget(m_errorWidget); - } -} - -void ZLoadingWidget::showLoading() -{ - if (m_loadingIndicator) { - m_loadingIndicator->start(); - } - setCurrentWidget(m_loadingIndicator->parentWidget()); -} - -ZLoadingWidget::~ZLoadingWidget() -{ - if (m_loadingIndicator) { - m_loadingIndicator->stop(); - } -} \ No newline at end of file diff --git a/src/zloadingwidget.h b/src/zloadingwidget.h deleted file mode 100644 index cabe010..0000000 --- a/src/zloadingwidget.h +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef ZLOADINGWIDGET_H -#define ZLOADINGWIDGET_H - -#include -#include -#include -#include -#include - -class ZLoadingErrorWidget : public QWidget -{ - Q_OBJECT -public: - ZLoadingErrorWidget(bool retryEnabled, QWidget *parent = nullptr) - { - - QVBoxLayout *layout = new QVBoxLayout(this); - layout->addStretch(); - m_errorLabel = new QLabel("An error occurred.", this); - m_errorLabel->setAlignment(Qt::AlignCenter); - m_errorLabel->setWordWrap(true); - m_errorLabel->setStyleSheet("QLabel { color: red; }"); - layout->addWidget(m_errorLabel); - - if (retryEnabled) { - layout->addSpacing(10); - m_retryButton = new QPushButton("Retry", this); - m_retryButton->setMaximumWidth(m_retryButton->sizeHint().width()); - layout->addWidget(m_retryButton, 0, Qt::AlignCenter); - connect(m_retryButton, &QPushButton::clicked, this, - [this]() { emit retryClicked(); }); - } - layout->addStretch(); - } - - void setText(const QString &text) { m_errorLabel->setText(text); }; - -private: - QLabel *m_errorLabel = nullptr; - QPushButton *m_retryButton = nullptr; -signals: - void retryClicked(); -}; - -class ZLoadingWidget : public QStackedWidget -{ - Q_OBJECT -public: - explicit ZLoadingWidget(bool retryEnabled = false, - QWidget *parent = nullptr); - ~ZLoadingWidget(); - void stop(bool showContent = true); - void showLoading(); - void setupContentWidget(QWidget *contentWidget); - void setupContentWidget(QLayout *contentLayout); - void setupErrorWidget(QWidget *errorWidget); - void setupErrorWidget(QLayout *errorLayout); - int setupAditionalWidget(QWidget *customWidget); - void switchToWidget(QWidget *widget); - void showError(); - void showError(const QString &errorMessage); - -private: - class QProcessIndicator *m_loadingIndicator = nullptr; - QWidget *m_contentWidget = nullptr; - QWidget *m_errorWidget = nullptr; - bool m_retryEnabled = false; -signals: - void retryClicked(); -}; - -#endif // ZLOADINGWIDGET_H diff --git a/src/ztabwidget.cpp b/src/ztabwidget.cpp deleted file mode 100644 index 1028a87..0000000 --- a/src/ztabwidget.cpp +++ /dev/null @@ -1,383 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "ztabwidget.h" -#include "iDescriptor-ui.h" -#include -#include -#include -#include -#include -#include -#include -#include - -QRect gliderEndRectForTab(const ZTab *tab) -{ - if (!tab) - return {}; - - // Approximate "title center" using the push-button contents rect center. - QStyleOptionButton opt; - opt.initFrom(tab); - opt.text = tab->text(); - opt.icon = tab->icon(); - opt.iconSize = tab->iconSize(); - - QRect contents = - tab->style()->subElementRect(QStyle::SE_PushButtonContents, &opt, tab); - if (!contents.isValid()) - contents = tab->rect(); - - const int centerX = tab->mapToParent(contents.center()).x(); - - // Half-width glider, clamped so it never exceeds contents width and never - // gets too tiny. - const int rawW = tab->width() / 1.5; - const int maxW = qMax(1, contents.width()); - const int w = qBound(12, qMin(rawW, maxW), tab->width()); - - const int x = centerX - (w / 2); - const int y = tab->pos().y() + tab->height() - 2; - return QRect(x, y, w, 2); -} - -ZTab::ZTab(const QString &text, QWidget *parent) : QPushButton(text, parent) -{ - setCheckable(true); -#ifndef WIN32 - setFixedHeight(40); -#else - setFixedHeight(40); -#endif - - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); -} - -ZTabWidget::ZTabWidget(QWidget *parent) : QWidget(parent), m_currentIndex(0) -{ - m_mainLayout = new QVBoxLayout(this); - m_mainLayout->setContentsMargins(10, 0, 10, 0); - m_mainLayout->setSpacing(0); - - // Create tab bar container - m_tabBar = new QWidget(); -#ifndef WIN32 - m_tabBar->setFixedHeight(40); -#else - m_tabBar->setFixedHeight(40); -#endif - m_tabBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - m_tabLayout = new QHBoxLayout(m_tabBar); - m_tabLayout->setSpacing(0); - m_tabLayout->setContentsMargins(0, 0, 0, 0); - - // Add drop shadow effect - QGraphicsDropShadowEffect *shadow = new QGraphicsDropShadowEffect(); - shadow->setBlurRadius(20); - shadow->setColor(QColor(24, 94, 224, 38)); // rgba(24, 94, 224, 0.15) - shadow->setOffset(0, 6); - m_tabBar->setGraphicsEffect(shadow); - - m_buttonGroup = new QButtonGroup(this); - m_buttonGroup->setExclusive(true); - - // Create stacked widget for content - m_stackedWidget = new QStackedWidget(); - m_stackedWidget->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - - // Add widgets to layout - m_mainLayout->addWidget(m_tabBar); - m_mainLayout->addWidget(m_stackedWidget, 1); - - setupGlider(); -} - -void ZTabWidget::setupGlider() -{ - m_glider = new QWidget(m_tabBar); - m_glider->setStyleSheet(QString("QWidget {" - " background-color: %1;" - " border-radius: %2px;" - "}") - .arg(COLOR_ACCENT_BLUE.name()) -#ifndef WIN32 - .arg(6) -#else - .arg(2) -#endif - ); - m_glider->hide(); // Hide initially until tabs are added -} - -ZTab *ZTabWidget::addTab(QWidget *widget, const QString &label) -{ - ZTab *tab = new ZTab(label, m_tabBar); - connect(tab, &ZTab::clicked, this, &ZTabWidget::onTabClicked); - int index = m_tabs.count(); - m_tabs.append(tab); - m_widgets.append(widget); - - m_tabLayout->addWidget(tab); - m_stackedWidget->addWidget(widget); - m_buttonGroup->addButton(tab, index); - - return tab; -} - -void ZTabWidget::setCurrentIndex(int index) -{ - if (index < 0 || index >= m_tabs.count() || index == m_currentIndex) { - return; - } - - m_currentIndex = index; - m_tabs[index]->setChecked(true); - m_stackedWidget->setCurrentIndex(index); -#ifdef WIN32 - animateWidget(m_stackedWidget->currentWidget()); -#endif - updateTabStyles(); - animateGlider(index); - - emit currentChanged(index); -} - -void ZTabWidget::finalizeStyles() -{ - if (m_tabs.isEmpty()) - return; - - m_currentIndex = 0; - ZTab *tab = m_tabs[m_currentIndex]; - if (!tab) - return; - - tab->setChecked(true); - - QTimer::singleShot(0, this, [this]() { - if (m_currentIndex >= 0 && m_currentIndex < m_tabs.size()) { -#ifndef WIN32 - animateGlider(m_currentIndex, true); - m_glider->show(); -#else - ZTab *tab = m_tabs[m_currentIndex]; - const QRect endRect = gliderEndRectForTab(tab); - if (m_gliderAnimation) { - m_gliderAnimation->stop(); - delete m_gliderAnimation; - m_gliderAnimation = nullptr; - } - m_glider->setGeometry(endRect); - m_glider->show(); -#endif - } - }); - - updateTabStyles(); -} - -int ZTabWidget::currentIndex() const { return m_currentIndex; } - -QWidget *ZTabWidget::widget(int index) const -{ - if (index < 0 || index >= m_widgets.count()) { - return nullptr; - } - return m_widgets[index]; -} - -void ZTabWidget::onTabClicked() -{ - ZTab *clickedTab = qobject_cast(sender()); - if (!clickedTab) - return; - - int index = m_tabs.indexOf(clickedTab); - if (index != -1) { - setCurrentIndex(index); - } -} - -void ZTabWidget::animateGlider(int index, bool onResize) -{ - if (index < 0 || index >= m_tabs.count()) - return; - - ZTab *targetTab = m_tabs[index]; - if (!targetTab) - return; - - const QRect endRect = gliderEndRectForTab(targetTab); - -#ifdef WIN32 - if (onResize || !m_glider->isVisible()) { - if (m_gliderAnimation) { - m_gliderAnimation->stop(); - delete m_gliderAnimation; - m_gliderAnimation = nullptr; - } - m_glider->setGeometry(endRect); - m_glider->show(); - return; - } - - const QRect startRect = m_glider->geometry(); - - const int left = qMin(startRect.left(), endRect.left()); - const int right = qMax(startRect.right(), endRect.right()); - const QRect stretchRect(left, endRect.y(), (right - left + 1), 2); - - if (m_gliderAnimation) { - m_gliderAnimation->stop(); - delete m_gliderAnimation; - m_gliderAnimation = nullptr; - } - - auto *group = new QSequentialAnimationGroup(this); - - auto *expandAnim = new QPropertyAnimation(m_glider, "geometry"); - expandAnim->setDuration(130); - expandAnim->setStartValue(startRect); - expandAnim->setEndValue(stretchRect); - expandAnim->setEasingCurve(QEasingCurve::OutCubic); - - auto *settleAnim = new QPropertyAnimation(m_glider, "geometry"); - settleAnim->setDuration(190); - settleAnim->setStartValue(stretchRect); - settleAnim->setEndValue(endRect); - settleAnim->setEasingCurve(QEasingCurve::OutCubic); - - group->addAnimation(expandAnim); - group->addAnimation(settleAnim); - - m_gliderAnimation = group; - group->start(); -#else - if (m_gliderAnimation == nullptr) { - m_gliderAnimation = new QPropertyAnimation(m_glider, "pos", this); - static_cast(m_gliderAnimation)->setDuration(250); - static_cast(m_gliderAnimation) - ->setEasingCurve(QEasingCurve::OutCubic); - } - - m_glider->setFixedSize(endRect.width(), 2); - m_gliderAnimation->stop(); - static_cast(m_gliderAnimation) - ->setStartValue(m_glider->pos()); - static_cast(m_gliderAnimation) - ->setEndValue(endRect.topLeft()); - m_gliderAnimation->start(); -#endif -} - -void ZTabWidget::animateWidget(QWidget *widget) -{ -#ifdef WIN32 - if (!widget) - return; - - // FIXME: doesn't work on Tool tab because we are using opacity in - // stylesheet - QGraphicsOpacityEffect *opacityEffect = - qobject_cast(widget->graphicsEffect()); - if (!opacityEffect) { - opacityEffect = new QGraphicsOpacityEffect(widget); - widget->setGraphicsEffect(opacityEffect); - } - - QPropertyAnimation *opacityAnim = - new QPropertyAnimation(opacityEffect, "opacity", this); - opacityAnim->setDuration(350); - opacityAnim->setStartValue(0.0); - opacityAnim->setEndValue(1.0); - opacityAnim->setEasingCurve(QEasingCurve::OutCubic); - opacityAnim->start(QAbstractAnimation::DeleteWhenStopped); - - QPropertyAnimation *posAnim = new QPropertyAnimation(widget, "pos", this); - posAnim->setDuration(350); - posAnim->setStartValue(QPoint(widget->pos().x(), widget->pos().y() + 20)); - posAnim->setEndValue(widget->pos()); - posAnim->setEasingCurve(QEasingCurve::OutCubic); - posAnim->start(QAbstractAnimation::DeleteWhenStopped); -#else - Q_UNUSED(widget); -#endif -} - -void ZTabWidget::updateTabStyles() -{ - const QString accentColor = - -#ifdef WIN32 - COLOR_ACCENT_BLUE.name(); -#else - "#185ee0"; -#endif - - for (int i = 0; i < m_tabs.count(); ++i) { - ZTab *tab = m_tabs[i]; - if (tab->isChecked()) { - tab->setStyleSheet(QString("ZTab {" - " color: %1;" -// " color: #d7e1f4ff;" -#ifdef WIN32 - "font-family : \"Segoe UI\", serif;" -#endif - " font-weight: 600;" - " font-size: 20px;" - " border: none;" - " outline: none;" - " background-color: transparent;" - "}" - "ZTab:hover {" - " background-color: transparent;" - "}") - .arg(accentColor)); - } else { - tab->setStyleSheet(QString("ZTab {" - " color: #666;" - // " color: #2b5693;" -#ifdef WIN32 - "font-family : \"Segoe UI\", serif;" -#endif - " font-weight: 600;" - " font-size: 20px;" - " border: none;" - " outline: none;" - " background-color: transparent;" - "}" - "ZTab:hover {" - " color: %1;" - " background-color: transparent;" - "}") - .arg(accentColor)); - } - } -} - -// Update glider position when widget is resized -void ZTabWidget::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - if (m_currentIndex >= 0 && m_currentIndex < m_tabs.count()) { - animateGlider(m_currentIndex, true); - } -} diff --git a/src/ztabwidget.h b/src/ztabwidget.h deleted file mode 100644 index 081aa5a..0000000 --- a/src/ztabwidget.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#ifndef ZTABWIDGET_H -#define ZTABWIDGET_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class ZTab : public QPushButton -{ - Q_OBJECT - -public: - explicit ZTab(const QString &text, QWidget *parent = nullptr); - void setIcon(const QIcon &icon); -}; - -class ZTabWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ZTabWidget(QWidget *parent = nullptr); - void finalizeStyles(); - ZTab *addTab(QWidget *widget, const QString &label); - void setCurrentIndex(int index); - int currentIndex() const; - QWidget *widget(int index) const; - -signals: - void currentChanged(int index); - -private slots: - void onTabClicked(); - -protected: - void resizeEvent(QResizeEvent *event) override; - -private: - QHBoxLayout *m_tabLayout; - QVBoxLayout *m_mainLayout; - QWidget *m_tabBar; - QStackedWidget *m_stackedWidget; - QButtonGroup *m_buttonGroup; - QWidget *m_glider; - QAbstractAnimation *m_gliderAnimation = nullptr; - QList m_tabs; - QList m_widgets; - int m_currentIndex; - - void setupGlider(); - void animateGlider(int index, bool onResize = false); - void animateWidget(QWidget *widget); - void updateTabStyles(); -}; - -#endif // ZTABWIDGET_H \ No newline at end of file