diff --git a/src/core/helpers/compare_product_type.cpp b/src/core/helpers/compare_product_type.cpp new file mode 100644 index 0000000..6e66f96 --- /dev/null +++ b/src/core/helpers/compare_product_type.cpp @@ -0,0 +1,113 @@ +// TODO: move function declarations to a header file +#include +#include +#include + +struct ProductTypeVersion { + int major; + int minor; + + ProductTypeVersion(int maj = 0, int min = 0) : major(maj), minor(min) {} + + // Compare two product type versions + // Returns: -1 if this < other, 0 if equal, 1 if this > other + int compareTo(const ProductTypeVersion &other) const + { + if (major != other.major) { + return major < other.major ? -1 : 1; + } + if (minor != other.minor) { + return minor < other.minor ? -1 : 1; + } + return 0; // Equal + } + + bool operator<(const ProductTypeVersion &other) const + { + return compareTo(other) < 0; + } + + bool operator==(const ProductTypeVersion &other) const + { + return compareTo(other) == 0; + } + + bool operator>(const ProductTypeVersion &other) const + { + return compareTo(other) > 0; + } +}; + +// Extract version numbers from iPhone product type string +// Example: "iPhone8,1" -> ProductTypeVersion{8, 1} +ProductTypeVersion extractProductTypeVersion(const std::string &productType) +{ + // Regex to match iPhone followed by major,minor numbers + std::regex pattern(R"(iPhone(\d+),(\d+))"); + std::smatch matches; + + if (std::regex_search(productType, matches, pattern)) { + if (matches.size() >= 3) { + try { + int major = std::stoi(matches[1].str()); + int minor = std::stoi(matches[2].str()); + return ProductTypeVersion(major, minor); + } catch (const std::invalid_argument &e) { + throw std::invalid_argument( + "Invalid numeric values in product type: " + productType); + } + } + } + + throw std::invalid_argument("Invalid iPhone product type format: " + + productType); +} + +/* use it only for iPhones*/ +bool compare_product_type(std::string productType, std::string otherProductType) +{ + try { + ProductTypeVersion version1 = extractProductTypeVersion(productType); + ProductTypeVersion version2 = + extractProductTypeVersion(otherProductType); + + // Return true if productType is newer/higher than otherProductType + return version1 > version2; + } catch (const std::exception &e) { + // Handle invalid product types - you might want to log this + return false; + } +} + +// Additional utility functions for more specific comparisons +bool are_product_types_equal(const std::string &productType, + const std::string &otherProductType) +{ + try { + ProductTypeVersion version1 = extractProductTypeVersion(productType); + ProductTypeVersion version2 = + extractProductTypeVersion(otherProductType); + return version1 == version2; + } catch (const std::exception &e) { + return false; + } +} + +bool is_product_type_newer(const std::string &productType, + const std::string &otherProductType) +{ + return compare_product_type(productType, otherProductType); +} + +bool is_product_type_older(const std::string &productType, + const std::string &otherProductType) +{ + try { + ProductTypeVersion version1 = extractProductTypeVersion(productType); + ProductTypeVersion version2 = + extractProductTypeVersion(otherProductType); + return version1 < version2; + } catch (const std::exception &e) { + return false; + } +} \ No newline at end of file diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp index 4c98128..17856bd 100644 --- a/src/core/services/init_device.cpp +++ b/src/core/services/init_device.cpp @@ -6,12 +6,26 @@ #include #include +std::string safeGet(const char *key, pugi::xml_node dict) +{ + for (pugi::xml_node child = dict.first_child(); child; + child = child.next_sibling()) { + if (strcmp(child.name(), "key") == 0 && + strcmp(child.text().as_string(), key) == 0) { + pugi::xml_node value = child.next_sibling(); + if (value) + return value.text().as_string(); + } + } + return ""; +}; + +// TODO: return tyype DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, afc_client_t &afcClient, plist_t &diagnostics, DeviceInfo &d) { pugi::xml_node dict = doc.child("plist").child("dict"); - auto safeGet = [&](const char *key) -> std::string { for (pugi::xml_node child = dict.first_child(); child; child = child.next_sibling()) { @@ -25,6 +39,21 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, return ""; }; + auto safeGetBool = [&](const char *key) -> bool { + for (pugi::xml_node child = dict.first_child(); child; + child = child.next_sibling()) { + if (strcmp(child.name(), "key") == 0 && + strcmp(child.text().as_string(), key) == 0) { + pugi::xml_node value = child.next_sibling(); + if (value && strcmp(value.name(), "true") == 0) + return true; + else + return false; + } + } + return false; + }; + d.deviceName = safeGet("DeviceName"); d.deviceClass = safeGet("DeviceClass"); d.deviceColor = safeGet("DeviceColor"); @@ -54,8 +83,13 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, } std::string _activationState = safeGet("ActivationState"); - // TODO: does "ProductionSOC: true" work as well ? - d.productionDevice = std::stoi(safeGet("FusingStatus")) == 3; + + /* older devices dont have fusing status lets default to ProductionSOC for + * now*/ + // std::string fStatus = safeGet("FusingStatus"); + // d.productionDevice = std::stoi(fStatus.empty() ? "0" : fStatus) == 3; + + d.productionDevice = safeGetBool("ProductionSOC"); if (_activationState == "Activated") { d.activationState = DeviceInfo::ActivationState::Activated; } else if (_activationState == "FactoryActivated") { @@ -170,12 +204,46 @@ IDescriptorInitDeviceResult init_idescriptor_device(const char *udid) return result; } - plist_t diagnostics = nullptr; + pugi::xml_document infoXml; + get_device_info_xml(udid, 0, 0, infoXml, client, result.device); + if (infoXml.empty()) { + qDebug() << "Failed to retrieve device info XML for UDID: " + << QString::fromUtf8(udid); + // Clean up resources before returning + // afc_client_free(result.afcClient); + // lockdownd_service_descriptor_free(result.lockdownService); + // lockdownd_client_free(result.client); + idevice_free(result.device); + return result; + } + + plist_t diagnostics = nullptr; + std::string productType = + safeGet("ProductType", infoXml.child("plist").child("dict")); + + bool is_iphone = + safeGet("DeviceClass", infoXml.child("plist").child("dict")) == + "iPhone"; + if (is_iphone) { + + qDebug() << "iPhone is newer than iPhone 8 ?" + << is_product_type_newer(productType, + std::string("iPhone10,1")); + } + + const char *batteryQuery = + is_iphone + ? is_product_type_newer(productType, std::string("iPhone8,1")) + ? "AppleSmartBattery" + : "AppleARMPMUCharger" + : "AppleARMPMUCharger"; // TODO: iPhone 8 and above should query AppleSmartBattery + // TODO: try catch here + if (diagnostics_relay_query_ioregistry_entry( - diagnostics_client, nullptr, "AppleARMPMUCharger", - &diagnostics) != DIAGNOSTICS_RELAY_E_SUCCESS && + diagnostics_client, nullptr, batteryQuery, &diagnostics) != + DIAGNOSTICS_RELAY_E_SUCCESS && !diagnostics) { qDebug() @@ -192,20 +260,6 @@ IDescriptorInitDeviceResult init_idescriptor_device(const char *udid) return result; } - pugi::xml_document infoXml; - get_device_info_xml(udid, 0, 0, infoXml, client, result.device); - - if (infoXml.empty()) { - qDebug() << "Failed to retrieve device info XML for UDID: " - << QString::fromUtf8(udid); - // Clean up resources before returning - // afc_client_free(result.afcClient); - // lockdownd_service_descriptor_free(result.lockdownService); - // lockdownd_client_free(result.client); - idevice_free(result.device); - return result; - } - // if (result.device) idevice_free(result.device); fullDeviceInfo(infoXml, afcClient, diagnostics, result.deviceInfo); diff --git a/src/devdiskimageswidget.cpp b/src/devdiskimageswidget.cpp index 1efc539..5909d47 100644 --- a/src/devdiskimageswidget.cpp +++ b/src/devdiskimageswidget.cpp @@ -95,6 +95,9 @@ void DevDiskImagesWidget::setupUi() SettingsManager::sharedInstance()->devdiskimgpath()); displayImages(); + if (DevDiskManager::sharedInstance()->isImageListReady()) { + m_stackedWidget->setCurrentWidget(m_imageListWidget); + } } void DevDiskImagesWidget::fetchImages() @@ -107,6 +110,8 @@ void DevDiskImagesWidget::fetchImages() void DevDiskImagesWidget::onImageListFetched(bool success, const QString &errorMessage) { + + qDebug() << "Image list fetched successfully"; if (!success) { m_statusLabel->setText( QString("Error fetching image list: %1").arg(errorMessage)); @@ -144,6 +149,8 @@ void DevDiskImagesWidget::displayImages() hasConnectedDevice = true; } + qDebug() << "Device version:" << deviceMajorVersion << "." + << deviceMinorVersion << "displayImages"; // Parse images using manager GetImagesSortedFinalResult sortedResult = DevDiskManager::sharedInstance()->parseImageList( @@ -153,6 +160,9 @@ void DevDiskImagesWidget::displayImages() auto compatibleImages = sortedResult.compatibleImages; auto otherImages = sortedResult.otherImages; + qDebug() << "Compatible images:" << compatibleImages.size(); + qDebug() << "Other images:" << otherImages.size(); + // Create UI items - compatible versions first auto createVersionItem = [&](const ImageInfo &info, bool isCompatible) { auto *itemWidget = new QWidget(); @@ -491,8 +501,7 @@ void DevDiskImagesWidget::mountImage(const QString &version) m_mountButton->setEnabled(false); m_mountButton->setText("Mounting..."); - bool success = DevDiskManager::sharedInstance()->mountImage( - version, udid); + bool success = DevDiskManager::sharedInstance()->mountImage(version, udid); m_mountButton->setEnabled(true); m_mountButton->setText("Mount"); diff --git a/src/devdiskmanager.cpp b/src/devdiskmanager.cpp index fba7c7c..2521df7 100644 --- a/src/devdiskmanager.cpp +++ b/src/devdiskmanager.cpp @@ -28,8 +28,8 @@ DevDiskManager::DevDiskManager(QObject *parent) : QObject{parent} QNetworkReply *DevDiskManager::fetchImageList() { - QUrl url("https://api.github.com/repos/mspvirajpatel/" - "Xcode_Developer_Disk_Images/git/trees/master?recursive=true"); + QUrl url("https://raw.githubusercontent.com/uncor3/resources/refs/heads/" + "main/DeveloperDiskImages.json"); QNetworkRequest request(url); auto *reply = m_networkManager->get(request); @@ -50,25 +50,69 @@ QNetworkReply *DevDiskManager::fetchImageList() QMap> DevDiskManager::parseDiskDir() { QJsonDocument doc = QJsonDocument::fromJson(m_imageListJsonData); - // if (!doc.isObject()) { - // return false; - // } + if (!doc.isObject()) { + qWarning() << "Invalid JSON response from image list API"; + return {}; + } QMap> - imageFiles; // dir -> {filename -> path} + imageFiles; // version -> {type -> url} - QJsonArray tree = doc.object()["tree"].toArray(); - for (const QJsonValue &value : tree) { - QJsonObject obj = value.toObject(); - QString path = obj["path"].toString(); - if (path.endsWith(".dmg") || path.endsWith(".dmg.signature")) { - QFileInfo fileInfo(path); - QString dir = fileInfo.path(); - QString filename = fileInfo.fileName(); - if (!dir.isEmpty() && dir != ".") - imageFiles[dir][filename] = path; + QJsonObject root = doc.object(); + for (auto it = root.constBegin(); it != root.constEnd(); ++it) { + const QString version = it.key(); + const QJsonObject versionData = it.value().toObject(); + + // Skip special entries + if (version == "Fallback") { + continue; + } + + QMap versionFiles; + + // Handle Image URLs + if (versionData.contains("Image")) { + QJsonArray imageArray = versionData["Image"].toArray(); + if (!imageArray.isEmpty()) { + versionFiles["DeveloperDiskImage.dmg"] = + imageArray[0].toString(); + } + } + + // Handle Signature URLs + if (versionData.contains("Signature")) { + QJsonArray sigArray = versionData["Signature"].toArray(); + if (!sigArray.isEmpty()) { + versionFiles["DeveloperDiskImage.dmg.signature"] = + sigArray[0].toString(); + } + } + + // Handle Trustcache URLs (for iOS 17+) + if (versionData.contains("Trustcache")) { + QJsonArray trustcacheArray = versionData["Trustcache"].toArray(); + if (!trustcacheArray.isEmpty()) { + versionFiles["Image.dmg.trustcache"] = + trustcacheArray[0].toString(); + } + } + + // Handle BuildManifest URLs (for iOS 17+) + if (versionData.contains("BuildManifest")) { + QJsonArray manifestArray = versionData["BuildManifest"].toArray(); + if (!manifestArray.isEmpty()) { + versionFiles["BuildManifest.plist"] = + manifestArray[0].toString(); + } + } + + // Only add versions that have at least an image file + if (!versionFiles.isEmpty() && + versionFiles.contains("DeveloperDiskImage.dmg")) { + imageFiles[version] = versionFiles; } } + return imageFiles; } @@ -136,8 +180,7 @@ GetImagesSortedResult DevDiskManager::getImagesSorted( for (auto it = imageFiles.constBegin(); it != imageFiles.constEnd(); ++it) { if (it.value().contains("DeveloperDiskImage.dmg") && it.value().contains("DeveloperDiskImage.dmg.signature")) { - QFileInfo dirInfo(it.key()); - QString version = dirInfo.fileName(); + QString version = it.key(); ImageInfo info; info.version = version; @@ -150,14 +193,24 @@ GetImagesSortedResult DevDiskManager::getImagesSorted( if (hasConnectedDevice) { QStringList versionParts = version.split('.'); if (versionParts.size() >= 1) { - bool ok; - int imageMajorVersion = versionParts[0].toInt(&ok); - if (ok) { + bool ma_ok; + bool mi_ok; + int imageMajorVersion = versionParts[0].toInt(&ma_ok); + int imageMinorVersion = (versionParts.size() >= 2) + ? versionParts[1].toInt(&mi_ok) + : 0; + if (ma_ok && mi_ok) { if (deviceMajorVersion >= 16) { info.isCompatible = (imageMajorVersion == 16); } else { - info.isCompatible = - (imageMajorVersion == deviceMajorVersion); + // FIXME: this seems to work only for older iphones + // so commented out but in the future , it may be + // enabled (imageMajorVersion == + // deviceMajorVersion); + if (imageMajorVersion == deviceMajorVersion && + imageMinorVersion == deviceMinorVersion) { + info.isCompatible = true; + } } } } @@ -203,7 +256,9 @@ DevDiskManager::downloadImage(const QString &version) return {nullptr, nullptr}; } - QString targetDir = QDir("devdiskimages").filePath(version); + QString targetDir = + QDir(SettingsManager::sharedInstance()->devdiskimgpath()) + .filePath(version); if (!QDir().mkpath(targetDir)) { qDebug() << "Could not create directory:" << targetDir; emit imageDownloadFinished( @@ -214,15 +269,11 @@ DevDiskManager::downloadImage(const QString &version) const ImageInfo &info = m_availableImages[version]; - QUrl dmgUrl("https://raw.githubusercontent.com/mspvirajpatel/" - "Xcode_Developer_Disk_Images/master/" + - info.dmgPath); + QUrl dmgUrl(info.dmgPath); QNetworkRequest dmgRequest(dmgUrl); QNetworkReply *dmgReply = m_networkManager->get(dmgRequest); - QUrl sigUrl("https://raw.githubusercontent.com/mspvirajpatel/" - "Xcode_Developer_Disk_Images/master/" + - info.sigPath); + QUrl sigUrl(info.sigPath); QNetworkRequest sigRequest(sigUrl); QNetworkReply *sigReply = m_networkManager->get(sigRequest); @@ -242,7 +293,14 @@ bool DevDiskManager::isImageDownloaded(const QString &version, bool DevDiskManager::downloadCompatibleImageInternal(iDescriptorDevice *device) { - GetImagesSortedFinalResult images = parseImageList(15, 0, "", 0); + + unsigned int device_version = idevice_get_device_version(device->device); + unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; + unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF; + qDebug() << "Device version:" << deviceMajorVersion << "." + << deviceMinorVersion; + GetImagesSortedFinalResult images = + parseImageList(deviceMajorVersion, deviceMinorVersion, "", 0); for (const ImageInfo &info : images.compatibleImages) { if (info.isDownloaded) { @@ -324,19 +382,31 @@ bool DevDiskManager::mountCompatibleImageInternal(iDescriptorDevice *device) return true; } + unsigned int device_version = idevice_get_device_version(device->device); + unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; + unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF; + + // TODO: use actual device version GetImagesSortedFinalResult images = - parseImageList(15, 0, res.output.c_str(), res.output.length()); + parseImageList(deviceMajorVersion, deviceMinorVersion, + res.output.c_str(), res.output.length()); // 1. Try to mount an already downloaded compatible image for (const ImageInfo &info : images.compatibleImages) { if (info.isDownloaded) { qDebug() << "There is a compatible image already downloaded:" << info.version; - return true; + qDebug() << "Attempting to mount image version" << info.version + << "on device:" << device->udid.c_str(); if (mountImage(info.version, device->udid.c_str())) { qDebug() << "Mounted existing image version" << info.version << "on device:" << device->udid.c_str(); return true; + } else { + qDebug() << "Failed to mount existing image version" + << info.version + << "on device:" << device->udid.c_str(); + return false; } } } @@ -495,10 +565,9 @@ void DevDiskManager::onFileDownloadFinished() // QString targetPath = // QDir(QDir(item->downloadPath).filePath(item->version)) + // TODO: change to settings path QString targetPath = QDir(QDir("./devdiskimages").filePath(item->version)) .filePath(filename); - // Saving downloaded file to: "/tmp/15.7/DeveloperDiskImage.dmg.signature" - // Saving downloaded file to: "/tmp/15.7/DeveloperDiskImage.dmg" QFile file(targetPath); qDebug() << "Saving downloaded file to:" << targetPath; @@ -609,4 +678,6 @@ GetMountedImageResult DevDiskManager::getMountedImage(const char *udid) false, "", "No disk image mounted (No signature found)"}; } return GetMountedImageResult{true, mounted_sig_str, "Success"}; -} \ No newline at end of file +} + +bool DevDiskManager::isImageListReady() const { return m_isImageListReady; } \ No newline at end of file diff --git a/src/devdiskmanager.h b/src/devdiskmanager.h index 5a52c2a..7205c11 100644 --- a/src/devdiskmanager.h +++ b/src/devdiskmanager.h @@ -44,6 +44,7 @@ public: GetMountedImageResult getMountedImage(const char *udid); bool mountCompatibleImage(iDescriptorDevice *device); bool downloadCompatibleImage(iDescriptorDevice *device); + bool isImageListReady() const; signals: void imageListFetched(bool success, diff --git a/src/fileexportdialog.cpp b/src/fileexportdialog.cpp new file mode 100644 index 0000000..69d428b --- /dev/null +++ b/src/fileexportdialog.cpp @@ -0,0 +1,151 @@ +#include "fileexportdialog.h" +#include +#include +#include +#include +#include +#include +#include + +// TODO: needs progress bar improvements +FileExportDialog::FileExportDialog(QWidget *parent) + : QDialog(parent), m_progressBar(nullptr), m_statusLabel(nullptr), + m_fileLabel(nullptr), m_cancelButton(nullptr), m_layout(nullptr) +{ + setupUI(); +} + +FileExportDialog::~FileExportDialog() +{ + // Qt handles cleanup automatically for child widgets +} + +void FileExportDialog::setupUI() +{ + setWindowTitle("Exporting Files"); + setWindowModality(Qt::WindowModal); + setFixedSize(400, 150); + + // Prevent user from closing dialog manually + setWindowFlags(windowFlags() & ~Qt::WindowCloseButtonHint); + + m_layout = new QVBoxLayout(this); + + // Status label + m_statusLabel = new QLabel("Preparing export..."); + m_statusLabel->setStyleSheet("font-weight: bold; font-size: 12px;"); + m_layout->addWidget(m_statusLabel); + + // File label + m_fileLabel = new QLabel(""); + m_fileLabel->setStyleSheet("color: #666; font-size: 10px;"); + m_fileLabel->setWordWrap(true); + m_layout->addWidget(m_fileLabel); + + // Progress bar + m_progressBar = new QProgressBar(); + m_progressBar->setRange(0, 100); + m_progressBar->setValue(0); + m_layout->addWidget(m_progressBar); + + // Cancel button + auto *buttonLayout = new QHBoxLayout(); + buttonLayout->addStretch(); + + m_cancelButton = new QPushButton("Cancel"); + m_cancelButton->setStyleSheet("QPushButton { " + " background-color: #dc3545; " + " color: white; " + " border: none; " + " padding: 6px 12px; " + " border-radius: 3px; " + " font-weight: bold; " + "} " + "QPushButton:hover { " + " background-color: #c82333; " + "} " + "QPushButton:pressed { " + " background-color: #bd2130; " + "}"); + + connect(m_cancelButton, &QPushButton::clicked, this, + [this]() { emit cancelRequested(); }); + + buttonLayout->addWidget(m_cancelButton); + m_layout->addLayout(buttonLayout); +} + +void FileExportDialog::onExportStarted(int totalFiles) +{ + m_statusLabel->setText( + QString("Starting export of %1 files...").arg(totalFiles)); + m_fileLabel->setText("Preparing..."); + m_progressBar->setValue(0); + m_progressBar->setRange(0, 100); + + show(); + QApplication::processEvents(); +} + +void FileExportDialog::onExportProgress(int completed, int total, + const QString ¤tFileName) +{ + if (!isVisible()) { + return; + } + + int percentage = total > 0 ? (completed * 100) / total : 0; + + m_statusLabel->setText( + QString("Exporting %1 of %2 files...").arg(completed).arg(total)); + m_fileLabel->setText(QString("Current file: %1").arg(currentFileName)); + m_progressBar->setValue(percentage); + + QApplication::processEvents(); +} + +void FileExportDialog::onExportFinished(int successful, int failed) +{ + // Hide the dialog first + hide(); + + // Show completion message + showCompletionMessage(successful, failed); + + // Close and schedule for deletion + close(); + deleteLater(); +} + +void FileExportDialog::onExportCancelled() +{ + // Hide the dialog first + hide(); + + // Show cancellation message + QMessageBox::information(parentWidget(), "Export Cancelled", + "The export operation has been cancelled."); + + // Close and schedule for deletion + close(); + deleteLater(); +} + +void FileExportDialog::showCompletionMessage(int successful, int failed) +{ + QString message; + + if (failed == 0) { + message = + QString("Successfully exported all %1 files!").arg(successful); + QMessageBox::information(parentWidget(), "Export Complete", message); + } else { + message = + QString("Export completed with %1 successful and %2 failed files.") + .arg(successful) + .arg(failed); + QMessageBox::warning(parentWidget(), "Export Complete", message); + } + + qDebug() << "Export finished:" << message; +} \ No newline at end of file diff --git a/src/fileexportdialog.h b/src/fileexportdialog.h new file mode 100644 index 0000000..ed8e924 --- /dev/null +++ b/src/fileexportdialog.h @@ -0,0 +1,43 @@ +#ifndef FILEEXPORTDIALOG_H +#define FILEEXPORTDIALOG_H + +#include + +QT_BEGIN_NAMESPACE +class QProgressBar; +class QLabel; +class QPushButton; +class QVBoxLayout; +QT_END_NAMESPACE + +class FileExportDialog : public QDialog +{ + Q_OBJECT + +public: + explicit FileExportDialog(QWidget *parent = nullptr); + ~FileExportDialog() override; + +public slots: + void onExportStarted(int totalFiles); + void onExportProgress(int completed, int total, + const QString ¤tFileName); + void onExportFinished(int successful, int failed); + void onExportCancelled(); + +signals: + void cancelRequested(); + +private: + void setupUI(); + void showCompletionMessage(int successful, int failed); + + // UI components + QProgressBar *m_progressBar; + QLabel *m_statusLabel; + QLabel *m_fileLabel; + QPushButton *m_cancelButton; + QVBoxLayout *m_layout; +}; + +#endif // FILEEXPORTDIALOG_H \ No newline at end of file diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp index 6d41e01..8cc02a3 100644 --- a/src/gallerywidget.cpp +++ b/src/gallerywidget.cpp @@ -1,10 +1,19 @@ #include "gallerywidget.h" +#include "fileexportdialog.h" #include "iDescriptor.h" #include "mediapreviewdialog.h" +#include "photoexportmanager.h" #include "photomodel.h" +#include #include +#include +#include +#include #include #include +#include +#include +#include #include #include @@ -59,33 +68,58 @@ void GalleryWidget::load() return; m_loaded = true; - char **files = nullptr; - // TODO:ignore directories - safe_afc_read_directory(m_device->afcClient, m_device->device, - "/DCIM/100APPLE", &files); + setupUI(); +} - auto *mainLayout = new QVBoxLayout(this); +GalleryWidget::GalleryWidget(iDescriptorDevice *device, QWidget *parent) + : QWidget{parent}, m_device(device), m_model(nullptr), + m_exportManager(nullptr) +{ + // Initialize export manager + m_exportManager = new PhotoExportManager(this); + + // Widget setup is done in load() method when gallery tab is activated +} + +void GalleryWidget::setupUI() +{ + m_mainLayout = new QVBoxLayout(this); + m_mainLayout->setContentsMargins(10, 10, 10, 10); + m_mainLayout->setSpacing(10); + + // Setup controls at the top + setupControlsLayout(); + + // Create list view m_listView = new QListView(this); - mainLayout->addWidget(m_listView); - setLayout(mainLayout); - m_listView->setViewMode(QListView::IconMode); m_listView->setFlow(QListView::LeftToRight); m_listView->setWrapping(true); m_listView->setResizeMode(QListView::Adjust); m_listView->setIconSize(QSize(120, 120)); m_listView->setSpacing(10); + m_listView->setSelectionMode(QAbstractItemView::ExtendedSelection); - PhotoModel *model = new PhotoModel(m_device, this); - m_listView->setModel(model); + // Create and set model + m_model = new PhotoModel(m_device, this); + m_listView->setModel(m_model); + + // Add to main layout + m_mainLayout->addWidget(m_listView); + setLayout(m_mainLayout); + + // Add progress widget after main layout is set + // m_mainLayout->insertWidget( + // 1, m_progressWidget); // Insert between controls and list view // Connect double-click to open preview dialog connect(m_listView, &QListView::doubleClicked, this, - [this, model](const QModelIndex &index) { + [this](const QModelIndex &index) { if (!index.isValid()) return; - QString filePath = model->data(index, Qt::UserRole).toString(); + QString filePath = + m_model->data(index, Qt::UserRole).toString(); if (filePath.isEmpty()) return; @@ -95,10 +129,282 @@ void GalleryWidget::load() previewDialog->setAttribute(Qt::WA_DeleteOnClose); previewDialog->show(); }); + + // Update export button states based on selection + connect(m_listView->selectionModel(), + &QItemSelectionModel::selectionChanged, this, [this]() { + bool hasSelection = + m_listView->selectionModel()->hasSelection(); + m_exportSelectedButton->setEnabled(hasSelection); + }); } -GalleryWidget::GalleryWidget(iDescriptorDevice *device, QWidget *parent) - : QWidget{parent}, m_device(device) +void GalleryWidget::setupControlsLayout() { - // Widget setup is done in load() method when gallery tab is activated + m_controlsLayout = new QHBoxLayout(); + m_controlsLayout->setSpacing(15); + + // Sort order combo box + QLabel *sortLabel = new QLabel("Sort:"); + sortLabel->setStyleSheet("font-weight: bold; color: #555;"); + m_sortComboBox = new QComboBox(); + m_sortComboBox->addItem("Newest First", + static_cast(PhotoModel::NewestFirst)); + m_sortComboBox->addItem("Oldest First", + static_cast(PhotoModel::OldestFirst)); + m_sortComboBox->setCurrentIndex(0); // Default to Newest First + m_sortComboBox->setStyleSheet("QComboBox { " + " padding: 5px 10px; " + " border: 1px solid #ccc; " + " border-radius: 4px; " + " background-color: white; " + " min-width: 100px; " + "} " + "QComboBox:hover { " + " border-color: #0078d4; " + "} " + "QComboBox::drop-down { " + " border: none; " + " width: 20px; " + "} " + "QComboBox::down-arrow { " + " width: 12px; " + " height: 12px; " + "}"); + + // Filter combo box + QLabel *filterLabel = new QLabel("Filter:"); + filterLabel->setStyleSheet("font-weight: bold; color: #555;"); + m_filterComboBox = new QComboBox(); + m_filterComboBox->addItem("All Media", static_cast(PhotoModel::All)); + m_filterComboBox->addItem("Images Only", + static_cast(PhotoModel::ImagesOnly)); + m_filterComboBox->addItem("Videos Only", + static_cast(PhotoModel::VideosOnly)); + m_filterComboBox->setCurrentIndex(0); // Default to All + m_filterComboBox->setStyleSheet(m_sortComboBox->styleSheet()); + + // Export buttons + m_exportSelectedButton = new QPushButton("Export Selected"); + m_exportSelectedButton->setEnabled(false); // Initially disabled + m_exportSelectedButton->setStyleSheet("QPushButton { " + " background-color: #0078d4; " + " color: white; " + " border: none; " + " padding: 8px 16px; " + " border-radius: 4px; " + " font-weight: bold; " + "} " + "QPushButton:hover:enabled { " + " background-color: #106ebe; " + "} " + "QPushButton:pressed:enabled { " + " background-color: #005a9e; " + "} " + "QPushButton:disabled { " + " background-color: #ccc; " + " color: #888; " + "}"); + + m_exportAllButton = new QPushButton("Export All"); + m_exportAllButton->setStyleSheet("QPushButton { " + " background-color: #28a745; " + " color: white; " + " border: none; " + " padding: 8px 16px; " + " border-radius: 4px; " + " font-weight: bold; " + "} " + "QPushButton:hover { " + " background-color: #218838; " + "} " + "QPushButton:pressed { " + " background-color: #1e7e34; " + "}"); + + // Connect signals + connect(m_sortComboBox, QOverload::of(&QComboBox::currentIndexChanged), + this, &GalleryWidget::onSortOrderChanged); + connect(m_filterComboBox, + QOverload::of(&QComboBox::currentIndexChanged), this, + &GalleryWidget::onFilterChanged); + connect(m_exportSelectedButton, &QPushButton::clicked, this, + &GalleryWidget::onExportSelected); + connect(m_exportAllButton, &QPushButton::clicked, this, + &GalleryWidget::onExportAll); + + // Add widgets to layout + m_controlsLayout->addWidget(sortLabel); + m_controlsLayout->addWidget(m_sortComboBox); + m_controlsLayout->addWidget(filterLabel); + m_controlsLayout->addWidget(m_filterComboBox); + m_controlsLayout->addStretch(); // Push export buttons to the right + m_controlsLayout->addWidget(m_exportSelectedButton); + m_controlsLayout->addWidget(m_exportAllButton); + + // Create a frame to contain the controls + QWidget *controlsWidget = new QWidget(); + controlsWidget->setLayout(m_controlsLayout); + controlsWidget->setStyleSheet("QWidget { " + " background-color: #f8f9fa; " + " border: 1px solid #dee2e6; " + " border-radius: 6px; " + " padding: 10px; " + "}"); + + m_mainLayout->addWidget(controlsWidget); +} + +void GalleryWidget::onSortOrderChanged() +{ + if (!m_model) + return; + + int sortValue = m_sortComboBox->currentData().toInt(); + PhotoModel::SortOrder order = static_cast(sortValue); + m_model->setSortOrder(order); + + qDebug() << "Sort order changed to:" + << (order == PhotoModel::NewestFirst ? "Newest First" + : "Oldest First"); +} + +void GalleryWidget::onFilterChanged() +{ + if (!m_model) + return; + + int filterValue = m_filterComboBox->currentData().toInt(); + PhotoModel::FilterType filter = + static_cast(filterValue); + m_model->setFilterType(filter); + + QString filterName = m_filterComboBox->currentText(); + qDebug() << "Filter changed to:" << filterName; +} + +void GalleryWidget::onExportSelected() +{ + if (!m_model || !m_listView->selectionModel()->hasSelection()) { + QMessageBox::information(this, "No Selection", + "Please select one or more items to export."); + return; + } + + if (m_exportManager->isExporting()) { + QMessageBox::information(this, "Export in Progress", + "An export operation is already in progress. " + "Please wait for it to complete."); + return; + } + + QModelIndexList selectedIndexes = + m_listView->selectionModel()->selectedIndexes(); + QStringList filePaths = m_model->getSelectedFilePaths(selectedIndexes); + + if (filePaths.isEmpty()) { + QMessageBox::warning(this, "Export Error", + "No valid files selected for export."); + return; + } + + QString exportDir = selectExportDirectory(); + if (exportDir.isEmpty()) { + return; // User cancelled directory selection + } + + qDebug() << "Starting export of selected files:" << filePaths.size() + << "items to" << exportDir; + + // Create export dialog and connect signals + auto *exportDialog = new FileExportDialog(this); + + // Connect PhotoExportManager signals to FileExportDialog + connect(m_exportManager, &PhotoExportManager::exportStarted, exportDialog, + &FileExportDialog::onExportStarted); + connect(m_exportManager, &PhotoExportManager::exportProgress, exportDialog, + &FileExportDialog::onExportProgress); + connect(m_exportManager, &PhotoExportManager::exportFinished, exportDialog, + &FileExportDialog::onExportFinished); + connect(m_exportManager, &PhotoExportManager::exportCancelled, exportDialog, + &FileExportDialog::onExportCancelled); + + // Connect cancel signal from dialog to export manager + connect(exportDialog, &FileExportDialog::cancelRequested, m_exportManager, + &PhotoExportManager::cancelExport); + + // Start the export + m_exportManager->exportFiles(m_device, filePaths, exportDir); +} + +void GalleryWidget::onExportAll() +{ + if (!m_model) + return; + + if (m_exportManager->isExporting()) { + QMessageBox::information(this, "Export in Progress", + "An export operation is already in progress. " + "Please wait for it to complete."); + return; + } + + QStringList filePaths = m_model->getFilteredFilePaths(); + + if (filePaths.isEmpty()) { + QMessageBox::information( + this, "No Items", + "There are no items to export with the current filter."); + return; + } + + QString message = + QString("Export all %1 items currently shown?").arg(filePaths.size()); + int reply = QMessageBox::question(this, "Export All", message, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + + if (reply != QMessageBox::Yes) { + return; + } + + QString exportDir = selectExportDirectory(); + if (exportDir.isEmpty()) { + return; // User cancelled directory selection + } + + qDebug() << "Starting export of all filtered files:" << filePaths.size() + << "items to" << exportDir; + + // Create export dialog and connect signals + auto *exportDialog = new FileExportDialog(this); + + // Connect PhotoExportManager signals to FileExportDialog + connect(m_exportManager, &PhotoExportManager::exportStarted, exportDialog, + &FileExportDialog::onExportStarted); + connect(m_exportManager, &PhotoExportManager::exportProgress, exportDialog, + &FileExportDialog::onExportProgress); + connect(m_exportManager, &PhotoExportManager::exportFinished, exportDialog, + &FileExportDialog::onExportFinished); + connect(m_exportManager, &PhotoExportManager::exportCancelled, exportDialog, + &FileExportDialog::onExportCancelled); + + // Connect cancel signal from dialog to export manager + connect(exportDialog, &FileExportDialog::cancelRequested, m_exportManager, + &PhotoExportManager::cancelExport); + + // Start the export + m_exportManager->exportFiles(m_device, filePaths, exportDir); +} + +QString GalleryWidget::selectExportDirectory() +{ + QString defaultDir = + QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + + QString selectedDir = QFileDialog::getExistingDirectory( + this, "Select Export Directory", defaultDir, + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + return selectedDir; } diff --git a/src/gallerywidget.h b/src/gallerywidget.h index e0bdd0b..c7c5e13 100644 --- a/src/gallerywidget.h +++ b/src/gallerywidget.h @@ -6,8 +6,15 @@ QT_BEGIN_NAMESPACE class QListView; +class QComboBox; +class QPushButton; +class QHBoxLayout; +class QVBoxLayout; QT_END_NAMESPACE +class PhotoModel; +class PhotoExportManager; + /** * @brief Widget for displaying a gallery of photos and videos from iOS devices * @@ -25,17 +32,36 @@ class GalleryWidget : public QWidget public: explicit GalleryWidget(iDescriptorDevice *device, QWidget *parent = nullptr); - -public slots: - /** - * @brief Load photos from device (called when tab becomes active) - */ void load(); +private slots: + void onSortOrderChanged(); + void onFilterChanged(); + void onExportSelected(); + void onExportAll(); + private: + void setupUI(); + void setupControlsLayout(); + QString selectExportDirectory(); + iDescriptorDevice *m_device; - QListView *m_listView; bool m_loaded = false; + + // UI components + QVBoxLayout *m_mainLayout; + QHBoxLayout *m_controlsLayout; + QListView *m_listView; + PhotoModel *m_model; + + // Control widgets + QComboBox *m_sortComboBox; + QComboBox *m_filterComboBox; + QPushButton *m_exportSelectedButton; + QPushButton *m_exportAllButton; + + // Export manager + PhotoExportManager *m_exportManager; }; #endif // GALLERYWIDGET_H diff --git a/src/iDescriptor.h b/src/iDescriptor.h index 2fb0142..40adb05 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -168,7 +168,37 @@ const std::unordered_map DEVICE_MAP = { {"iPhone9,4", "iPhone 7 Plus (GSM+CDMA)"}, {"iPhone10,1", "iPhone 8 (GSM)"}, {"iPhone10,2", "iPhone 8 Plus (GSM)"}, - {"iPhone10,3", "iPhone X (GSM)"}}; + {"iPhone10,3", "iPhone X (GSM)"}, + {"iPhone10,4", "iPhone 8 (GSM+CDMA)"}, + {"iPhone10,5", "iPhone 8 Plus (GSM+CDMA)"}, + {"iPhone10,6", "iPhone X (GSM+CDMA)"}, + {"iPhone11,2", "iPhone XS"}, + {"iPhone11,4", "iPhone XS Max"}, + {"iPhone11,6", "iPhone XS Max (China)"}, + {"iPhone11,8", "iPhone XR"}, + {"iPhone12,1", "iPhone 11"}, + {"iPhone12,3", "iPhone 11 Pro"}, + {"iPhone12,5", "iPhone 11 Pro Max"}, + {"iPhone12,8", "iPhone SE (2nd generation)"}, + {"iPhone13,1", "iPhone 12 mini"}, + {"iPhone13,2", "iPhone 12"}, + {"iPhone13,3", "iPhone 12 Pro"}, + {"iPhone13,4", "iPhone 12 Pro Max"}, + {"iPhone14,4", "iPhone 13 mini"}, + {"iPhone14,5", "iPhone 13"}, + {"iPhone14,2", "iPhone 13 Pro"}, + {"iPhone14,3", "iPhone 13 Pro Max"}, + {"iPhone14,6", "iPhone SE (3rd generation)"}, + {"iPhone15,2", "iPhone 14 Pro"}, + {"iPhone15,3", "iPhone 14 Pro Max"}, + {"iPad1,1", "iPad 1st generation"}, + {"iPad2,1", "iPad 2 (WiFi)"}, + {"iPad2,2", "iPad 2 (GSM)"}, + {"iPad2,3", "iPad 2 (CDMA)"}, + {"iPad2,4", "iPad 2 (Rev A)"}, + {"iPad3,1", "iPad 3rd generation (WiFi)"}, + {"iPad3,2", "iPad 3rd generation (GSM)"}, +}; struct RecoveryDeviceInfo : public QObject { Q_OBJECT @@ -299,4 +329,45 @@ struct GetImagesSortedResult { struct GetImagesSortedFinalResult { QList compatibleImages; QList otherImages; -}; \ No newline at end of file +}; + +/** + * @brief Compare two iPhone product types to determine which is newer + * @param productType First iPhone product type (e.g., "iPhone8,1") + * @param otherProductType Second iPhone product type (e.g., "iPhone7,2") + * @return true if productType is newer than otherProductType, false otherwise + * + * Examples: + * - compare_product_type("iPhone8,1", "iPhone7,2") returns true + * - compare_product_type("iPhone6,1", "iPhone8,1") returns false + * - compare_product_type("iPhone8,2", "iPhone8,1") returns true + */ +bool compare_product_type(std::string productType, + std::string otherProductType); + +/** + * @brief Check if two iPhone product types are exactly equal + * @param productType First iPhone product type + * @param otherProductType Second iPhone product type + * @return true if both product types are identical + */ +bool are_product_types_equal(const std::string &productType, + const std::string &otherProductType); + +/** + * @brief Check if first product type is newer than second + * @param productType First iPhone product type + * @param otherProductType Second iPhone product type + * @return true if productType is newer than otherProductType + */ +bool is_product_type_newer(const std::string &productType, + const std::string &otherProductType); + +/** + * @brief Check if first product type is older than second + * @param productType First iPhone product type + * @param otherProductType Second iPhone product type + * @return true if productType is older than otherProductType + */ +bool is_product_type_older(const std::string &productType, + const std::string &otherProductType); diff --git a/src/jailbrokenwidget.cpp b/src/jailbrokenwidget.cpp index a8f75fe..743301f 100644 --- a/src/jailbrokenwidget.cpp +++ b/src/jailbrokenwidget.cpp @@ -1,989 +1,8 @@ #include "jailbrokenwidget.h" -#include -#include -#undef slots -#undef signals -#include // Correct header for QtConcurrent::run -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _WIN32 -#define EXPORT __declspec(dllexport) -#include -#else -#define EXPORT __attribute__((visibility("default"))) -#include -#include -#endif -#include "frida-core.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include // keep for other code, but not used for transfer -#include -#include - -namespace fs = std::filesystem; - -// Forward declarations -struct Application { - std::string name; - std::string identifier; - unsigned int pid; -}; - -struct SSHConfig { - std::string host = "localhost"; - int port = 2222; - std::string user = "root"; - std::string password = "alpine"; - std::string key_filename = ""; -}; - -// Progress callback types -using GeneralProgressCallback = - std::function; -using DownloadProgressCallback = std::function; -using ZipProgressCallback = std::function; -using ErrorCallback = std::function; - -class IOSDumper -{ -private: - // Callback storage - std::vector general_progress_callbacks; - std::vector download_progress_callbacks; - std::vector zip_progress_callbacks; - std::vector error_callbacks; - - // Threading - std::mutex callback_mutex; - std::condition_variable finished_cv; - std::mutex finished_mutex; - std::atomic finished{false}; - - // Device and session management - FridaDevice *device = nullptr; - FridaSession *session = nullptr; - ssh_session sshSession = nullptr; - ssh_scp scp = nullptr; // Renamed from sftp - - // File management - std::string temp_dir; - std::string payload_path = "./Payload"; - std::map file_dict; - - // JavaScript dump script content - std::string dump_js_content; - - void notify_progress(const std::string &message, - const std::string &type = "info", int current = 0, - int total = 100); - void notify_download_progress(int current, int total); - void notify_zip_progress(int current, int total); - void notify_error(const std::string &message); - - bool init_frida(); - bool get_usb_device(); - bool connect_ssh(const SSHConfig &config); - bool create_directory(const std::string &path); - std::vector get_applications(); - bool open_target_app(const std::string &name_or_bundleid, bool kill_running, - std::string &display_name, - std::string &bundle_identifier); - bool download_file(const std::string &remote_path, - const std::string &local_path, bool recursive = false); - bool generate_ipa(const std::string &display_name); - bool load_dump_script(); - - static void on_frida_message(FridaScript *script, const char *message, - GBytes *data, gpointer user_data); - void handle_frida_message(const std::string &message); - - void cleanup(); - -public: - IOSDumper(); - ~IOSDumper(); - - // Callback management - void add_general_progress_callback(GeneralProgressCallback callback); - void add_download_progress_callback(DownloadProgressCallback callback); - void add_zip_progress_callback(ZipProgressCallback callback); - void add_error_callback(ErrorCallback callback); - void clear_progress_callbacks(); - - // Legacy callback support - void add_progress_callback(GeneralProgressCallback callback); - - // Main functionality - bool dump_app(const std::string &name_or_bundleid, - const std::string &output_name = "", - const SSHConfig &ssh_config = SSHConfig(), - bool kill_running = true); - std::vector> get_all_applications(); - - // Legacy function - bool start(const std::string &name_or_bundleid, bool kill_running = true, - const SSHConfig &ssh_config = SSHConfig()); -}; - -// Implementation -IOSDumper::IOSDumper() -{ - // Initialize temp directory - temp_dir = fs::temp_directory_path(); - payload_path = temp_dir + "/Payload"; - - // Initialize Frida - init_frida(); - - // Load dump script (this would be embedded or loaded from file) - load_dump_script(); -} - -IOSDumper::~IOSDumper() { cleanup(); } - -bool IOSDumper::init_frida() -{ - frida_init(); - return true; -} - -void IOSDumper::cleanup() -{ - if (session) { - frida_session_detach_sync(session, nullptr, nullptr); - g_object_unref(session); - session = nullptr; - } - - if (device) { - g_object_unref(device); - device = nullptr; - } - - if (scp) { - ssh_scp_free(scp); - scp = nullptr; - } - if (sshSession) { - ssh_disconnect(sshSession); - ssh_free(sshSession); - sshSession = nullptr; - } - - // Clean up temp directory - if (fs::exists(payload_path)) { - fs::remove_all(payload_path); - } - - frida_deinit(); -} - -void IOSDumper::add_general_progress_callback(GeneralProgressCallback callback) -{ - std::lock_guard lock(callback_mutex); - general_progress_callbacks.push_back(callback); -} - -void IOSDumper::add_download_progress_callback( - DownloadProgressCallback callback) -{ - std::lock_guard lock(callback_mutex); - download_progress_callbacks.push_back(callback); -} - -void IOSDumper::add_zip_progress_callback(ZipProgressCallback callback) -{ - std::lock_guard lock(callback_mutex); - zip_progress_callbacks.push_back(callback); -} - -void IOSDumper::add_error_callback(ErrorCallback callback) -{ - std::lock_guard lock(callback_mutex); - error_callbacks.push_back(callback); -} - -void IOSDumper::clear_progress_callbacks() -{ - std::lock_guard lock(callback_mutex); - general_progress_callbacks.clear(); - download_progress_callbacks.clear(); - zip_progress_callbacks.clear(); - error_callbacks.clear(); -} - -void IOSDumper::add_progress_callback(GeneralProgressCallback callback) -{ - add_general_progress_callback(callback); -} - -void IOSDumper::notify_progress(const std::string &message, - const std::string &type, int current, int total) -{ - std::lock_guard lock(callback_mutex); - for (const auto &callback : general_progress_callbacks) { - try { - callback(message, type, current, total); - } catch (...) { - // Ignore callback errors - } - } -} - -void IOSDumper::notify_download_progress(int current, int total) -{ - int progress = (total > 0) ? (current * 100 / total) : 0; - std::lock_guard lock(callback_mutex); - for (const auto &callback : download_progress_callbacks) { - try { - callback(progress); - } catch (...) { - // Ignore callback errors - } - } -} - -void IOSDumper::notify_zip_progress(int current, int total) -{ - int progress = (total > 0) ? (current * 100 / total) : 0; - std::lock_guard lock(callback_mutex); - for (const auto &callback : zip_progress_callbacks) { - try { - callback(progress); - } catch (...) { - // Ignore callback errors - } - } -} - -void IOSDumper::notify_error(const std::string &message) -{ - std::lock_guard lock(callback_mutex); - for (const auto &callback : error_callbacks) { - try { - callback(message); - } catch (...) { - // Ignore callback errors - } - } -} - -bool IOSDumper::get_usb_device() -{ - FridaDeviceManager *device_manager = frida_device_manager_new(); - - while (!device) { - notify_progress("Waiting for USB device...", "waiting"); - - GError *error = nullptr; - FridaDeviceList *devices = frida_device_manager_enumerate_devices_sync( - device_manager, nullptr, &error); - - if (error) { - notify_error("Failed to enumerate devices: " + - std::string(error->message)); - g_error_free(error); - g_object_unref(device_manager); - return false; - } - - int device_count = frida_device_list_size(devices); - printf("Found %d devices\n", device_count); - for (int i = 0; i < device_count; i++) { - FridaDevice *dev = frida_device_list_get(devices, i); - // FridaDeviceType type = frida_device_get_type(dev); - GType dev_type = frida_device_get_dtype(dev); - printf("Type of device %d: %s\n", i, g_type_name(dev_type)); - if (dev_type == FRIDA_DEVICE_TYPE_USB) { - // Handle USB device - device = dev; - g_object_ref(device); - notify_progress("Connected to device: " + - std::string(frida_device_get_name(device)), - "success"); - break; - } - g_object_unref(dev); - } - - g_object_unref(devices); - - if (!device) { - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - } - - g_object_unref(device_manager); - return true; -} - -bool IOSDumper::connect_ssh(const SSHConfig &config) -{ - notify_progress("Connecting to device via SSH...", "info"); - - sshSession = ssh_new(); - if (!sshSession) { - notify_error("Failed to create SSH session"); - return false; - } - try { - ssh_options_set(sshSession, SSH_OPTIONS_HOST, config.host.c_str()); - ssh_options_set(sshSession, SSH_OPTIONS_PORT, &config.port); - ssh_options_set(sshSession, SSH_OPTIONS_USER, config.user.c_str()); - int rc = ssh_connect(sshSession); - if (rc != SSH_OK) { - notify_error("Failed to connect to SSH: " + - std::string(ssh_get_error(sshSession))); - return false; - } - - // Accept new host keys automatically - rc = ssh_session_is_known_server(sshSession); - printf("SSH session is known server: %d\n", rc); - if (rc == SSH_SERVER_NOT_KNOWN || rc == SSH_SERVER_FILE_NOT_FOUND) { - // Accept the key and update known_hosts - notify_progress("Trying to add host key to known_hosts", "info"); - rc = ssh_session_update_known_hosts(sshSession); - if (rc != SSH_OK) { - notify_error("Failed to accept new SSH host key"); - ssh_free(sshSession); - return false; - } - } else if (rc == SSH_SERVER_KNOWN_OK) { - // Host key is already known, continue - } else if (rc == SSH_SERVER_KNOWN_CHANGED) { - notify_error("SSH host key has changed!"); - return false; - } else if (rc == SSH_SERVER_FOUND_OTHER) { - notify_error("SSH host key type mismatch!"); - return false; - } else if (rc == SSH_SERVER_ERROR) { - notify_error("SSH server error: " + - std::string(ssh_get_error(sshSession))); - return false; - } - - if (!config.key_filename.empty()) { - ssh_key key; - rc = ssh_pki_import_privkey_file(config.key_filename.c_str(), - nullptr, nullptr, nullptr, &key); - if (rc != SSH_OK) { - notify_error("Failed to load SSH key"); - return false; - } - rc = ssh_userauth_publickey(sshSession, nullptr, key); - ssh_key_free(key); - } else { - rc = ssh_userauth_password(sshSession, nullptr, - config.password.c_str()); - } - - if (rc != SSH_AUTH_SUCCESS) { - notify_error("SSH authentication failed"); - return false; - } - - // Use SCP instead of SFTP - scp = ssh_scp_new(sshSession, SSH_SCP_READ, "/"); - if (!scp) { - notify_error("Failed to create SCP session"); - return false; - } - - rc = ssh_scp_init(scp); - if (rc != SSH_OK) { - notify_error("Failed to initialize SCP"); - ssh_scp_free(scp); - scp = nullptr; - return false; - } - - return true; - } catch (const std::exception &e) { - std::cerr << e.what() << '\n'; - notify_error("Exception during SSH connection: " + - std::string(e.what())); - ssh_free(sshSession); - return false; - } -} - -bool IOSDumper::create_directory(const std::string &path) -{ - if (fs::exists(path)) { - fs::remove_all(path); - } - - try { - fs::create_directories(path); - return true; - } catch (const std::exception &e) { - notify_error("Error creating directory: " + std::string(e.what())); - return false; - } -} - -std::vector IOSDumper::get_applications() -{ - std::vector apps; - - if (!device) { - notify_error("No device connected"); - return apps; - } - - GError *error = nullptr; - FridaApplicationList *applications = - frida_device_enumerate_applications_sync(device, nullptr, nullptr, - &error); - - if (error) { - notify_error("Failed to enumerate applications: " + - std::string(error->message)); - g_error_free(error); - return apps; - } - - int app_count = frida_application_list_size(applications); - notify_progress("Found " + std::to_string(app_count) + " applications", - "info"); - - for (int i = 0; i < app_count; i++) { - FridaApplication *app = frida_application_list_get(applications, i); - Application application; - application.name = frida_application_get_name(app); - application.identifier = frida_application_get_identifier(app); - application.pid = frida_application_get_pid(app); - apps.push_back(application); - g_object_unref(app); - } - - g_object_unref(applications); - return apps; -} - -bool IOSDumper::open_target_app(const std::string &name_or_bundleid, - bool kill_running, std::string &display_name, - std::string &bundle_identifier) -{ - notify_progress("Starting target app: " + name_or_bundleid, "info"); - - auto applications = get_applications(); - unsigned int pid = 0; - - for (const auto &app : applications) { - if (app.identifier == name_or_bundleid || - app.name == name_or_bundleid) { - pid = app.pid; - display_name = app.name; - bundle_identifier = app.identifier; - break; - } - } - - if (bundle_identifier.empty()) { - notify_error("App not found: " + name_or_bundleid); - return false; - } - - GError *error = nullptr; - - if (pid == 0) { - notify_progress("Spawning app: " + display_name, "info"); - pid = frida_device_spawn_sync(device, bundle_identifier.c_str(), - nullptr, nullptr, &error); - if (error) { - notify_error("Failed to spawn app: " + std::string(error->message)); - g_error_free(error); - return false; - } - - // session = frida_device_attach_sync(device, pid, nullptr, &error); - session = - frida_device_attach_sync(device, pid, nullptr, nullptr, &error); - if (error) { - notify_error("Failed to attach to app: " + - std::string(error->message)); - g_error_free(error); - return false; - } - - frida_device_resume_sync(device, pid, nullptr, &error); - if (error) { - notify_error("Failed to resume app: " + - std::string(error->message)); - g_error_free(error); - return false; - } - } else { - if (kill_running) { - notify_progress("Killing running app: " + display_name + - " (PID: " + std::to_string(pid) + ")", - "info"); - frida_device_kill_sync(device, pid, nullptr, &error); - if (error) { - notify_error("Failed to kill app: " + - std::string(error->message)); - g_error_free(error); - return false; - } - - notify_progress("Spawning app: " + display_name, "info"); - pid = frida_device_spawn_sync(device, bundle_identifier.c_str(), - nullptr, nullptr, &error); - if (error) { - notify_error("Failed to spawn app: " + - std::string(error->message)); - g_error_free(error); - return false; - } - - session = - frida_device_attach_sync(device, pid, nullptr, nullptr, &error); - if (error) { - notify_error("Failed to attach to app: " + - std::string(error->message)); - g_error_free(error); - return false; - } - - frida_device_resume_sync(device, pid, nullptr, &error); - if (error) { - notify_error("Failed to resume app: " + - std::string(error->message)); - g_error_free(error); - return false; - } - } else { - notify_progress("Attaching to running app: " + display_name + - " (PID: " + std::to_string(pid) + ")", - "info"); - session = - frida_device_attach_sync(device, pid, nullptr, nullptr, &error); - if (error) { - notify_error("Failed to attach to app: " + - std::string(error->message)); - g_error_free(error); - return false; - } - } - } - - return true; -} - -bool IOSDumper::load_dump_script() -{ - QFile file(":resources/dump.js"); // Use resource path - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - notify_error("Failed to open dump.js from resources"); - return false; - } - QTextStream in(&file); - dump_js_content = in.readAll().toStdString(); - file.close(); - return true; -} -void IOSDumper::on_frida_message(FridaScript *script, const char *message, - GBytes *data, gpointer user_data) -{ - printf("Frida message received: %s\n", message); - IOSDumper *dumper = static_cast(user_data); - dumper->handle_frida_message(std::string(message)); -} - -void IOSDumper::handle_frida_message(const std::string &message) -{ - printf("Received message: %s\n", message.c_str()); - - // Use Qt JSON parsing - QByteArray jsonBytes(message.c_str(), message.size()); - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(jsonBytes, &parseError); - - if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { - notify_error( - "Failed to parse JSON message: " + - std::string(parseError.errorString().toUtf8().constData())); - return; - } - - QJsonObject obj = doc.object(); - - if (obj.contains("payload")) { - QJsonObject payload = obj.value("payload").toObject(); - - if (payload.contains("dump") && payload.contains("path")) { - QString dump_path = payload.value("dump").toString(); - QString origin_path = payload.value("path").toString(); - - std::string local_path = - payload_path + "/" + - fs::path(dump_path.toStdString()).filename().string(); - download_file(dump_path.toStdString(), local_path); - - int index = origin_path.indexOf(".app/"); - if (index != -1) { - std::string dict_key = - fs::path(dump_path.toStdString()).filename().string(); - std::string dict_value = - origin_path.mid(index + 5).toStdString(); - file_dict[dict_key] = dict_value; - } - } - if (payload.contains("app")) { - QString app_path = payload.value("app").toString(); - std::string local_path = - payload_path + "/" + - fs::path(app_path.toStdString()).filename().string(); - download_file(app_path.toStdString(), local_path, true); - - file_dict["app"] = - fs::path(app_path.toStdString()).filename().string(); - } - if (payload.contains("done")) { - notify_progress("Download completed", "success"); - finished = true; - finished_cv.notify_all(); - } - } else if (obj.contains("done")) { - notify_progress("Download completed", "success"); - finished = true; - finished_cv.notify_all(); - } -} - -bool IOSDumper::download_file(const std::string &remote_path, - const std::string &local_path, bool recursive) -{ - // SCP only supports file transfer, not recursive directory download. - // For recursive, you would need to implement directory traversal via SSH - // commands. Here we only support single file download. - - int rc = ssh_scp_pull_request(scp); - if (rc != SSH_SCP_REQUEST_NEWFILE) { - notify_error("Failed to request file via SCP: " + remote_path); - return false; - } - - size_t file_size = ssh_scp_request_get_size(scp); - - std::ofstream out(local_path, std::ios::binary); - if (!out) { - notify_error("Failed to create local file: " + local_path); - ssh_scp_deny_request(scp, "Local file error"); - return false; - } - - char buffer[131072]; - size_t total = 0; - while (total < file_size) { - size_t to_read = std::min(sizeof(buffer), file_size - total); - int nread = ssh_scp_read(scp, buffer, to_read); - if (nread < 0) { - notify_error("Error reading from SCP"); - out.close(); - return false; - } - out.write(buffer, nread); - total += nread; - notify_download_progress(total, file_size); - } - - out.close(); - ssh_scp_accept_request(scp); - - notify_progress("Downloaded " + remote_path + " to " + local_path, - "success"); - - return true; -} - -bool IOSDumper::generate_ipa(const std::string &display_name) -{ - std::string ipa_filename = display_name + ".ipa"; - notify_progress("Generating \"" + ipa_filename + "\"", "info"); - - try { - // Move files according to file_dict - std::string app_name = file_dict["app"]; - fs::path app_path = fs::path(payload_path) / app_name; - - // If app_path exists and is not a directory, remove it - if (fs::exists(app_path) && !fs::is_directory(app_path)) { - fs::remove(app_path); - fs::create_directory(app_path); - } else if (!fs::exists(app_path)) { - fs::create_directory(app_path); - } - - for (const auto &[key, value] : file_dict) { - if (key != "app") { - fs::path from_path = fs::path(payload_path) / key; - fs::path to_path = app_path / value; - fs::create_directories(to_path.parent_path()); - fs::rename(from_path, to_path); - } - } - - // Use libzip to create IPA file - std::string ipa_path = (fs::current_path() / ipa_filename).string(); - int errorp; - zip_t *zip = zip_open(ipa_path.c_str(), ZIP_CREATE | ZIP_EXCL, &errorp); - if (!zip) { - notify_error("Failed to create IPA file with libzip"); - return false; - } - - // Add all files in Payload recursively - for (const auto &entry : - fs::recursive_directory_iterator(payload_path)) { - if (entry.is_regular_file()) { - std::string file_path = entry.path().string(); - // Archive name should be relative to temp_dir/Payload - std::string archive_name = - fs::relative(entry.path(), fs::path(payload_path)).string(); - - zip_source_t *source = - zip_source_file(zip, file_path.c_str(), 0, -1); - if (!source) { - notify_error("Failed to create zip source for: " + - file_path); - zip_close(zip); - return false; - } - - if (zip_file_add(zip, archive_name.c_str(), source, - ZIP_FL_OVERWRITE) < 0) { - notify_error("Failed to add file to zip: " + file_path); - zip_source_free(source); - zip_close(zip); - return false; - } - } - } - - zip_close(zip); - - // Remove Payload directory - fs::remove_all(payload_path); - - notify_progress("Successfully created " + ipa_filename, "success"); - return true; - } catch (const std::exception &e) { - notify_error("Error generating IPA: " + std::string(e.what())); - return false; - } -} - -bool IOSDumper::dump_app(const std::string &name_or_bundleid, - const std::string &output_name, - const SSHConfig &ssh_config, bool kill_running) -{ - finished = false; - - try { - // Get device - if (!get_usb_device()) { - return false; - } - - // Connect SSH - if (!connect_ssh(ssh_config)) { - return false; - } - - // Create payload directory - if (!create_directory(payload_path)) { - return false; - } - - // Open target app - std::string display_name, bundle_identifier; - if (!open_target_app(name_or_bundleid, kill_running, display_name, - bundle_identifier)) { - return false; - } - - // Create and load dump script - GError *error = nullptr; - FridaScript *script = frida_session_create_script_sync( - session, dump_js_content.c_str(), nullptr, nullptr, &error); - if (error) { - notify_error("Failed to create script: " + - std::string(error->message)); - g_error_free(error); - return false; - } - - // Set up message handler before loading - g_signal_connect(script, "message", G_CALLBACK(on_frida_message), this); - - // Load the script before posting - frida_script_load_sync(script, nullptr, &error); - if (error) { - notify_error("Failed to load script: " + - std::string(error->message)); - g_error_free(error); - g_object_unref(script); - return false; - } - - // Now post the message to start the dump - frida_script_post(script, "{\"type\": \"dump\"}", nullptr); - - notify_progress("Starting dump of " + display_name, "info"); - // frida_script_load_sync(script, "{\"type\":\"dump\"}", nullptr, - // &error); REMOVE THIS LINE: frida_script_load_sync(script, nullptr, - // &error); - if (error) { - notify_error("Failed to start dump: " + - std::string(error->message)); - g_error_free(error); - g_object_unref(script); - return false; - } - - // Wait for completion - std::unique_lock lock(finished_mutex); - finished_cv.wait(lock, [this] { return finished.load(); }); - - // Generate IPA - - // IOSDumper::download_file - - std::string output_ipa = - output_name.empty() ? display_name : output_name; - if (output_ipa.ends_with(".ipa")) { - output_ipa = output_ipa.substr(0, output_ipa.length() - 4); - } - - bool success = generate_ipa(output_ipa); - - g_object_unref(script); - return success; - } catch (const std::exception &e) { - notify_error("Error: " + std::string(e.what())); - return false; - } -} - -std::vector> -IOSDumper::get_all_applications() -{ - std::vector> result; - - if (!get_usb_device()) { - return result; - } - - auto applications = get_applications(); - for (const auto &app : applications) { - result.emplace_back(app.name, app.identifier); - } - - return result; -} - -bool IOSDumper::start(const std::string &name_or_bundleid, bool kill_running, - const SSHConfig &ssh_config) -{ - return dump_app(name_or_bundleid, "", ssh_config, kill_running); -} - -// C API for shared library -extern "C" { -EXPORT IOSDumper *create_dumper() { return new IOSDumper(); } - -EXPORT void destroy_dumper(IOSDumper *dumper) { delete dumper; } - -EXPORT bool dump_app_c(IOSDumper *dumper, const char *name_or_bundleid, - const char *output_name, const char *ssh_host, - int ssh_port, const char *ssh_user, - const char *ssh_password, bool kill_running) -{ - SSHConfig config; - config.host = ssh_host ? ssh_host : "localhost"; - config.port = ssh_port > 0 ? ssh_port : 2222; - config.user = ssh_user ? ssh_user : "root"; - config.password = ssh_password ? ssh_password : "alpine"; - - return dumper->dump_app(name_or_bundleid, output_name ? output_name : "", - config, kill_running); -} - -EXPORT void add_progress_callback_c(IOSDumper *dumper, - void (*callback)(const char *message, - const char *type, - int current, int total)) -{ - dumper->add_general_progress_callback([callback](const std::string &message, - const std::string &type, - int current, int total) { - callback(message.c_str(), type.c_str(), current, total); - }); -} - -EXPORT void add_error_callback_c(IOSDumper *dumper, - void (*callback)(const char *message)) -{ - dumper->add_error_callback( - [callback](const std::string &message) { callback(message.c_str()); }); -} -} JailbrokenWidget::JailbrokenWidget(QWidget *parent) : QWidget{parent} { - // Initialization code can go here - setWindowTitle("Jailbroken Device Widget"); - setMinimumSize(400, 300); - setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc; " - "border-radius: 5px;"); - - setLayout(new QVBoxLayout(this)); - QPushButton *infoButton = new QPushButton("Show Jailbreak Info", this); - connect(infoButton, &QPushButton::clicked, this, [this]() { - // Run dump logic in a background thread to avoid UI freeze - QtConcurrent::run([] { - IOSDumper dumper; - dumper.add_progress_callback([](const std::string &message, - const std::string &type, - int current, int total) { - printf("[%s] %s (%d/%d)\n", type.c_str(), message.c_str(), - current, total); - }); - dumper.add_error_callback([](const std::string &message) { - fprintf(stderr, "Error: %s\n", message.c_str()); - }); - SSHConfig ssh_config; - ssh_config.host = "0.0.0.0"; - ssh_config.port = 3333; - ssh_config.user = "root"; - ssh_config.password = "alpine"; - dumper.dump_app("Firefox", "FirefoxDump", ssh_config, true); - }); - }); + // TODO: implement } diff --git a/src/mediapreviewdialog.cpp b/src/mediapreviewdialog.cpp index 0b477dc..8ce9910 100644 --- a/src/mediapreviewdialog.cpp +++ b/src/mediapreviewdialog.cpp @@ -174,25 +174,26 @@ void MediaPreviewDialog::loadMedia() void MediaPreviewDialog::loadImage() { - // Load image asynchronously - auto future = QtConcurrent::run([this]() { - return PhotoModel::loadThumbnailFromDevice(m_device, m_filePath, - QSize(4096, 4096), ""); - }); + // TODO + // // Load image asynchronously + // auto future = QtConcurrent::run([this]() { + // return PhotoModel::loadThumbnailFromDevice(m_device, m_filePath, + // QSize(4096, 4096), ""); + // }); - auto *watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, - [this, watcher]() { - QPixmap pixmap = watcher->result(); - if (!pixmap.isNull()) { - m_originalPixmap = pixmap; - onImageLoaded(); - } else { - onImageLoadFailed(); - } - watcher->deleteLater(); - }); - watcher->setFuture(future); + // auto *watcher = new QFutureWatcher(this); + // connect(watcher, &QFutureWatcher::finished, this, + // [this, watcher]() { + // QPixmap pixmap = watcher->result(); + // if (!pixmap.isNull()) { + // m_originalPixmap = pixmap; + // onImageLoaded(); + // } else { + // onImageLoadFailed(); + // } + // watcher->deleteLater(); + // }); + // watcher->setFuture(future); } void MediaPreviewDialog::loadVideo() diff --git a/src/photoexportmanager.cpp b/src/photoexportmanager.cpp new file mode 100644 index 0000000..425acb8 --- /dev/null +++ b/src/photoexportmanager.cpp @@ -0,0 +1,247 @@ +#include "photoexportmanager.h" +#include +#include +#include +#include +#include +#include +#include + +PhotoExportManager::PhotoExportManager(QObject *parent) + : QObject(parent), m_device(nullptr), m_isExporting(false), + m_cancelRequested(false), m_workerThread(nullptr) +{ +} + +void PhotoExportManager::exportFiles(iDescriptorDevice *device, + const QStringList &filePaths, + const QString &outputDirectory) +{ + QMutexLocker locker(&m_mutex); + + if (m_isExporting) { + qWarning() << "Export operation already in progress"; + return; + } + + if (!device || !device->afcClient) { + qWarning() << "Invalid device or AFC client"; + return; + } + + if (filePaths.isEmpty()) { + qWarning() << "No files to export"; + return; + } + + // Validate output directory + QDir outputDir(outputDirectory); + if (!outputDir.exists()) { + if (!outputDir.mkpath(".")) { + qWarning() << "Could not create output directory:" + << outputDirectory; + return; + } + } + + m_device = device; + m_filePaths = filePaths; + m_outputDirectory = outputDirectory; + m_isExporting = true; + m_cancelRequested = false; + + qDebug() << "Starting export of" << filePaths.size() << "files to" + << outputDirectory; + + emit exportStarted(filePaths.size()); + + // Start export in worker thread + m_workerThread = QThread::create([this]() { performExport(); }); + + // TODO: refactor to qfuture + connect(m_workerThread, &QThread::finished, m_workerThread, + &QThread::deleteLater); + connect(m_workerThread, &QThread::finished, this, [this]() { + QMutexLocker locker(&m_mutex); + m_workerThread = nullptr; + m_isExporting = false; + }); + + m_workerThread->start(); +} + +void PhotoExportManager::cancelExport() +{ + QMutexLocker locker(&m_mutex); + if (m_isExporting) { + m_cancelRequested = true; + qDebug() << "Export cancellation requested"; + } +} + +void PhotoExportManager::performExport() +{ + int successful = 0; + int failed = 0; + + for (int i = 0; i < m_filePaths.size(); ++i) { + // Check for cancellation + { + QMutexLocker locker(&m_mutex); + if (m_cancelRequested) { + qDebug() << "Export cancelled by user"; + emit exportCancelled(); + return; + } + } + + const QString &devicePath = m_filePaths.at(i); + QString fileName = extractFileName(devicePath); + + QString outputPath = QDir(m_outputDirectory).filePath(fileName); + + // Generate unique path if file exists + outputPath = generateUniqueOutputPath(outputPath); + + ExportResult result = + exportSingleFile(m_device->afcClient, devicePath, outputPath); + + if (result.success) { + successful++; + qDebug() << "Successfully exported:" << fileName; + } else { + failed++; + qWarning() << "Failed to export" << fileName << ":" + << result.errorMessage; + } + + emit fileExported(result); + emit exportProgress(i + 1, m_filePaths.size(), fileName); + } + + // hideProgressDialog(); + + qDebug() << "Export completed - Success:" << successful + << "Failed:" << failed; + emit exportFinished(successful, failed); +} + +PhotoExportManager::ExportResult PhotoExportManager::exportSingleFile( + afc_client_t afc, const QString &devicePath, const QString &outputPath) +{ + ExportResult result; + result.filePath = devicePath; + result.outputPath = outputPath; + result.success = false; + + // Open file on device + uint64_t handle = 0; + afc_error_t afc_err = afc_file_open(afc, devicePath.toUtf8().constData(), + AFC_FOPEN_RDONLY, &handle); + + if (afc_err != AFC_E_SUCCESS) { + result.errorMessage = + QString("Failed to open file on device: %1 (AFC error: %2)") + .arg(devicePath) + .arg(static_cast(afc_err)); + return result; + } + + // Open local output file + QFile outputFile(outputPath); + if (!outputFile.open(QIODevice::WriteOnly)) { + result.errorMessage = QString("Failed to create local file: %1 (%2)") + .arg(outputPath) + .arg(outputFile.errorString()); + afc_file_close(afc, handle); + return result; + } + + // Copy data from device to local file + char buffer[4096]; + uint32_t bytesRead = 0; + qint64 totalBytes = 0; + + while (afc_file_read(afc, handle, buffer, sizeof(buffer), &bytesRead) == + AFC_E_SUCCESS && + bytesRead > 0) { + // Check for cancellation during file copy + { + QMutexLocker locker(&m_mutex); + if (m_cancelRequested) { + outputFile.close(); + outputFile.remove(); // Clean up partial file + afc_file_close(afc, handle); + result.errorMessage = "Export cancelled"; + return result; + } + } + + qint64 bytesWritten = outputFile.write(buffer, bytesRead); + if (bytesWritten != bytesRead) { + result.errorMessage = + QString("Write error: only wrote %1 of %2 bytes") + .arg(bytesWritten) + .arg(bytesRead); + outputFile.close(); + outputFile.remove(); // Clean up partial file + afc_file_close(afc, handle); + return result; + } + + totalBytes += bytesRead; + } + + // Clean up + outputFile.close(); + afc_file_close(afc, handle); + + if (totalBytes == 0) { + result.errorMessage = "No data read from device file"; + outputFile.remove(); // Clean up empty file + return result; + } + + result.success = true; + qDebug() << "Exported" << totalBytes << "bytes from" << devicePath << "to" + << outputPath; + return result; +} + +QString PhotoExportManager::extractFileName(const QString &devicePath) const +{ + // Extract filename from device path (similar to strrchr in C) + int lastSlash = devicePath.lastIndexOf('/'); + if (lastSlash != -1 && lastSlash < devicePath.length() - 1) { + return devicePath.mid(lastSlash + 1); + } + return devicePath; // Return full path if no slash found +} + +QString +PhotoExportManager::generateUniqueOutputPath(const QString &basePath) const +{ + if (!QFile::exists(basePath)) { + return basePath; + } + + QFileInfo fileInfo(basePath); + QString baseName = fileInfo.completeBaseName(); + QString suffix = fileInfo.suffix(); + QString directory = fileInfo.absolutePath(); + + int counter = 1; + QString uniquePath; + + do { + QString newName = QString("%1_%2").arg(baseName).arg(counter); + if (!suffix.isEmpty()) { + newName += "." + suffix; + } + uniquePath = QDir(directory).filePath(newName); + counter++; + } while (QFile::exists(uniquePath) && + counter < 10000); // Prevent infinite loop + + return uniquePath; +} \ No newline at end of file diff --git a/src/photoexportmanager.h b/src/photoexportmanager.h new file mode 100644 index 0000000..a088087 --- /dev/null +++ b/src/photoexportmanager.h @@ -0,0 +1,68 @@ +#ifndef PHOTOEXPORTMANAGER_H +#define PHOTOEXPORTMANAGER_H + +#include "iDescriptor.h" +#include +#include +#include +#include +#include +#include + +class PhotoExportManager : public QObject +{ + Q_OBJECT + +public: + explicit PhotoExportManager(QObject *parent = nullptr); + + struct ExportResult { + QString filePath; + QString outputPath; + bool success; + QString errorMessage; + }; + + // Start export operation + void exportFiles(iDescriptorDevice *device, const QStringList &filePaths, + const QString &outputDirectory); + + // Check if export is currently running + bool isExporting() const { return m_isExporting; } + + // Cancel current export operation + void cancelExport(); + +signals: + void exportStarted(int totalFiles); + void fileExported(const PhotoExportManager::ExportResult &result); + void exportProgress(int completed, int total, + const QString ¤tFileName); + void exportFinished(int successful, int failed); + void exportCancelled(); + +private slots: + void performExport(); + +private: + // Export single file using AFC + ExportResult exportSingleFile(afc_client_t afc, const QString &devicePath, + const QString &outputPath); + + // Extract filename from device path + QString extractFileName(const QString &devicePath) const; + + // Generate unique output path if file exists + QString generateUniqueOutputPath(const QString &basePath) const; + + // Member variables + iDescriptorDevice *m_device; + QStringList m_filePaths; + QString m_outputDirectory; + bool m_isExporting; + bool m_cancelRequested; + QMutex m_mutex; + QThread *m_workerThread; +}; + +#endif // PHOTOEXPORTMANAGER_H \ No newline at end of file diff --git a/src/photomodel.cpp b/src/photomodel.cpp index 1376d90..ddce602 100644 --- a/src/photomodel.cpp +++ b/src/photomodel.cpp @@ -7,16 +7,19 @@ #include #include #include +#include #include #include #include #include +#include // Forward declare your helper function QByteArray read_afc_file_to_byte_array(afc_client_t afcClient, const char *path); PhotoModel::PhotoModel(iDescriptorDevice *device, QObject *parent) - : QAbstractListModel(parent), m_device(device), m_thumbnailSize(256, 256) + : QAbstractListModel(parent), m_device(device), m_thumbnailSize(256, 256), + m_sortOrder(NewestFirst), m_filterType(All) { // Set up cache directory for persistent storage m_cacheDir = @@ -360,6 +363,7 @@ QPixmap PhotoModel::loadThumbnailFromDevice(iDescriptorDevice *device, void PhotoModel::populatePhotoPaths() { beginResetModel(); + m_allPhotos.clear(); m_photos.clear(); // Your existing logic to populate photo paths @@ -371,10 +375,9 @@ void PhotoModel::populatePhotoPaths() if (files) { for (int i = 0; files[i]; i++) { QString fileName = QString::fromUtf8(files[i]); - if ( - // fileName.endsWith(".JPG", Qt::CaseInsensitive) || - // fileName.endsWith(".PNG", Qt::CaseInsensitive) || - // fileName.endsWith(".HEIC", Qt::CaseInsensitive) || + if (fileName.endsWith(".JPG", Qt::CaseInsensitive) || + fileName.endsWith(".PNG", Qt::CaseInsensitive) || + fileName.endsWith(".HEIC", Qt::CaseInsensitive) || fileName.endsWith(".MOV", Qt::CaseInsensitive) || fileName.endsWith(".MP4", Qt::CaseInsensitive) || fileName.endsWith(".M4V", Qt::CaseInsensitive)) { @@ -383,14 +386,219 @@ void PhotoModel::populatePhotoPaths() info.filePath = QString(photoDir) + "/" + fileName; info.fileName = fileName; info.thumbnailRequested = false; + info.fileType = determineFileType(fileName); + info.dateTime = extractDateTimeFromFile(info.filePath); - m_photos.append(info); + m_allPhotos.append(info); } } afc_dictionary_free(files); } + // Apply initial filtering and sorting + applyFilterAndSort(); + endResetModel(); - qDebug() << "Loaded" << m_photos.size() << "photos from device"; + qDebug() << "Loaded" << m_allPhotos.size() << "media files from device"; + qDebug() << "After filtering:" << m_photos.size() << "items shown"; +} + +// Sorting and filtering methods +void PhotoModel::setSortOrder(SortOrder order) +{ + if (m_sortOrder != order) { + m_sortOrder = order; + applyFilterAndSort(); + } +} + +void PhotoModel::setFilterType(FilterType filter) +{ + if (m_filterType != filter) { + m_filterType = filter; + applyFilterAndSort(); + } +} + +void PhotoModel::applyFilterAndSort() +{ + beginResetModel(); + + // Filter photos + m_photos.clear(); + for (const PhotoInfo &info : m_allPhotos) { + if (matchesFilter(info)) { + m_photos.append(info); + } + } + + // Sort photos + sortPhotos(m_photos); + + endResetModel(); + + qDebug() << "Applied filter and sort - showing" << m_photos.size() << "of" + << m_allPhotos.size() << "items"; +} + +void PhotoModel::sortPhotos(QList &photos) const +{ + std::sort(photos.begin(), photos.end(), + [this](const PhotoInfo &a, const PhotoInfo &b) { + if (m_sortOrder == NewestFirst) { + return a.dateTime > b.dateTime; + } else { + return a.dateTime < b.dateTime; + } + }); +} + +bool PhotoModel::matchesFilter(const PhotoInfo &info) const +{ + switch (m_filterType) { + case All: + return true; + case ImagesOnly: + return info.fileType == PhotoInfo::Image; + case VideosOnly: + return info.fileType == PhotoInfo::Video; + default: + return true; + } +} + +// Export functionality +QStringList +PhotoModel::getSelectedFilePaths(const QModelIndexList &indexes) const +{ + QStringList paths; + for (const QModelIndex &index : indexes) { + if (index.isValid() && index.row() < m_photos.size()) { + paths.append(m_photos.at(index.row()).filePath); + } + } + return paths; +} + +QString PhotoModel::getFilePath(const QModelIndex &index) const +{ + if (index.isValid() && index.row() < m_photos.size()) { + return m_photos.at(index.row()).filePath; + } + return QString(); +} + +PhotoInfo::FileType PhotoModel::getFileType(const QModelIndex &index) const +{ + if (index.isValid() && index.row() < m_photos.size()) { + return m_photos.at(index.row()).fileType; + } + return PhotoInfo::Image; +} + +QStringList PhotoModel::getAllFilePaths() const +{ + QStringList paths; + for (const PhotoInfo &info : m_allPhotos) { + paths.append(info.filePath); + } + return paths; +} + +QStringList PhotoModel::getFilteredFilePaths() const +{ + QStringList paths; + for (const PhotoInfo &info : m_photos) { + paths.append(info.filePath); + } + return paths; +} + +// Helper methods +QDateTime PhotoModel::extractDateTimeFromFile(const QString &filePath) const +{ + // Use AFC to get actual file creation time from device + plist_t info = nullptr; + afc_error_t afc_err = afc_get_file_info_plist( + // TODO:AFC CLIENT IS NOT LONG LIVED + m_device->afcClient, filePath.toUtf8().constData(), &info); + + if (afc_err == AFC_E_SUCCESS && info) { + // Try to get st_birthtime (creation time) first + plist_t birthtime_node = plist_dict_get_item(info, "st_birthtime"); + if (birthtime_node && + plist_get_node_type(birthtime_node) == PLIST_UINT) { + uint64_t birthtime_ns = 0; + plist_get_uint_val(birthtime_node, &birthtime_ns); + + // Convert nanoseconds since epoch to QDateTime + // The timestamp appears to be in nanoseconds since Unix epoch + uint64_t seconds = birthtime_ns / 1000000000ULL; + QDateTime dateTime = + QDateTime::fromSecsSinceEpoch(seconds, Qt::UTC); + + plist_free(info); + if (dateTime.isValid()) { + return dateTime; + } + } + + // Fallback to st_mtime (modification time) if birthtime not available + plist_t mtime_node = plist_dict_get_item(info, "st_mtime"); + if (mtime_node && plist_get_node_type(mtime_node) == PLIST_UINT) { + uint64_t mtime_ns = 0; + plist_get_uint_val(mtime_node, &mtime_ns); + + // Convert nanoseconds since epoch to QDateTime + uint64_t seconds = mtime_ns / 1000000000ULL; + QDateTime dateTime = + QDateTime::fromSecsSinceEpoch(seconds, Qt::UTC); + + plist_free(info); + if (dateTime.isValid()) { + return dateTime; + } + } + + plist_free(info); + } + + // Final fallback: try to extract date from filename pattern like + // IMG_20231025_143052.jpg + QFileInfo fileInfo(filePath); + QString baseName = fileInfo.baseName(); + + QRegularExpression dateRegex( + R"((\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2}))"); + QRegularExpressionMatch match = dateRegex.match(baseName); + + if (match.hasMatch()) { + int year = match.captured(1).toInt(); + int month = match.captured(2).toInt(); + int day = match.captured(3).toInt(); + int hour = match.captured(4).toInt(); + int minute = match.captured(5).toInt(); + int second = match.captured(6).toInt(); + + QDateTime dateTime(QDate(year, month, day), + QTime(hour, minute, second)); + if (dateTime.isValid()) { + return dateTime; + } + } + + // Ultimate fallback: return current time + return QDateTime::currentDateTime(); +} + +PhotoInfo::FileType PhotoModel::determineFileType(const QString &fileName) const +{ + if (fileName.endsWith(".MOV", Qt::CaseInsensitive) || + fileName.endsWith(".MP4", Qt::CaseInsensitive) || + fileName.endsWith(".M4V", Qt::CaseInsensitive)) { + return PhotoInfo::Video; + } else { + return PhotoInfo::Image; + } } \ No newline at end of file diff --git a/src/photomodel.h b/src/photomodel.h index 2a50529..b4c1432 100644 --- a/src/photomodel.h +++ b/src/photomodel.h @@ -1,61 +1,108 @@ #ifndef PHOTOMODEL_H #define PHOTOMODEL_H -#include "iDescriptor.h" // For iDescriptorDevice +#include "iDescriptor.h" #include #include +#include +#include #include -#include -#include -#include +#include +#include #include +struct PhotoInfo { + QString filePath; + QString fileName; + QDateTime dateTime; + bool thumbnailRequested = false; + + enum FileType { Image, Video }; + FileType fileType; +}; + class PhotoModel : public QAbstractListModel { Q_OBJECT public: + enum SortOrder { NewestFirst, OldestFirst }; + + enum FilterType { All, ImagesOnly, VideosOnly }; + explicit PhotoModel(iDescriptorDevice *device, QObject *parent = nullptr); ~PhotoModel(); - // QAbstractListModel interface + // QAbstractItemModel interface int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + // Thumbnail management void setThumbnailSize(const QSize &size); void clearCache(); + + // Sorting and filtering + void setSortOrder(SortOrder order); + SortOrder sortOrder() const { return m_sortOrder; } + + void setFilterType(FilterType filter); + FilterType filterType() const { return m_filterType; } + + // Export functionality + QStringList getSelectedFilePaths(const QModelIndexList &indexes) const; + QString getFilePath(const QModelIndex &index) const; + PhotoInfo::FileType getFileType(const QModelIndex &index) const; + + // Get all items for export + QStringList getAllFilePaths() const; + QStringList getFilteredFilePaths() const; + +signals: + void thumbnailNeedsLoading(int index); + void exportRequested(const QStringList &filePaths); + +private slots: + void requestThumbnail(int index); + +private: + // Data members + iDescriptorDevice *m_device; + QList m_allPhotos; // All photos from device + QList m_photos; // Currently filtered/sorted photos + + // Thumbnail management + QSize m_thumbnailSize; + mutable QCache m_thumbnailCache; + QString m_cacheDir; + mutable QHash *> m_activeLoaders; + mutable QSet m_loadingPaths; + + // Sorting and filtering + SortOrder m_sortOrder; + FilterType m_filterType; + + // Helper methods + void populatePhotoPaths(); + void applyFilterAndSort(); + void sortPhotos(QList &photos) const; + bool matchesFilter(const PhotoInfo &info) const; + + QString getThumbnailCacheKey(const QString &filePath) const; + QString getThumbnailCachePath(const QString &filePath) const; + + QDateTime extractDateTimeFromFile(const QString &filePath) const; + PhotoInfo::FileType determineFileType(const QString &fileName) const; + + // Static helper methods static QPixmap loadThumbnailFromDevice(iDescriptorDevice *device, const QString &filePath, const QSize &size, const QString &cachePath); + static QPixmap generateVideoThumbnail(iDescriptorDevice *device, const QString &filePath, const QSize &requestedSize); - -signals: - void thumbnailNeedsLoading(int index); - -private: - struct PhotoInfo { - QString filePath; - QString fileName; - bool thumbnailRequested = false; - }; - - // Helper functions - QString getThumbnailCacheKey(const QString &filePath) const; - QString getThumbnailCachePath(const QString &filePath) const; - void requestThumbnail(int index); - void populatePhotoPaths(); - - // Member variables - iDescriptorDevice *m_device; - QList m_photos; - mutable QCache m_thumbnailCache; - mutable QHash *> m_activeLoaders; - mutable QSet m_loadingPaths; // Additional safety net - QSize m_thumbnailSize; - QString m_cacheDir; }; + #endif // PHOTOMODEL_H \ No newline at end of file diff --git a/src/virtual_location.cpp b/src/virtual_location.cpp index bd99e92..59d014a 100644 --- a/src/virtual_location.cpp +++ b/src/virtual_location.cpp @@ -1,4 +1,5 @@ #include "virtual_location.h" +#include "appcontext.h" #include "devdiskmanager.h" #include "iDescriptor.h" #include @@ -24,9 +25,6 @@ VirtualLocation::VirtualLocation(iDescriptorDevice *device, QWidget *parent) : QWidget{parent}, m_device(device) { // Create the main layout - bool res = DevDiskManager::sharedInstance()->mountCompatibleImage( - m_device, QString("/tmp")); - qDebug() << "Mount result:" << res; QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->setContentsMargins(10, 10, 10, 10); mainLayout->setSpacing(10); @@ -152,6 +150,20 @@ VirtualLocation::VirtualLocation(iDescriptorDevice *device, QWidget *parent) qDebug() << "QuickWidget status:" << m_quickWidget->status(); qDebug() << "QuickWidget errors:" << m_quickWidget->errors(); + + connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, + [this](const std::string &udid) { + if (m_device->udid == udid) { + this->close(); + this->deleteLater(); + } + // qDebug() << "VirtualLocation detected device change to" + // << (device ? device->udid.c_str() : "null"); + }); + + bool res = + DevDiskManager::sharedInstance()->downloadCompatibleImage(m_device); + qDebug() << "Mount result:" << res; } void VirtualLocation::onQuickWidgetStatusChanged(QQuickWidget::Status status) @@ -286,6 +298,15 @@ void VirtualLocation::updateInputsFromMap(double latitude, double longitude) void VirtualLocation::onApplyClicked() { + bool devImgSuccess = + DevDiskManager::sharedInstance()->mountCompatibleImage(m_device); + if (!devImgSuccess) { + warn("Failed to mount developer image on device. Cannot set location."); + qDebug() << "Failed to mount developer image on device. Cannot set " + "location."; + return; + } + bool latOk, lonOk, altOk; double latitude = m_latitudeEdit->text().toDouble(&latOk); double longitude = m_longitudeEdit->text().toDouble(&lonOk);