diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 44bf4a8..3c0bf4b 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -67,6 +67,10 @@ jobs: libzip-dev \ libssh-dev \ libfuse3-dev \ + libavformat-dev \ + libavcodec-dev \ + libavutil-dev \ + libswscale-dev - name: Install Qt uses: jurplel/install-qt-action@v3 diff --git a/lib/zupdater b/lib/zupdater index ba63a9f..bd50e5f 160000 --- a/lib/zupdater +++ b/lib/zupdater @@ -1 +1 @@ -Subproject commit ba63a9f0dd6e9e9756381ec3928916ff1c2bebbd +Subproject commit bd50e5f76d2ead1acc2f625b4cc043b399e4815e diff --git a/resources.qrc b/resources.qrc index 5d6e357..14b0dfe 100644 --- a/resources.qrc +++ b/resources.qrc @@ -64,5 +64,6 @@ resources/ipad-mockups/ipad.png resources/DeveloperDiskImages.json resources/keychain.mp4 + resources/wireless-gallery-import.mp4 \ No newline at end of file diff --git a/resources/wireless-import.mp4 b/resources/wireless-gallery-import.mp4 similarity index 100% rename from resources/wireless-import.mp4 rename to resources/wireless-gallery-import.mp4 diff --git a/src/appcontext.cpp b/src/appcontext.cpp index b3b63ba..fe8e832 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -143,9 +143,9 @@ int AppContext::getConnectedDeviceCount() const */ void AppContext::removeDevice(QString _udid) { - const std::string uuid = _udid.toStdString(); + const std::string udid = _udid.toStdString(); qDebug() << "AppContext::removeDevice device with UUID:" - << QString::fromStdString(uuid); + << QString::fromStdString(udid); if (m_pendingDevices.contains(_udid)) { m_pendingDevices.removeAll(_udid); @@ -157,16 +157,16 @@ void AppContext::removeDevice(QString _udid) " not found in pending devices."; } - if (!m_devices.contains(uuid)) { + if (!m_devices.contains(udid)) { qDebug() << "Device with UUID " + _udid + " not found in normal devices."; return; } - iDescriptorDevice *device = m_devices[uuid]; - m_devices.remove(uuid); + iDescriptorDevice *device = m_devices[udid]; + m_devices.remove(udid); - emit deviceRemoved(uuid); + emit deviceRemoved(udid); emit deviceChange(); std::lock_guard lock(*device->mutex); @@ -202,9 +202,9 @@ void AppContext::removeRecoveryDevice(uint64_t ecid) } #endif -iDescriptorDevice *AppContext::getDevice(const std::string &uuid) +iDescriptorDevice *AppContext::getDevice(const std::string &udid) { - return m_devices.value(uuid, nullptr); + return m_devices.value(udid, nullptr); } QList AppContext::getAllDevices() @@ -283,9 +283,9 @@ void AppContext::setCurrentDeviceSelection(const DeviceSelection &selection) { qDebug() << "New selection -" << " Type:" << selection.type - << " UUID:" << QString::fromStdString(selection.uuid) + << " UDID:" << QString::fromStdString(selection.udid) << " ECID:" << selection.ecid << " Section:" << selection.section; - if (m_currentSelection.uuid == selection.uuid && + if (m_currentSelection.udid == selection.udid && m_currentSelection.ecid == selection.ecid && m_currentSelection.section == selection.section) { qDebug() << "setCurrentDeviceSelection: No change in selection"; diff --git a/src/devicemanagerwidget.cpp b/src/devicemanagerwidget.cpp index dacda8c..508f209 100644 --- a/src/devicemanagerwidget.cpp +++ b/src/devicemanagerwidget.cpp @@ -318,13 +318,13 @@ void DeviceManagerWidget::onDeviceSelectionChanged( switch (selection.type) { case DeviceSelection::Normal: - if (m_deviceWidgets.contains(selection.uuid)) { - if (m_currentDeviceUuid != selection.uuid) { - setCurrentDevice(selection.uuid); + if (m_deviceWidgets.contains(selection.udid)) { + if (m_currentDeviceUuid != selection.udid) { + setCurrentDevice(selection.udid); } // Handle navigation section - QWidget *tabWidget = m_deviceWidgets[selection.uuid].first; + QWidget *tabWidget = m_deviceWidgets[selection.udid].first; DeviceMenuWidget *deviceMenuWidget = qobject_cast(tabWidget); qDebug() << "Switching to tab:" << selection.section @@ -349,8 +349,8 @@ void DeviceManagerWidget::onDeviceSelectionChanged( #endif case DeviceSelection::Pending: - if (m_pendingDeviceWidgets.contains(selection.uuid)) { - QWidget *tabWidget = m_pendingDeviceWidgets[selection.uuid].first; + if (m_pendingDeviceWidgets.contains(selection.udid)) { + QWidget *tabWidget = m_pendingDeviceWidgets[selection.udid].first; if (tabWidget) { m_stackedWidget->setCurrentWidget(tabWidget); // Clear current device since we're viewing pending device diff --git a/src/devicependingwidget.cpp b/src/devicependingwidget.cpp index c8c85e6..55b582b 100644 --- a/src/devicependingwidget.cpp +++ b/src/devicependingwidget.cpp @@ -28,11 +28,12 @@ DevicePendingWidget::DevicePendingWidget(bool locked, QWidget *parent) layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(5); - m_label = new QLabel(m_locked ? "Please unlock the screen" - : "Please click on trust on the popup", - this); - - layout->addWidget(m_label); + m_label = new QLabel( + m_locked ? "Please unlock the screen and click on trust on the popup" + : "Please click on trust on the popup", + this); + m_label->setWordWrap(true); + layout->addWidget(m_label, 0, Qt::AlignCenter); setLayout(layout); } diff --git a/src/devicesidebarwidget.cpp b/src/devicesidebarwidget.cpp index 775dc27..e7760da 100644 --- a/src/devicesidebarwidget.cpp +++ b/src/devicesidebarwidget.cpp @@ -393,8 +393,8 @@ void DeviceSidebarWidget::updateSelection() // Set selection based on current selection if (m_currentSelection.type == DeviceSelection::Normal && - m_deviceItems.contains(m_currentSelection.uuid)) { - m_deviceItems[m_currentSelection.uuid]->setSelected(true); + m_deviceItems.contains(m_currentSelection.udid)) { + m_deviceItems[m_currentSelection.udid]->setSelected(true); } else if (m_currentSelection.type == DeviceSelection::Recovery && m_recoveryItems.contains(m_currentSelection.ecid)) { m_recoveryItems[m_currentSelection.ecid]->setSelected(true); diff --git a/src/devicesidebarwidget.h b/src/devicesidebarwidget.h index 1437b8a..d54ac6c 100644 --- a/src/devicesidebarwidget.h +++ b/src/devicesidebarwidget.h @@ -113,12 +113,12 @@ signals: struct DeviceSelection { enum Type { Normal, Recovery, Pending }; Type type; - std::string uuid; + std::string udid; uint64_t ecid = 0; QString section = "Info"; - DeviceSelection(const std::string &deviceUuid, const QString &nav = "") - : type(Normal), uuid(deviceUuid), section(nav) + DeviceSelection(const std::string &deviceUdid, const QString &nav = "") + : type(Normal), udid(deviceUdid), section(nav) { } DeviceSelection(uint64_t recoveryEcid) : type(Recovery), ecid(recoveryEcid) diff --git a/src/httpserver.cpp b/src/httpserver.cpp new file mode 100644 index 0000000..453a1c2 --- /dev/null +++ b/src/httpserver.cpp @@ -0,0 +1,257 @@ +/* + * iDescriptor: A free and open-source idevice management tool. + * + * Copyright (C) 2025 Uncore + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include "httpserver.h" +#include "iDescriptor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +HttpServer::HttpServer(QObject *parent) + : QObject(parent), server(new QTcpServer(this)), port(8080) +{ + connect(server, &QTcpServer::newConnection, this, + &HttpServer::onNewConnection); +} + +HttpServer::~HttpServer() { stop(); } + +void HttpServer::start(const QStringList &files) +{ + fileList = files; + + // Generate unique JSON filename + QString timestamp = + QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss"); + jsonFileName = QString("%1-idescriptor-import.json").arg(timestamp); + + // Try to bind to port 8080, if fails try other ports + for (int tryPort = 8080; tryPort <= 8090; ++tryPort) { + if (server->listen(QHostAddress::Any, tryPort)) { + port = tryPort; + emit serverStarted(); + return; + } + } + + emit serverError("Could not bind to any port between 8080-8090"); +} + +void HttpServer::stop() +{ + if (server->isListening()) { + server->close(); + } +} + +int HttpServer::getPort() const { return port; } + +void HttpServer::onNewConnection() +{ + QTcpSocket *socket = server->nextPendingConnection(); + connect(socket, &QTcpSocket::readyRead, this, &HttpServer::onReadyRead); + connect(socket, &QTcpSocket::disconnected, this, + &HttpServer::onDisconnected); +} + +void HttpServer::onReadyRead() +{ + QTcpSocket *socket = qobject_cast(sender()); + if (!socket) + return; + + QByteArray data = socket->readAll(); + QString request = QString::fromUtf8(data); + + // Parse HTTP request + QStringList lines = request.split("\r\n"); + if (lines.isEmpty()) + return; + + QString requestLine = lines.first(); + QStringList parts = requestLine.split(" "); + if (parts.size() < 2) + return; + + QString method = parts[0]; + QString path = parts[1]; + + if (method == "GET") { + handleRequest(socket, path); + } else { + sendResponse(socket, 405, "text/plain", "Method Not Allowed"); + } +} + +void HttpServer::onDisconnected() +{ + QTcpSocket *socket = qobject_cast(sender()); + if (socket) { + socket->deleteLater(); + } +} + +void HttpServer::handleRequest(QTcpSocket *socket, const QString &path) +{ + // Serve JSON manifest + if (path == QString("/%1").arg(jsonFileName)) { + sendJsonManifest(socket); + return; + } + + // Serve files from /serve/ directory + if (path.startsWith("/serve/")) { + QString encodedFileName = path.mid(7); // Remove "/serve/" + QString fileName = QUrl::fromPercentEncoding(encodedFileName.toUtf8()); + + // Find the file in our list + QString targetFile; + for (const QString &file : fileList) { + QFileInfo info(file); + if (info.fileName() == fileName) { + targetFile = file; + break; + } + } + + if (!targetFile.isEmpty()) { + sendFile(socket, targetFile); + return; + } + } + + sendResponse(socket, 404, "text/html", + "

404 Not Found

The requested file was " + "not found.

"); +} + +void HttpServer::sendResponse(QTcpSocket *socket, int statusCode, + const QString &contentType, + const QByteArray &data) +{ + QString statusText; + switch (statusCode) { + case 200: + statusText = "OK"; + break; + case 404: + statusText = "Not Found"; + break; + case 405: + statusText = "Method Not Allowed"; + break; + case 500: + statusText = "Internal Server Error"; + break; + default: + statusText = "Unknown"; + break; + } + + QString response = + QString("HTTP/1.1 %1 %2\r\n").arg(statusCode).arg(statusText); + response += QString("Content-Type: %1\r\n").arg(contentType); + response += QString("Content-Length: %1\r\n").arg(data.size()); + response += "Access-Control-Allow-Origin: *\r\n"; + response += "Connection: close\r\n"; + response += "\r\n"; + + socket->write(response.toUtf8()); + socket->write(data); + socket->disconnectFromHost(); +} + +void HttpServer::sendFile(QTcpSocket *socket, const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + sendResponse(socket, 404, "text/plain", "File not found"); + return; + } + + QByteArray data = file.readAll(); + QString mimeType = getMimeType(filePath); + + // Emit progress signal + QFileInfo info(filePath); + emit downloadProgress(info.fileName(), data.size(), data.size()); + + sendResponse(socket, 200, mimeType, data); +} + +void HttpServer::sendJsonManifest(QTcpSocket *socket) +{ + QString jsonContent = generateJsonManifest(); + sendResponse(socket, 200, "application/json", jsonContent.toUtf8()); +} + +QString HttpServer::generateJsonManifest() const +{ + QString serverIP = getLocalIP(); + + QJsonObject manifest; + QJsonArray items; + + for (const QString &file : fileList) { + QFileInfo info(file); + QJsonObject item; + item["path"] = QString("http://%1:%2/serve/%3") + .arg(serverIP) + .arg(port) + .arg(QString::fromUtf8( + QUrl::toPercentEncoding(info.fileName()))); + items.append(item); + } + + manifest["items"] = items; + + QJsonDocument doc(manifest); + return doc.toJson(); +} + +QString HttpServer::getLocalIP() const +{ + foreach (const QNetworkInterface &interface, + QNetworkInterface::allInterfaces()) { + if (interface.flags().testFlag(QNetworkInterface::IsUp) && + !interface.flags().testFlag(QNetworkInterface::IsLoopBack)) { + foreach (const QNetworkAddressEntry &entry, + interface.addressEntries()) { + if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) { + return entry.ip().toString(); + } + } + } + } + return "127.0.0.1"; +} + +QString HttpServer::getMimeType(const QString &filePath) const +{ + QMimeDatabase db; + QMimeType type = db.mimeTypeForFile(filePath); + return type.name(); +} diff --git a/src/simplehttpserver.h b/src/httpserver.h similarity index 87% rename from src/simplehttpserver.h rename to src/httpserver.h index 96547d7..2a8780c 100644 --- a/src/simplehttpserver.h +++ b/src/httpserver.h @@ -17,8 +17,8 @@ * along with this program. If not, see . */ -#ifndef SIMPLEHTTPSERVER_H -#define SIMPLEHTTPSERVER_H +#ifndef HTTPSERVER_H +#define HTTPSERVER_H #include #include @@ -27,13 +27,13 @@ #include #include -class SimpleHttpServer : public QObject +class HttpServer : public QObject { Q_OBJECT public: - explicit SimpleHttpServer(QObject *parent = nullptr); - ~SimpleHttpServer(); + explicit HttpServer(QObject *parent = nullptr); + ~HttpServer(); void start(const QStringList &files); void stop(); @@ -62,11 +62,9 @@ private: const QString &contentType, const QByteArray &data); void sendFile(QTcpSocket *socket, const QString &filePath); void sendJsonManifest(QTcpSocket *socket); - void sendHtmlPage(QTcpSocket *socket); QString generateJsonManifest() const; - QString generateHtmlPage() const; QString getMimeType(const QString &filePath) const; QString getLocalIP() const; }; -#endif // SIMPLEHTTPSERVER_H +#endif // HTTPSERVER_H diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h index 16964aa..952e544 100644 --- a/src/iDescriptor-ui.h +++ b/src/iDescriptor-ui.h @@ -25,9 +25,11 @@ #include #include #include +#include #include #include #include +#include #include #ifdef Q_OS_MAC @@ -369,3 +371,28 @@ protected: QLabel::mouseReleaseEvent(event); } }; + +class ZSlider : public QSlider +{ + Q_OBJECT + +public: + explicit ZSlider(QWidget *parent = nullptr) : QSlider(parent) {} + explicit ZSlider(Qt::Orientation orientation, QWidget *parent = nullptr) + : QSlider(orientation, parent) + { + } + +protected: + void mousePressEvent(QMouseEvent *event) override + { + if (event->button() == Qt::LeftButton) { + // Set the value to the position of the click + int value = QStyle::sliderValueFromPosition( + minimum(), maximum(), event->pos().x(), width()); + setValue(value); + } + // Let the base class handle the rest of the event + QSlider::mousePressEvent(event); + } +}; \ No newline at end of file diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ae4ac7f..835ddae 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -80,8 +80,9 @@ void handleCallback(const idevice_event_t *event, void *userData) case IDEVICE_DEVICE_PAIRED: { if (event->conn_type == CONNECTION_NETWORK) { - warn("Network devices are not supported but a network device was " - "received in event listener. Please report this issue."); + qDebug() + << "Network devices are not supported but a network device was " + "received in event listener. Please report this issue."; return; } qDebug() << "Device paired: " << QString::fromUtf8(event->udid); @@ -241,7 +242,6 @@ MainWindow::MainWindow(QWidget *parent) } qDebug() << "Subscribed to device events successfully."; createMenus(); - // Example usage with customization UpdateProcedure updateProcedure; bool packageManagerManaged = false; @@ -256,15 +256,27 @@ MainWindow::MainWindow(QWidget *parent) } #endif + /* + struct UpdateProcedure { + bool openFile; + bool openFileDir; + bool quitApp; + QString boxInformativeText; + QString boxText; + }; + */ switch (ZUpdater::detectPlatform()) { // todo: adjust for portable case Platform::Windows: updateProcedure = UpdateProcedure{ - true, - false, - true, - "The application will now quit to install the update.", - "Do you want to install the downloaded update now?", + !isPortable, + isPortable, + !isPortable, + isPortable ? "New portable version downloaded, app location will " + "be shown after this message" + : "The application will now quit to install the update.", + isPortable ? "New portable version downloaded" + : "Do you want to install the downloaded update now?", }; break; // todo: adjust for pkg managers @@ -296,13 +308,23 @@ MainWindow::MainWindow(QWidget *parent) }; } + // FIXME: fix repo name m_updater = new ZUpdater("uncor3/libtest", APP_VERSION, "iDescriptor", updateProcedure, isPortable, packageManagerManaged, this); - qDebug() << "Checking for updates..."; +#if defined(PACKAGE_MANAGER_MANAGED) && defined(__linux__) + m_updater->setPackageManagerManagedMessage( + QString( + "You seem to have installed iDescriptor using a package manager. " + "Please use %1 to update it.") + .arg(PACKAGE_MANAGER_HINT)); +#endif + SettingsManager::sharedInstance()->doIfEnabled( - SettingsManager::Setting::AutoCheckUpdates, - [this]() { m_updater->checkForUpdates(); }); + SettingsManager::Setting::AutoCheckUpdates, [this]() { + qDebug() << "Checking for updates..."; + m_updater->checkForUpdates(); + }); } void MainWindow::createMenus() diff --git a/src/mediapreviewdialog.cpp b/src/mediapreviewdialog.cpp index 6954b18..1a49f59 100644 --- a/src/mediapreviewdialog.cpp +++ b/src/mediapreviewdialog.cpp @@ -43,6 +43,10 @@ #include #include #include +#include "appcontext.h" +#include "iDescriptor-ui.h" + + MediaPreviewDialog::MediaPreviewDialog(iDescriptorDevice *device, afc_client_t afcClient, @@ -72,6 +76,11 @@ MediaPreviewDialog::MediaPreviewDialog(iDescriptorDevice *device, setupUI(); loadMedia(); + connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, [this](const std::string &udid) { + if (udid == m_device->udid) { + close(); + } + }); } MediaPreviewDialog::~MediaPreviewDialog() @@ -481,16 +490,16 @@ void MediaPreviewDialog::setupVideoControls() &MediaPreviewDialog::onRepeatToggled); // Timeline slider - m_timelineSlider = new QSlider(Qt::Horizontal, this); + m_timelineSlider = new ZSlider(Qt::Horizontal, this); m_timelineSlider->setMinimum(0); m_timelineSlider->setMaximum(1000); m_timelineSlider->setValue(0); m_timelineSlider->setToolTip("Seek timeline"); - connect(m_timelineSlider, &QSlider::valueChanged, this, + connect(m_timelineSlider, &ZSlider::valueChanged, this, &MediaPreviewDialog::onTimelineValueChanged); - connect(m_timelineSlider, &QSlider::sliderPressed, this, + connect(m_timelineSlider, &ZSlider::sliderPressed, this, &MediaPreviewDialog::onTimelinePressed); - connect(m_timelineSlider, &QSlider::sliderReleased, this, + connect(m_timelineSlider, &ZSlider::sliderReleased, this, &MediaPreviewDialog::onTimelineReleased); // Time label diff --git a/src/mediapreviewdialog.h b/src/mediapreviewdialog.h index afff1ec..5aca232 100644 --- a/src/mediapreviewdialog.h +++ b/src/mediapreviewdialog.h @@ -20,6 +20,7 @@ #ifndef MEDIAPREVIEWDIALOG_H #define MEDIAPREVIEWDIALOG_H +#include "iDescriptor-ui.h" #include "iDescriptor.h" #include #include @@ -120,7 +121,7 @@ private: QPushButton *m_playPauseBtn; QPushButton *m_stopBtn; QPushButton *m_repeatBtn; - QSlider *m_timelineSlider; + ZSlider *m_timelineSlider; QLabel *m_timeLabel; QSlider *m_volumeSlider; QLabel *m_volumeLabel; diff --git a/src/photoimportdialog.cpp b/src/photoimportdialog.cpp index 89da104..c131c67 100644 --- a/src/photoimportdialog.cpp +++ b/src/photoimportdialog.cpp @@ -18,7 +18,7 @@ */ #include "photoimportdialog.h" -#include "simplehttpserver.h" +#include "httpserver.h" #include #include #include @@ -117,12 +117,12 @@ void PhotoImportDialog::init() progressBar->setRange(0, 0); // Indeterminate progress // Create and start HTTP server - m_httpServer = new SimpleHttpServer(this); - connect(m_httpServer, &SimpleHttpServer::serverStarted, this, + m_httpServer = new HttpServer(this); + connect(m_httpServer, &HttpServer::serverStarted, this, &PhotoImportDialog::onServerStarted); - connect(m_httpServer, &SimpleHttpServer::serverError, this, + connect(m_httpServer, &HttpServer::serverError, this, &PhotoImportDialog::onServerError); - connect(m_httpServer, &SimpleHttpServer::downloadProgress, this, + connect(m_httpServer, &HttpServer::downloadProgress, this, &PhotoImportDialog::onDownloadProgress); m_httpServer->start(selectedFiles); diff --git a/src/photoimportdialog.h b/src/photoimportdialog.h index 6cfbe3a..975d950 100644 --- a/src/photoimportdialog.h +++ b/src/photoimportdialog.h @@ -20,6 +20,7 @@ #ifndef PHOTOIMPORTDIALOG_H #define PHOTOIMPORTDIALOG_H +#include "httpserver.h" #include #include #include @@ -29,8 +30,6 @@ #include #include -class SimpleHttpServer; - class PhotoImportDialog : public QDialog { Q_OBJECT @@ -59,7 +58,7 @@ private: QProgressBar *progressBar; QLabel *progressLabel; - SimpleHttpServer *m_httpServer; + HttpServer *m_httpServer; void setupUI(); void generateQRCode(const QString &url); diff --git a/src/settingswidget.cpp b/src/settingswidget.cpp index e81786c..facbe20 100644 --- a/src/settingswidget.cpp +++ b/src/settingswidget.cpp @@ -88,12 +88,9 @@ void SettingsWidget::setupUI() themeLayout->addWidget(new QLabel("Theme:")); m_themeCombo = new QComboBox(); - /* FIXME: Theme control on Linux needs to be implemented */ -#ifdef __linux__ + /* FIXME: Theme control needs to be implemented */ m_themeCombo->addItems({"System Default"}); -#else - m_themeCombo->addItems({"System Default", "Light", "Dark"}); -#endif + // m_themeCombo->addItems({"System Default", "Light", "Dark"}); themeLayout->addWidget(m_themeCombo); themeLayout->addStretch(); diff --git a/src/simplehttpserver.cpp b/src/simplehttpserver.cpp deleted file mode 100644 index 8b858cc..0000000 --- a/src/simplehttpserver.cpp +++ /dev/null @@ -1,437 +0,0 @@ -/* - * iDescriptor: A free and open-source idevice management tool. - * - * Copyright (C) 2025 Uncore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#include "simplehttpserver.h" -#include "iDescriptor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -SimpleHttpServer::SimpleHttpServer(QObject *parent) - : QObject(parent), server(new QTcpServer(this)), port(8080) -{ - connect(server, &QTcpServer::newConnection, this, - &SimpleHttpServer::onNewConnection); -} - -SimpleHttpServer::~SimpleHttpServer() { stop(); } - -void SimpleHttpServer::start(const QStringList &files) -{ - fileList = files; - - // Generate unique JSON filename - QString timestamp = - QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss"); - jsonFileName = QString("%1-idescriptor-import.json").arg(timestamp); - - // Try to bind to port 8080, if fails try other ports - for (int tryPort = 8080; tryPort <= 8090; ++tryPort) { - if (server->listen(QHostAddress::Any, tryPort)) { - port = tryPort; - emit serverStarted(); - return; - } - } - - emit serverError("Could not bind to any port between 8080-8090"); -} - -void SimpleHttpServer::stop() -{ - if (server->isListening()) { - server->close(); - } -} - -int SimpleHttpServer::getPort() const { return port; } - -void SimpleHttpServer::onNewConnection() -{ - QTcpSocket *socket = server->nextPendingConnection(); - connect(socket, &QTcpSocket::readyRead, this, - &SimpleHttpServer::onReadyRead); - connect(socket, &QTcpSocket::disconnected, this, - &SimpleHttpServer::onDisconnected); -} - -void SimpleHttpServer::onReadyRead() -{ - QTcpSocket *socket = qobject_cast(sender()); - if (!socket) - return; - - QByteArray data = socket->readAll(); - QString request = QString::fromUtf8(data); - - // Parse HTTP request - QStringList lines = request.split("\r\n"); - if (lines.isEmpty()) - return; - - QString requestLine = lines.first(); - QStringList parts = requestLine.split(" "); - if (parts.size() < 2) - return; - - QString method = parts[0]; - QString path = parts[1]; - - if (method == "GET") { - handleRequest(socket, path); - } else { - sendResponse(socket, 405, "text/plain", "Method Not Allowed"); - } -} - -void SimpleHttpServer::onDisconnected() -{ - QTcpSocket *socket = qobject_cast(sender()); - if (socket) { - socket->deleteLater(); - } -} - -void SimpleHttpServer::handleRequest(QTcpSocket *socket, const QString &path) -{ - // Serve HTML page at root - if (path == "/" || path == "/index.html") { - sendHtmlPage(socket); - return; - } - - // Serve JSON manifest - if (path == QString("/%1").arg(jsonFileName)) { - sendJsonManifest(socket); - return; - } - - if (path == "/import.shortcut") { - // Generate import shortcut file - QString shortcutPath = - QString("%1/resources/import.shortcut").arg(SOURCE_DIR); - QFile shortcutFile(shortcutPath); - if (shortcutFile.open(QIODevice::ReadOnly)) { - QByteArray data = shortcutFile.readAll(); - sendResponse(socket, 200, "application/octet-stream", data); - return; - } else { - sendResponse(socket, 404, "text/plain", "Shortcut file not found"); - return; - } - } - - // Serve files from /serve/ directory - if (path.startsWith("/serve/")) { - QString fileName = path.mid(7); // Remove "/serve/" - - // Find the file in our list - QString targetFile; - for (const QString &file : fileList) { - QFileInfo info(file); - if (info.fileName() == fileName) { - targetFile = file; - break; - } - } - - if (!targetFile.isEmpty()) { - sendFile(socket, targetFile); - return; - } - } - - sendResponse(socket, 404, "text/html", - "

404 Not Found

The requested file was " - "not found.

"); -} - -void SimpleHttpServer::sendResponse(QTcpSocket *socket, int statusCode, - const QString &contentType, - const QByteArray &data) -{ - QString statusText; - switch (statusCode) { - case 200: - statusText = "OK"; - break; - case 404: - statusText = "Not Found"; - break; - case 405: - statusText = "Method Not Allowed"; - break; - case 500: - statusText = "Internal Server Error"; - break; - default: - statusText = "Unknown"; - break; - } - - QString response = - QString("HTTP/1.1 %1 %2\r\n").arg(statusCode).arg(statusText); - response += QString("Content-Type: %1\r\n").arg(contentType); - response += QString("Content-Length: %1\r\n").arg(data.size()); - response += "Access-Control-Allow-Origin: *\r\n"; - response += "Connection: close\r\n"; - response += "\r\n"; - - socket->write(response.toUtf8()); - socket->write(data); - socket->disconnectFromHost(); -} - -void SimpleHttpServer::sendFile(QTcpSocket *socket, const QString &filePath) -{ - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly)) { - sendResponse(socket, 404, "text/plain", "File not found"); - return; - } - - QByteArray data = file.readAll(); - QString mimeType = getMimeType(filePath); - - // Emit progress signal - QFileInfo info(filePath); - emit downloadProgress(info.fileName(), data.size(), data.size()); - - sendResponse(socket, 200, mimeType, data); -} - -void SimpleHttpServer::sendJsonManifest(QTcpSocket *socket) -{ - QString jsonContent = generateJsonManifest(); - sendResponse(socket, 200, "application/json", jsonContent.toUtf8()); -} - -void SimpleHttpServer::sendHtmlPage(QTcpSocket *socket) -{ - QString htmlContent = generateHtmlPage(); - sendResponse(socket, 200, "text/html", htmlContent.toUtf8()); -} - -QString SimpleHttpServer::generateJsonManifest() const -{ - QString serverIP = getLocalIP(); - - QJsonObject manifest; - QJsonArray items; - - for (const QString &file : fileList) { - QFileInfo info(file); - QJsonObject item; - item["path"] = QString("http://%1:%2/serve/%3") - .arg(serverIP) - .arg(port) - .arg(info.fileName()); - items.append(item); - } - - manifest["items"] = items; - - QJsonDocument doc(manifest); - return doc.toJson(); -} - -QString SimpleHttpServer::generateHtmlPage() const -{ - QString serverIP = getLocalIP(); - QString shortcutPath = - QString("shortcuts://import-shortcut?url=http://%1:%2/import.shortcut") - .arg(serverIP) - .arg(port); - - QString html = QString(R"( - - - - - - iDescriptor Photo Import - - - -
-

📱 iDescriptor Photo Import

- -
- Server Address: %1:%2
- JSON Manifest: %3 -
- -
-

Ready to serve %4 files

-
- -
-

Instructions:

-
    -
  1. Copy the server address below
  2. -
  3. Download the shortcut to your iOS device
  4. -
  5. Run the shortcut and paste the server address when prompted
  6. -
  7. The shortcut will automatically import all photos to your Gallery
  8. -
-
- - - - -
- - - )") - .arg(serverIP) - .arg(port) - .arg(jsonFileName) - .arg(fileList.size()) - .arg(shortcutPath); - - return html; -} - -QString SimpleHttpServer::getLocalIP() const -{ - foreach (const QNetworkInterface &interface, - QNetworkInterface::allInterfaces()) { - if (interface.flags().testFlag(QNetworkInterface::IsUp) && - !interface.flags().testFlag(QNetworkInterface::IsLoopBack)) { - foreach (const QNetworkAddressEntry &entry, - interface.addressEntries()) { - if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) { - return entry.ip().toString(); - } - } - } - } - return "127.0.0.1"; -} - -QString SimpleHttpServer::getMimeType(const QString &filePath) const -{ - QMimeDatabase db; - QMimeType type = db.mimeTypeForFile(filePath); - return type.name(); -} diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index a1d2f91..762103f 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -387,16 +387,16 @@ void ToolboxWidget::onCurrentDeviceChanged(const DeviceSelection &selection) { if (selection.type == DeviceSelection::Normal) { int index = - m_deviceCombo->findData(QString::fromStdString(selection.uuid)); + m_deviceCombo->findData(QString::fromStdString(selection.udid)); if (index != -1) { // Block signals to prevent recursive calls when we update the UI m_deviceCombo->blockSignals(true); m_deviceCombo->setCurrentIndex(index); m_deviceCombo->blockSignals(false); - m_uuid = selection.uuid; + m_uuid = selection.udid; m_currentDevice = - AppContext::sharedInstance()->getDevice(selection.uuid); + AppContext::sharedInstance()->getDevice(selection.udid); } } else { // Handle recovery, pending, or no device selection @@ -442,37 +442,23 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) msgBox.exec(); } break; case iDescriptorTool::MountDevImage: { - GetMountedImageResult result = - DevDiskManager::sharedInstance()->getMountedImage( - m_currentDevice->udid.c_str()); - - if (!result.success) { - QMessageBox::warning(this, "Failure", result.message.c_str()); - return; - } - - if (result.success && result.sig.empty()) { - bool devImgSuccess = - DevDiskManager::sharedInstance()->mountCompatibleImage( - m_currentDevice); - if (!devImgSuccess) { - QMessageBox::warning( - this, "Failure", - "Failed to mount developer image on device. " - "Try with a different cable."); - qDebug() - << "Failed to mount developer image on device. Cannot set " - "location."; - return; - } - } - - QMessageBox::information( - this, "Success", - QString("There is already a developer image mounted on device %1.") - .arg(QString::fromStdString( - m_currentDevice->deviceInfo.productType))); + DevDiskImageHelper *devDiskImageHelper = + new DevDiskImageHelper(m_currentDevice, this); + connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted, + this, [this, devDiskImageHelper](bool success) { + devDiskImageHelper->deleteLater(); + if (success) { + QMessageBox::information( + this, "Success", + "Developer image mounted successfully."); + } else { + QMessageBox::warning( + this, "Failure", + "Failed to mount developer image."); + } + }); + devDiskImageHelper->start(); } break; case iDescriptorTool::VirtualLocation: { // Handle virtual location functionality diff --git a/src/wirelessgalleryimportwidget.cpp b/src/wirelessgalleryimportwidget.cpp index ae174e3..ebc5808 100644 --- a/src/wirelessgalleryimportwidget.cpp +++ b/src/wirelessgalleryimportwidget.cpp @@ -38,6 +38,7 @@ WirelessGalleryImportWidget::WirelessGalleryImportWidget(QWidget *parent) { setupUI(); setMinimumSize(800, 600); + setWindowTitle("Wireless Gallery Import - iDescriptor"); QTimer::singleShot(100, this, &WirelessGalleryImportWidget::setupTutorialVideo); } @@ -131,7 +132,8 @@ void WirelessGalleryImportWidget::setupTutorialVideo() QSizePolicy::Expanding); m_tutorialPlayer->setVideoOutput(m_tutorialVideoWidget); - m_tutorialPlayer->setSource(QUrl("qrc:/resources/airplayer-tutorial.mp4")); + m_tutorialPlayer->setSource( + QUrl("qrc:/resources/wireless-gallery-import.mp4")); m_tutorialVideoWidget->setAspectRatioMode( Qt::AspectRatioMode::KeepAspectRatioByExpanding);