From 3ae707b7543e7a1ad011a704d1c093561048b3f5 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Sat, 1 Nov 2025 23:31:59 +0000 Subject: [PATCH] integrate ZUpdater & refactor device management and UI components - Integrated ZUpdater to handle auto updating - Updated DeviceManagerWidget to improve device selection logic and ensure current device is set correctly when devices are added or removed. - Enhanced FileExplorerWidget to reset the view when sidebar items are clicked. - Changed ZIconWidget to inherit from QAbstractButton for better button behavior and removed unnecessary mouse event handling. - Updated iDescriptor to include device version parsing and improved device version retrieval logic. - Refactored iFuseDiskUnmountButton and iFuseDiskUnmountButton to use ZIconWidget for a consistent UI. - Improved iFuseWidget to handle device selection more robustly and update UI accordingly. - Added SponsorAppCard and SponsorWidget to promote sponsorship within the app. - Updated ToolboxWidget to streamline device selection and toolbox functionality. - General code cleanup and comments for better maintainability. --- .gitmodules | 3 + .vscode/c_cpp_properties.json | 45 +-- .vscode/settings.json | 37 +- CMakeLists.txt | 9 +- lib/zupdater | 1 + resources.qrc | 2 + resources/DeveloperDiskImages.json | 413 +++++++++++++++++++++ resources/icons/MdiGithub.png | Bin 0 -> 16757 bytes src/afcexplorerwidget.h | 2 +- src/appcontext.cpp | 5 + src/appinstalldialog.cpp | 24 ++ src/appswidget.cpp | 453 ++++++++++++++++++++---- src/appswidget.h | 58 ++- src/core/services/get_mounted_image.cpp | 2 +- src/core/services/init_device.cpp | 9 + src/core/services/mount_dev_image.cpp | 28 +- src/devdiskimagehelper.cpp | 35 +- src/devdiskimageswidget.cpp | 33 +- src/devdiskmanager.cpp | 144 ++++---- src/devdiskmanager.h | 11 +- src/deviceimagewidget.cpp | 108 ++++-- src/deviceimagewidget.h | 1 + src/devicemanagerwidget.cpp | 38 +- src/fileexplorerwidget.cpp | 2 + src/iDescriptor-ui.h | 57 +-- src/iDescriptor.h | 62 +++- src/ifusediskunmountbutton.cpp | 7 +- src/ifusediskunmountbutton.h | 4 +- src/ifusewidget.cpp | 9 +- src/mainwindow.cpp | 74 +++- src/mainwindow.h | 3 + src/realtimescreenwidget.cpp | 58 +-- src/realtimescreenwidget.h | 2 +- src/settingsmanager.cpp | 13 +- src/settingsmanager.h | 2 +- src/settingswidget.cpp | 3 +- src/sponsorappcard.cpp | 117 ++++++ src/sponsorappcard.h | 15 + src/sponsorwidget.cpp | 25 ++ src/sponsorwidget.h | 13 + src/toolboxwidget.cpp | 71 ++-- 41 files changed, 1553 insertions(+), 445 deletions(-) create mode 160000 lib/zupdater create mode 100644 resources/DeveloperDiskImages.json create mode 100644 resources/icons/MdiGithub.png create mode 100644 src/sponsorappcard.cpp create mode 100644 src/sponsorappcard.h create mode 100644 src/sponsorwidget.cpp create mode 100644 src/sponsorwidget.h diff --git a/.gitmodules b/.gitmodules index 6c0d5e7..a7feee4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/ipatool-go"] path = lib/ipatool-go url = https://github.com/uncor3/libipatool-go.git +[submodule "lib/zupdater"] + path = lib/zupdater + url = https://github.com/uncor3/ZUpdater diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index be8b15b..0b6e02f 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -1,22 +1,25 @@ { - "configurations": [ - { - "name": "Linux", - "includePath": [ - "${workspaceFolder}/**", - "/usr/include/qt/**", - "/usr/local/opt/qt/**", - "${workspaceFolder}/build/Desktop-Debug/iDescriptor_autogen/include", - "${workspaceFolder}/build/Desktop-Debug/src/lib/**", - "/usr/local/include/**", - "/usr/include/qt6/**" - ], - "defines": [], - "compilerPath": "/usr/bin/gcc", - "cStandard": "c17", - "cppStandard": "gnu++17", - "intelliSenseMode": "linux-gcc-x64" - } - ], - "version": 4 -} + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**", + "/usr/include/qt/**", + "/usr/local/opt/qt/**", + "${workspaceFolder}/build/Desktop-Debug/iDescriptor_autogen/include", + "${workspaceFolder}/build/Desktop-Debug/src/lib/**", + "/usr/local/include/**", + "/usr/include/qt6/**", + "/usr/include/qt6/QtBluetooth/**", + "${workspaceFolder}/lib/zupdater/src", + "/usr/include/qt6/QtConcurrent" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "c17", + "cppStandard": "gnu++17", + "intelliSenseMode": "linux-gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 72a9d0f..4ec39cc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -167,6 +167,41 @@ "qvideowidget": "cpp", "qhostaddress": "cpp", "qtcpsocket": "cpp", - "qstackedwidget": "cpp" + "qstackedwidget": "cpp", + "qnetworkrequest": "cpp", + "qdesktopservices": "cpp", + "qsettings": "cpp", + "qpropertyanimation": "cpp", + "qquickitem": "cpp", + "qparallelanimationgroup": "cpp", + "qbluetoothdeviceinfo": "cpp", + "qlowenergyadvertisingparameters": "cpp", + "qlowenergycontroller": "cpp", + "qlowenergydescriptordata": "cpp", + "qquickstyle": "cpp", + "qcheckbox": "cpp", + "qpixmap": "cpp", + "qstyle": "cpp", + "qpainterpath": "cpp", + "qpainter": "cpp", + "qenterevent": "cpp", + "qfiledialog": "cpp", + "qgraphicsscene": "cpp", + "qpoint": "cpp", + "qprocessenvironment": "cpp", + "qprocess": "cpp", + "qsplitter": "cpp", + "qinputdialog": "cpp", + "qtreewidget": "cpp", + "qmenu": "cpp", + "qlistview": "cpp", + "qmutexlocker": "cpp", + "qmenubar": "cpp", + "qstack": "cpp", + "qstandarditemmodel": "cpp", + "qimage": "cpp", + "qabstractbutton": "cpp", + "qtnetwork": "cpp", + "qtcore": "cpp" } } diff --git a/CMakeLists.txt b/CMakeLists.txt index c5f1828..46e292b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -187,7 +187,7 @@ endif() add_subdirectory(lib/airplay) add_subdirectory(lib/ipatool-go) - +add_subdirectory(lib/zupdater) if (WIN32) set(app_icon_resource_windows "${CMAKE_CURRENT_SOURCE_DIR}/idescriptor.rc") @@ -243,8 +243,13 @@ target_link_libraries(iDescriptor PRIVATE PkgConfig::ZIP airplay ipatool-go + ZUpdater ) - + +target_include_directories(iDescriptor PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/lib/zupdater/src +) + if(APPLE) find_library(CORE_SERVICES_FRAMEWORK CoreServices REQUIRED) target_link_libraries(iDescriptor PRIVATE diff --git a/lib/zupdater b/lib/zupdater new file mode 160000 index 0000000..31ccc0b --- /dev/null +++ b/lib/zupdater @@ -0,0 +1 @@ +Subproject commit 31ccc0bcaebecc482c735e46b00cf4def75718cb diff --git a/resources.qrc b/resources.qrc index c32814f..dccfbe9 100644 --- a/resources.qrc +++ b/resources.qrc @@ -17,6 +17,7 @@ resources/icons/MdiLightMagnify.png resources/icons/IcBaselineInsertDriveFile.png resources/icons/MaterialSymbolsLightHome.png + resources/icons/MdiGithub.png resources/icons/app-icon/icon.ico qml/MapView.qml @@ -48,5 +49,6 @@ resources/connect.png resources/airplayer-tutorial.mp4 resources/ipad-mockups/ipad.png + resources/DeveloperDiskImages.json \ No newline at end of file diff --git a/resources/DeveloperDiskImages.json b/resources/DeveloperDiskImages.json new file mode 100644 index 0000000..39118b1 --- /dev/null +++ b/resources/DeveloperDiskImages.json @@ -0,0 +1,413 @@ +{ + "10.0": { + "Image": [ + "https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/raw/master/Developer%20Disk%20Image/10.0%20(14A345)/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/raw/master/Developer%20Disk%20Image/10.0%20(14A345)/DeveloperDiskImage.dmg.signature" + ] + }, + "10.1": { + "Image": [ + "https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/raw/master/Developer%20Disk%20Image/10.1%20(14B72)/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/raw/master/Developer%20Disk%20Image/10.1%20(14B72)/DeveloperDiskImage.dmg.signature" + ] + }, + "10.2": { + "Image": [ + "https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/raw/master/Developer%20Disk%20Image/10.2%20(14C5062c)/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/raw/master/Developer%20Disk%20Image/10.2%20(14C5062c)/DeveloperDiskImage.dmg.signature" + ] + }, + "10.3": { + "Image": [ + "https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/blob/master/Developer%20Disk%20Image/10.3%20(14E269)/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/blob/master/Developer%20Disk%20Image/10.3%20(14E269)/DeveloperDiskImage.dmg.signature" + ] + }, + "11.0": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.0/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.0/DeveloperDiskImage.dmg.signature" + ] + }, + "11.1": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.1/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.1/DeveloperDiskImage.dmg.signature" + ] + }, + "11.2": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.2/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.2/DeveloperDiskImage.dmg.signature" + ] + }, + "11.3": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.3/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.3/DeveloperDiskImage.dmg.signature" + ] + }, + "11.4": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.4/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/11.4/DeveloperDiskImage.dmg.signature" + ] + }, + "12.0": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/12.0/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/12.0/DeveloperDiskImage.dmg.signature" + ] + }, + "12.1": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/12.1/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/12.1/DeveloperDiskImage.dmg.signature" + ] + }, + "12.2": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/12.2/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/12.2/DeveloperDiskImage.dmg.signature" + ] + }, + "12.3": { + "Image": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/12.3/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/luannguyenkhoa/LocationSimulator/raw/master/LocationSimulator/12.3/DeveloperDiskImage.dmg.signature" + ] + }, + "12.4": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/12/12.4/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/12/12.4/DeveloperDiskImage.dmg.signature" + ] + }, + "12.5": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/12/12.4/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/12/12.4/DeveloperDiskImage.dmg.signature" + ] + }, + "13.0": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.0/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.0/DeveloperDiskImage.dmg.signature" + ] + }, + "13.1": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.1/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.1/DeveloperDiskImage.dmg.signature" + ] + }, + "13.2": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.2/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.2/DeveloperDiskImage.dmg.signature" + ] + }, + "13.3": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.3/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.3/DeveloperDiskImage.dmg.signature" + ] + }, + "13.4": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.4/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.4/DeveloperDiskImage.dmg.signature" + ] + }, + "13.5": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.5/DeveloperDiskImage.dmg.signature" + ] + }, + "13.6": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.6/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.6/DeveloperDiskImage.dmg.signature" + ] + }, + "13.7": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.7/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/13/13.7/DeveloperDiskImage.dmg.signature" + ] + }, + "14.0": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.0/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.0/DeveloperDiskImage.dmg.signature" + ] + }, + "14.1": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.1/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.1/DeveloperDiskImage.dmg.signature" + ] + }, + "14.2": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.2/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.2/DeveloperDiskImage.dmg.signature" + ] + }, + "14.3": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.3/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.3/DeveloperDiskImage.dmg.signature" + ] + }, + "14.4": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.4/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.4/DeveloperDiskImage.dmg.signature" + ] + }, + "14.5": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.5/DeveloperDiskImage.dmg" + ] + }, + "14.6": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.5/DeveloperDiskImage.dmg.signature" + ] + }, + "14.7": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.5/DeveloperDiskImage.dmg.signature" + ] + }, + "14.8": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/14/14.5/DeveloperDiskImage.dmg.signature" + ] + }, + "15.0": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.0/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.0/DeveloperDiskImage.dmg.signature" + ] + }, + "15.1": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.0/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.0/DeveloperDiskImage.dmg.signature" + ] + }, + "15.2": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.2/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.2/DeveloperDiskImage.dmg.signature" + ] + }, + "15.3": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.2/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.2/DeveloperDiskImage.dmg.signature" + ] + }, + "15.4": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.4/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.4/DeveloperDiskImage.dmg.signature" + ] + }, + "15.5": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.5/DeveloperDiskImage.dmg.signature" + ] + }, + "15.6": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.6/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.6/DeveloperDiskImage.dmg.signature" + ] + }, + "15.7": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.7/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.7/DeveloperDiskImage.dmg.signature" + ] + }, + "15.8": { + "Image": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/pdso/DeveloperDiskImage/raw/master/15/15.5/DeveloperDiskImage.dmg.signature" + ] + }, + "16.0": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.0/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.0/DeveloperDiskImage.dmg.signature" + ] + }, + "16.1": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.1/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.1/DeveloperDiskImage.dmg.signature" + ] + }, + "16.2": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.2/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.2/DeveloperDiskImage.dmg.signature" + ] + }, + "16.3": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.3/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.3/DeveloperDiskImage.dmg.signature" + ] + }, + "16.4": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.4/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.4/DeveloperDiskImage.dmg.signature" + ] + }, + "16.5": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.5/DeveloperDiskImage.dmg.signature" + ] + }, + "16.6": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.5/DeveloperDiskImage.dmg.signature" + ] + }, + "16.7": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.5/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/16.5/DeveloperDiskImage.dmg.signature" + ] + }, + "17.0": { + "Image": [ + "https://github.com/doronz88/DeveloperDiskImage/raw/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg" + ], + "Trustcache": [ + "https://github.com/doronz88/DeveloperDiskImage/raw/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache" + ], + "BuildManifest": [ + "https://github.com/doronz88/DeveloperDiskImage/raw/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist" + ] + }, + "Fallback": { + "Image": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/%@/DeveloperDiskImage.dmg" + ], + "Signature": [ + "https://github.com/mspvirajpatel/Xcode_Developer_Disk_Images/raw/master/Developer%20Disk%20Image/%@/DeveloperDiskImage.dmg.signature" + ] + } +} diff --git a/resources/icons/MdiGithub.png b/resources/icons/MdiGithub.png new file mode 100644 index 0000000000000000000000000000000000000000..8c3db867276997cfa10c6b0d59648263cfae8a84 GIT binary patch literal 16757 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 #include #include +#include +#include #include #include #include @@ -27,6 +29,28 @@ AppInstallDialog::AppInstallDialog(const QString &appName, // App info section QHBoxLayout *appInfoLayout = new QHBoxLayout(); QLabel *iconLabel = new QLabel(); + fetchAppIconFromApple( + bundleId, + [iconLabel](const QPixmap &pixmap) { + 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); + } + }, + this); QPixmap icon = QApplication::style() ->standardIcon(QStyle::SP_ComputerIcon) .pixmap(64, 64); diff --git a/src/appswidget.cpp b/src/appswidget.cpp index 959c3de..0abd909 100644 --- a/src/appswidget.cpp +++ b/src/appswidget.cpp @@ -6,10 +6,12 @@ #include "appstoremanager.h" #include "iDescriptor-ui.h" #include "logindialog.h" +#include "sponsorwidget.h" #include "zlineedit.h" #include #include #include +#include #include #include #include @@ -23,11 +25,15 @@ #include #include #include +#include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -35,7 +41,90 @@ #include #include +// Helper struct for semantic version comparison +struct AppVersion { + int major = 0; + int minor = 0; + int patch = 0; + + static AppVersion fromString(QString versionString) + { + // Keep only digits and dots for comparison + versionString.remove(QRegularExpression("[^\\d.]")); + AppVersion v; + QStringList parts = versionString.split('.'); + if (parts.size() > 0) + v.major = parts[0].toInt(); + if (parts.size() > 1) + v.minor = parts[1].toInt(); + if (parts.size() > 2) + v.patch = parts[2].toInt(); + return v; + } + + bool operator<(const AppVersion &other) const + { + if (major != other.major) + return major < other.major; + if (minor != other.minor) + return minor < other.minor; + return patch < other.patch; + } + + bool operator==(const AppVersion &other) const + { + return major == other.major && minor == other.minor && + patch == other.patch; + } + + bool operator>(const AppVersion &other) const + { + return !(*this < other || *this == other); + } + bool operator<=(const AppVersion &other) const + { + return (*this < other || *this == other); + } + bool operator>=(const AppVersion &other) const { return !(*this < other); } +}; + +// Checks if the current app version matches a given version condition +bool versionMatches(const QString ¤tVersionStr, + const QString &conditionStr) +{ + AppVersion currentVersion = AppVersion::fromString(currentVersionStr); + AppVersion conditionVersion = AppVersion::fromString(conditionStr); + + if (conditionStr.startsWith("<=")) + return currentVersion <= conditionVersion; + if (conditionStr.startsWith(">=")) + return currentVersion >= conditionVersion; + if (conditionStr.startsWith("<")) + return currentVersion < conditionVersion; + if (conditionStr.startsWith(">")) + return currentVersion > conditionVersion; + + // Exact match + return currentVersion == conditionVersion; +} + +QJsonObject getVersionedConfig(const QJsonObject &rootObj) +{ + QStringList keys = rootObj.keys(); + for (const QString &key : keys) { + if (versionMatches(APP_VERSION, key)) { + return rootObj[key].toObject(); + } + } +} + // watch for login and logout events +AppsWidget *AppsWidget::sharedInstance() +{ + static AppsWidget instance; + return &instance; +} + AppsWidget::AppsWidget(QWidget *parent) : QWidget(parent), m_isLoggedIn(false) { m_debounceTimer = new QTimer(this); @@ -47,8 +136,9 @@ 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;"); @@ -110,7 +200,7 @@ void AppsWidget::setupUI() mainLayout->addWidget(m_stackedWidget); // Show default apps initially - showDefaultApps(); + showLoading("Loading apps..."); // Connections connect(m_loginButton, &QPushButton::clicked, this, &AppsWidget::onLoginClicked); @@ -123,6 +213,60 @@ void AppsWidget::setupUI() &AppsWidget::onAppStoreInitialized); connect(m_manager, &AppStoreManager::loggedOut, this, &AppsWidget::onAppStoreInitialized); + + // fetch sponsors + QUrl sponsorsUrl("http://localhost:5173/sponsors.json"); + 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(); + + // Store the platinum members to be used when populating the + // grid + 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"; + showDefaultApps(); + reply->deleteLater(); + } catch (...) { + qDebug() << "Exception occurred while processing sponsors"; + showDefaultApps(); + reply->deleteLater(); + } + }); } void AppsWidget::onAppStoreInitialized(const QJsonObject &accountInfo) @@ -242,26 +386,102 @@ void AppsWidget::populateDefaultApps() if (!gridLayout) return; - // Create sample app cards - createAppCard("Instagram", "com.burbn.instagram", - "Photo & Video sharing social network", "", gridLayout, 0, 0); - createAppCard("WhatsApp", "net.whatsapp.WhatsApp", - "Free messaging and video calling", "", gridLayout, 0, 1); - createAppCard("Spotify", "com.spotify.client", - "Music streaming and podcast platform", "", gridLayout, 0, 2); - createAppCard("YouTube", "com.google.ios.youtube", - "Video sharing and streaming platform", "", gridLayout, 1, 0); - createAppCard("X", "com.atebits.Tweetie2", "Social media and microblogging", - "", gridLayout, 1, 1); - createAppCard("TikTok", "com.zhiliaoapp.musically", - "Short-form video hosting service", "", gridLayout, 1, 2); - createAppCard("Twitch", "tv.twitch", "Live streaming platform", "", - gridLayout, 2, 0); - createAppCard("Telegram", "ph.telegra.Telegraph", - "Cloud-based instant messaging", "", gridLayout, 2, 1); - createAppCard("Reddit", "com.reddit.Reddit", - "Social news aggregation platform", "", gridLayout, 2, 2); + int row = 0; + int col = 0; + const int maxCols = 3; + // Helper lambda to advance the grid position + 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(); + + createAppCard(name, bundleId, description, logoUrl, url, gridLayout, + row, col, 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(); + createAppCard(name, bundleId, description, logoUrl, url, gridLayout, + row, col, SponsorType(SponsorType::Gold)); + 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(); + + 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(); + + createAppCard(name, bundleId, description, "", url, gridLayout, row, + col, 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(); + createAppCard(name, bundleId, description, "", url, gridLayout, row, + col, SponsorType(SponsorType::Bronze)); + advanceGridPos(); + } gridLayout->setRowStretch(gridLayout->rowCount(), 1); } @@ -281,10 +501,37 @@ void AppsWidget::clearAppGrid() } } +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 &iconPath, QGridLayout *gridLayout, - int row, int col) + const QString &logoUrl, + const QString &websiteUrl, + QGridLayout *gridLayout, int row, int col, + const SponsorType &sponsorType) { QWidget *cardWidget = new QWidget(); @@ -301,74 +548,146 @@ void AppsWidget::createAppCard(const QString &name, const QString &bundleId, iconLabel->setAlignment(Qt::AlignCenter); cardLayout->addWidget(iconLabel); - fetchAppIconFromApple( - bundleId, - [iconLabel](const QPixmap &pixmap) { - if (!pixmap.isNull()) { - QPixmap scaled = - pixmap.scaled(64, 64, Qt::KeepAspectRatioByExpanding, - Qt::SmoothTransformation); - QPixmap rounded(64, 64); - rounded.fill(Qt::transparent); + // If logoUrl is provided and bundleId is empty, use the logo directly + if (!logoUrl.isEmpty()) { + QUrl url(logoUrl); + QNetworkRequest request(url); + QNetworkReply *reply = m_networkManager->get(request); + connect(reply, &QNetworkReply::finished, [reply, iconLabel]() { + if (reply->error() == QNetworkReply::NoError) { + QByteArray data = reply->readAll(); + QPixmap pixmap; + if (pixmap.loadFromData(data)) { + 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(); + 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); + iconLabel->setPixmap(rounded); + } } - }, - cardWidget); + reply->deleteLater(); + }); + } else if (!bundleId.isEmpty()) { + // Use Apple's API for app icons + fetchAppIconFromApple( + bundleId, + [iconLabel](const QPixmap &pixmap) { + 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); + } + }, + cardWidget); + } // Vertical layout for name and description QVBoxLayout *textLayout = new QVBoxLayout(); - // App name + // App name with sponsor indicator + QHBoxLayout *nameLayout = new QHBoxLayout(); QLabel *nameLabel = new QLabel(name); nameLabel->setStyleSheet("font-size: 16px;"); - nameLabel->setAlignment(Qt::AlignCenter); nameLabel->setWordWrap(true); - textLayout->addWidget(nameLabel); + nameLayout->addWidget(nameLabel); + + // Add sponsor type indicator + if (!sponsorType.isEmpty()) { + QLabel *sponsorLabel = new QLabel(sponsorType.name); + QString textColor = (sponsorType.level == SponsorType::Platinum || + sponsorType.level == SponsorType::Silver) + ? "#333" + : "white"; + sponsorLabel->setStyleSheet(QString("font-size: 10px; " + "font-weight: bold; " + "color: %1; " + "background-color: %2; " + "border-radius: 4px; " + "padding: 2px 6px; " + "margin-left: 8px;") + .arg(textColor, sponsorType.color)); + 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::AlignCenter); + descLabel->setAlignment(Qt::AlignLeft); descLabel->setWordWrap(true); textLayout->addWidget(descLabel); - cardLayout->addStretch(); cardLayout->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); + if (!bundleId.isEmpty()) { + 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 *downloadIpaLabel = new ZLabel("Download IPA"); - downloadIpaLabel->setAlignment(Qt::AlignCenter); - downloadIpaLabel->setCursor(Qt::PointingHandCursor); + 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(installLabel, &ZLabel::clicked, this, - [this, name, bundleId, description]() { - onAppCardClicked(name, bundleId, description); - }); - - connect(downloadIpaLabel, &ZLabel::clicked, this, + connect( + downloadIpaLabel, &ZLabel::clicked, this, [this, name, bundleId]() { onDownloadIpaClicked(name, bundleId); }); - buttonsLayout->addStretch(); - buttonsLayout->addWidget(installLabel); - buttonsLayout->addWidget(downloadIpaLabel); + 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); @@ -434,7 +753,6 @@ void AppsWidget::onSearchTextChanged() { m_debounceTimer->start(300); } void AppsWidget::performSearch() { - QString searchTerm = m_searchEdit->text().trimmed(); if (searchTerm.isEmpty()) { showDefaultApps(); @@ -510,7 +828,8 @@ void AppsWidget::onSearchFinished(bool success, const QString &results) QString bundleId = appObj.value("bundleId").toString(); QString description = "Version: " + appObj.value("version").toString(); - createAppCard(name, bundleId, description, "", gridLayout, row, col); + createAppCard(name, bundleId, description, "", "", gridLayout, row, + col); col++; if (col >= maxCols) { diff --git a/src/appswidget.h b/src/appswidget.h index 9c13b4d..469dac1 100644 --- a/src/appswidget.h +++ b/src/appswidget.h @@ -9,8 +9,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -20,16 +22,52 @@ #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); - -private slots: - void onLoginClicked(); + static AppsWidget *sharedInstance(); void onAppCardClicked(const QString &appName, const QString &bundleId, const QString &description); +private slots: + void onLoginClicked(); void onDownloadIpaClicked(const QString &name, const QString &bundleId); void onSearchTextChanged(); void performSearch(); @@ -39,8 +77,10 @@ private slots: private: void setupUI(); void createAppCard(const QString &name, const QString &bundleId, - const QString &description, const QString &iconPath, - QGridLayout *gridLayout, int row, int col); + const QString &description, const QString &logoUrl, + const QString &websiteUrl, QGridLayout *gridLayout, + int row, int col, + const SponsorType &sponsorType = SponsorType()); void setupDefaultAppsPage(); void setupLoadingPage(); void setupErrorPage(); @@ -49,6 +89,7 @@ private: void showError(const QString &message); void clearAppGrid(); void populateDefaultApps(); + void createSponsorCard(QGridLayout *gridLayout, int row, int col); QStackedWidget *m_stackedWidget; QWidget *m_defaultAppsPage; @@ -63,11 +104,18 @@ private: 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; }; #endif // APPSWIDGET_H diff --git a/src/core/services/get_mounted_image.cpp b/src/core/services/get_mounted_image.cpp index 0f8bd70..ac5e097 100644 --- a/src/core/services/get_mounted_image.cpp +++ b/src/core/services/get_mounted_image.cpp @@ -41,7 +41,7 @@ plist_t _get_mounted_image(const char *udid) mobile_image_mounter_error_t err = MOBILE_IMAGE_MOUNTER_E_UNKNOWN_ERROR; plist_t result = NULL; size_t sig_length = 0; - char *imagetype = "Developer"; + const char *imagetype = "Developer"; if (IDEVICE_E_SUCCESS != idevice_new_with_options(&device, udid, diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp index 2b051fd..6bd5a0d 100644 --- a/src/core/services/init_device.cpp +++ b/src/core/services/init_device.cpp @@ -174,6 +174,15 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, d.firmwareVersion = safeGet("FirmwareVersion"); d.productVersion = safeGet("ProductVersion"); + QString q_version = QString::fromStdString(d.productVersion); + QStringList parts = q_version.split('.'); + + int major = (parts.length() > 0) ? parts[0].toInt() : 0; + int minor = (parts.length() > 1) ? parts[1].toInt() : 0; + int patch = (parts.length() > 2) ? parts[2].toInt() : 0; + + d.parsedDeviceVersion = IDESCRIPTOR_DEVICE_VERSION(major, minor, patch); + /*DiskInfo*/ try { d.diskInfo.totalDiskCapacity = diff --git a/src/core/services/mount_dev_image.cpp b/src/core/services/mount_dev_image.cpp index 5a9a68a..0443e23 100644 --- a/src/core/services/mount_dev_image.cpp +++ b/src/core/services/mount_dev_image.cpp @@ -59,17 +59,14 @@ static const char *imagetype = NULL; static const char PKG_PATH[] = "PublicStaging"; static const char PATH_PREFIX[] = "/private/var/mobile/Media"; -#ifndef SOURCE_DIR -#define SOURCE_DIR "." -#endif - static ssize_t mim_upload_cb(void *buf, size_t size, void *userdata) { return fread(buf, 1, size, (FILE *)userdata); } // extend the mobile_image_mounter_error_t type and return sucess if there is // already a disk image -mobile_image_mounter_error_t mount_dev_image(const char *udid, +mobile_image_mounter_error_t mount_dev_image(idevice_t device, + unsigned int device_version, const char *image_dir_path) { mobile_image_mounter_client_t mim = NULL; @@ -79,7 +76,6 @@ mobile_image_mounter_error_t mount_dev_image(const char *udid, lockdownd_error_t ldret = LOCKDOWN_E_UNKNOWN_ERROR; afc_client_t afc = NULL; lockdownd_service_descriptor_t service = NULL; - idevice_t device = NULL; char *image_path = NULL; char *image_sig_path = NULL; FILE *f = NULL; @@ -87,7 +83,6 @@ mobile_image_mounter_error_t mount_dev_image(const char *udid, plist_t mount_options = NULL; char *targetname = NULL; char *mountname = NULL; - unsigned int device_version = 0; disk_image_upload_type_t disk_image_upload_type = DISK_IMAGE_UPLOAD_TYPE_AFC; @@ -95,14 +90,6 @@ mobile_image_mounter_error_t mount_dev_image(const char *udid, plist_t result = NULL; size_t sig_length = 0; - if (IDEVICE_E_SUCCESS != - idevice_new_with_options(&device, udid, IDEVICE_LOOKUP_USBMUX)) { - qDebug() << "ERROR: Could not create idevice!"; - res = -1; - goto leave; - } - - device_version = get_device_version(device); if (LOCKDOWN_E_SUCCESS != (ldret = lockdownd_client_new_with_handshake( device, &lckd, TOOL_NAME))) { qDebug() << "ERROR: Could not connect to lockdownd service!"; @@ -110,7 +97,7 @@ mobile_image_mounter_error_t mount_dev_image(const char *udid, goto leave; } - if (device_version >= IDEVICE_DEVICE_VERSION(7, 0, 0)) { + if (device_version >= IDESCRIPTOR_DEVICE_VERSION(7, 0, 0)) { disk_image_upload_type = DISK_IMAGE_UPLOAD_TYPE_UPLOAD_IMAGE; } @@ -152,7 +139,7 @@ mobile_image_mounter_error_t mount_dev_image(const char *udid, qDebug() << "Using image:" << image_path; qDebug() << "Using signature:" << image_sig_path; - if (device_version >= IDEVICE_DEVICE_VERSION(16, 0, 0)) { + if (device_version >= IDESCRIPTOR_DEVICE_VERSION(16, 0, 0)) { uint8_t dev_mode_status = 0; plist_t val = NULL; ldret = lockdownd_get_value(lckd, "com.apple.security.mac.amfi", @@ -195,14 +182,14 @@ mobile_image_mounter_error_t mount_dev_image(const char *udid, goto leave; } image_size = fst.st_size; - if (device_version < IDEVICE_DEVICE_VERSION(17, 0, 0) && + if (device_version < IDESCRIPTOR_DEVICE_VERSION(17, 0, 0) && stat(image_sig_path, &fst) != 0) { qDebug() << "ERROR: stat:" << image_sig_path << ":" << strerror(errno); res = -1; goto leave; } - if (device_version < IDEVICE_DEVICE_VERSION(17, 0, 0)) { + if (device_version < IDESCRIPTOR_DEVICE_VERSION(17, 0, 0)) { f = fopen(image_sig_path, "rb"); if (!f) { qDebug() << "Error opening signature file" << image_sig_path << ":" @@ -604,9 +591,6 @@ leave: if (lckd) { lockdownd_client_free(lckd); } - if (device) { - idevice_free(device); - } if (image_path) { free(image_path); } diff --git a/src/devdiskimagehelper.cpp b/src/devdiskimagehelper.cpp index 18da300..61220c4 100644 --- a/src/devdiskimagehelper.cpp +++ b/src/devdiskimagehelper.cpp @@ -70,20 +70,23 @@ void DevDiskImageHelper::setupUI() void DevDiskImageHelper::start() { - // if (m_mode == Mode::AutoMount) { - // hide(); - // } else { - // show(); - // } - m_loadingIndicator->start(); showStatus("Please wait..."); - // Start download check in background - DevDiskManager::sharedInstance()->downloadCompatibleImage(m_device); + unsigned int device_version = get_device_version(m_device->device); + unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; + unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF; - // Check mounted image status - QTimer::singleShot(500, this, &DevDiskImageHelper::checkAndMount); + // we dont have developer disk images for ios 6 and below + if (deviceMajorVersion > 5) { + // TODO: maybe check isMountAvailable and finishWithFailure if false + const bool isMountAvailable = + DevDiskManager::sharedInstance()->downloadCompatibleImage(m_device); + QTimer::singleShot(500, this, &DevDiskImageHelper::checkAndMount); + } else { + finishWithSuccess(); + return; + } } void DevDiskImageHelper::checkAndMount() @@ -91,7 +94,8 @@ void DevDiskImageHelper::checkAndMount() GetMountedImageResult result = DevDiskManager::sharedInstance()->getMountedImage( m_device->udid.c_str()); - + qDebug() << "checkAndMount result:" << result.success + << result.message.c_str() << QString::fromStdString(result.sig); if (!result.success) { showRetryUI(QString::fromStdString(result.message)); return; @@ -103,6 +107,8 @@ void DevDiskImageHelper::checkAndMount() finishWithSuccess(); return; } + + onMountButtonClicked(); } void DevDiskImageHelper::onMountButtonClicked() @@ -139,8 +145,8 @@ void DevDiskImageHelper::onMountButtonClicked() showStatus("Mounting developer disk image..."); mobile_image_mounter_error_t err = - DevDiskManager::sharedInstance()->mountImage( - versionToMount, QString::fromStdString(m_device->udid)); + DevDiskManager::sharedInstance()->mountImage(versionToMount, + m_device); m_isMounting = false; if (err == MOBILE_IMAGE_MOUNTER_E_SUCCESS) { @@ -199,8 +205,7 @@ void DevDiskImageHelper::onImageDownloadFinished(const QString &version, showStatus("Download complete. Mounting..."); mobile_image_mounter_error_t err = - DevDiskManager::sharedInstance()->mountImage( - version, QString::fromStdString(m_device->udid)); + DevDiskManager::sharedInstance()->mountImage(version, m_device); if (err == MOBILE_IMAGE_MOUNTER_E_SUCCESS) { showStatus("Developer disk image mounted successfully"); diff --git a/src/devdiskimageswidget.cpp b/src/devdiskimageswidget.cpp index 628e22e..86bfd37 100644 --- a/src/devdiskimageswidget.cpp +++ b/src/devdiskimageswidget.cpp @@ -51,7 +51,6 @@ DevDiskImagesWidget::DevDiskImagesWidget(iDescriptorDevice *device, [this](QListWidgetItem *item) { m_mountButton->setEnabled(item != nullptr); }); - // connect } void DevDiskImagesWidget::setupUi() @@ -76,25 +75,6 @@ void DevDiskImagesWidget::setupUi() mountLayout->addWidget(m_check_mountedButton); layout->addLayout(mountLayout); - auto *pathLayout = new QHBoxLayout(); - // main path/info row (no shadow) - auto *pathWidget = new QWidget(); - pathWidget->setLayout(pathLayout); - QLabel *tipLabel = - new QLabel("You can change the download path from settings :"); - tipLabel->setStyleSheet("font-size: 9px;"); - pathLayout->addWidget(tipLabel); - QPushButton *openSettingsButton = new QPushButton("Open Settings"); - openSettingsButton->setSizePolicy(QSizePolicy::Preferred, - QSizePolicy::Preferred); - pathLayout->addWidget(openSettingsButton); - pathLayout->addStretch(); - connect(openSettingsButton, &QPushButton::clicked, this, [this]() { - SettingsManager::sharedInstance()->showSettingsDialog(); - }); - pathLayout->setContentsMargins(10, 10, 10, 10); - layout->addWidget(pathWidget); - m_stackedWidget = new QStackedWidget(this); layout->addWidget(m_stackedWidget); @@ -128,13 +108,13 @@ void DevDiskImagesWidget::setupUi() m_stackedWidget->addWidget(m_imageListWidget); - if (DevDiskManager::sharedInstance()->isImageListReady()) { + 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); - } else { - m_processIndicator->start(); - m_stackedWidget->setCurrentIndex(0); // Show loading page - } + }); } void DevDiskImagesWidget::fetchImages() @@ -258,6 +238,7 @@ void DevDiskImagesWidget::displayImages() auto *downloadButton = new QPushButton(info.isDownloaded ? "Re-download" : "Download"); + downloadButton->setDefault(true); downloadButton->setProperty("version", info.version); connect(downloadButton, &QPushButton::clicked, this, &DevDiskImagesWidget::onDownloadButtonClicked); @@ -584,7 +565,7 @@ void DevDiskImagesWidget::mountImage(const QString &version) m_mountButton->setText("Mounting..."); mobile_image_mounter_error_t err = - DevDiskManager::sharedInstance()->mountImage(version, udid); + DevDiskManager::sharedInstance()->mountImage(version, m_currentDevice); auto updateUI = [&]() { m_mountButton->setEnabled(true); diff --git a/src/devdiskmanager.cpp b/src/devdiskmanager.cpp index ca1e56a..797a9aa 100644 --- a/src/devdiskmanager.cpp +++ b/src/devdiskmanager.cpp @@ -23,29 +23,55 @@ DevDiskManager *DevDiskManager::sharedInstance() DevDiskManager::DevDiskManager(QObject *parent) : QObject{parent} { m_networkManager = new QNetworkAccessManager(this); - m_isImageListReady = false; // Explicitly set initial state - fetchImageList(); + populateImageList(); } -QNetworkReply *DevDiskManager::fetchImageList() +/* + * 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(":/resources/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."; + } + } + // TODO:change url QUrl url("https://raw.githubusercontent.com/uncor3/resources/refs/heads/" "main/DeveloperDiskImages.json"); QNetworkRequest request(url); auto *reply = m_networkManager->get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - if (reply->error() != QNetworkReply::NoError) { - emit imageListFetched(false, reply->errorString()); - } else { + connect(reply, &QNetworkReply::finished, this, [this, localPath, reply]() { + if (reply->error() == QNetworkReply::NoError) { + // FIXME: better have this settings + QDir().mkdir(QDir::homePath() + "/.idescriptor"); m_imageListJsonData = reply->readAll(); - m_isImageListReady = true; // Set the flag on success - emit imageListFetched(true); + QFile file(localPath); + if (file.open(QIODevice::WriteOnly)) { + file.write(m_imageListJsonData); + file.close(); + } } reply->deleteLater(); }); - - return reply; } QMap> DevDiskManager::parseDiskDir() @@ -291,9 +317,8 @@ bool DevDiskManager::isImageDownloaded(const QString &version, return QFile::exists(dmgPath) && QFile::exists(sigPath); } -bool DevDiskManager::downloadCompatibleImageInternal(iDescriptorDevice *device) +bool DevDiskManager::downloadCompatibleImage(iDescriptorDevice *device) { - unsigned int device_version = get_device_version(device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF; @@ -353,33 +378,8 @@ bool DevDiskManager::downloadCompatibleImageInternal(iDescriptorDevice *device) return false; } -bool DevDiskManager::downloadCompatibleImage(iDescriptorDevice *device) -{ - if (m_isImageListReady) { - // If the list is already fetched, run the logic immediately. - return downloadCompatibleImageInternal(device); - } else { - // Otherwise, connect to the signal and wait. - qDebug() << "Image list not ready, waiting for it to be fetched..."; - connect( - this, &DevDiskManager::imageListFetched, this, - [this, device](bool success) { - if (success) { - qDebug() << "Image list is now ready. Retrying download..."; - downloadCompatibleImageInternal(device); - } else { - qDebug() << "Failed to fetch image list. Cannot download."; - } - }, - Qt::SingleShotConnection); - - // The operation is now asynchronous, the immediate return value - // indicates that the process has started. - return true; - } -} - -bool DevDiskManager::mountCompatibleImageInternal(iDescriptorDevice *device) +// FIXME:DOES NOT CHECK IF THERE IS ALREADY AN IMAGE MOUNTED +bool DevDiskManager::mountCompatibleImage(iDescriptorDevice *device) { unsigned int device_version = get_device_version(device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; @@ -400,7 +400,7 @@ bool DevDiskManager::mountCompatibleImageInternal(iDescriptorDevice *device) qDebug() << "Attempting to mount image version" << info.version << "on device:" << device->udid.c_str(); if (MOBILE_IMAGE_MOUNTER_E_SUCCESS == - mountImage(info.version, device->udid.c_str())) { + mountImage(info.version, device)) { qDebug() << "Mounted existing image version" << info.version << "on device:" << device->udid.c_str(); return true; @@ -434,7 +434,7 @@ bool DevDiskManager::mountCompatibleImageInternal(iDescriptorDevice *device) if (success && finishedVersion == versionToDownload) { qDebug() << "Download finished for" << finishedVersion << ". Now attempting to mount."; - mountImage(finishedVersion, device->udid.c_str()); + mountImage(finishedVersion, device); // TODO: You might want to emit another signal here to // notify the UI of the final mount result. } else if (!success) { @@ -473,33 +473,9 @@ bool DevDiskManager::mountCompatibleImageInternal(iDescriptorDevice *device) return false; } -// FIXME:DOES NOT CHECK IF THERE IS ALREADY AN IMAGE MOUNTED -bool DevDiskManager::mountCompatibleImage(iDescriptorDevice *device) -{ - if (m_isImageListReady) { - // If the list is already fetched, run the logic immediately. - return mountCompatibleImageInternal(device); - } else { - // Otherwise, connect to the signal and wait. - qDebug() << "Image list not ready, waiting for it to be fetched..."; - connect( - this, &DevDiskManager::imageListFetched, this, - [this, device](bool success) { - if (success) { - qDebug() << "Image list is now ready. Retrying mount..."; - mountCompatibleImageInternal(device); - } else { - qDebug() << "Failed to fetch image list. Cannot mount."; - } - }, - Qt::SingleShotConnection); - return true; - } -} - -mobile_image_mounter_error_t DevDiskManager::mountImage(const QString &version, - const QString &udid) +mobile_image_mounter_error_t +DevDiskManager::mountImage(const QString &version, iDescriptorDevice *device) { const QString downloadPath = SettingsManager::sharedInstance()->devdiskimgpath(); @@ -508,7 +484,8 @@ mobile_image_mounter_error_t DevDiskManager::mountImage(const QString &version, } QString versionPath = QDir(downloadPath).filePath(version); - return mount_dev_image(udid.toUtf8().constData(), + return mount_dev_image(device->device, + device->deviceInfo.parsedDeviceVersion, versionPath.toUtf8().constData()); } @@ -632,6 +609,14 @@ bool DevDiskManager::compareSignatures(const char *signature_file_path, return matches; } +/* + older devices return something like this: + { + "ImagePresent": true, + "ImageSignature": <7b16200b 2ead1830 a59809d1 51e9060b ... 8a 9844eb07 + e0b8e0>, "Status": "Complete" + } +*/ GetMountedImageResult DevDiskManager::getMountedImage(const char *udid) { /* @@ -640,6 +625,7 @@ GetMountedImageResult DevDiskManager::getMountedImage(const char *udid) dictionary */ plist_t result = _get_mounted_image(udid); + plist_print(result); const char *lockedErr = "DeviceLocked"; PlistNavigator r = PlistNavigator(result); @@ -655,6 +641,22 @@ GetMountedImageResult DevDiskManager::getMountedImage(const char *udid) return GetMountedImageResult{false, "", "Unknown error"}; } + // for older devices + bool image_present = r["ImagePresent"].getBool(); + + /* FIXME: returning a madeup sig because iDescriptorTool::MountDevImage + * depends on it in toolboxwidget.cpp we can read the actual signature from + * the device but it’s not really necessary since we + * just need to know if there is an image mounted or + * not also we would need to check the ios version + * to know whether to treat ImageSignature as plist array or data + */ + if (image_present) { + plist_free(result); + return GetMountedImageResult{true, "FIXME", + "There is already an image mounted."}; + } + plist_t sig_array_node = r["ImageSignature"].getNode(); if (sig_array_node == NULL) { @@ -680,6 +682,4 @@ GetMountedImageResult DevDiskManager::getMountedImage(const char *udid) true, "", "No disk image mounted (No signature found)"}; } return GetMountedImageResult{true, mounted_sig_str, "Success"}; -} - -bool DevDiskManager::isImageListReady() const { return m_isImageListReady; } \ No newline at end of file +} \ No newline at end of file diff --git a/src/devdiskmanager.h b/src/devdiskmanager.h index 294555f..ca5b72c 100644 --- a/src/devdiskmanager.h +++ b/src/devdiskmanager.h @@ -17,9 +17,6 @@ public: explicit DevDiskManager(QObject *parent = nullptr); static DevDiskManager *sharedInstance(); - // TODO:public or private? - // Image list management - QNetworkReply *fetchImageList(); QList parseImageList(int deviceMajorVersion, int deviceMinorVersion, const char *mounted_sig, @@ -35,7 +32,7 @@ public: // Mount operations mobile_image_mounter_error_t mountImage(const QString &version, - const QString &udid); + iDescriptorDevice *device); bool unmountImage(); // Signature comparison @@ -46,7 +43,6 @@ public: GetMountedImageResult getMountedImage(const char *udid); bool mountCompatibleImage(iDescriptorDevice *device); bool downloadCompatibleImage(iDescriptorDevice *device); - bool isImageListReady() const; signals: void imageListFetched(bool success, @@ -76,14 +72,11 @@ private: QMap m_activeDownloads; QMap> parseDiskDir(); - // TODO:move this to header - bool m_isImageListReady = false; QList getImagesSorted(QMap> imageFiles, int deviceMajorVersion, int deviceMinorVersion, const char *mounted_sig, uint64_t mounted_sig_len); - bool mountCompatibleImageInternal(iDescriptorDevice *device); - bool downloadCompatibleImageInternal(iDescriptorDevice *device); + void populateImageList(); }; #endif // DEVDISKMANAGER_H diff --git a/src/deviceimagewidget.cpp b/src/deviceimagewidget.cpp index 1ea2012..f8da224 100644 --- a/src/deviceimagewidget.cpp +++ b/src/deviceimagewidget.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -11,12 +12,13 @@ DeviceImageWidget::DeviceImageWidget(iDescriptorDevice *device, QWidget *parent) { 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(); @@ -46,11 +48,15 @@ void DeviceImageWidget::setupDeviceImage() QString DeviceImageWidget::getDeviceMockupPath() const { - QString displayName = - QString::fromStdString(m_device->deviceInfo.productType); - QString mockupName = getMockupNameFromDisplayName(displayName); + 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(mockupName); + return QString(":/resources/iphone-mockups/iphone-%1.png") + .arg(m_mockupName); } QString DeviceImageWidget::getWallpaperPath() const @@ -96,7 +102,6 @@ QString DeviceImageWidget::getWallpaperPath() const // For future versions, use the latest available wallpaper wallpaperVersion = "ios26"; } - return QString(":/resources/ios-wallpapers/iphone-%1.png") .arg(wallpaperVersion); } @@ -128,9 +133,10 @@ QString DeviceImageWidget::getMockupNameFromDisplayName( return "4"; } else if (displayName.contains("iPhone 3", Qt::CaseInsensitive)) { return "3"; + } else if (displayName.contains("iPad", Qt::CaseInsensitive)) { + return "iPad"; } else { - // Unknown device, use iPhone X as default - return "x"; + return "unknown"; } } @@ -227,62 +233,92 @@ QPixmap DeviceImageWidget::createCompositeImage() const return mockup; // Return just the mockup } - // Start with the mockup as the base layer - QPixmap composite = mockup.copy(); + // Create composite with mockup dimensions + QPixmap composite(mockup.size()); + composite.fill(Qt::transparent); + QPainter painter(&composite); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform); - // Use pre-calculated screen areas for optimal performance QRect screenRect; - QString mockupName = getMockupNameFromDisplayName( - QString::fromStdString(m_device->deviceInfo.productType)); + // QString mockupName = "16"; - if (mockupName == "3") { + bool useRoundedCorners = false; + int cornerRadius = 45; + bool isUnknown = (m_mockupName == "unknown"); + + if (m_mockupName == "3") { screenRect = QRect(145, 72, 209, 310); - } else if (mockupName == "4") { + } else if (m_mockupName == "4") { screenRect = QRect(414, 181, 380, 548); - } else if (mockupName == "5") { + } else if (m_mockupName == "5") { screenRect = QRect(27, 106, 304, 537); - } else if (mockupName == "6") { + } else if (m_mockupName == "6") { screenRect = QRect(68, 348, 1279, 2270); - } else if (mockupName == "x") { - screenRect = QRect(245, 429, 2389, 5003); - } else if (mockupName == "15") { - screenRect = QRect(15, 49, 337, 688); - } else if (mockupName == "16") { - screenRect = QRect(17, 54, 333, 682); + } else if (m_mockupName == "x") { + screenRect = QRect(245, 200, 2389, 5303); + useRoundedCorners = true; + } else if (m_mockupName == "15") { + screenRect = QRect(15, 18, 337, 715); + useRoundedCorners = true; + } else if (m_mockupName == "16") { + screenRect = QRect(17, 18, 333, 715); + useRoundedCorners = true; + } else if (m_mockupName == "iPad") { + screenRect = QRect(33, 36, 471, 680); + } else if (m_mockupName == "unknown") { + screenRect = QRect(33, 36, 471, 680); } else { - // Fallback for unknown devices screenRect = QRect(mockup.width() * 0.12, mockup.height() * 0.08, mockup.width() * 0.76, mockup.height() * 0.84); } - // Draw wallpaper BEHIND the mockup (into the screen area) 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); - // Draw current time in the center of the screen + 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"); - // Setup text rendering with better font sizing QFont timeFont; timeFont.setFamily("SF Pro Display, Helvetica, Arial"); - - // Scale font size based on screen dimensions int fontSize = screenRect.width() / 5; timeFont.setPointSize(fontSize); timeFont.setWeight(QFont::Light); - painter.setFont(timeFont); - // Draw text shadow for better readability - painter.setPen(QColor(0, 0, 0, 150)); - painter.drawText(screenRect.adjusted(2, 2, 2, 2), Qt::AlignCenter, - currentTime); - - // Draw main text - perfectly centered in the screen area painter.setPen(QColor(255, 255, 255, 255)); painter.drawText(screenRect, Qt::AlignCenter, currentTime); diff --git a/src/deviceimagewidget.h b/src/deviceimagewidget.h index a1bc10b..7f68cad 100644 --- a/src/deviceimagewidget.h +++ b/src/deviceimagewidget.h @@ -19,6 +19,7 @@ private slots: void updateTime(); private: + QString m_mockupName; void setupDeviceImage(); QString getDeviceMockupPath() const; QString getWallpaperPath() const; diff --git a/src/devicemanagerwidget.cpp b/src/devicemanagerwidget.cpp index d5c2a53..de85b18 100644 --- a/src/devicemanagerwidget.cpp +++ b/src/devicemanagerwidget.cpp @@ -18,7 +18,10 @@ DeviceManagerWidget::DeviceManagerWidget(QWidget *parent) // Apply settings-based behavior for switching to new device SettingsManager::sharedInstance()->doIfEnabled( SettingsManager::Setting::SwitchToNewDevice, - [this, device]() { setCurrentDevice(device->udid); }); + [this, device]() { + AppContext::sharedInstance()->setCurrentDeviceSelection( + DeviceSelection(device->udid)); + }); emit updateNoDevicesConnected(); }); @@ -26,6 +29,10 @@ DeviceManagerWidget::DeviceManagerWidget(QWidget *parent) connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, [this](const std::string &uuid) { removeDevice(uuid); + auto devices = AppContext::sharedInstance()->getAllDevices(); + if (!devices.isEmpty()) + AppContext::sharedInstance()->setCurrentDeviceSelection( + DeviceSelection(devices.first()->udid)); emit updateNoDevicesConnected(); }); @@ -44,6 +51,13 @@ DeviceManagerWidget::DeviceManagerWidget(QWidget *parent) connect(AppContext::sharedInstance(), &AppContext::devicePaired, this, [this](iDescriptorDevice *device) { addPairedDevice(device); + SettingsManager::sharedInstance()->doIfEnabled( + SettingsManager::Setting::SwitchToNewDevice, + [this, device]() { + AppContext::sharedInstance()->setCurrentDeviceSelection( + DeviceSelection(device->udid)); + }); + emit updateNoDevicesConnected(); }); @@ -107,12 +121,6 @@ void DeviceManagerWidget::addDevice(iDescriptorDevice *device) m_stackedWidget->addWidget(deviceWidget); m_deviceWidgets[device->udid] = std::pair{deviceWidget, m_sidebar->addDevice(tabTitle, device->udid)}; - - // todo - // If this is the first device, make it current - // if (m_currentDeviceIndex == -1) { - // setCurrentDevice(deviceIndex); - // } } void DeviceManagerWidget::addRecoveryDevice( @@ -248,10 +256,10 @@ void DeviceManagerWidget::removeDevice(const std::string &uuid) m_sidebar->removeDevice(uuid); deviceWidget->deleteLater(); - // TODO: - if (m_deviceWidgets.count() > 0) { - setCurrentDevice(m_deviceWidgets.firstKey()); - } + // // TODO: + // if (m_deviceWidgets.count() > 0) { + // setCurrentDevice(m_deviceWidgets.firstKey()); + // } } } @@ -268,13 +276,13 @@ void DeviceManagerWidget::setCurrentDevice(const std::string &uuid) m_currentDeviceUuid = uuid; - // Update stacked widget QWidget *widget = m_deviceWidgets[uuid].first; m_stackedWidget->setCurrentWidget(widget); - // Update sidebar selection through the AppContext to keep state consistent - AppContext::sharedInstance()->setCurrentDeviceSelection( - DeviceSelection(uuid)); + // This creates a feedback loop. The widget should only react to state + // changes from AppContext, not create them here. + // AppContext::sharedInstance()->setCurrentDeviceSelection( + // DeviceSelection(uuid)); } std::string DeviceManagerWidget::getCurrentDevice() const diff --git a/src/fileexplorerwidget.cpp b/src/fileexplorerwidget.cpp index 6ecb621..efefa8b 100644 --- a/src/fileexplorerwidget.cpp +++ b/src/fileexplorerwidget.cpp @@ -164,8 +164,10 @@ 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); } diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h index 98f6094..df20fec 100644 --- a/src/iDescriptor-ui.h +++ b/src/iDescriptor-ui.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -104,16 +105,18 @@ public: } } }; -class ZIconWidget : public QWidget + +class ZIconWidget : public QAbstractButton { Q_OBJECT public: ZIconWidget(const QIcon &icon, const QString &tooltip, QWidget *parent = nullptr) - : QWidget(parent), m_icon(icon), m_iconSize(24, 24), m_pressed(false) + : QAbstractButton(parent), m_icon(icon) { setToolTip(tooltip); setFixedSize(32, 32); + setIconSize(QSize(24, 24)); setCursor(Qt::PointingHandCursor); connect(qApp, &QApplication::paletteChanged, this, [this]() { update(); }); @@ -124,14 +127,6 @@ public: m_icon = ZIcon(icon); update(); } - void setIconSize(const QSize &size) - { - m_iconSize = size; - update(); - } - -signals: - void clicked(); protected: void paintEvent(QPaintEvent *event) override @@ -141,59 +136,23 @@ protected: painter.setRenderHint(QPainter::Antialiasing); // Draw background circle when hovered or pressed - if (underMouse() || m_pressed) { + if (underMouse() || isDown()) { QColor bgColor = palette().color(QPalette::Highlight); - bgColor.setAlpha(m_pressed ? 60 : 30); + bgColor.setAlpha(isDown() ? 60 : 30); painter.setBrush(bgColor); painter.setPen(Qt::NoPen); painter.drawEllipse(rect().adjusted(2, 2, -2, -2)); } QRect iconRect = rect(); - iconRect.setSize(m_iconSize); + iconRect.setSize(iconSize()); // Use iconSize() from QAbstractButton iconRect.moveCenter(rect().center()); m_icon.paint(&painter, iconRect, palette()); } - void mousePressEvent(QMouseEvent *event) override - { - if (event->button() == Qt::LeftButton) { - m_pressed = true; - update(); - } - QWidget::mousePressEvent(event); - } - - void mouseReleaseEvent(QMouseEvent *event) override - { - if (event->button() == Qt::LeftButton && m_pressed) { - m_pressed = false; - update(); - if (rect().contains(event->pos())) { - emit clicked(); - } - } - QWidget::mouseReleaseEvent(event); - } - - void enterEvent(QEnterEvent *event) override - { - Q_UNUSED(event) - update(); - } - - void leaveEvent(QEvent *event) override - { - Q_UNUSED(event) - m_pressed = false; - update(); - } - private: ZIcon m_icon; - QSize m_iconSize; - bool m_pressed; }; enum class iDescriptorTool { diff --git a/src/iDescriptor.h b/src/iDescriptor.h index ee9f995..f6967a7 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -16,8 +17,8 @@ #define TOOL_NAME "iDescriptor" #define APP_LABEL "iDescriptor" -#define APP_VERSION "0.0.1" -#define APP_COPYRIGHT "© 2023 Uncore. All rights reserved." +#define APP_VERSION "0.1.0" +#define APP_COPYRIGHT "© 2025 Uncore. All rights reserved." #define AFC2_SERVICE_NAME "com.apple.afc2" #define RECOVERY_CLIENT_CONNECTION_TRIES 3 #define APPLE_VENDOR_ID 0x05ac @@ -138,6 +139,7 @@ struct DeviceInfo { std::string marketingName; std::string regionRaw; std::string region; + unsigned int parsedDeviceVersion; }; struct iDescriptorDevice { @@ -292,9 +294,9 @@ bool shutdown(idevice_t device); TakeScreenshotResult take_screenshot(screenshotr_client_t shotr); -mobile_image_mounter_error_t mount_dev_image(const char *udid, +mobile_image_mounter_error_t mount_dev_image(idevice_t device, + unsigned int device_version, const char *image_dir_path); - struct GetMountedImageResult { bool success; std::string sig; @@ -404,14 +406,8 @@ bool isDarkMode(); instproxy_error_t install_IPA(idevice_t device, afc_client_t afc, const char *filePath); -typedef struct _idevice_private { - char *udid; - uint32_t mux_id; - enum idevice_connection_type conn_type; - void *conn_data; - int version; - int device_class; -}; +#define IDESCRIPTOR_DEVICE_VERSION(maj, min, patch) \ + ((((maj) & 0xFF) << 16) | (((min) & 0xFF) << 8) | ((patch) & 0xFF)) /* we need this because idevice_get_device_version @@ -420,9 +416,45 @@ typedef struct _idevice_private { */ inline unsigned int get_device_version(idevice_t _device) { - _idevice_private *idevice = reinterpret_cast<_idevice_private *>(_device); - if (!idevice) { + if (!_device) { return 0; } - return static_cast(idevice->version); + + lockdownd_client_t lockdown = NULL; + if (lockdownd_client_new_with_handshake( + _device, &lockdown, "iDescriptor") != LOCKDOWN_E_SUCCESS) { + return 0; + } + + plist_t node = NULL; + if (lockdownd_get_value(lockdown, NULL, "ProductVersion", &node) != + LOCKDOWN_E_SUCCESS) { + lockdownd_client_free(lockdown); + return 0; + } + + unsigned int version_number = 0; + if (node && plist_get_node_type(node) == PLIST_STRING) { + char *version_string = NULL; + plist_get_string_val(node, &version_string); + if (version_string) { + QString q_version = QString(version_string); + QStringList parts = q_version.split('.'); + + int major = (parts.length() > 0) ? parts[0].toInt() : 0; + int minor = (parts.length() > 1) ? parts[1].toInt() : 0; + int patch = (parts.length() > 2) ? parts[2].toInt() : 0; + + version_number = IDESCRIPTOR_DEVICE_VERSION(major, minor, patch); + + free(version_string); + } + } + + if (node) { + plist_free(node); + } + lockdownd_client_free(lockdown); + + return version_number; } \ No newline at end of file diff --git a/src/ifusediskunmountbutton.cpp b/src/ifusediskunmountbutton.cpp index 00784b4..08931ff 100644 --- a/src/ifusediskunmountbutton.cpp +++ b/src/ifusediskunmountbutton.cpp @@ -1,14 +1,13 @@ #include "ifusediskunmountbutton.h" +#include "iDescriptor-ui.h" #include #include iFuseDiskUnmountButton::iFuseDiskUnmountButton(const QString &path, QWidget *parent) - : QPushButton{parent} + : ZIconWidget{QIcon(":/resources/icons/ClarityHardDiskSolidAlerted.png"), + "Unmount iFuse at " + path, parent} { - setIcon(QIcon(":/resources/icons/ClarityHardDiskSolidAlerted.png")); - setToolTip("Unmount iFuse at " + path); - setFlat(true); setCursor(Qt::PointingHandCursor); setFixedSize(24, 24); } diff --git a/src/ifusediskunmountbutton.h b/src/ifusediskunmountbutton.h index 1e7573b..eaeedd4 100644 --- a/src/ifusediskunmountbutton.h +++ b/src/ifusediskunmountbutton.h @@ -1,9 +1,9 @@ #ifndef IFUSEDISKUNMOUNTBUTTON_H #define IFUSEDISKUNMOUNTBUTTON_H -#include +#include "iDescriptor-ui.h" -class iFuseDiskUnmountButton : public QPushButton +class iFuseDiskUnmountButton : public ZIconWidget { Q_OBJECT public: diff --git a/src/ifusewidget.cpp b/src/ifusewidget.cpp index 2d969ec..b9f83aa 100644 --- a/src/ifusewidget.cpp +++ b/src/ifusewidget.cpp @@ -112,6 +112,7 @@ void iFuseWidget::updateDeviceComboBox() QList devices = AppContext::sharedInstance()->getAllDevices(); + m_deviceComboBox->blockSignals(true); m_deviceComboBox->clear(); m_deviceComboBox->setEnabled(true); m_mountButton->setEnabled(true); @@ -123,6 +124,7 @@ void iFuseWidget::updateDeviceComboBox() m_deviceComboBox->addItem(displayText, QString::fromStdString(device->udid)); } + m_deviceComboBox->blockSignals(false); // Try to find and select the device passed to the widget int deviceIndex = -1; @@ -404,7 +406,12 @@ void iFuseWidget::updateUI() AppContext::sharedInstance()->getAllDevices(); if (devices.isEmpty()) { - close(); + m_device = nullptr; + m_deviceComboBox->clear(); + m_deviceComboBox->setEnabled(false); + m_mountButton->setEnabled(false); + m_mountPathLabel->setText("No device connected."); + deleteLater(); return; } updateDeviceComboBox(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a9b7403..f4e8154 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -168,7 +168,7 @@ MainWindow::MainWindow(QWidget *parent) // Add tabs with icons QIcon deviceIcon(":/resources/icons/MdiLightningBolt.png"); m_customTabWidget->addTab(m_mainStackedWidget, deviceIcon, "iDevice"); - m_customTabWidget->addTab(new AppsWidget(this), "Apps"); + m_customTabWidget->addTab(AppsWidget::sharedInstance(), "Apps"); m_customTabWidget->addTab(new ToolboxWidget(this), "Toolbox"); auto *jailbrokenWidget = new JailbrokenWidget(this); @@ -185,17 +185,23 @@ MainWindow::MainWindow(QWidget *parent) // Qt::SingleShotConnection); // settings button - QPushButton *settingsButton = new QPushButton(); - settingsButton->setIcon( - QIcon(":/resources/icons/MingcuteSettings7Line.png")); - settingsButton->setToolTip("Settings"); - settingsButton->setFlat(true); + ZIconWidget *settingsButton = new ZIconWidget( + QIcon(":/resources/icons/MingcuteSettings7Line.png"), "Settings"); settingsButton->setCursor(Qt::PointingHandCursor); settingsButton->setFixedSize(24, 24); - connect(settingsButton, &QPushButton::clicked, this, [this]() { + connect(settingsButton, &ZIconWidget::clicked, this, [this]() { SettingsManager::sharedInstance()->showSettingsDialog(); }); + ZIconWidget *githubButton = new ZIconWidget( + QIcon(":/resources/icons/MdiGithub.png"), "iDescriptor on GitHub"); + githubButton->setCursor(Qt::PointingHandCursor); + githubButton->setFixedSize(24, 24); + connect(githubButton, &ZIconWidget::clicked, this, []() { + QDesktopServices::openUrl( + QUrl("https://github.com/uncor3/iDescriptor")); + }); + m_connectedDeviceCountLabel = new QLabel("iDescriptor: no devices"); m_connectedDeviceCountLabel->setContentsMargins(5, 0, 5, 0); m_connectedDeviceCountLabel->setStyleSheet( @@ -204,6 +210,7 @@ MainWindow::MainWindow(QWidget *parent) ui->statusbar->addWidget(m_connectedDeviceCountLabel); ui->statusbar->setContentsMargins(0, 0, 0, 0); ui->statusbar->addPermanentWidget(settingsButton); + ui->statusbar->addPermanentWidget(githubButton); #ifdef __linux__ QList mounted_iFusePaths = iFuseManager::getMountPoints(); @@ -243,6 +250,56 @@ MainWindow::MainWindow(QWidget *parent) } qDebug() << "Subscribed to device events successfully."; createMenus(); + // Example usage with customization + + UpdateProcedure updateProcedure; + + switch (ZUpdater::detectPlatform()) { + // todo: adjust for portable + case Platform::Windows: + updateProcedure = UpdateProcedure{ + true, + false, + true, + "The application will now quit to install the update.", + "Do you want to install the downloaded update now?", + }; + break; + // todo: adjust for pkg managers + case Platform::MacOS: + updateProcedure = UpdateProcedure{ + false, + false, + true, + "The application will now quit to install the update.", + "Do you want to install the downloaded update now?", + }; + break; + // todo: adjust for pkg managers + case Platform::Linux: + updateProcedure = UpdateProcedure{ + false, + true, + false, + "There is new update available", + "Would you like to download it now?", + }; + break; + default: + updateProcedure = UpdateProcedure{ + false, false, false, "", "", + }; + } + + m_updater = new ZUpdater("uncor3/libtest", APP_VERSION, "iDescriptor", + updateProcedure, + false, // isPortable - set to true if running + // portable version on Windows + false, // isPackageManaged - set to true if + // installed via package manager on Linux + this); + qDebug() << "Checking for updates..."; + m_updater->checkForUpdates(); } void MainWindow::createMenus() @@ -280,5 +337,6 @@ MainWindow::~MainWindow() idevice_event_unsubscribe(); irecv_device_event_unsubscribe(context); delete ui; + delete m_updater; sleep(2); // Give some time for cleanup to finish -} \ No newline at end of file +} diff --git a/src/mainwindow.h b/src/mainwindow.h index 7347203..a6c2de9 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,5 +1,7 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H +#include "ZDownloader.h" +#include "ZUpdater.h" #include "customtabwidget.h" #include "devicemanagerwidget.h" #include "iDescriptor.h" @@ -34,5 +36,6 @@ private: DeviceManagerWidget *m_deviceManager; QStackedWidget *m_mainStackedWidget; QLabel *m_connectedDeviceCountLabel; + ZUpdater *m_updater = nullptr; }; #endif // MAINWINDOW_H diff --git a/src/realtimescreenwidget.cpp b/src/realtimescreenwidget.cpp index 5dc1459..9a53aa1 100644 --- a/src/realtimescreenwidget.cpp +++ b/src/realtimescreenwidget.cpp @@ -12,7 +12,7 @@ #include #include #include - +// todo: rename to livescreenwidget RealtimeScreenWidget::RealtimeScreenWidget(iDescriptorDevice *device, QWidget *parent) : QWidget{parent}, m_device(device), m_timer(nullptr), @@ -87,6 +87,11 @@ RealtimeScreenWidget::RealtimeScreenWidget(iDescriptorDevice *device, } }); + const bool initializeScreenshotServiceSuccess = + initializeScreenshotService(false); + if (initializeScreenshotServiceSuccess) + return; + // Start the initialization process - auto-mount mode auto *helper = new DevDiskImageHelper(m_device, this); @@ -97,9 +102,9 @@ RealtimeScreenWidget::RealtimeScreenWidget(iDescriptorDevice *device, if (success) { // for some reason it does not work immediately, so delay a bit - QTimer::singleShot( - 1000, this, - &RealtimeScreenWidget::initializeScreenshotService); + QTimer::singleShot(1000, this, [this]() { + initializeScreenshotService(true); + }); } else { m_statusLabel->setText("Failed to mount developer disk image"); QMessageBox::critical(this, "Mount Failed", @@ -123,7 +128,7 @@ RealtimeScreenWidget::~RealtimeScreenWidget() } } -void RealtimeScreenWidget::initializeScreenshotService() +bool RealtimeScreenWidget::initializeScreenshotService(bool notify) { lockdownd_client_t lockdownClient = nullptr; lockdownd_service_descriptor_t service = nullptr; @@ -136,11 +141,12 @@ void RealtimeScreenWidget::initializeScreenshotService() if (ldret != LOCKDOWN_E_SUCCESS) { m_statusLabel->setText("Failed to connect to lockdown service"); - QMessageBox::critical(this, "Connection Failed", - "Could not connect to lockdown service.\n" - "Error code: " + - QString::number(ldret)); - return; + if (notify) + QMessageBox::critical(this, "Connection Failed", + "Could not connect to lockdown service.\n" + "Error code: " + + QString::number(ldret)); + return false; } lockdownd_error_t lerr = lockdownd_start_service( @@ -151,14 +157,17 @@ void RealtimeScreenWidget::initializeScreenshotService() if (lerr != LOCKDOWN_E_SUCCESS) { m_statusLabel->setText("Failed to start screenshot service"); - QMessageBox::critical( - this, "Service Failed", - "Could not start screenshot service on device.\n" - "Please ensure the developer disk image is properly mounted."); + qDebug() << lerr << "lockdownd_start_service"; + if (notify) + QMessageBox::critical( + this, "Service Failed", + "Could not start screenshot service on device.\n" + "Please ensure the developer disk image is properly " + "mounted."); if (service) { lockdownd_service_descriptor_free(service); } - return; + return false; } screenshotr_error_t screrr = @@ -169,22 +178,25 @@ void RealtimeScreenWidget::initializeScreenshotService() qDebug() << screrr << "screenshotr_client_new"; if (screrr != SCREENSHOTR_E_SUCCESS) { m_statusLabel->setText("Failed to create screenshot client"); - QMessageBox::critical(this, "Client Failed", - "Could not create screenshot client.\n" - "Error code: " + - QString::number(screrr)); - return; + if (notify) + QMessageBox::critical(this, "Client Failed", + "Could not create screenshot client.\n" + "Error code: " + + QString::number(screrr)); + return false; } // Successfully initialized, start capturing m_statusLabel->setText("Capturing at " + QString::number(m_fps) + " FPS"); startCapturing(); - + return true; } catch (const std::exception &e) { m_statusLabel->setText("Exception occurred"); - QMessageBox::critical(this, "Exception", - QString("Exception occurred: %1").arg(e.what())); + if (notify) + QMessageBox::critical( + this, "Exception", + QString("Exception occurred: %1").arg(e.what())); if (lockdownClient) { lockdownd_client_free(lockdownClient); diff --git a/src/realtimescreenwidget.h b/src/realtimescreenwidget.h index 2fd832e..8a825f5 100644 --- a/src/realtimescreenwidget.h +++ b/src/realtimescreenwidget.h @@ -17,7 +17,7 @@ public: ~RealtimeScreenWidget(); private: - void initializeScreenshotService(); + bool initializeScreenshotService(bool notify); void updateScreenshot(); void startCapturing(); diff --git a/src/settingsmanager.cpp b/src/settingsmanager.cpp index 03e92c9..1d96c54 100644 --- a/src/settingsmanager.cpp +++ b/src/settingsmanager.cpp @@ -4,8 +4,6 @@ #include #include -#define DEFAULT_DEVDISKIMGPATH "./devdiskimages" - SettingsManager *SettingsManager::sharedInstance() { static SettingsManager instance; @@ -46,13 +44,13 @@ QString SettingsManager::devdiskimgpath() const QString SettingsManager::downloadPath() const { return m_settings - ->value("downloadPath", SettingsManager::docsPath() + "/devdiskimages") + ->value("downloadPath", SettingsManager::homePath() + "/devdiskimages") .toString(); } -QString SettingsManager::docsPath() +QString SettingsManager::homePath() { - return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + + return QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.idescriptor"; } @@ -154,6 +152,7 @@ void SettingsManager::doIfEnabled(Setting setting, std::function action) return; } + qDebug() << "enabled" << switchToNewDevice(); if (shouldExecute && action) { action(); } @@ -161,12 +160,12 @@ void SettingsManager::doIfEnabled(Setting setting, std::function action) void SettingsManager::resetToDefaults() { - setDownloadPath(DEFAULT_DEVDISKIMGPATH); + setDownloadPath(SettingsManager::homePath() + "/devdiskimages"); setAutoCheckUpdates(true); setAutoRaiseWindow(true); setSwitchToNewDevice(true); #ifndef __APPLE__ - setUnmountiFuseOnExit(true); + setUnmountiFuseOnExit(false); #endif setTheme("System Default"); setConnectionTimeout(30); diff --git a/src/settingsmanager.h b/src/settingsmanager.h index 5ddef7a..343ddcc 100644 --- a/src/settingsmanager.h +++ b/src/settingsmanager.h @@ -26,7 +26,7 @@ public: Theme, ConnectionTimeout }; - static QString docsPath(); + static QString homePath(); // Existing methods QString devdiskimgpath() const; void clearKeys(const QString &keyPrefix); diff --git a/src/settingswidget.cpp b/src/settingswidget.cpp index 8505871..2adce92 100644 --- a/src/settingswidget.cpp +++ b/src/settingswidget.cpp @@ -253,8 +253,7 @@ void SettingsWidget::saveSettings() void SettingsWidget::resetToDefaults() { - SettingsManager *sm = SettingsManager::sharedInstance(); - sm->resetToDefaults(); + SettingsManager::sharedInstance()->resetToDefaults(); // Reload UI with default values loadSettings(); diff --git a/src/sponsorappcard.cpp b/src/sponsorappcard.cpp new file mode 100644 index 0000000..eca869a --- /dev/null +++ b/src/sponsorappcard.cpp @@ -0,0 +1,117 @@ +#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( + bundleId, + [iconLabel](const QPixmap &pixmap) { + 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); + } + }, + this); + + // 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 new file mode 100644 index 0000000..a44966a --- /dev/null +++ b/src/sponsorappcard.h @@ -0,0 +1,15 @@ +#ifndef SPONSORAPPCARD_H +#define SPONSORAPPCARD_H + +#include + +class SponsorAppCard : public QWidget +{ + Q_OBJECT +public: + explicit SponsorAppCard(QWidget *parent = nullptr); + +signals: +}; + +#endif // SPONSORAPPCARD_H diff --git a/src/sponsorwidget.cpp b/src/sponsorwidget.cpp new file mode 100644 index 0000000..7ef3941 --- /dev/null +++ b/src/sponsorwidget.cpp @@ -0,0 +1,25 @@ +#include "sponsorwidget.h" +#include "sponsorappcard.h" +#include +#include + +SponsorWidget::SponsorWidget(QWidget *parent) : QWidget(parent) +{ + setLayout(new QVBoxLayout(this)); + QLabel *sponsorTitle = new QLabel("Would you like to sponsor us?"); + 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"); + sponsorDesc->setWordWrap(true); + layout()->addWidget(sponsorTitle); + layout()->addWidget(sponsorDesc); + 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 new file mode 100644 index 0000000..f8d0982 --- /dev/null +++ b/src/sponsorwidget.h @@ -0,0 +1,13 @@ +#ifndef SPONSORWIDGET_H +#define SPONSORWIDGET_H + +#include + +class SponsorWidget : public QWidget +{ + Q_OBJECT +public: + SponsorWidget(QWidget *parent = nullptr); +}; + +#endif // SPONSORWIDGET_H diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index 8384def..e1ecc6b 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -116,10 +116,7 @@ void ToolboxWidget::setupUI() QList mainToolWidgets; mainToolWidgets.append( - {iDescriptorTool::Airplayer, - "Start an airplayer service to cast your device screen " - "(does not require a device to be connected)", - false, ""}); + {iDescriptorTool::Airplayer, "Cast your device screen ", false, ""}); mainToolWidgets.append({iDescriptorTool::VirtualLocation, "Simulate GPS location on your device", true, ""}); mainToolWidgets.append( @@ -284,6 +281,7 @@ ClickableWidget *ToolboxWidget::createToolbox(iDescriptorTool tool, void ToolboxWidget::updateDeviceList() { + m_deviceCombo->blockSignals(true); m_deviceCombo->clear(); QList devices = @@ -292,21 +290,24 @@ void ToolboxWidget::updateDeviceList() if (devices.isEmpty()) { m_deviceCombo->addItem("No device connected"); m_deviceCombo->setEnabled(false); - m_uuid.clear(); // No device, clear uuid } else { m_deviceCombo->setEnabled(true); - QString shortUdid = - QString::fromStdString(devices.first()->udid).left(8) + "..."; for (iDescriptorDevice *device : devices) { + QString shortUdid = + QString::fromStdString(device->udid).left(8) + "..."; m_deviceCombo->addItem( QString::fromStdString(device->deviceInfo.productType) + " / " + shortUdid, QString::fromStdString(device->udid)); } - // TODO: - m_uuid = devices.first()->udid; - m_currentDevice = devices.first(); // Set current device to first one } + + // After rebuilding the list, explicitly sync the UI to match the + // state from AppContext. This avoids creating a feedback loop. + onCurrentDeviceChanged( + AppContext::sharedInstance()->getCurrentDeviceSelection()); + + m_deviceCombo->blockSignals(false); } void ToolboxWidget::updateToolboxStates() @@ -339,25 +340,14 @@ void ToolboxWidget::updateUI() void ToolboxWidget::onDeviceSelectionChanged() { - // Handle device selection change QString selectedUdid = m_deviceCombo->currentData().toString(); - qDebug() << "Selected device UDID:" << selectedUdid; - - // Update m_uuid and m_currentDevice if a valid device is selected - QList devices = - AppContext::sharedInstance()->getAllDevices(); - for (iDescriptorDevice *device : devices) { - if (QString::fromStdString(device->udid) == selectedUdid) { - m_uuid = device->udid; - m_currentDevice = device; - // Also update the AppContext to keep everything in sync - AppContext::sharedInstance()->setCurrentDeviceSelection( - DeviceSelection(m_uuid)); - return; - } + if (selectedUdid.isEmpty()) { + return; } - m_uuid.clear(); - m_currentDevice = nullptr; + + // Update the selected device in main menu + AppContext::sharedInstance()->setCurrentDeviceSelection( + DeviceSelection(selectedUdid.toStdString())); } void ToolboxWidget::onCurrentDeviceChanged(const DeviceSelection &selection) @@ -366,24 +356,19 @@ void ToolboxWidget::onCurrentDeviceChanged(const DeviceSelection &selection) int index = m_deviceCombo->findData(QString::fromStdString(selection.uuid)); if (index != -1) { - // Block signals to prevent recursive calls + // Block signals to prevent recursive calls when we update the UI m_deviceCombo->blockSignals(true); m_deviceCombo->setCurrentIndex(index); m_deviceCombo->blockSignals(false); - // Update internal state m_uuid = selection.uuid; - QList devices = - AppContext::sharedInstance()->getAllDevices(); - for (iDescriptorDevice *device : devices) { - if (device->udid == selection.uuid) { - m_currentDevice = device; - break; - } - } + m_currentDevice = + AppContext::sharedInstance()->getDevice(selection.uuid); } } else { - // TODO: recovery and no device selection + // Handle recovery, pending, or no device selection + m_uuid.clear(); + m_currentDevice = nullptr; } } @@ -434,7 +419,7 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) return; } - if (result.success || result.sig.empty()) { + if (result.success && result.sig.empty()) { bool devImgSuccess = DevDiskManager::sharedInstance()->mountCompatibleImage( m_currentDevice); @@ -450,6 +435,12 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) } } + QMessageBox::information( + this, "Success", + QString("There is already a developer image mounted on device %1.") + .arg(QString::fromStdString( + m_currentDevice->deviceInfo.productType))); + } break; case iDescriptorTool::VirtualLocation: { // Handle virtual location functionality @@ -506,6 +497,8 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) case iDescriptorTool::iFuse: { if (!m_ifuseWidget) { m_ifuseWidget = new iFuseWidget(m_currentDevice); + qDebug() << "Created iFuseWidget" + << m_currentDevice->deviceInfo.productType.c_str(); m_ifuseWidget->setAttribute(Qt::WA_DeleteOnClose); connect(m_ifuseWidget, &QObject::destroyed, this, [this]() { m_ifuseWidget = nullptr; });