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