implement WelcomeWidget , fix bugs , add tutorial videos

- Introduced WelcomeWidget to display a welcome message when no devices are connected, replacing the previous "No devices detected" page in MainWindow.
- Replaced ClickableLabel with ZLabel in ifusewidget.h for improved UI consistency.
- Removed PCFileExplorerWidget and its associated header file, streamlining the codebase.
- Updated PhotoImportDialog to improve server start process and UI elements, including renaming buttons and adjusting labels.
- Modified RealtimeScreenWidget to increase delay before initializing screenshot service for better reliability.
- Enhanced SimpleHttpServer to include a method for retrieving the JSON file name.
- Updated ToolboxWidget to integrate WirelessPhotoImportWidget, allowing for wireless photo imports.
- Added WirelessPhotoImportWidget to facilitate the selection and import of photos, including a tutorial video feature.
- Created a new WelcomeWidget to guide users on connecting their iOS devices.
This commit is contained in:
uncor3
2025-10-18 22:16:15 +00:00
parent 8d7b027992
commit c783123b8d
39 changed files with 1183 additions and 570 deletions
+3
View File
@@ -45,5 +45,8 @@
<file>resources/iphone-mockups/iphone-x.png</file>
<file>resources/iphone-mockups/iphone-15.png</file>
<file>resources/iphone-mockups/iphone-16.png</file>
<file>resources/connect.png</file>
<file>resources/airplayer-tutorial.mp4</file>
<file>resources/ipad-mockups/ipad.png</file>
</qresource>
</RCC>
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+314 -131
View File
@@ -1,13 +1,20 @@
#include "airplaywindow.h"
#include <QApplication>
#include <QCheckBox>
#include <QCloseEvent>
#include <QDebug>
#include <QFileInfo>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QMediaPlayer>
#include <QMessageBox>
#include <QPalette>
#include <QPixmap>
#include <QPushButton>
#include <QProcess>
#include <QStackedWidget>
#include <QVBoxLayout>
#include <QVideoWidget>
#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<void(uint8_t *, int, int)> 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<QLabel *>();
if (instructionLabel && !instructionLabel->text().contains("Follow")) {
// Find the instruction label (not title or loading label)
QList<QLabel *> labels = m_tutorialWidget->findChildren<QLabel *>();
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
+59 -32
View File
@@ -1,14 +1,43 @@
#ifndef AIRPLAYWINDOW_H
#define AIRPLAYWINDOW_H
#include "qprocessindicator.h"
#include <QCheckBox>
#include <QCloseEvent>
#include <QLabel>
#include <QMainWindow>
#include <QMediaPlayer>
#include <QMutex>
#include <QStackedWidget>
#include <QThread>
#include <QTimer>
#include <cstdint>
#include <functional>
#include <QVBoxLayout>
#include <QVideoWidget>
#include <QWaitCondition>
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<void(uint8_t *, int, int)> qt_video_callback;
#endif // AIRPLAYWINDOW_H
+16
View File
@@ -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;
}
+6
View File
@@ -1,6 +1,7 @@
#ifndef APPCONTEXT_H
#define APPCONTEXT_H
#include "devicesidebarwidget.h"
#include "iDescriptor.h"
#include <QDBusConnection>
#include <QDBusMessage>
@@ -21,10 +22,14 @@ public:
~AppContext();
int getConnectedDeviceCount() const;
void setCurrentDeviceSelection(const DeviceSelection &selection);
const DeviceSelection &getCurrentDeviceSelection() const;
private:
QMap<std::string, iDescriptorDevice *> m_devices;
QMap<uint64_t, iDescriptorRecoveryDevice *> 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,
+3 -3
View File
@@ -1,5 +1,5 @@
#include "appdownloaddialog.h"
#include "clickablelabel.h"
#include "iDescriptor-ui.h"
#include "libipatool-go.h"
#include <QDesktopServices>
#include <QFileDialog>
@@ -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);
+2 -2
View File
@@ -2,7 +2,7 @@
#define APPDOWNLOADDIALOG_H
#include "appdownloadbasedialog.h"
#include "clickablelabel.h"
#include "iDescriptor-ui.h"
#include <QDialog>
#include <QLabel>
#include <QPushButton>
@@ -21,7 +21,7 @@ private slots:
private:
QString m_outputDir;
QPushButton *m_dirButton;
ClickableLabel *m_dirLabel;
ZLabel *m_dirLabel;
QString m_bundleId;
};
+7 -4
View File
@@ -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);
-10
View File
@@ -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(); }
-24
View File
@@ -1,24 +0,0 @@
#ifndef CLICKABLELABEL_H
#define CLICKABLELABEL_H
#include <QLabel>
#include <QWidget>
#include <Qt>
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
+5 -1
View File
@@ -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);
+113
View File
@@ -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 + ")";
}
+1
View File
@@ -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[];
+4 -5
View File
@@ -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))});
+7 -5
View File
@@ -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
-1
View File
@@ -22,7 +22,6 @@ public:
std::string getCurrentDevice() const;
signals:
void deviceChanged(std::string deviceUuid);
void updateNoDevicesConnected();
private slots:
+7 -2
View File
@@ -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()
+22 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include <QApplication>
#include <QGraphicsView>
#include <QLabel>
#include <QMainWindow>
#include <QMouseEvent>
#include <QPainter>
@@ -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);
}
};
+2
View File
@@ -136,6 +136,8 @@ struct DeviceInfo {
bool is_iPhone;
bool oldDevice;
std::string marketingName;
std::string regionRaw;
std::string region;
};
struct iDescriptorDevice {
+4 -18
View File
@@ -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);
+2 -2
View File
@@ -2,7 +2,7 @@
#define IFUSEWIDGET_H
#include "appcontext.h"
#include "clickablelabel.h"
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include <QCheckBox>
#include <QComboBox>
@@ -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;
+5 -15
View File
@@ -13,6 +13,7 @@
#include "jailbrokenwidget.h"
#include "libirecovery.h"
#include "toolboxwidget.h"
#include "welcomewidget.h"
#include <QHBoxLayout>
#include <QStack>
#include <QStackedWidget>
@@ -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(
-174
View File
@@ -1,174 +0,0 @@
#include "pcfileexplorerwidget.h"
#include "photoimportdialog.h"
#include <QDir>
#include <QFileInfo>
#include <QHeaderView>
#include <QMessageBox>
#include <QMimeDatabase>
#include <QStandardPaths>
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;
}
-48
View File
@@ -1,48 +0,0 @@
#ifndef PCFILEEXPLORERWIDGET_H
#define PCFILEEXPLORERWIDGET_H
#include <QFileSystemModel>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QProgressBar>
#include <QPushButton>
#include <QSplitter>
#include <QStringList>
#include <QTreeView>
#include <QVBoxLayout>
#include <QWidget>
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
+32 -52
View File
@@ -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)
+3 -4
View File
@@ -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);
+1 -2
View File
@@ -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");
+1 -1
View File
@@ -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);
+66 -20
View File
@@ -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 <QApplication>
#include <QDebug>
#include <QMessageBox>
@@ -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<iDescriptorDevice *> 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);
+6
View File
@@ -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 <QComboBox>
#include <QGridLayout>
#include <QHBoxLayout>
@@ -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:
};
+12 -12
View File
@@ -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)
+124
View File
@@ -0,0 +1,124 @@
#include "welcomewidget.h"
#include "responsiveqlabel.h"
#include <QApplication>
#include <QDesktopServices>
#include <QEvent>
#include <QFont>
#include <QMouseEvent>
#include <QPalette>
#include <QUrl>
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<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::LeftButton) {
QDesktopServices::openUrl(
QUrl("https://github.com/uncor3/iDescriptor"));
return true;
}
}
return QWidget::eventFilter(watched, event);
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef WELCOMEWIDGET_H
#define WELCOMEWIDGET_H
#include "responsiveqlabel.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QVBoxLayout>
#include <QWidget>
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
+262
View File
@@ -0,0 +1,262 @@
#include "wirelessphotoimportwidget.h"
#include "photoimportdialog.h"
#include <QFileDialog>
#include <QFileInfo>
#include <QHeaderView>
#include <QMessageBox>
#include <QMimeDatabase>
#include <QPushButton>
#include <QStandardPaths>
#include <QTimer>
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;
}
+59
View File
@@ -0,0 +1,59 @@
#ifndef WIRELESSPHOTOIMPORTWIDGET_H
#define WIRELESSPHOTOIMPORTWIDGET_H
#include "qprocessindicator.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QMediaPlayer>
#include <QPushButton>
#include <QScrollArea>
#include <QStringList>
#include <QVBoxLayout>
#include <QVideoWidget>
#include <QWidget>
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