refactor asset serving & fix typos

- Added necessary libraries for Linux build in build-linux.yml
- Updated submodule reference for zupdater
- Added new video resource for wireless gallery import
- use UDID instead of UUID across multiple files
- Improved user interface messages and layout in various widgets
- Refactor http server
This commit is contained in:
uncor3
2025-11-13 05:14:37 +00:00
parent 9584538f0e
commit 8b0a673a55
21 changed files with 402 additions and 535 deletions
+4
View File
@@ -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
+1
View File
@@ -64,5 +64,6 @@
<file>resources/ipad-mockups/ipad.png</file>
<file>resources/DeveloperDiskImages.json</file>
<file>resources/keychain.mp4</file>
<file>resources/wireless-gallery-import.mp4</file>
</qresource>
</RCC>
+10 -10
View File
@@ -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<std::recursive_mutex> 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<iDescriptorDevice *> 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";
+6 -6
View File
@@ -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<DeviceMenuWidget *>(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
+6 -5
View File
@@ -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);
}
+2 -2
View File
@@ -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);
+3 -3
View File
@@ -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)
+257
View File
@@ -0,0 +1,257 @@
/*
* iDescriptor: A free and open-source idevice management tool.
*
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "httpserver.h"
#include "iDescriptor.h"
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMimeDatabase>
#include <QNetworkInterface>
#include <QRandomGenerator>
#include <QUrl>
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<QTcpSocket *>(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<QTcpSocket *>(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",
"<html><body><h1>404 Not Found</h1><p>The requested file was "
"not found.</p></body></html>");
}
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();
}
+6 -8
View File
@@ -17,8 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef SIMPLEHTTPSERVER_H
#define SIMPLEHTTPSERVER_H
#ifndef HTTPSERVER_H
#define HTTPSERVER_H
#include <QMap>
#include <QObject>
@@ -27,13 +27,13 @@
#include <QTcpSocket>
#include <QTimer>
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
+27
View File
@@ -25,9 +25,11 @@
#include <QMainWindow>
#include <QMouseEvent>
#include <QPainter>
#include <QSlider>
#include <QSplitter>
#include <QSplitterHandle>
#include <QStyleOption>
#include <QWheelEvent>
#include <QWidget>
#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);
}
};
+33 -11
View File
@@ -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()
+13 -4
View File
@@ -43,6 +43,10 @@
#include <QWheelEvent>
#include <QtConcurrent/QtConcurrent>
#include <QtGlobal>
#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
+2 -1
View File
@@ -20,6 +20,7 @@
#ifndef MEDIAPREVIEWDIALOG_H
#define MEDIAPREVIEWDIALOG_H
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include <QCoreApplication>
#include <QDialog>
@@ -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;
+5 -5
View File
@@ -18,7 +18,7 @@
*/
#include "photoimportdialog.h"
#include "simplehttpserver.h"
#include "httpserver.h"
#include <QApplication>
#include <QDateTime>
#include <QFileInfo>
@@ -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);
+2 -3
View File
@@ -20,6 +20,7 @@
#ifndef PHOTOIMPORTDIALOG_H
#define PHOTOIMPORTDIALOG_H
#include "httpserver.h"
#include <QDialog>
#include <QHBoxLayout>
#include <QLabel>
@@ -29,8 +30,6 @@
#include <QStringList>
#include <QVBoxLayout>
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);
+2 -5
View File
@@ -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();
-437
View File
@@ -1,437 +0,0 @@
/*
* iDescriptor: A free and open-source idevice management tool.
*
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "simplehttpserver.h"
#include "iDescriptor.h"
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMimeDatabase>
#include <QNetworkInterface>
#include <QRandomGenerator>
#include <QUrl>
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<QTcpSocket *>(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<QTcpSocket *>(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",
"<html><body><h1>404 Not Found</h1><p>The requested file was "
"not found.</p></body></html>");
}
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"(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iDescriptor Photo Import</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f7;
line-height: 1.6;
}
.container {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
h1 {
color: #1d1d1f;
text-align: center;
margin-bottom: 30px;
}
.server-info {
background: #f6f6f6;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
font-family: monospace;
}
.button {
display: inline-block;
background: #007AFF;
color: white;
padding: 12px 24px;
border: none;
border-radius: 8px;
text-decoration: none;
margin: 10px 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
.button:hover {
background: #0056CC;
}
.button.secondary {
background: #34C759;
}
.button.secondary:hover {
background: #28A745;
}
.instructions {
background: #e8f4fd;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.file-list {
background: #f8f8f8;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
max-height: 200px;
overflow-y: auto;
}
.file-count {
font-weight: bold;
color: #007AFF;
}
</style>
</head>
<body>
<div class="container">
<h1>📱 iDescriptor Photo Import</h1>
<div class="server-info">
<strong>Server Address:</strong> %1:%2<br>
<strong>JSON Manifest:</strong> %3
</div>
<div class="file-list">
<p class="file-count">Ready to serve %4 files</p>
</div>
<div class="instructions">
<h3>Instructions:</h3>
<ol>
<li>Copy the server address below</li>
<li>Download the shortcut to your iOS device</li>
<li>Run the shortcut and paste the server address when prompted</li>
<li>The shortcut will automatically import all photos to your Gallery</li>
</ol>
</div>
<div style="text-align: center;">
<a href="%5" class="button secondary">Download Shortcut</a>
</div>
<script>
window.onload = function() {
// function copyAddress() {
// const address = '%1:%2';
// window.navigator.clipboard.writeText(address).then(function() {
// alert('Server address copied to clipboard!');
// }).catch(function(err) {
// prompt('Copy this address:', address);
// });
// }
// function unsecuredCopyToClipboard(text) {
// const textArea = document.createElement("textarea");
// textArea.value = text;
// document.body.appendChild(textArea);
// textArea.focus();
// textArea.select();
// try {
// document.execCommand('copy');
// } catch (err) {
// console.error('Unable to copy to clipboard', err);
// }
// document.body.removeChild(textArea);
// }
// unsecuredCopyToClipboard('%1:%2');
// document.querySelector('a').click();
}
</script>
</div>
</body>
</html>
)")
.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();
}
+19 -33
View File
@@ -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
+3 -1
View File
@@ -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);