mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-22 03:45:51 +08:00
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:
+314
-131
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
@@ -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);
|
||||
|
||||
@@ -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(); }
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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 + ")";
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
@@ -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))});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,6 @@ public:
|
||||
std::string getCurrentDevice() const;
|
||||
|
||||
signals:
|
||||
void deviceChanged(std::string deviceUuid);
|
||||
void updateNoDevicesConnected();
|
||||
|
||||
private slots:
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -136,6 +136,8 @@ struct DeviceInfo {
|
||||
bool is_iPhone;
|
||||
bool oldDevice;
|
||||
std::string marketingName;
|
||||
std::string regionRaw;
|
||||
std::string region;
|
||||
};
|
||||
|
||||
struct iDescriptorDevice {
|
||||
|
||||
+4
-18
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user