diff --git a/lib/airplay b/lib/airplay
index eb981f3..92948b8 160000
--- a/lib/airplay
+++ b/lib/airplay
@@ -1 +1 @@
-Subproject commit eb981f39d24e266802d940c63479fa64146a8a74
+Subproject commit 92948b8dbccca90625fd67e7801d8f4c4d64c6a2
diff --git a/resources.qrc b/resources.qrc
index caf10ad..ea2202e 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -45,5 +45,8 @@
resources/iphone-mockups/iphone-x.png
resources/iphone-mockups/iphone-15.png
resources/iphone-mockups/iphone-16.png
+ resources/connect.png
+ resources/airplayer-tutorial.mp4
+ resources/ipad-mockups/ipad.png
\ No newline at end of file
diff --git a/resources/airplayer-tutorial.mp4 b/resources/airplayer-tutorial.mp4
new file mode 100755
index 0000000..16d39f0
Binary files /dev/null and b/resources/airplayer-tutorial.mp4 differ
diff --git a/resources/connect.png b/resources/connect.png
new file mode 100755
index 0000000..4601df1
Binary files /dev/null and b/resources/connect.png differ
diff --git a/resources/ipad-mockups/ipad.png b/resources/ipad-mockups/ipad.png
new file mode 100755
index 0000000..4a538b4
Binary files /dev/null and b/resources/ipad-mockups/ipad.png differ
diff --git a/src/airplaywindow.cpp b/src/airplaywindow.cpp
index abbfcd4..75f8a9d 100644
--- a/src/airplaywindow.cpp
+++ b/src/airplaywindow.cpp
@@ -1,13 +1,20 @@
#include "airplaywindow.h"
#include
#include
+#include
#include
+#include
+#include
#include
#include
+#include
#include
+#include
#include
-#include
+#include
+#include
#include
+#include
#ifdef Q_OS_LINUX
// V4L2 includes
@@ -20,16 +27,18 @@
#endif
// Include the rpiplay server functions
+#include "../lib/airplay/renderers/video_renderer.h"
extern "C" {
-int start_server_qt(const char *name);
+int start_server_qt(const char *name, void *callbacks);
int stop_server_qt();
}
-// Global callback for video renderer
-std::function qt_video_callback;
-
AirPlayWindow::AirPlayWindow(QWidget *parent)
- : QMainWindow(parent), m_videoLabel(nullptr), m_statusLabel(nullptr),
+ : QMainWindow(parent), m_stackedWidget(nullptr), m_tutorialWidget(nullptr),
+ m_streamingWidget(nullptr), m_loadingIndicator(nullptr),
+ m_loadingLabel(nullptr), m_tutorialPlayer(nullptr),
+ m_tutorialVideoWidget(nullptr), m_videoLabel(nullptr),
+ m_tutorialLayout(nullptr), m_v4l2Checkbox(nullptr),
m_serverThread(nullptr), m_serverRunning(false)
#ifdef Q_OS_LINUX
,
@@ -38,21 +47,8 @@ AirPlayWindow::AirPlayWindow(QWidget *parent)
{
setupUI();
- // Setup video callback
- qt_video_callback = [this](uint8_t *data, int width, int height) {
-#ifdef Q_OS_LINUX
- // V4L2 output if enabled
- if (m_v4l2_enabled) {
- writeFrameToV4L2(data, width, height);
- }
-#endif
-
- QByteArray frameData((const char *)data, width * height * 3);
- QMetaObject::invokeMethod(this, "updateVideoFrame",
- Qt::QueuedConnection,
- Q_ARG(QByteArray, frameData),
- Q_ARG(int, width), Q_ARG(int, height));
- };
+ // Auto-start server after UI setup
+ QTimer::singleShot(500, this, &AirPlayWindow::startAirPlayServer);
}
AirPlayWindow::~AirPlayWindow()
@@ -61,71 +57,121 @@ AirPlayWindow::~AirPlayWindow()
#ifdef Q_OS_LINUX
closeV4L2();
#endif
- qt_video_callback = nullptr;
}
void AirPlayWindow::setupUI()
{
- setWindowTitle("AirPlay Receiver");
- resize(800, 600);
+ setWindowTitle("AirPlay Receiver - iDescriptor");
+ setMinimumSize(800, 600);
+ resize(1000, 700);
- QWidget *centralWidget = new QWidget(this);
- setCentralWidget(centralWidget);
+ // Create stacked widget
+ m_stackedWidget = new QStackedWidget(this);
+ setCentralWidget(m_stackedWidget);
- QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
+ m_tutorialWidget = new QWidget();
+ m_tutorialLayout = new QVBoxLayout(m_tutorialWidget);
+ m_tutorialLayout->setContentsMargins(40, 40, 40, 40);
+ m_tutorialLayout->setSpacing(20);
- // Status area
- QHBoxLayout *statusLayout = new QHBoxLayout();
- m_statusLabel = new QLabel("Server: Stopped");
- m_statusLabel->setStyleSheet("QLabel { padding: 5px; background-color: "
- "#f0f0f0; border: 1px solid #ccc; }");
+ m_loadingIndicator = new QProcessIndicator();
+ m_loadingIndicator->setType(QProcessIndicator::line_rotate);
+ m_loadingIndicator->setFixedSize(64, 32);
+ m_loadingIndicator->start();
- QPushButton *startBtn = new QPushButton("Start Server");
- QPushButton *stopBtn = new QPushButton("Stop Server");
+ QHBoxLayout *loadingLayout = new QHBoxLayout();
+ loadingLayout->setSpacing(1);
+ m_loadingLabel = new QLabel("Starting AirPlay Server...");
+ m_loadingLabel->setAlignment(Qt::AlignCenter);
- connect(startBtn, &QPushButton::clicked, this,
- &AirPlayWindow::startAirPlayServer);
- connect(stopBtn, &QPushButton::clicked, this,
- &AirPlayWindow::stopAirPlayServer);
+ loadingLayout->addWidget(m_loadingLabel);
+ loadingLayout->addWidget(m_loadingIndicator);
- statusLayout->addWidget(m_statusLabel);
- statusLayout->addStretch();
+ m_tutorialLayout->addLayout(loadingLayout);
+ m_tutorialLayout->addSpacing(1);
+
+ QTimer::singleShot(100, this, &AirPlayWindow::setupTutorialVideo);
+
+ m_streamingWidget = new QWidget();
+ QVBoxLayout *streamingLayout = new QVBoxLayout(m_streamingWidget);
+ streamingLayout->setContentsMargins(10, 10, 10, 10);
+ streamingLayout->setSpacing(10);
#ifdef Q_OS_LINUX
- // V4L2 controls
- QCheckBox *v4l2CheckBox = new QCheckBox("Enable V4L2 Output");
- connect(v4l2CheckBox, &QCheckBox::toggled, this, [this](bool enabled) {
- m_v4l2_enabled = enabled;
- if (!enabled) {
- closeV4L2();
- }
- qDebug() << "V4L2 output" << (enabled ? "enabled" : "disabled");
- });
-
- QPushButton *testV4L2Btn = new QPushButton("Test V4L2");
- testV4L2Btn->setToolTip("Test V4L2 loopback device availability");
- connect(testV4L2Btn, &QPushButton::clicked, this,
- [this]() { testV4L2Device(); });
-
- statusLayout->addWidget(v4l2CheckBox);
- statusLayout->addWidget(testV4L2Btn);
+ // Add V4L2 checkbox at the top of streaming view
+ setupV4L2Checkbox();
+ if (m_v4l2Checkbox) {
+ streamingLayout->addWidget(m_v4l2Checkbox);
+ }
#endif
- statusLayout->addWidget(startBtn);
- statusLayout->addWidget(stopBtn);
- // Video display area
- m_videoLabel = new QLabel("Waiting for AirPlay connection...");
+ // Video display
+ m_videoLabel = new QLabel();
m_videoLabel->setMinimumSize(640, 480);
- m_videoLabel->setStyleSheet(
- "QLabel { background-color: black; color: white; }");
m_videoLabel->setAlignment(Qt::AlignCenter);
- m_videoLabel->setScaledContents(true);
+ m_videoLabel->setScaledContents(false);
+ streamingLayout->addWidget(m_videoLabel, 1);
- mainLayout->addLayout(statusLayout);
- mainLayout->addWidget(m_videoLabel, 1);
+ // Add all widgets to stacked widget
+ m_stackedWidget->addWidget(m_tutorialWidget);
+ m_stackedWidget->addWidget(m_streamingWidget);
- // Auto-start server
- startAirPlayServer();
+ // Start with tutorial widget
+ m_stackedWidget->setCurrentWidget(m_tutorialWidget);
+
+#ifdef Q_OS_LINUX
+ m_v4l2_enabled = false; // Disable V4L2 by default
+#endif
+}
+
+void AirPlayWindow::setupTutorialVideo()
+{
+ m_tutorialPlayer = new QMediaPlayer(this);
+ m_tutorialVideoWidget = new QVideoWidget();
+ m_tutorialVideoWidget->setSizePolicy(QSizePolicy::Expanding,
+ QSizePolicy::Expanding);
+
+ m_tutorialPlayer->setVideoOutput(m_tutorialVideoWidget);
+ m_tutorialPlayer->setSource(QUrl("qrc:/resources/airplayer-tutorial.mp4"));
+ m_tutorialVideoWidget->setAspectRatioMode(
+ Qt::AspectRatioMode::KeepAspectRatioByExpanding);
+ m_tutorialVideoWidget->setStyleSheet(
+ "QVideoWidget { background-color: transparent; }");
+ // Loop the tutorial video
+ connect(m_tutorialPlayer, &QMediaPlayer::mediaStatusChanged, this,
+ [this](QMediaPlayer::MediaStatus status) {
+ if (status == QMediaPlayer::EndOfMedia) {
+ m_tutorialPlayer->setPosition(0);
+ m_tutorialPlayer->play();
+ }
+ });
+
+ // Auto-play when ready
+ connect(m_tutorialPlayer, &QMediaPlayer::mediaStatusChanged, this,
+ [this](QMediaPlayer::MediaStatus status) {
+ if (status == QMediaPlayer::LoadedMedia) {
+ m_tutorialPlayer->play();
+ }
+ });
+ m_tutorialVideoWidget->setVisible(false);
+ m_tutorialLayout->addWidget(m_tutorialVideoWidget, 1);
+}
+
+void AirPlayWindow::showTutorialView()
+{
+ m_stackedWidget->setCurrentWidget(m_tutorialWidget);
+ if (m_tutorialPlayer) {
+ m_tutorialPlayer->play();
+ }
+}
+
+void AirPlayWindow::showStreamingView()
+{
+ m_loadingIndicator->stop();
+ m_stackedWidget->setCurrentWidget(m_streamingWidget);
+ if (m_tutorialPlayer) {
+ m_tutorialPlayer->pause();
+ }
}
void AirPlayWindow::startAirPlayServer()
@@ -138,6 +184,8 @@ void AirPlayWindow::startAirPlayServer()
&AirPlayWindow::onServerStatusChanged);
connect(m_serverThread, &AirPlayServerThread::videoFrameReady, this,
&AirPlayWindow::updateVideoFrame);
+ connect(m_serverThread, &AirPlayServerThread::clientConnectionChanged, this,
+ &AirPlayWindow::onClientConnectionChanged);
m_serverThread->start();
}
@@ -151,7 +199,6 @@ void AirPlayWindow::stopAirPlayServer()
m_serverThread = nullptr;
}
m_serverRunning = false;
- m_statusLabel->setText("Server: Stopped");
}
void AirPlayWindow::updateVideoFrame(QByteArray frameData, int width,
@@ -160,16 +207,108 @@ void AirPlayWindow::updateVideoFrame(QByteArray frameData, int width,
if (frameData.size() != width * height * 3)
return;
+#ifdef Q_OS_LINUX
+ // V4L2 output if enabled
+ if (m_v4l2_enabled) {
+ writeFrameToV4L2((uint8_t *)frameData.data(), width, height);
+ // Show message instead of rendering video when V4L2 is active
+ m_videoLabel->setText("Currently being shared via virtual camera");
+ return;
+ }
+#endif
+
QImage image((const uchar *)frameData.data(), width, height,
QImage::Format_RGB888);
QPixmap pixmap = QPixmap::fromImage(image);
- m_videoLabel->setPixmap(pixmap);
+
+ // Scale pixmap to fit label while maintaining aspect ratio
+ QSize labelSize = m_videoLabel->size();
+ QPixmap scaledPixmap =
+ pixmap.scaled(labelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ m_videoLabel->setPixmap(scaledPixmap);
}
void AirPlayWindow::onServerStatusChanged(bool running)
{
m_serverRunning = running;
- m_statusLabel->setText(running ? "Server: Running" : "Server: Stopped");
+
+ if (running) {
+ // Server started successfully, hide loading indicator and show tutorial
+ // video
+ m_loadingLabel->setText("Waiting for device connection...");
+
+ // Show tutorial video and instructions
+ m_tutorialVideoWidget->setVisible(true);
+ QLabel *instructionLabel = m_tutorialWidget->findChild();
+ if (instructionLabel && !instructionLabel->text().contains("Follow")) {
+ // Find the instruction label (not title or loading label)
+ QList labels = m_tutorialWidget->findChildren();
+ for (QLabel *label : labels) {
+ if (label->text().contains("Follow")) {
+ label->setVisible(true);
+ break;
+ }
+ }
+ }
+
+ if (m_tutorialPlayer) {
+ m_tutorialPlayer->play();
+ }
+ }
+}
+
+void AirPlayWindow::onClientConnectionChanged(bool connected)
+{
+ m_clientConnected = connected;
+ if (connected) {
+ m_loadingLabel->setText("Device connected - receiving stream...");
+
+ showStreamingView();
+ } else {
+ m_loadingLabel->setText("Waiting for device connection...");
+ showTutorialView();
+ }
+}
+
+void AirPlayWindow::onV4L2CheckboxToggled(bool enabled)
+{
+ if (enabled) {
+ // Check if V4L2 loopback exists
+ if (!checkV4L2LoopbackExists()) {
+ // Show message and ask to create V4L2 loopback
+ QMessageBox::StandardButton reply = QMessageBox::question(
+ this, "V4L2 Loopback Required",
+ "Virtual camera device is required for V4L2 output.\n\n"
+ "This will create a virtual camera that other applications can "
+ "use "
+ "to receive the AirPlay stream. The operation requires "
+ "administrator privileges.\n\n"
+ "Do you want to create the virtual camera device?",
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+
+ if (reply == QMessageBox::Yes) {
+ if (createV4L2Loopback()) {
+ m_v4l2_enabled = true;
+
+ } else {
+ m_v4l2Checkbox->setChecked(false);
+ m_v4l2_enabled = false;
+ QMessageBox::warning(
+ this, "Error",
+ "Failed to create virtual camera device. Please ensure "
+ "you have the necessary permissions.");
+ }
+ } else {
+ m_v4l2Checkbox->setChecked(false);
+ m_v4l2_enabled = false;
+ }
+ } else {
+ m_v4l2_enabled = true;
+ }
+ } else {
+ m_v4l2_enabled = false;
+ closeV4L2();
+ }
}
// AirPlayServerThread implementation
@@ -184,20 +323,52 @@ AirPlayServerThread::~AirPlayServerThread()
wait();
}
-void AirPlayServerThread::stopServer() { m_shouldStop = true; }
+void AirPlayServerThread::stopServer()
+{
+ QMutexLocker locker(&m_mutex);
+ m_shouldStop = true;
+ m_waitCondition.wakeAll();
+}
+
+// Global pointer to current server thread for callbacks
+static AirPlayServerThread *g_currentServerThread = nullptr;
+
+// Static callback wrappers for C interface
+extern "C" void qt_video_callback(uint8_t *data, int width, int height)
+{
+ if (g_currentServerThread) {
+ QByteArray frameData((const char *)data, width * height * 3);
+ emit g_currentServerThread->videoFrameReady(frameData, width, height);
+ }
+}
+
+extern "C" void qt_connection_callback(bool connected)
+{
+ if (g_currentServerThread) {
+ emit g_currentServerThread->clientConnectionChanged(connected);
+ }
+}
void AirPlayServerThread::run()
{
+ g_currentServerThread = this;
emit statusChanged(true);
- // Start the server (you'll need to adapt the rpiplay server code)
- start_server_qt("iDescriptor");
+ // Create callbacks structure
+ video_renderer_qt_callbacks_t callbacks;
+ callbacks.video_callback = qt_video_callback;
+ callbacks.connection_callback = qt_connection_callback;
+ start_server_qt("iDescriptor", &callbacks);
+
+ // Wait efficiently until stopServer() is called
+ QMutexLocker locker(&m_mutex);
while (!m_shouldStop) {
- msleep(100);
+ m_waitCondition.wait(&m_mutex);
}
stop_server_qt();
+ g_currentServerThread = nullptr;
emit statusChanged(false);
}
@@ -210,12 +381,6 @@ void AirPlayWindow::initV4L2(int width, int height, const char *device)
m_v4l2_fd = open(device, O_WRONLY);
if (m_v4l2_fd < 0) {
qWarning("Failed to open V4L2 device %s: %s", device, strerror(errno));
- QMessageBox::warning(
- this, "V4L2 Error",
- QString("Failed to open V4L2 device %1.\n"
- "Make sure v4l2loopback module is loaded:\n"
- "sudo modprobe v4l2loopback")
- .arg(device));
return;
}
@@ -233,9 +398,6 @@ void AirPlayWindow::initV4L2(int width, int height, const char *device)
qWarning("Failed to set V4L2 format: %s", strerror(errno));
::close(m_v4l2_fd);
m_v4l2_fd = -1;
- QMessageBox::warning(
- this, "V4L2 Error",
- "Failed to set V4L2 video format. Device may not support RGB24.");
return;
}
@@ -249,7 +411,6 @@ void AirPlayWindow::closeV4L2()
if (m_v4l2_fd >= 0) {
::close(m_v4l2_fd);
m_v4l2_fd = -1;
- qDebug("V4L2 device closed.");
}
}
@@ -272,52 +433,74 @@ void AirPlayWindow::writeFrameToV4L2(uint8_t *data, int width, int height)
}
}
-void AirPlayWindow::testV4L2Device()
+bool AirPlayWindow::checkV4L2LoopbackExists()
{
- const char *device = "/dev/video0";
- int fd = open(device, O_WRONLY);
-
- if (fd < 0) {
- QMessageBox::critical(this, "V4L2 Test",
- QString("Failed to open V4L2 device %1.\n"
- "Error: %2\n\n"
- "To fix this, run:\n"
- "sudo modprobe v4l2loopback video_nr=0")
- .arg(device)
- .arg(strerror(errno)));
- return;
+ try {
+ QFileInfo videoDevice("/dev/video0");
+ return videoDevice.exists();
+ } catch (...) {
+ qWarning("Exception occurred while checking for V4L2 loopback device");
+ return false;
}
-
- // Test if device supports V4L2_PIX_FMT_RGB24
- struct v4l2_format fmt;
- memset(&fmt, 0, sizeof(fmt));
- fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
- fmt.fmt.pix.width = 1280;
- fmt.fmt.pix.height = 720;
- fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24;
- fmt.fmt.pix.field = V4L2_FIELD_NONE;
- fmt.fmt.pix.bytesperline = 1280 * 3;
- fmt.fmt.pix.sizeimage = 1280 * 720 * 3;
-
- if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
- ::close(fd);
- QMessageBox::warning(
- this, "V4L2 Test",
- QString("V4L2 device %1 exists but doesn't support RGB24 format.\n"
- "Error: %2")
- .arg(device)
- .arg(strerror(errno)));
- return;
- }
-
- ::close(fd);
- QMessageBox::information(
- this, "V4L2 Test",
- QString("✓ V4L2 device %1 is working correctly!\n\n"
- "You can now:\n"
- "• View output with: ffplay %1\n"
- "• Use in OBS as Video Capture Device\n"
- "• Record with: ffmpeg -f v4l2 -i %1 output.mp4")
- .arg(device));
}
-#endif
+
+bool AirPlayWindow::createV4L2Loopback()
+{
+ try {
+ QProcess process;
+
+ // Use pkexec to run modprobe with administrator privileges
+ QStringList arguments;
+ arguments << "modprobe" << "v4l2loopback" << "devices=1"
+ << "video_nr=0" << "card_label=\"iDescriptor Virtual Camera\""
+ << "exclusive_caps=1";
+
+ process.start("pkexec", arguments);
+
+ if (!process.waitForStarted(5000)) {
+ qWarning("Failed to start pkexec process");
+ return false;
+ }
+
+ if (!process.waitForFinished(10000)) {
+ qWarning("Timeout waiting for modprobe to complete");
+ process.kill();
+ return false;
+ }
+
+ int exitCode = process.exitCode();
+ if (exitCode != 0) {
+ QString errorOutput = process.readAllStandardError();
+ qWarning("modprobe failed with exit code %d: %s", exitCode,
+ errorOutput.toUtf8().constData());
+ return false;
+ }
+
+ // Wait a bit for the device to be created
+ QThread::msleep(500);
+
+ // Verify the device was created
+ return checkV4L2LoopbackExists();
+
+ } catch (...) {
+ qWarning("Exception occurred while creating V4L2 loopback device");
+ return false;
+ }
+}
+
+void AirPlayWindow::setupV4L2Checkbox()
+{
+ try {
+ m_v4l2Checkbox = new QCheckBox("Enable V4L2 Virtual Camera Output");
+ m_v4l2Checkbox->setToolTip("Enable output to virtual camera device "
+ "that other applications can use");
+ m_v4l2Checkbox->setChecked(false);
+
+ connect(m_v4l2Checkbox, &QCheckBox::toggled, this,
+ &AirPlayWindow::onV4L2CheckboxToggled);
+
+ } catch (...) {
+ qWarning("Exception occurred while setting up V4L2 checkbox");
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/airplaywindow.h b/src/airplaywindow.h
index b88c68d..5282255 100644
--- a/src/airplaywindow.h
+++ b/src/airplaywindow.h
@@ -1,14 +1,43 @@
#ifndef AIRPLAYWINDOW_H
#define AIRPLAYWINDOW_H
+#include "qprocessindicator.h"
+#include
+#include
#include
#include
+#include
+#include
+#include
#include
#include
-#include
-#include
+#include
+#include
+#include
-class AirPlayServerThread;
+class AirPlayServerThread : public QThread
+{
+ Q_OBJECT
+
+public:
+ explicit AirPlayServerThread(QObject *parent = nullptr);
+ ~AirPlayServerThread() override;
+
+ void stopServer();
+
+signals:
+ void statusChanged(bool running);
+ void videoFrameReady(QByteArray frameData, int width, int height);
+ void clientConnectionChanged(bool connected);
+
+protected:
+ void run() override;
+
+private:
+ bool m_shouldStop;
+ QMutex m_mutex;
+ QWaitCondition m_waitCondition;
+};
class AirPlayWindow : public QMainWindow
{
@@ -20,57 +49,55 @@ public:
public slots:
void updateVideoFrame(QByteArray frameData, int width, int height);
+ void onClientConnectionChanged(bool connected);
private slots:
void onServerStatusChanged(bool running);
+ void onV4L2CheckboxToggled(bool enabled);
private:
void setupUI();
void startAirPlayServer();
void stopAirPlayServer();
+ void setupTutorialVideo();
+ void showTutorialView();
+ void showStreamingView();
+ // UI Components
+ QStackedWidget *m_stackedWidget;
+ QWidget *m_tutorialWidget;
+ QWidget *m_streamingWidget;
+
+ QProcessIndicator *m_loadingIndicator;
+ QLabel *m_loadingLabel;
+ QMediaPlayer *m_tutorialPlayer;
+ QVideoWidget *m_tutorialVideoWidget;
QLabel *m_videoLabel;
- QLabel *m_statusLabel;
+ QVBoxLayout *m_tutorialLayout;
+ QCheckBox *m_v4l2Checkbox;
+
AirPlayServerThread *m_serverThread;
bool m_serverRunning;
+ bool m_clientConnected = false;
#ifdef Q_OS_LINUX
- // V4L2 members
+public:
+ // V4L2 members - public for C callback access
int m_v4l2_fd;
int m_v4l2_width;
int m_v4l2_height;
- bool m_v4l2_enabled;
+ bool m_v4l2_enabled = false;
// V4L2 methods
+ void writeFrameToV4L2(uint8_t *data, int width, int height);
+
+private:
void initV4L2(int width, int height, const char *device);
void closeV4L2();
- void writeFrameToV4L2(uint8_t *data, int width, int height);
- void testV4L2Device();
+ bool checkV4L2LoopbackExists();
+ bool createV4L2Loopback();
+ void setupV4L2Checkbox();
#endif
};
-class AirPlayServerThread : public QThread
-{
- Q_OBJECT
-
-public:
- explicit AirPlayServerThread(QObject *parent = nullptr);
- ~AirPlayServerThread();
-
- void stopServer();
-
-signals:
- void statusChanged(bool running);
- void videoFrameReady(QByteArray frameData, int width, int height);
-
-protected:
- void run() override;
-
-private:
- bool m_shouldStop;
-};
-
-// Global callback for video renderer
-extern std::function qt_video_callback;
-
#endif // AIRPLAYWINDOW_H
diff --git a/src/appcontext.cpp b/src/appcontext.cpp
index 4018e87..09ca1b5 100644
--- a/src/appcontext.cpp
+++ b/src/appcontext.cpp
@@ -244,4 +244,20 @@ AppContext::~AppContext()
delete recoveryDevice->mutex;
delete recoveryDevice;
}
+}
+
+void AppContext::setCurrentDeviceSelection(const DeviceSelection &selection)
+{
+ if (m_currentSelection.uuid == selection.uuid &&
+ m_currentSelection.ecid == selection.ecid &&
+ m_currentSelection.section == selection.section) {
+ return; // No change
+ }
+ m_currentSelection = selection;
+ emit currentDeviceSelectionChanged(m_currentSelection);
+}
+
+const DeviceSelection &AppContext::getCurrentDeviceSelection() const
+{
+ return m_currentSelection;
}
\ No newline at end of file
diff --git a/src/appcontext.h b/src/appcontext.h
index 4c76b1f..2f8dfc8 100644
--- a/src/appcontext.h
+++ b/src/appcontext.h
@@ -1,6 +1,7 @@
#ifndef APPCONTEXT_H
#define APPCONTEXT_H
+#include "devicesidebarwidget.h"
#include "iDescriptor.h"
#include
#include
@@ -21,10 +22,14 @@ public:
~AppContext();
int getConnectedDeviceCount() const;
+ void setCurrentDeviceSelection(const DeviceSelection &selection);
+ const DeviceSelection &getCurrentDeviceSelection() const;
+
private:
QMap m_devices;
QMap m_recoveryDevices;
QStringList m_pendingDevices;
+ DeviceSelection m_currentSelection = DeviceSelection("");
signals:
void deviceAdded(iDescriptorDevice *device);
void deviceRemoved(const std::string &udid);
@@ -44,6 +49,7 @@ signals:
do anything you want
*/
void deviceChange();
+ void currentDeviceSelectionChanged(const DeviceSelection &selection);
public slots:
void removeDevice(QString udid);
void addDevice(QString udid, idevice_connection_type connType,
diff --git a/src/appdownloaddialog.cpp b/src/appdownloaddialog.cpp
index de942ee..baaa4db 100644
--- a/src/appdownloaddialog.cpp
+++ b/src/appdownloaddialog.cpp
@@ -1,5 +1,5 @@
#include "appdownloaddialog.h"
-#include "clickablelabel.h"
+#include "iDescriptor-ui.h"
#include "libipatool-go.h"
#include
#include
@@ -35,10 +35,10 @@ AppDownloadDialog::AppDownloadDialog(const QString &appName,
dirTextLabel->setStyleSheet("font-size: 14px;");
dirLayout->addWidget(dirTextLabel);
- m_dirLabel = new ClickableLabel(this);
+ m_dirLabel = new ZLabel(this);
m_dirLabel->setText(m_outputDir);
m_dirLabel->setStyleSheet("font-size: 14px; color: #007AFF;");
- connect(m_dirLabel, &ClickableLabel::clicked, this, [this]() {
+ connect(m_dirLabel, &ZLabel::clicked, this, [this]() {
QDesktopServices::openUrl(QUrl::fromLocalFile(m_outputDir));
});
m_dirLabel->setCursor(Qt::PointingHandCursor);
diff --git a/src/appdownloaddialog.h b/src/appdownloaddialog.h
index 9083550..a0f5362 100644
--- a/src/appdownloaddialog.h
+++ b/src/appdownloaddialog.h
@@ -2,7 +2,7 @@
#define APPDOWNLOADDIALOG_H
#include "appdownloadbasedialog.h"
-#include "clickablelabel.h"
+#include "iDescriptor-ui.h"
#include
#include
#include
@@ -21,7 +21,7 @@ private slots:
private:
QString m_outputDir;
QPushButton *m_dirButton;
- ClickableLabel *m_dirLabel;
+ ZLabel *m_dirLabel;
QString m_bundleId;
};
diff --git a/src/appswidget.cpp b/src/appswidget.cpp
index d296546..83835c1 100644
--- a/src/appswidget.cpp
+++ b/src/appswidget.cpp
@@ -347,19 +347,22 @@ void AppsWidget::createAppCard(const QString &name, const QString &bundleId,
QVBoxLayout *buttonsLayout = new QVBoxLayout();
// Install button placeholder
- QPushButton *installLabel = new QPushButton("Install");
- QPushButton *downloadIpaLabel = new QPushButton("Download IPA");
+ ZLabel *installLabel = new ZLabel("Install");
+ installLabel->setAlignment(Qt::AlignCenter);
+
+ ZLabel *downloadIpaLabel = new ZLabel("Download IPA");
+ downloadIpaLabel->setAlignment(Qt::AlignCenter);
installLabel->setStyleSheet("font-size: 12px; color: #007AFF; font-weight: "
"bold; background-color: transparent;");
installLabel->setCursor(Qt::PointingHandCursor);
installLabel->setFixedHeight(30);
- connect(installLabel, &QPushButton::clicked, this,
+ connect(installLabel, &ZLabel::clicked, this,
[this, name, bundleId, description]() {
onAppCardClicked(name, bundleId, description);
});
- connect(downloadIpaLabel, &QPushButton::clicked, this,
+ connect(downloadIpaLabel, &ZLabel::clicked, this,
[this, name, bundleId]() { onDownloadIpaClicked(name, bundleId); });
buttonsLayout->addWidget(installLabel);
diff --git a/src/clickablelabel.cpp b/src/clickablelabel.cpp
deleted file mode 100644
index 005b9ec..0000000
--- a/src/clickablelabel.cpp
+++ /dev/null
@@ -1,10 +0,0 @@
-#include "clickablelabel.h"
-
-ClickableLabel::ClickableLabel(QWidget *parent, Qt::WindowFlags f)
- : QLabel(parent)
-{
-}
-
-ClickableLabel::~ClickableLabel() {}
-
-void ClickableLabel::mousePressEvent(QMouseEvent *event) { emit clicked(); }
diff --git a/src/clickablelabel.h b/src/clickablelabel.h
deleted file mode 100644
index b42377f..0000000
--- a/src/clickablelabel.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#ifndef CLICKABLELABEL_H
-#define CLICKABLELABEL_H
-
-#include
-#include
-#include
-
-class ClickableLabel : public QLabel
-{
- Q_OBJECT
-
-public:
- explicit ClickableLabel(QWidget *parent = Q_NULLPTR,
- Qt::WindowFlags f = Qt::WindowFlags());
- ~ClickableLabel();
-
-signals:
- void clicked();
-
-protected:
- void mousePressEvent(QMouseEvent *event);
-};
-
-#endif // CLICKABLELABEL_H
\ No newline at end of file
diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp
index abaff90..2b051fd 100644
--- a/src/core/services/init_device.cpp
+++ b/src/core/services/init_device.cpp
@@ -99,6 +99,8 @@ void parseDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d)
d.batteryInfo.fullyCharged = ioreg["FullyCharged"].getBool();
+ qDebug() << "Stalebatteryinfo:"
+ << ioreg["BatteryData"]["StateOfCharge"].getUInt();
/* data is stale here so we need to calculate */
// d.batteryInfo.currentBatteryLevel =
// ioreg["BatteryData"]["StateOfCharge"].getUInt();
@@ -231,7 +233,9 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc,
d.activationState =
DeviceInfo::ActivationState::Unactivated; // Default value
}
- // TODO:RegionInfo: LL/A
+ std::string regionInfo = safeGet("RegionInfo");
+ d.regionRaw = regionInfo;
+ d.region = DeviceDatabase::parseRegionInfo(regionInfo);
std::string rawProductType = safeGet("ProductType");
const DeviceDatabaseInfo *info =
DeviceDatabase::findByIdentifier(rawProductType);
diff --git a/src/devicedatabase.cpp b/src/devicedatabase.cpp
index c21bd95..c1126eb 100644
--- a/src/devicedatabase.cpp
+++ b/src/devicedatabase.cpp
@@ -440,3 +440,116 @@ DeviceDatabase::findByHwModel(const std::string &hwModel)
}
return nullptr;
}
+
+std::string DeviceDatabase::parseRegionInfo(const std::string &code)
+{
+ // North America
+ if (code == "LL/A")
+ return "United States, Canada";
+ if (code == "LL")
+ return "United States, Canada";
+
+ // Latin America
+ if (code == "LA/A")
+ return "Latin America";
+ if (code == "BR/A" || code == "BZ/A")
+ return "Brazil";
+ if (code == "CL/A")
+ return "Chile";
+ if (code == "CO/A")
+ return "Colombia";
+ if (code == "MX/A")
+ return "Mexico";
+ if (code == "AR/A")
+ return "Argentina";
+
+ // Asia Pacific
+ if (code == "J/A")
+ return "Japan";
+ if (code == "KH/A")
+ return "Thailand, Cambodia";
+ if (code == "MY/A")
+ return "Malaysia";
+ if (code == "ZP/A")
+ return "Hong Kong, Macau";
+ if (code == "CH/A")
+ return "China";
+ if (code == "TA/A")
+ return "Taiwan";
+ if (code == "KR/A")
+ return "Korea";
+ if (code == "SG/A")
+ return "Singapore";
+ if (code == "IN/A")
+ return "India";
+ if (code == "TH/A")
+ return "Thailand";
+ if (code == "VN/A")
+ return "Vietnam";
+ if (code == "ID/A")
+ return "Indonesia";
+ if (code == "PH/A")
+ return "Philippines";
+ if (code == "NZ/A")
+ return "New Zealand";
+ if (code == "AU/A" || code == "X/A")
+ return "Australia";
+
+ // Europe
+ if (code == "ZA/A")
+ return "South Africa";
+ if (code == "AB/A")
+ return "Egypt, Jordan, Saudi Arabia, UAE";
+ if (code == "AE/A")
+ return "United Arab Emirates";
+ if (code == "B/A")
+ return "United Kingdom, Ireland";
+ if (code == "FB/A")
+ return "France, Luxembourg";
+ if (code == "FD/A")
+ return "Austria, Liechtenstein, Switzerland";
+ if (code == "GR/A")
+ return "Greece";
+ if (code == "HN/A")
+ return "India";
+ if (code == "IP/A")
+ return "Italy";
+ if (code == "KN/A")
+ return "Denmark, Norway";
+ if (code == "KS/A")
+ return "Finland, Sweden";
+ if (code == "LZ/A")
+ return "Paraguay, Uruguay";
+ if (code == "MG/A")
+ return "Hungary";
+ if (code == "PO/A")
+ return "Poland";
+ if (code == "PP/A")
+ return "Philippines";
+ if (code == "RO/A")
+ return "Romania";
+ if (code == "RS/A")
+ return "Russia";
+ if (code == "SL/A")
+ return "Slovakia";
+ if (code == "SO/A")
+ return "South Africa";
+ if (code == "T/A")
+ return "Italy";
+ if (code == "TU/A")
+ return "Turkey";
+ if (code == "Y/A")
+ return "Spain";
+ if (code == "ZD/A")
+ return "Germany, Luxembourg";
+
+ // Middle East
+ if (code == "HB/A")
+ return "Israel";
+
+ // Canada
+ if (code == "C/A")
+ return "Canada (English, French)";
+
+ return "Unknown Region (" + code + ")";
+}
\ No newline at end of file
diff --git a/src/devicedatabase.h b/src/devicedatabase.h
index 55a0a87..d1efc24 100644
--- a/src/devicedatabase.h
+++ b/src/devicedatabase.h
@@ -21,6 +21,7 @@ public:
static const DeviceDatabaseInfo *
findByIdentifier(const std::string &identifier);
static const DeviceDatabaseInfo *findByHwModel(const std::string &hwModel);
+ static std::string parseRegionInfo(const std::string &code);
private:
static const DeviceDatabaseInfo m_devices[];
diff --git a/src/deviceinfowidget.cpp b/src/deviceinfowidget.cpp
index 2244502..777e703 100644
--- a/src/deviceinfowidget.cpp
+++ b/src/deviceinfowidget.cpp
@@ -233,15 +233,14 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent)
infoItems.append(
{"Hardware Model:", createValueLabel(QString::fromStdString(
device->deviceInfo.hardwareModel))});
+ infoItems.append({"Region:", createValueLabel(QString::fromStdString(
+ device->deviceInfo.region))});
infoItems.append(
{"Hardware Platform:", createValueLabel(QString::fromStdString(
device->deviceInfo.hardwarePlatform))});
infoItems.append(
- {"Ethernet Address:", createValueLabel(QString::fromStdString(
- device->deviceInfo.ethernetAddress))});
- infoItems.append(
- {"Bluetooth Address:", createValueLabel(QString::fromStdString(
- device->deviceInfo.bluetoothAddress))});
+ {"Battery Cycle:", createValueLabel(QString::number(
+ m_device->deviceInfo.batteryInfo.cycleCount))});
infoItems.append(
{"Firmware Version:", createValueLabel(QString::fromStdString(
device->deviceInfo.firmwareVersion))});
diff --git a/src/devicemanagerwidget.cpp b/src/devicemanagerwidget.cpp
index 860cead..d5c2a53 100644
--- a/src/devicemanagerwidget.cpp
+++ b/src/devicemanagerwidget.cpp
@@ -64,6 +64,8 @@ DeviceManagerWidget::DeviceManagerWidget(QWidget *parent)
removePendingDevice(udid);
emit updateNoDevicesConnected();
});
+ onDeviceSelectionChanged(
+ AppContext::sharedInstance()->getCurrentDeviceSelection());
}
void DeviceManagerWidget::setupUI()
@@ -83,7 +85,8 @@ void DeviceManagerWidget::setupUI()
m_mainLayout->addWidget(m_stackedWidget);
// Connect signals
- connect(m_sidebar, &DeviceSidebarWidget::deviceSelectionChanged, this,
+ connect(AppContext::sharedInstance(),
+ &AppContext::currentDeviceSelectionChanged, this,
&DeviceManagerWidget::onDeviceSelectionChanged);
}
@@ -269,10 +272,9 @@ void DeviceManagerWidget::setCurrentDevice(const std::string &uuid)
QWidget *widget = m_deviceWidgets[uuid].first;
m_stackedWidget->setCurrentWidget(widget);
- // Update sidebar selection
- m_sidebar->setCurrentSelection(DeviceSelection(uuid));
-
- emit deviceChanged(uuid);
+ // Update sidebar selection through the AppContext to keep state consistent
+ AppContext::sharedInstance()->setCurrentDeviceSelection(
+ DeviceSelection(uuid));
}
std::string DeviceManagerWidget::getCurrentDevice() const
diff --git a/src/devicemanagerwidget.h b/src/devicemanagerwidget.h
index e505ed3..e1a6554 100644
--- a/src/devicemanagerwidget.h
+++ b/src/devicemanagerwidget.h
@@ -22,7 +22,6 @@ public:
std::string getCurrentDevice() const;
signals:
- void deviceChanged(std::string deviceUuid);
void updateNoDevicesConnected();
private slots:
diff --git a/src/devicesidebarwidget.cpp b/src/devicesidebarwidget.cpp
index b0f18f1..b162d38 100644
--- a/src/devicesidebarwidget.cpp
+++ b/src/devicesidebarwidget.cpp
@@ -1,4 +1,5 @@
#include "devicesidebarwidget.h"
+#include "appcontext.h"
#include "iDescriptor-ui.h"
#include "loadingspinnerwidget.h"
#include "qprocessindicator.h"
@@ -270,6 +271,11 @@ DeviceSidebarWidget::DeviceSidebarWidget(QWidget *parent)
// Set minimum width
setMinimumWidth(200);
setMaximumWidth(250);
+
+ // Listen to AppContext selection changes
+ connect(AppContext::sharedInstance(),
+ &AppContext::currentDeviceSelectionChanged, this,
+ &DeviceSidebarWidget::setCurrentSelection);
}
DeviceSidebarItem *DeviceSidebarWidget::addDevice(const QString &deviceName,
@@ -353,8 +359,7 @@ void DeviceSidebarWidget::setCurrentSelection(const DeviceSelection &selection)
void DeviceSidebarWidget::onItemSelected(const DeviceSelection &selection)
{
- setCurrentSelection(selection);
- emit deviceSelectionChanged(selection);
+ AppContext::sharedInstance()->setCurrentDeviceSelection(selection);
}
void DeviceSidebarWidget::updateSelection()
diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h
index 7d441a1..98f6094 100644
--- a/src/iDescriptor-ui.h
+++ b/src/iDescriptor-ui.h
@@ -1,6 +1,7 @@
#pragma once
#include
#include
+#include
#include
#include
#include
@@ -205,7 +206,7 @@ enum class iDescriptorTool {
RecoveryMode,
QueryMobileGestalt,
DeveloperDiskImages,
- WirelessFileImport,
+ WirelessPhotoImport,
CableInfoWidget,
/*
TODO: to be implemented
@@ -308,3 +309,23 @@ protected:
return new ModernSplitterHandle(orientation(), this);
}
};
+
+class ZLabel : public QLabel
+{
+ Q_OBJECT
+public:
+ using QLabel::QLabel;
+
+signals:
+ void clicked();
+
+protected:
+ void mouseReleaseEvent(QMouseEvent *event) override
+ {
+ if (event->button() == Qt::LeftButton &&
+ rect().contains(event->pos())) {
+ emit clicked();
+ }
+ QLabel::mouseReleaseEvent(event);
+ }
+};
diff --git a/src/iDescriptor.h b/src/iDescriptor.h
index 4863095..53103e5 100644
--- a/src/iDescriptor.h
+++ b/src/iDescriptor.h
@@ -136,6 +136,8 @@ struct DeviceInfo {
bool is_iPhone;
bool oldDevice;
std::string marketingName;
+ std::string regionRaw;
+ std::string region;
};
struct iDescriptorDevice {
diff --git a/src/ifusewidget.cpp b/src/ifusewidget.cpp
index 705402d..63f887b 100644
--- a/src/ifusewidget.cpp
+++ b/src/ifusewidget.cpp
@@ -1,5 +1,5 @@
#include "ifusewidget.h"
-#include "clickablelabel.h"
+#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "ifusediskunmountbutton.h"
#include "ifusemanager.h"
@@ -72,7 +72,7 @@ void iFuseWidget::setupUI()
QHBoxLayout *pathLayout = new QHBoxLayout(pathWidget);
pathLayout->setContentsMargins(0, 0, 0, 0);
- m_mountPathLabel = new ClickableLabel();
+ m_mountPathLabel = new ZLabel(this);
m_mountPathLabel->setText("Mount directory will be shown here");
m_mountPathLabel->setStyleSheet("QLabel { "
"border: 1px solid #ccc; "
@@ -93,24 +93,10 @@ void iFuseWidget::setupUI()
pathLayout->addWidget(m_folderPickerButton);
m_mainLayout->addWidget(pathWidget);
- // Delete on unmount checkbox
-
// Mount button
m_mountButton = new QPushButton("Mount Device");
m_mountButton->setMinimumHeight(40);
- m_mountButton->setStyleSheet("QPushButton { "
- "background-color: #007aff; "
- "color: white; "
- "border: none; "
- "border-radius: 6px; "
- "font-weight: bold; "
- "}"
- "QPushButton:hover { "
- "background-color: #0056cc; "
- "}"
- "QPushButton:disabled { "
- "background-color: #cccccc; "
- "}");
+ m_mountButton->setDefault(true);
m_mainLayout->addWidget(m_mountButton);
// Add stretch to push everything to the top
@@ -119,7 +105,7 @@ void iFuseWidget::setupUI()
// Connect signals
connect(m_folderPickerButton, &QPushButton::clicked, this,
&iFuseWidget::onFolderPickerClicked);
- connect(m_mountPathLabel, &ClickableLabel::clicked, this,
+ connect(m_mountPathLabel, &ZLabel::clicked, this,
&iFuseWidget::onMountPathClicked);
connect(m_mountButton, &QPushButton::clicked, this,
&iFuseWidget::onMountClicked);
diff --git a/src/ifusewidget.h b/src/ifusewidget.h
index c62256a..c44b45c 100644
--- a/src/ifusewidget.h
+++ b/src/ifusewidget.h
@@ -2,7 +2,7 @@
#define IFUSEWIDGET_H
#include "appcontext.h"
-#include "clickablelabel.h"
+#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include
#include
@@ -46,7 +46,7 @@ private:
QLabel *m_descriptionLabel;
QLabel *m_statusLabel;
QComboBox *m_deviceComboBox;
- ClickableLabel *m_mountPathLabel;
+ ZLabel *m_mountPathLabel;
QPushButton *m_folderPickerButton;
QLabel *m_folderNameLabel;
QPushButton *m_mountButton;
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index d66170e..edb9f5a 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -13,6 +13,7 @@
#include "jailbrokenwidget.h"
#include "libirecovery.h"
#include "toolboxwidget.h"
+#include "welcomewidget.h"
#include
#include
#include
@@ -128,22 +129,12 @@ MainWindow::MainWindow(QWidget *parent)
// Create device manager and stacked widget for main tab
m_mainStackedWidget = new QStackedWidget();
- // No devices page
- QWidget *noDevicesPage = new QWidget();
- QVBoxLayout *noDeviceLayout = new QVBoxLayout(noDevicesPage);
- noDeviceLayout->addStretch();
- QHBoxLayout *labelLayout = new QHBoxLayout();
- labelLayout->addStretch();
- QLabel *noDeviceLabel = new QLabel("No devices detected");
- noDeviceLabel->setAlignment(Qt::AlignCenter);
- labelLayout->addWidget(noDeviceLabel);
- labelLayout->addStretch();
- noDeviceLayout->addLayout(labelLayout);
- noDeviceLayout->addStretch();
+ // Welcome page (shown when no devices are connected)
+ WelcomeWidget *welcomePage = new WelcomeWidget(this);
m_deviceManager = new DeviceManagerWidget(this);
- m_mainStackedWidget->addWidget(noDevicesPage);
+ m_mainStackedWidget->addWidget(welcomePage);
m_mainStackedWidget->addWidget(m_deviceManager);
connect(m_deviceManager, &DeviceManagerWidget::updateNoDevicesConnected,
@@ -245,8 +236,7 @@ void MainWindow::updateNoDevicesConnected()
if (AppContext::sharedInstance()->noDevicesConnected()) {
m_connectedDeviceCountLabel->setText("iDescriptor: no devices");
- return m_mainStackedWidget->setCurrentIndex(
- 0); // Show "No Devices Connected" page
+ return m_mainStackedWidget->setCurrentIndex(0); // Show Welcome page
}
int deviceCount = AppContext::sharedInstance()->getConnectedDeviceCount();
m_connectedDeviceCountLabel->setText(
diff --git a/src/pcfileexplorerwidget.cpp b/src/pcfileexplorerwidget.cpp
deleted file mode 100644
index 9f1b0fd..0000000
--- a/src/pcfileexplorerwidget.cpp
+++ /dev/null
@@ -1,174 +0,0 @@
-#include "pcfileexplorerwidget.h"
-#include "photoimportdialog.h"
-#include
-#include
-#include
-#include
-#include
-#include
-
-PCFileExplorerWidget::PCFileExplorerWidget(QWidget *parent)
- : QWidget(parent), containsDirectories(false)
-{
- setupUI();
- setupFileSystemModel();
- connect(treeView->selectionModel(), &QItemSelectionModel::selectionChanged,
- this, &PCFileExplorerWidget::onSelectionChanged);
- connect(importButton, &QPushButton::clicked, this,
- &PCFileExplorerWidget::onImportPhotos);
-}
-
-void PCFileExplorerWidget::setupUI()
-{
- QVBoxLayout *mainLayout = new QVBoxLayout(this);
-
- // Create splitter for tree view and preview
- QSplitter *splitter = new QSplitter(Qt::Horizontal, this);
-
- // Tree view for file system
- treeView = new QTreeView(this);
- treeView->setSelectionMode(QAbstractItemView::ExtendedSelection);
- splitter->addWidget(treeView);
-
- // Preview list
- previewList = new QListWidget(this);
- previewList->setMaximumWidth(300);
- splitter->addWidget(previewList);
-
- mainLayout->addWidget(splitter);
-
- // Status and import button
- QHBoxLayout *bottomLayout = new QHBoxLayout();
- statusLabel = new QLabel("Select files or directories to import", this);
- importButton = new QPushButton("Import Photos to iOS", this);
- importButton->setEnabled(false);
-
- bottomLayout->addWidget(statusLabel);
- bottomLayout->addStretch();
- bottomLayout->addWidget(importButton);
-
- mainLayout->addLayout(bottomLayout);
-}
-
-void PCFileExplorerWidget::setupFileSystemModel()
-{
- model = new QFileSystemModel(this);
- model->setRootPath(
- QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
- treeView->setModel(model);
-
- // Set root to home directory
- treeView->setRootIndex(model->index(
- QStandardPaths::writableLocation(QStandardPaths::HomeLocation)));
-
- // Hide size, type, and date columns for cleaner look
- treeView->setColumnWidth(0, 250);
- treeView->hideColumn(1);
- treeView->hideColumn(2);
- treeView->hideColumn(3);
-}
-
-QStringList PCFileExplorerWidget::getGalleryCompatibleExtensions() const
-{
- return {"jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp", "heic",
- "heif", "mp4", "mov", "avi", "mkv", "m4v", "3gp", "webm"};
-}
-
-bool PCFileExplorerWidget::isGalleryCompatible(const QString &filePath) const
-{
- QFileInfo info(filePath);
- QString ext = info.suffix().toLower();
- return getGalleryCompatibleExtensions().contains(ext);
-}
-
-void PCFileExplorerWidget::scanDirectory(const QString &dirPath,
- QStringList &files) const
-{
- QDir dir(dirPath);
- QFileInfoList entries =
- dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
-
- for (const QFileInfo &entry : entries) {
- if (entry.isFile() && isGalleryCompatible(entry.absoluteFilePath())) {
- files.append(entry.absoluteFilePath());
- } else if (entry.isDir()) {
- scanDirectory(entry.absoluteFilePath(), files);
- }
- }
-}
-
-void PCFileExplorerWidget::onSelectionChanged()
-{
- selectedFiles.clear();
- containsDirectories = false;
-
- QModelIndexList indexes = treeView->selectionModel()->selectedIndexes();
- if (indexes.isEmpty()) {
- importButton->setEnabled(false);
- statusLabel->setText("Select files or directories to import");
- previewList->clear();
- return;
- }
-
- // Process selected items
- for (const QModelIndex &index : indexes) {
- if (index.column() != 0)
- continue; // Only process first column
-
- QString path = model->filePath(index);
- QFileInfo info(path);
-
- if (info.isFile()) {
- if (isGalleryCompatible(path)) {
- selectedFiles.append(path);
- }
- } else if (info.isDir()) {
- containsDirectories = true;
- scanDirectory(path, selectedFiles);
- }
- }
-
- refreshPreview();
-
- importButton->setEnabled(!selectedFiles.isEmpty());
- statusLabel->setText(QString("Selected %1 gallery-compatible files")
- .arg(selectedFiles.size()));
-}
-
-void PCFileExplorerWidget::refreshPreview()
-{
- previewList->clear();
-
- int maxPreview = 50; // Limit preview items
- for (int i = 0; i < selectedFiles.size() && i < maxPreview; ++i) {
- QFileInfo info(selectedFiles[i]);
- previewList->addItem(info.fileName());
- }
-
- if (selectedFiles.size() > maxPreview) {
- previewList->addItem(QString("... and %1 more files")
- .arg(selectedFiles.size() - maxPreview));
- }
-}
-
-void PCFileExplorerWidget::onImportPhotos()
-{
- if (selectedFiles.isEmpty()) {
- QMessageBox::warning(this, "No Files",
- "No gallery-compatible files selected.");
- return;
- }
-
- PhotoImportDialog dialog(selectedFiles, containsDirectories, this);
- dialog.exec();
-}
-
-QStringList PCFileExplorerWidget::getSelectedFiles() const
-{
- return selectedFiles;
-}
-
-bool PCFileExplorerWidget::hasDirectories() const
-{
- return containsDirectories;
-}
diff --git a/src/pcfileexplorerwidget.h b/src/pcfileexplorerwidget.h
deleted file mode 100644
index aa18c47..0000000
--- a/src/pcfileexplorerwidget.h
+++ /dev/null
@@ -1,48 +0,0 @@
-#ifndef PCFILEEXPLORERWIDGET_H
-#define PCFILEEXPLORERWIDGET_H
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-class PCFileExplorerWidget : public QWidget
-{
- Q_OBJECT
-
-public:
- explicit PCFileExplorerWidget(QWidget *parent = nullptr);
-
- QStringList getSelectedFiles() const;
- bool hasDirectories() const;
-
-private slots:
- void onSelectionChanged();
- void onImportPhotos();
- void refreshPreview();
-
-private:
- QTreeView *treeView;
- QFileSystemModel *model;
- QListWidget *previewList;
- QPushButton *importButton;
- QLabel *statusLabel;
-
- QStringList selectedFiles;
- bool containsDirectories;
-
- void setupUI();
- void setupFileSystemModel();
- bool isGalleryCompatible(const QString &filePath) const;
- void scanDirectory(const QString &dirPath, QStringList &files) const;
- QStringList getGalleryCompatibleExtensions() const;
-};
-
-#endif // PCFILEEXPLORERWIDGET_H
diff --git a/src/photoimportdialog.cpp b/src/photoimportdialog.cpp
index 60767b7..087abb5 100644
--- a/src/photoimportdialog.cpp
+++ b/src/photoimportdialog.cpp
@@ -13,19 +13,19 @@
PhotoImportDialog::PhotoImportDialog(const QStringList &files,
bool hasDirectories, QWidget *parent)
: QDialog(parent), selectedFiles(files),
- containsDirectories(hasDirectories), httpServer(nullptr)
+ containsDirectories(hasDirectories), m_httpServer(nullptr)
{
setupUI();
setModal(true);
resize(600, 500);
- setWindowTitle("Import Photos to iOS");
+ setWindowTitle("Import Photos to iDevice - iDescriptor");
}
PhotoImportDialog::~PhotoImportDialog()
{
- if (httpServer) {
- httpServer->stop();
- delete httpServer;
+ if (m_httpServer) {
+ m_httpServer->stop();
+ delete m_httpServer;
}
}
@@ -39,10 +39,6 @@ void PhotoImportDialog::setupUI()
new QLabel("⚠️ Warning: Selected items contain directories. All "
"gallery-compatible files will be included.",
this);
- warningLabel->setStyleSheet(
- "color: orange; font-weight: bold; padding: 10px; "
- "background-color: #fff3cd; border: 1px solid #ffeaa7; "
- "border-radius: 5px;");
warningLabel->setWordWrap(true);
mainLayout->addWidget(warningLabel);
}
@@ -65,19 +61,11 @@ void PhotoImportDialog::setupUI()
qrCodeLabel = new QLabel(this);
qrCodeLabel->setAlignment(Qt::AlignCenter);
qrCodeLabel->setMinimumSize(200, 200);
- qrCodeLabel->setStyleSheet(
- "border: 1px solid #ccc; background-color: white;");
qrCodeLabel->setText("QR Code will appear here after starting server");
mainLayout->addWidget(qrCodeLabel);
// Instructions
- instructionLabel = new QLabel(
- "1. Click 'Start Server' to begin\n2. Scan the QR code to open the web "
- "interface\n3. Copy the server address and download the shortcut\n4. "
- "Run the shortcut on your iOS device to import photos",
- this);
- instructionLabel->setStyleSheet(
- "padding: 10px; background-color: #f8f9fa; border-radius: 5px;");
+ instructionLabel = new QLabel("Loading", this);
mainLayout->addWidget(instructionLabel);
// Progress tracking
@@ -92,61 +80,56 @@ void PhotoImportDialog::setupUI()
// Buttons
QHBoxLayout *buttonLayout = new QHBoxLayout();
- confirmButton = new QPushButton("Start Server", this);
- cancelButton = new QPushButton("Cancel", this);
+ m_cancelButton = new QPushButton("Cancel", this);
buttonLayout->addStretch();
- buttonLayout->addWidget(confirmButton);
- buttonLayout->addWidget(cancelButton);
+ buttonLayout->addWidget(m_cancelButton);
mainLayout->addLayout(buttonLayout);
- connect(confirmButton, &QPushButton::clicked, this,
- &PhotoImportDialog::onConfirmImport);
- connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
+ connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject);
+
+ QTimer::singleShot(0, this, &PhotoImportDialog::init);
}
-void PhotoImportDialog::onConfirmImport()
+void PhotoImportDialog::init()
{
- confirmButton->setEnabled(false);
- confirmButton->setText("Starting Server...");
progressBar->setVisible(true);
progressBar->setRange(0, 0); // Indeterminate progress
// Create and start HTTP server
- httpServer = new SimpleHttpServer(this);
- connect(httpServer, &SimpleHttpServer::serverStarted, this,
+ m_httpServer = new SimpleHttpServer(this);
+ connect(m_httpServer, &SimpleHttpServer::serverStarted, this,
&PhotoImportDialog::onServerStarted);
- connect(httpServer, &SimpleHttpServer::serverError, this,
+ connect(m_httpServer, &SimpleHttpServer::serverError, this,
&PhotoImportDialog::onServerError);
- connect(httpServer, &SimpleHttpServer::downloadProgress, this,
+ connect(m_httpServer, &SimpleHttpServer::downloadProgress, this,
&PhotoImportDialog::onDownloadProgress);
- httpServer->start(selectedFiles);
+ m_httpServer->start(selectedFiles);
}
void PhotoImportDialog::onServerStarted()
{
progressBar->setVisible(false);
- confirmButton->setText("Server Running");
QString localIP = getLocalIP();
- int port = httpServer->getPort();
+ int port = m_httpServer->getPort();
+ QString jsonFileName = m_httpServer->getJsonFileName();
- // Generate QR code for the web interface
- QString webUrl = QString("http://%1:%2/").arg(localIP).arg(port);
- // generateQRCode(webUrl);
- generateQRCode(QString("shortcuts://import-shortcut?url=%1/import.shortcut")
- .arg(webUrl));
-
- instructionLabel->setText(
- QString("✅ Server started at %1:%2\n\n1. Scan the QR code to open the "
- "web interface\n2. Copy the server address and download the "
- "shortcut\n3. Run the shortcut on your iOS device\n\nWeb "
- "Interface: %3")
+ generateQRCode(
+ QString("http://192.168.1.149:5173/?local=%1&port=%2&file=%3")
+ // QString("https://uncor3.github.io/test-2?local=%1&port=%2&file=%3")
.arg(localIP)
.arg(port)
- .arg(webUrl));
+ .arg(jsonFileName));
+
+ instructionLabel->setText(
+ QString("Server started at %1:%2\n\n1. Scan the QR code to open the "
+ "web interface\n2. Copy the server address and download the "
+ "shortcut\n3. Run the shortcut on your iOS device")
+ .arg(localIP)
+ .arg(port));
progressLabel->setVisible(true);
progressLabel->setText("Waiting for downloads...");
@@ -158,19 +141,16 @@ void PhotoImportDialog::onDownloadProgress(const QString &fileName,
progressLabel->setText(QString("Downloaded: %1 (%2 KB)")
.arg(fileName)
.arg(bytesDownloaded / 1024));
-
- // You could implement more sophisticated progress tracking here
- // For now, just show which file was downloaded
}
void PhotoImportDialog::onServerError(const QString &error)
{
progressBar->setVisible(false);
- confirmButton->setEnabled(true);
- confirmButton->setText("Start Server");
+ m_cancelButton->setEnabled(true);
QMessageBox::critical(this, "Server Error",
QString("Failed to start server: %1").arg(error));
+ QDialog::reject();
}
void PhotoImportDialog::generateQRCode(const QString &url)
diff --git a/src/photoimportdialog.h b/src/photoimportdialog.h
index 8ef5135..e4e3c44 100644
--- a/src/photoimportdialog.h
+++ b/src/photoimportdialog.h
@@ -22,7 +22,7 @@ public:
~PhotoImportDialog();
private slots:
- void onConfirmImport();
+ void init();
void onServerStarted();
void onServerError(const QString &error);
void onDownloadProgress(const QString &fileName, int bytesDownloaded,
@@ -36,12 +36,11 @@ private:
QLabel *warningLabel;
QLabel *qrCodeLabel;
QLabel *instructionLabel;
- QPushButton *confirmButton;
- QPushButton *cancelButton;
+ QPushButton *m_cancelButton;
QProgressBar *progressBar;
QLabel *progressLabel;
- SimpleHttpServer *httpServer;
+ SimpleHttpServer *m_httpServer;
void setupUI();
void generateQRCode(const QString &url);
diff --git a/src/realtimescreenwidget.cpp b/src/realtimescreenwidget.cpp
index 33a4ce5..1871b47 100644
--- a/src/realtimescreenwidget.cpp
+++ b/src/realtimescreenwidget.cpp
@@ -20,7 +20,6 @@ RealtimeScreenWidget::RealtimeScreenWidget(iDescriptorDevice *device,
{
setWindowTitle("Real-time Screen - iDescriptor");
- // Check iOS version first
unsigned int device_version = idevice_get_device_version(m_device->device);
unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
@@ -99,7 +98,7 @@ RealtimeScreenWidget::RealtimeScreenWidget(iDescriptorDevice *device,
if (success) {
// for some reason it does not work immediately, so delay a bit
QTimer::singleShot(
- 100, this,
+ 1000, this,
&RealtimeScreenWidget::initializeScreenshotService);
} else {
m_statusLabel->setText("Failed to mount developer disk image");
diff --git a/src/simplehttpserver.h b/src/simplehttpserver.h
index 56b3e80..7c1b689 100644
--- a/src/simplehttpserver.h
+++ b/src/simplehttpserver.h
@@ -19,7 +19,7 @@ public:
void start(const QStringList &files);
void stop();
int getPort() const;
-
+ QString getJsonFileName() const { return jsonFileName; }
signals:
void serverStarted();
void serverError(const QString &error);
diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp
index ab82a85..55c1c16 100644
--- a/src/toolboxwidget.cpp
+++ b/src/toolboxwidget.cpp
@@ -6,13 +6,13 @@
#include "devdiskmanager.h"
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
-#ifdef __APPLE__
+#ifndef __APPLE__
#include "ifusewidget.h"
#endif
-#include "pcfileexplorerwidget.h"
#include "querymobilegestaltwidget.h"
#include "realtimescreenwidget.h"
#include "virtuallocationwidget.h"
+#include "wirelessphotoimportwidget.h"
#include
#include
#include
@@ -68,6 +68,9 @@ ToolboxWidget::ToolboxWidget(QWidget *parent) : QWidget{parent}
connect(AppContext::sharedInstance(), &AppContext::deviceChange, this,
&ToolboxWidget::updateUI);
+ connect(AppContext::sharedInstance(),
+ &AppContext::currentDeviceSelectionChanged, this,
+ &ToolboxWidget::onCurrentDeviceChanged);
}
void ToolboxWidget::setupUI()
@@ -127,10 +130,11 @@ void ToolboxWidget::setupUI()
"Query device hardware information", true, ""});
mainToolWidgets.append({iDescriptorTool::DeveloperDiskImages,
"Manage developer disk images", false, ""});
- mainToolWidgets.append({iDescriptorTool::WirelessFileImport,
- "Import files wirelessly to your iDevice", false,
- ""});
-#ifdef __APPLE__
+ mainToolWidgets.append(
+ {iDescriptorTool::WirelessPhotoImport,
+ "Import photos wirelessly to your iDevice (requires Shortcut app)",
+ false, ""});
+#ifndef __APPLE__
mainToolWidgets.append({iDescriptorTool::iFuse,
"Mount your iPhone's filesystem on your PC", true,
""});
@@ -239,8 +243,8 @@ ClickableWidget *ToolboxWidget::createToolbox(iDescriptorTool tool,
case iDescriptorTool::DeveloperDiskImages:
title = "Dev Disk Images";
break;
- case iDescriptorTool::WirelessFileImport:
- title = "Wireless File Import";
+ case iDescriptorTool::WirelessPhotoImport:
+ title = "Wireless Photo Import";
break;
case iDescriptorTool::iFuse:
title = "iFuse Mount";
@@ -346,6 +350,9 @@ void ToolboxWidget::onDeviceSelectionChanged()
if (QString::fromStdString(device->udid) == selectedUdid) {
m_uuid = device->udid;
m_currentDevice = device;
+ // Also update the AppContext to keep everything in sync
+ AppContext::sharedInstance()->setCurrentDeviceSelection(
+ DeviceSelection(m_uuid));
return;
}
}
@@ -353,16 +360,50 @@ void ToolboxWidget::onDeviceSelectionChanged()
m_currentDevice = nullptr;
}
+void ToolboxWidget::onCurrentDeviceChanged(const DeviceSelection &selection)
+{
+ if (selection.type == DeviceSelection::Normal) {
+ int index =
+ m_deviceCombo->findData(QString::fromStdString(selection.uuid));
+ if (index != -1) {
+ // Block signals to prevent recursive calls
+ m_deviceCombo->blockSignals(true);
+ m_deviceCombo->setCurrentIndex(index);
+ m_deviceCombo->blockSignals(false);
+
+ // Update internal state
+ m_uuid = selection.uuid;
+ QList devices =
+ AppContext::sharedInstance()->getAllDevices();
+ for (iDescriptorDevice *device : devices) {
+ if (device->udid == selection.uuid) {
+ m_currentDevice = device;
+ break;
+ }
+ }
+ }
+ } else {
+ // TODO: recovery and no device selection
+ }
+}
+
void ToolboxWidget::onToolboxClicked(iDescriptorTool tool)
{
switch (tool) {
case iDescriptorTool::Airplayer: {
- AirPlayWindow *airplayWindow = new AirPlayWindow();
- airplayWindow->setAttribute(Qt::WA_DeleteOnClose);
- airplayWindow->setWindowFlag(Qt::Window);
- airplayWindow->resize(400, 300);
- airplayWindow->show();
+ if (!m_airplayWindow) {
+ m_airplayWindow = new AirPlayWindow();
+ connect(m_airplayWindow, &QObject::destroyed, this,
+ [this]() { m_airplayWindow = nullptr; });
+ m_airplayWindow->setAttribute(Qt::WA_DeleteOnClose);
+ m_airplayWindow->setWindowFlag(Qt::Window);
+ m_airplayWindow->resize(400, 300);
+ m_airplayWindow->show();
+ } else {
+ m_airplayWindow->raise();
+ m_airplayWindow->activateWindow();
+ }
} break;
case iDescriptorTool::RealtimeScreen: {
@@ -447,14 +488,19 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool)
m_devDiskImagesWidget->activateWindow();
}
} break;
- case iDescriptorTool::WirelessFileImport: {
- PCFileExplorerWidget *fileExplorer = new PCFileExplorerWidget();
- fileExplorer->setAttribute(Qt::WA_DeleteOnClose);
- fileExplorer->setWindowFlag(Qt::Window);
- fileExplorer->resize(800, 600);
- fileExplorer->show();
+ case iDescriptorTool::WirelessPhotoImport: {
+ if (!m_wirelessPhotoImportWidget) {
+ m_wirelessPhotoImportWidget = new WirelessPhotoImportWidget();
+ m_wirelessPhotoImportWidget->setAttribute(Qt::WA_DeleteOnClose);
+ m_wirelessPhotoImportWidget->setWindowFlag(Qt::Window);
+ // m_wirelessPhotoImportWidget->resize(800, 600);
+ m_wirelessPhotoImportWidget->show();
+ } else {
+ m_wirelessPhotoImportWidget->show();
+ m_wirelessPhotoImportWidget->show();
+ }
} break;
-#ifdef __APPLE__
+#ifndef __APPLE__
case iDescriptorTool::iFuse: {
iFuseWidget *ifuseWidget = new iFuseWidget(m_currentDevice);
ifuseWidget->setAttribute(Qt::WA_DeleteOnClose);
diff --git a/src/toolboxwidget.h b/src/toolboxwidget.h
index 123529c..2969bf4 100644
--- a/src/toolboxwidget.h
+++ b/src/toolboxwidget.h
@@ -1,10 +1,13 @@
#ifndef TOOLBOXWIDGET_H
#define TOOLBOXWIDGET_H
+#include "airplaywindow.h"
#include "devdiskimageswidget.h"
+#include "devicesidebarwidget.h"
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "networkdeviceswidget.h"
+#include "wirelessphotoimportwidget.h"
#include
#include
#include
@@ -25,6 +28,7 @@ public:
private slots:
void onDeviceSelectionChanged();
void onToolboxClicked(iDescriptorTool tool);
+ void onCurrentDeviceChanged(const DeviceSelection &selection);
private:
void setupUI();
@@ -45,6 +49,8 @@ private:
std::string m_uuid;
DevDiskImagesWidget *m_devDiskImagesWidget = nullptr;
NetworkDevicesWidget *m_networkDevicesWidget = nullptr;
+ AirPlayWindow *m_airplayWindow = nullptr;
+ WirelessPhotoImportWidget *m_wirelessPhotoImportWidget = nullptr;
signals:
};
diff --git a/src/virtuallocationwidget.cpp b/src/virtuallocationwidget.cpp
index 6d44fd2..5416418 100644
--- a/src/virtuallocationwidget.cpp
+++ b/src/virtuallocationwidget.cpp
@@ -125,19 +125,19 @@ VirtualLocation::VirtualLocation(iDescriptorDevice *device, QWidget *parent)
DevDiskManager::sharedInstance()->downloadCompatibleImage(m_device);
- QTimer::singleShot(0, this, [this]() {
- unsigned int device_version =
- idevice_get_device_version(m_device->device);
- unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
+ unsigned int device_version = idevice_get_device_version(m_device->device);
+ unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
- if (deviceMajorVersion > 16) {
- QMessageBox::information(this, "Info",
- "Virtual Location feature requires iOS "
- "16 or earlier. Support for iOS " +
- QString::number(deviceMajorVersion) +
- " is still under development.");
- }
- });
+ if (deviceMajorVersion > 16) {
+ QMessageBox::warning(
+ this, "Unsupported iOS Version",
+ "Real-time Screen feature requires iOS 16 or earlier.\n"
+ "Your device is running iOS " +
+ QString::number(deviceMajorVersion) +
+ ", which is not yet supported.");
+ QTimer::singleShot(0, this, &QWidget::close);
+ return;
+ }
}
void VirtualLocation::onQuickWidgetStatusChanged(QQuickWidget::Status status)
diff --git a/src/welcomewidget.cpp b/src/welcomewidget.cpp
new file mode 100644
index 0000000..fe33fc8
--- /dev/null
+++ b/src/welcomewidget.cpp
@@ -0,0 +1,124 @@
+#include "welcomewidget.h"
+#include "responsiveqlabel.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) { setupUI(); }
+
+void WelcomeWidget::setupUI()
+{
+ // Main layout with proper spacing and margins
+ m_mainLayout = new QVBoxLayout(this);
+ m_mainLayout->setContentsMargins(40, 60, 40, 60);
+ m_mainLayout->setSpacing(0);
+
+ // Add top stretch
+ m_mainLayout->addStretch(1);
+
+ // Welcome title
+ m_titleLabel = createStyledLabel("Welcome to iDescriptor", 28, true);
+ m_titleLabel->setAlignment(Qt::AlignCenter);
+ m_mainLayout->addWidget(m_titleLabel);
+ m_mainLayout->addSpacing(12);
+
+ // Subtitle
+ m_subtitleLabel = createStyledLabel("100% Open-Source & Free", 16, false);
+ m_subtitleLabel->setAlignment(Qt::AlignCenter);
+ QPalette palette = m_subtitleLabel->palette();
+ palette.setColor(QPalette::WindowText,
+ palette.color(QPalette::WindowText).lighter(140));
+ m_subtitleLabel->setPalette(palette);
+ m_mainLayout->addWidget(m_subtitleLabel);
+ m_mainLayout->addSpacing(40);
+
+ m_imageLabel = new ResponsiveQLabel();
+ m_imageLabel->setPixmap(QPixmap(":/resources/connect.png"));
+ // Let the pixmap scale while preserving aspect ratio
+ m_imageLabel->setScaledContents(true);
+ // Prefer centered, not full-width expansion
+ m_imageLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+ // Cap size so it stays nicely centered on large windows
+ m_imageLabel->setMaximumSize(480, 320);
+ m_imageLabel->setStyleSheet("background: transparent; border: none;");
+
+ m_imageLabel->setAlignment(Qt::AlignCenter);
+ m_mainLayout->addWidget(m_imageLabel, 0, Qt::AlignHCenter);
+ m_mainLayout->addSpacing(32);
+
+ // Instruction text
+ m_instructionLabel = createStyledLabel(
+ "Please connect an iOS device to get started", 14, false);
+ m_instructionLabel->setAlignment(Qt::AlignCenter);
+ QPalette instructionPalette = m_instructionLabel->palette();
+ instructionPalette.setColor(
+ QPalette::WindowText,
+ instructionPalette.color(QPalette::WindowText).lighter(120));
+ m_instructionLabel->setPalette(instructionPalette);
+ m_mainLayout->addWidget(m_instructionLabel);
+ m_mainLayout->addSpacing(24);
+
+ // GitHub link
+ m_githubLabel =
+ createStyledLabel("Found an issue? Report it on GitHub", 12, false);
+ m_githubLabel->setAlignment(Qt::AlignCenter);
+ m_githubLabel->setCursor(Qt::PointingHandCursor);
+
+ // Make it look like a link
+ QPalette githubPalette = m_githubLabel->palette();
+ githubPalette.setColor(QPalette::WindowText,
+ QColor(0, 122, 255)); // Apple blue
+ m_githubLabel->setPalette(githubPalette);
+
+ // Connect click functionality using installEventFilter
+ m_githubLabel->installEventFilter(this);
+
+ m_mainLayout->addWidget(m_githubLabel);
+
+ // Add bottom stretch
+ m_mainLayout->addStretch(1);
+
+ // Set minimum size
+ setMinimumSize(400, 500);
+}
+
+QLabel *WelcomeWidget::createStyledLabel(const QString &text, int fontSize,
+ bool isBold)
+{
+ QLabel *label = new QLabel(text);
+
+ QFont font = label->font();
+ if (fontSize > 0) {
+ font.setPointSize(fontSize);
+ }
+ if (isBold) {
+ font.setWeight(QFont::Medium);
+ }
+
+ // Use system font on macOS for better integration
+#ifdef Q_OS_MAC
+ font.setFamily(".AppleSystemUIFont");
+#endif
+
+ label->setFont(font);
+ label->setWordWrap(true);
+
+ return label;
+}
+
+bool WelcomeWidget::eventFilter(QObject *watched, QEvent *event)
+{
+ if (watched == m_githubLabel && event->type() == QEvent::MouseButtonPress) {
+ QMouseEvent *mouseEvent = static_cast(event);
+ if (mouseEvent->button() == Qt::LeftButton) {
+ QDesktopServices::openUrl(
+ QUrl("https://github.com/uncor3/iDescriptor"));
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
\ No newline at end of file
diff --git a/src/welcomewidget.h b/src/welcomewidget.h
new file mode 100644
index 0000000..d08c75e
--- /dev/null
+++ b/src/welcomewidget.h
@@ -0,0 +1,34 @@
+#ifndef WELCOMEWIDGET_H
+#define WELCOMEWIDGET_H
+
+#include "responsiveqlabel.h"
+#include
+#include
+#include
+#include
+#include
+
+class WelcomeWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit WelcomeWidget(QWidget *parent = nullptr);
+
+protected:
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
+private:
+ void setupUI();
+ QLabel *createStyledLabel(const QString &text, int fontSize = 0,
+ bool isBold = false);
+
+ QVBoxLayout *m_mainLayout;
+ QLabel *m_titleLabel;
+ QLabel *m_subtitleLabel;
+ ResponsiveQLabel *m_imageLabel;
+ QLabel *m_instructionLabel;
+ QLabel *m_githubLabel;
+};
+
+#endif // WELCOMEWIDGET_H
\ No newline at end of file
diff --git a/src/wirelessphotoimportwidget.cpp b/src/wirelessphotoimportwidget.cpp
new file mode 100644
index 0000000..94b7731
--- /dev/null
+++ b/src/wirelessphotoimportwidget.cpp
@@ -0,0 +1,262 @@
+#include "wirelessphotoimportwidget.h"
+#include "photoimportdialog.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+WirelessPhotoImportWidget::WirelessPhotoImportWidget(QWidget *parent)
+ : QWidget(parent), m_leftPanel(nullptr), m_scrollArea(nullptr),
+ m_scrollContent(nullptr), m_fileListLayout(nullptr),
+ m_browseButton(nullptr), m_importButton(nullptr), m_statusLabel(nullptr),
+ m_rightPanel(nullptr), m_tutorialPlayer(nullptr),
+ m_tutorialVideoWidget(nullptr), m_loadingIndicator(nullptr),
+ m_loadingLabel(nullptr), m_tutorialLayout(nullptr)
+{
+ setupUI();
+ QTimer::singleShot(100, this,
+ &WirelessPhotoImportWidget::setupTutorialVideo);
+}
+
+WirelessPhotoImportWidget::~WirelessPhotoImportWidget()
+{
+ if (m_tutorialPlayer) {
+ m_tutorialPlayer->stop();
+ }
+}
+
+void WirelessPhotoImportWidget::setupUI()
+{
+ QHBoxLayout *mainLayout = new QHBoxLayout(this);
+ mainLayout->setContentsMargins(10, 10, 10, 10);
+ mainLayout->setSpacing(10);
+
+ // Left panel - file selection
+ m_leftPanel = new QWidget();
+ QVBoxLayout *leftLayout = new QVBoxLayout(m_leftPanel);
+ leftLayout->setContentsMargins(0, 0, 0, 0);
+ leftLayout->setSpacing(10);
+
+ // Browse button
+ m_browseButton = new QPushButton("Select Files");
+ connect(m_browseButton, &QPushButton::clicked, this,
+ &WirelessPhotoImportWidget::onBrowseFiles);
+ leftLayout->addWidget(m_browseButton);
+
+ // Status label
+ m_statusLabel = new QLabel("No files selected");
+ m_statusLabel->setWordWrap(true);
+ leftLayout->addWidget(m_statusLabel);
+
+ // Scroll area for file list
+ m_scrollArea = new QScrollArea();
+ m_scrollArea->setWidgetResizable(true);
+ m_scrollArea->setMinimumWidth(300);
+
+ m_scrollContent = new QWidget();
+ m_fileListLayout = new QVBoxLayout(m_scrollContent);
+ m_fileListLayout->setContentsMargins(5, 5, 5, 5);
+ m_fileListLayout->setSpacing(5);
+ m_fileListLayout->addStretch();
+
+ m_scrollArea->setWidget(m_scrollContent);
+ leftLayout->addWidget(m_scrollArea, 1);
+
+ // Import button
+ m_importButton = new QPushButton("Import Photos to iOS");
+ m_importButton->setEnabled(false);
+ connect(m_importButton, &QPushButton::clicked, this,
+ &WirelessPhotoImportWidget::onImportPhotos);
+ leftLayout->addWidget(m_importButton);
+
+ mainLayout->addWidget(m_leftPanel, 1);
+
+ // Right panel - tutorial video
+ m_rightPanel = new QWidget();
+ m_tutorialLayout = new QVBoxLayout(m_rightPanel);
+ m_tutorialLayout->setContentsMargins(0, 0, 0, 0);
+ m_tutorialLayout->setSpacing(10);
+
+ // Loading indicator
+ m_loadingIndicator = new QProcessIndicator();
+ m_loadingIndicator->setType(QProcessIndicator::line_rotate);
+ m_loadingIndicator->setFixedSize(64, 32);
+ m_loadingIndicator->start();
+
+ QHBoxLayout *loadingLayout = new QHBoxLayout();
+ m_loadingLabel = new QLabel("Loading tutorial...");
+ m_loadingLabel->setAlignment(Qt::AlignCenter);
+
+ loadingLayout->addWidget(m_loadingLabel);
+ loadingLayout->addWidget(m_loadingIndicator);
+ loadingLayout->setAlignment(Qt::AlignCenter);
+
+ m_tutorialLayout->addStretch();
+ m_tutorialLayout->addLayout(loadingLayout);
+ m_tutorialLayout->addStretch();
+
+ mainLayout->addWidget(m_rightPanel, 1);
+}
+
+void WirelessPhotoImportWidget::setupTutorialVideo()
+{
+ m_tutorialPlayer = new QMediaPlayer(this);
+ m_tutorialVideoWidget = new QVideoWidget();
+ m_tutorialVideoWidget->setSizePolicy(QSizePolicy::Expanding,
+ QSizePolicy::Expanding);
+
+ m_tutorialPlayer->setVideoOutput(m_tutorialVideoWidget);
+ m_tutorialPlayer->setSource(QUrl("qrc:/resources/airplayer-tutorial.mp4"));
+ m_tutorialVideoWidget->setAspectRatioMode(
+ Qt::AspectRatioMode::KeepAspectRatioByExpanding);
+
+ // Loop the tutorial video
+ connect(m_tutorialPlayer, &QMediaPlayer::mediaStatusChanged, this,
+ [this](QMediaPlayer::MediaStatus status) {
+ if (status == QMediaPlayer::EndOfMedia) {
+ m_tutorialPlayer->setPosition(0);
+ m_tutorialPlayer->play();
+ }
+ });
+
+ // Auto-play when ready and hide loading indicator
+ connect(m_tutorialPlayer, &QMediaPlayer::mediaStatusChanged, this,
+ [this](QMediaPlayer::MediaStatus status) {
+ if (status == QMediaPlayer::LoadedMedia) {
+ m_loadingIndicator->stop();
+ m_loadingIndicator->setVisible(false);
+ m_loadingLabel->setVisible(false);
+ m_tutorialPlayer->play();
+ }
+ });
+
+ // Clear the loading layout and add video widget
+ QLayoutItem *child;
+ while ((child = m_tutorialLayout->takeAt(0)) != nullptr) {
+ if (child->widget()) {
+ child->widget()->setParent(nullptr);
+ }
+ delete child;
+ }
+
+ m_tutorialLayout->addWidget(m_tutorialVideoWidget);
+}
+
+void WirelessPhotoImportWidget::onBrowseFiles()
+{
+ QStringList files = QFileDialog::getOpenFileNames(
+ this, "Select Photos/Videos to Import",
+ QStandardPaths::writableLocation(QStandardPaths::PicturesLocation),
+ "Media Files (*.jpg *.jpeg *.png *.gif *.bmp *.tiff *.tif *.webp "
+ "*.heic *.heif *.mp4 *.mov *.avi *.mkv *.m4v *.3gp *.webm);;All Files "
+ "(*)");
+
+ if (files.isEmpty()) {
+ return;
+ }
+
+ // Filter out non-compatible files
+ QStringList compatibleFiles;
+ for (const QString &file : files) {
+ if (isGalleryCompatible(file)) {
+ compatibleFiles.append(file);
+ }
+ }
+
+ m_selectedFiles = compatibleFiles;
+ updateFileList();
+ updateStatusLabel();
+}
+
+void WirelessPhotoImportWidget::updateFileList()
+{
+ // Clear existing file list
+ QLayoutItem *child;
+ while ((child = m_fileListLayout->takeAt(0)) != nullptr) {
+ if (child->widget()) {
+ child->widget()->deleteLater();
+ }
+ delete child;
+ }
+
+ // Add files to the list
+ for (int i = 0; i < m_selectedFiles.size(); ++i) {
+ QFileInfo fileInfo(m_selectedFiles[i]);
+
+ QWidget *fileItem = new QWidget();
+ QHBoxLayout *fileLayout = new QHBoxLayout(fileItem);
+ fileLayout->setContentsMargins(5, 5, 5, 5);
+ fileLayout->setSpacing(0);
+
+ QLabel *fileLabel = new QLabel(fileInfo.fileName());
+ fileLabel->setWordWrap(true);
+
+ QPushButton *removeButton = new QPushButton("Remove");
+ removeButton->setMaximumWidth(80);
+
+ int index = i;
+ connect(removeButton, &QPushButton::clicked, this,
+ [this, index]() { onRemoveFile(index); });
+
+ fileLayout->addWidget(fileLabel, 1);
+ fileLayout->addWidget(removeButton);
+
+ m_fileListLayout->insertWidget(m_fileListLayout->count() - 1, fileItem);
+ }
+
+ m_importButton->setEnabled(!m_selectedFiles.isEmpty());
+}
+
+void WirelessPhotoImportWidget::updateStatusLabel()
+{
+ if (m_selectedFiles.isEmpty()) {
+ m_statusLabel->setText("No files selected");
+ } else {
+ m_statusLabel->setText(
+ QString("Selected %1 file(s)").arg(m_selectedFiles.size()));
+ }
+}
+
+void WirelessPhotoImportWidget::onRemoveFile(int index)
+{
+ if (index >= 0 && index < m_selectedFiles.size()) {
+ m_selectedFiles.removeAt(index);
+ updateFileList();
+ updateStatusLabel();
+ }
+}
+
+QStringList WirelessPhotoImportWidget::getGalleryCompatibleExtensions() const
+{
+ return {"jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp", "heic",
+ "heif", "mp4", "mov", "avi", "mkv", "m4v", "3gp", "webm"};
+}
+
+bool WirelessPhotoImportWidget::isGalleryCompatible(
+ const QString &filePath) const
+{
+ QFileInfo info(filePath);
+ QString ext = info.suffix().toLower();
+ return getGalleryCompatibleExtensions().contains(ext);
+}
+
+void WirelessPhotoImportWidget::onImportPhotos()
+{
+ if (m_selectedFiles.isEmpty()) {
+ QMessageBox::warning(this, "No Files",
+ "No gallery-compatible files selected.");
+ return;
+ }
+
+ PhotoImportDialog dialog(m_selectedFiles, false, this);
+ dialog.exec();
+}
+
+QStringList WirelessPhotoImportWidget::getSelectedFiles() const
+{
+ return m_selectedFiles;
+}
diff --git a/src/wirelessphotoimportwidget.h b/src/wirelessphotoimportwidget.h
new file mode 100644
index 0000000..459b114
--- /dev/null
+++ b/src/wirelessphotoimportwidget.h
@@ -0,0 +1,59 @@
+#ifndef WIRELESSPHOTOIMPORTWIDGET_H
+#define WIRELESSPHOTOIMPORTWIDGET_H
+
+#include "qprocessindicator.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class WirelessPhotoImportWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit WirelessPhotoImportWidget(QWidget *parent = nullptr);
+ ~WirelessPhotoImportWidget();
+
+ QStringList getSelectedFiles() const;
+
+private slots:
+ void onBrowseFiles();
+ void onImportPhotos();
+ void onRemoveFile(int index);
+ void setupTutorialVideo();
+
+private:
+ // Left panel - file selection
+ QWidget *m_leftPanel;
+ QScrollArea *m_scrollArea;
+ QWidget *m_scrollContent;
+ QVBoxLayout *m_fileListLayout;
+ QPushButton *m_browseButton;
+ QPushButton *m_importButton;
+ QLabel *m_statusLabel;
+
+ // Right panel - tutorial video
+ QWidget *m_rightPanel;
+ QMediaPlayer *m_tutorialPlayer;
+ QVideoWidget *m_tutorialVideoWidget;
+ QProcessIndicator *m_loadingIndicator;
+ QLabel *m_loadingLabel;
+ QVBoxLayout *m_tutorialLayout;
+
+ QStringList m_selectedFiles;
+
+ void setupUI();
+ void updateFileList();
+ void updateStatusLabel();
+ bool isGalleryCompatible(const QString &filePath) const;
+ QStringList getGalleryCompatibleExtensions() const;
+};
+
+#endif // WIRELESSPHOTOIMPORTWIDGET_H