diff --git a/src/devdiskimageswidget.cpp b/src/devdiskimageswidget.cpp index c5316f3..ba10959 100644 --- a/src/devdiskimageswidget.cpp +++ b/src/devdiskimageswidget.cpp @@ -1,6 +1,7 @@ #include "devdiskimageswidget.h" #include "appcontext.h" #include "iDescriptor.h" +#include #include #include #include @@ -20,7 +21,9 @@ #include #include #include +#include #include + // Handle errors , event though it failed, ui thinks it is mounted /*DetailedError: Error Domain=com.apple.MobileStorage.ErrorDomain Code=-2 * "Failed to mount @@ -52,6 +55,9 @@ DevDiskImagesWidget::DevDiskImagesWidget(iDescriptorDevice *device, &DevDiskImagesWidget::updateDeviceList); connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, &DevDiskImagesWidget::updateDeviceList); + connect(m_deviceComboBox, + QOverload::of(&QComboBox::currentIndexChanged), this, + &DevDiskImagesWidget::onDeviceSelectionChanged); } void DevDiskImagesWidget::setupUi() @@ -82,9 +88,21 @@ void DevDiskImagesWidget::setupUi() m_stackedWidget = new QStackedWidget(this); layout->addWidget(m_stackedWidget); - m_statusLabel = new QLabel(tr("Fetching image list...")); - m_statusLabel->setAlignment(Qt::AlignCenter); - m_stackedWidget->addWidget(m_statusLabel); + m_initialStatusLabel = new QLabel("Fetching image list..."); + m_initialStatusLabel->setAlignment(Qt::AlignCenter); + m_stackedWidget->addWidget(m_initialStatusLabel); + + m_errorWidget = new QWidget(this); + QPushButton *retryButton = new QPushButton(tr("Retry"), m_errorWidget); + connect(retryButton, &QPushButton::clicked, this, + &DevDiskImagesWidget::fetchImageList); + + auto *errorLayout = new QVBoxLayout(m_errorWidget); + m_statusLabel = new QLabel(""); + errorLayout->addWidget(m_statusLabel); + errorLayout->addWidget(retryButton); + errorLayout->addStretch(); + m_stackedWidget->addWidget(m_errorWidget); m_imageListWidget = new QListWidget(this); m_stackedWidget->addWidget(m_imageListWidget); @@ -96,8 +114,7 @@ void DevDiskImagesWidget::setupUi() void DevDiskImagesWidget::fetchImageList() { - m_stackedWidget->setCurrentWidget(m_statusLabel); - m_statusLabel->setText(tr("Fetching image list...")); + m_stackedWidget->setCurrentWidget(m_initialStatusLabel); QUrl url("https://api.github.com/repos/mspvirajpatel/" "Xcode_Developer_Disk_Images/git/trees/master?recursive=true"); @@ -110,8 +127,9 @@ void DevDiskImagesWidget::fetchImageList() void DevDiskImagesWidget::onImageListFetchFinished(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { - m_statusLabel->setText( - tr("Error fetching image list: %1").arg(reply->errorString())); + m_errorWidget->setVisible(true); + m_statusLabel->setText(reply->errorString()); + m_stackedWidget->setCurrentWidget(m_errorWidget); reply->deleteLater(); return; } @@ -124,6 +142,17 @@ void DevDiskImagesWidget::onImageListFetchFinished(QNetworkReply *reply) m_stackedWidget->setCurrentWidget(m_imageListWidget); } +void DevDiskImagesWidget::onDeviceSelectionChanged(int index) +{ + if (index < 0 || + index >= AppContext::sharedInstance()->getAllDevices().size()) { + m_currentDevice = nullptr; + } else { + m_currentDevice = AppContext::sharedInstance()->getAllDevices()[index]; + } + parseAndDisplayImages(m_imageListJsonData); +} + void DevDiskImagesWidget::parseAndDisplayImages(const QByteArray &jsonData) { m_imageListWidget->clear(); @@ -131,8 +160,8 @@ void DevDiskImagesWidget::parseAndDisplayImages(const QByteArray &jsonData) QJsonDocument doc = QJsonDocument::fromJson(jsonData); if (!doc.isObject()) { - m_statusLabel->setText(tr("Failed to parse JSON response.")); - m_stackedWidget->setCurrentWidget(m_statusLabel); + m_statusLabel->setText(tr("Invalid image list format.")); + m_stackedWidget->setCurrentWidget(m_errorWidget); return; } @@ -152,6 +181,29 @@ void DevDiskImagesWidget::parseAndDisplayImages(const QByteArray &jsonData) } } + // Get device iOS version for compatibility checking + QString deviceVersion; + int deviceMajorVersion = 0; + int deviceMinorVersion = 0; + bool hasConnectedDevice = false; + + if (m_currentDevice && m_currentDevice->device) { + // TODO : use the macro IDEVICE_DEVICE_VERSION + unsigned int device_version = + idevice_get_device_version(m_currentDevice->device); + deviceMajorVersion = (device_version >> 16) & 0xFF; + deviceMinorVersion = (device_version >> 8) & 0xFF; + deviceVersion = + QString("%1.%2").arg(deviceMajorVersion).arg(deviceMinorVersion); + hasConnectedDevice = true; + } + + qDebug() << "Has connected device:" << hasConnectedDevice; + + // Separate compatible and other versions + QStringList compatibleVersions; + QStringList otherVersions; + for (auto it = imageFiles.constBegin(); it != imageFiles.constEnd(); ++it) { if (it.value().contains("DeveloperDiskImage.dmg") && it.value().contains("DeveloperDiskImage.dmg.signature")) { @@ -161,36 +213,130 @@ void DevDiskImagesWidget::parseAndDisplayImages(const QByteArray &jsonData) it.value()["DeveloperDiskImage.dmg"], it.value()["DeveloperDiskImage.dmg.signature"]}; - auto *itemWidget = new QWidget(); - auto *itemLayout = new QHBoxLayout(itemWidget); - itemLayout->addWidget(new QLabel(version)); - - QString versionPath = QDir(m_downloadPath).filePath(version); - bool exists = QDir(versionPath).exists(); - - if (exists) { - itemLayout->addWidget(new QLabel(tr("(already exists)"))); + // Determine compatibility + bool isCompatible = false; + if (hasConnectedDevice) { + // Parse version string (e.g., "15.0", "16.1") + QStringList versionParts = version.split('.'); + if (versionParts.size() >= 1) { + bool ok; + int imageMajorVersion = versionParts[0].toInt(&ok); + if (ok) { + // iOS 16+ uses iOS 16 images, earlier versions use + // exact or lower version + if (deviceMajorVersion >= 16) { + isCompatible = (imageMajorVersion == 16); + } else { + isCompatible = + (imageMajorVersion == deviceMajorVersion); + } + } + } } - itemLayout->addStretch(); - - auto *progressBar = new QProgressBar(); - progressBar->setVisible(false); - itemLayout->addWidget(progressBar); - - auto *downloadButton = - new QPushButton(exists ? tr("Re-download") : tr("Download")); - downloadButton->setProperty("version", version); - connect(downloadButton, &QPushButton::clicked, this, - &DevDiskImagesWidget::onDownloadButtonClicked); - itemLayout->addWidget(downloadButton); - - auto *listItem = new QListWidgetItem(m_imageListWidget); - listItem->setSizeHint(itemWidget->sizeHint()); - m_imageListWidget->addItem(listItem); - m_imageListWidget->setItemWidget(listItem, itemWidget); + if (isCompatible) { + compatibleVersions.append(version); + } else { + otherVersions.append(version); + } } } + + // Sort versions (compatible ones first, then others) + auto versionSort = [](const QString &a, const QString &b) { + QStringList aParts = a.split('.'); + QStringList bParts = b.split('.'); + + for (int i = 0; i < qMax(aParts.size(), bParts.size()); ++i) { + int aNum = (i < aParts.size()) ? aParts[i].toInt() : 0; + int bNum = (i < bParts.size()) ? bParts[i].toInt() : 0; + + if (aNum != bNum) { + return aNum > bNum; // Descending order (newest first) + } + } + return false; + }; + + std::sort(compatibleVersions.begin(), compatibleVersions.end(), + versionSort); + std::sort(otherVersions.begin(), otherVersions.end(), versionSort); + + // Create UI items - compatible versions first + auto createVersionItem = [&](const QString &version, bool isCompatible) { + auto *itemWidget = new QWidget(); + auto *itemLayout = new QHBoxLayout(itemWidget); + + auto *versionLabel = new QLabel(version); + if (isCompatible) { + versionLabel->setStyleSheet( + "QLabel { font-weight: bold; color: #2E7D32; }"); + } + itemLayout->addWidget(versionLabel); + + // Add compatibility label + if (hasConnectedDevice) { + if (isCompatible) { + auto *compatLabel = new QLabel(tr("✓ Compatible")); + compatLabel->setStyleSheet( + "QLabel { color: #2E7D32; font-weight: bold; }"); + itemLayout->addWidget(compatLabel); + } else { + auto *incompatLabel = new QLabel(tr("⚠ Not recommended")); + incompatLabel->setStyleSheet("QLabel { color: #F57C00; }"); + itemLayout->addWidget(incompatLabel); + } + } + + QString versionPath = QDir(m_downloadPath).filePath(version); + bool exists = QDir(versionPath).exists(); + + if (exists) { + itemLayout->addWidget(new QLabel(tr("(already exists)"))); + } + + itemLayout->addStretch(); + + auto *progressBar = new QProgressBar(); + progressBar->setVisible(false); + itemLayout->addWidget(progressBar); + + auto *downloadButton = + new QPushButton(exists ? tr("Re-download") : tr("Download")); + downloadButton->setProperty("version", version); + connect(downloadButton, &QPushButton::clicked, this, + &DevDiskImagesWidget::onDownloadButtonClicked); + itemLayout->addWidget(downloadButton); + + auto *listItem = new QListWidgetItem(m_imageListWidget); + listItem->setSizeHint(itemWidget->sizeHint()); + m_imageListWidget->addItem(listItem); + m_imageListWidget->setItemWidget(listItem, itemWidget); + }; + + // Add compatible versions first + for (const QString &version : compatibleVersions) { + createVersionItem(version, true); + } + + // Add separator if we have both compatible and other versions + if (!compatibleVersions.isEmpty() && !otherVersions.isEmpty()) { + auto *separatorItem = new QListWidgetItem(m_imageListWidget); + auto *separatorWidget = new QWidget(); + auto *separatorLayout = new QHBoxLayout(separatorWidget); + auto *separatorLabel = new QLabel(tr("Other versions")); + separatorLabel->setStyleSheet( + "QLabel { font-weight: bold; color: #757575; margin: 10px 0; }"); + separatorLayout->addWidget(separatorLabel); + separatorItem->setSizeHint(separatorWidget->sizeHint()); + m_imageListWidget->addItem(separatorItem); + m_imageListWidget->setItemWidget(separatorItem, separatorWidget); + } + + // Add other versions + for (const QString &version : otherVersions) { + createVersionItem(version, false); + } } void DevDiskImagesWidget::onDownloadButtonClicked() @@ -371,15 +517,22 @@ void DevDiskImagesWidget::onFileDownloadFinished() void DevDiskImagesWidget::updateDeviceList() { - QString currentUdid; + auto devices = AppContext::sharedInstance()->getAllDevices(); + qDebug() << "devdiskwidget devices:" << devices.size(); + QString currentUdid = ""; if (m_deviceComboBox->count() > 0 && m_deviceComboBox->currentIndex() >= 0) { currentUdid = m_deviceComboBox->currentData().toString(); } + // Temporarily disconnect to avoid triggering onDeviceSelectionChanged + // multiple times + disconnect(m_deviceComboBox, + QOverload::of(&QComboBox::currentIndexChanged), this, + &DevDiskImagesWidget::onDeviceSelectionChanged); + m_deviceComboBox->clear(); - auto devices = AppContext::sharedInstance()->getAllDevices(); int newIndex = -1; for (int i = 0; i < devices.size(); ++i) { auto *device = devices.at(i); @@ -395,6 +548,30 @@ void DevDiskImagesWidget::updateDeviceList() if (newIndex != -1) { m_deviceComboBox->setCurrentIndex(newIndex); + m_currentDevice = devices.at(newIndex); + } else if (!devices.isEmpty()) { + // If no previous device was selected but devices are available, select + // the first one + m_deviceComboBox->setCurrentIndex(0); + m_currentDevice = devices.at(0); + } else { + m_currentDevice = nullptr; + } + + // Reconnect the signal + connect(m_deviceComboBox, + QOverload::of(&QComboBox::currentIndexChanged), this, + &DevDiskImagesWidget::onDeviceSelectionChanged); + + qDebug() << "devdiskwidget device:" << m_deviceComboBox->currentText(); + qDebug() << "devdiskwidget Current device:" + << (m_currentDevice + ? m_currentDevice->deviceInfo.deviceName.c_str() + : "None"); + + // Refresh the UI with the updated device information + if (!m_imageListJsonData.isEmpty()) { + parseAndDisplayImages(m_imageListJsonData); } } @@ -433,6 +610,7 @@ void DevDiskImagesWidget::mountImage(const QString &version) return; } + // TODO: add a refresh button QString versionPath = QDir(m_downloadPath).filePath(version); if (!QDir(versionPath).exists()) { QMessageBox::warning( @@ -488,3 +666,31 @@ void DevDiskImagesWidget::changeDownloadDirectory() } } } + +void DevDiskImagesWidget::closeEvent(QCloseEvent *event) +{ + if (!m_activeDownloads.isEmpty()) { + auto reply = QMessageBox::question( + this, tr("Downloads in Progress"), + tr("There are %1 download(s) in progress. Do you really want to " + "close and cancel all downloads?") + .arg(m_activeDownloads.size()), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (reply == QMessageBox::No) { + event->ignore(); + return; + } + + // Cancel all active downloads + for (auto it = m_activeDownloads.begin(); it != m_activeDownloads.end(); + ++it) { + QNetworkReply *reply = it.key(); + if (reply) { + reply->abort(); + } + } + } + + event->accept(); +} \ No newline at end of file diff --git a/src/devdiskimageswidget.h b/src/devdiskimageswidget.h index 80d942d..5e153b6 100644 --- a/src/devdiskimageswidget.h +++ b/src/devdiskimageswidget.h @@ -37,7 +37,8 @@ private: void parseAndDisplayImages(const QByteArray &jsonData); void startDownload(const QString &version); void mountImage(const QString &version); - + void onDeviceSelectionChanged(int index); + void closeEvent(QCloseEvent *event) override; struct DownloadItem { QNetworkReply *dmgReply = nullptr; QNetworkReply *sigReply = nullptr; @@ -53,6 +54,8 @@ private: QStackedWidget *m_stackedWidget; QListWidget *m_imageListWidget; QLabel *m_statusLabel; + QLabel *m_initialStatusLabel; + QWidget *m_errorWidget; QLineEdit *m_downloadPathEdit; QComboBox *m_deviceComboBox; QPushButton *m_mountButton;