diff --git a/CMakeLists.txt b/CMakeLists.txt index 7dbbe77..012d836 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,17 +137,23 @@ pkg_check_modules(PLIST REQUIRED IMPORTED_TARGET libplist-2.0) # ) file(GLOB PROJECT_SOURCES - src/*.cpp - src/core/helpers/*.cpp - src/core/services/*.cpp - src/platform/*.cpp - src/platform/*.mm - src/platform/*.m - src/*.h - src/*.ui - resources.qrc +src/*.cpp +src/core/helpers/*.cpp +src/core/services/*.cpp +src/*.h +src/*.ui +resources.qrc ) +if(MACOS) + list(APPEND PROJECT_SOURCES + src/platform/macos/*.mm + src/platform/macos/*.h + ) +endif() + + + add_subdirectory(lib/airplay) add_subdirectory(lib/ipatool-go) diff --git a/icons/ClarityHardDiskSolidAlerted.png b/icons/ClarityHardDiskSolidAlerted.png new file mode 100644 index 0000000..f6af879 Binary files /dev/null and b/icons/ClarityHardDiskSolidAlerted.png differ diff --git a/resources.qrc b/resources.qrc index cc8c805..dbfad0c 100644 --- a/resources.qrc +++ b/resources.qrc @@ -4,8 +4,9 @@ icons/video-x-generic.png icons/MdiLightningBolt.png icons/MingcuteSettings7Line.png + icons/ClarityHardDiskSolidAlerted.png qml/MapView.qml resources/dump.js resources/iphone.png - + \ No newline at end of file diff --git a/src/airplaywindow.cpp b/src/airplaywindow.cpp index 958064e..abbfcd4 100644 --- a/src/airplaywindow.cpp +++ b/src/airplaywindow.cpp @@ -9,6 +9,7 @@ #include #include +#ifdef Q_OS_LINUX // V4L2 includes #include #include @@ -16,6 +17,7 @@ #include #include #include +#endif // Include the rpiplay server functions extern "C" { @@ -28,17 +30,22 @@ std::function qt_video_callback; AirPlayWindow::AirPlayWindow(QWidget *parent) : QMainWindow(parent), m_videoLabel(nullptr), m_statusLabel(nullptr), - m_serverThread(nullptr), m_serverRunning(false), m_v4l2_fd(-1), - m_v4l2_width(0), m_v4l2_height(0), m_v4l2_enabled(false) + m_serverThread(nullptr), m_serverRunning(false) +#ifdef Q_OS_LINUX + , + m_v4l2_fd(-1), m_v4l2_width(0), m_v4l2_height(0), m_v4l2_enabled(false) +#endif { 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", @@ -51,7 +58,9 @@ AirPlayWindow::AirPlayWindow(QWidget *parent) AirPlayWindow::~AirPlayWindow() { stopAirPlayServer(); +#ifdef Q_OS_LINUX closeV4L2(); +#endif qt_video_callback = nullptr; } @@ -82,6 +91,7 @@ void AirPlayWindow::setupUI() statusLayout->addWidget(m_statusLabel); statusLayout->addStretch(); +#ifdef Q_OS_LINUX // V4L2 controls QCheckBox *v4l2CheckBox = new QCheckBox("Enable V4L2 Output"); connect(v4l2CheckBox, &QCheckBox::toggled, this, [this](bool enabled) { @@ -99,6 +109,7 @@ void AirPlayWindow::setupUI() statusLayout->addWidget(v4l2CheckBox); statusLayout->addWidget(testV4L2Btn); +#endif statusLayout->addWidget(startBtn); statusLayout->addWidget(stopBtn); @@ -190,6 +201,7 @@ void AirPlayServerThread::run() emit statusChanged(false); } +#ifdef Q_OS_LINUX // V4L2 Implementation void AirPlayWindow::initV4L2(int width, int height, const char *device) { @@ -308,3 +320,4 @@ void AirPlayWindow::testV4L2Device() "• Record with: ffmpeg -f v4l2 -i %1 output.mp4") .arg(device)); } +#endif diff --git a/src/airplaywindow.h b/src/airplaywindow.h index 301673e..b88c68d 100644 --- a/src/airplaywindow.h +++ b/src/airplaywindow.h @@ -34,6 +34,7 @@ private: AirPlayServerThread *m_serverThread; bool m_serverRunning; +#ifdef Q_OS_LINUX // V4L2 members int m_v4l2_fd; int m_v4l2_width; @@ -45,6 +46,7 @@ private: void closeV4L2(); void writeFrameToV4L2(uint8_t *data, int width, int height); void testV4L2Device(); +#endif }; class AirPlayServerThread : public QThread diff --git a/src/customtabwidget.cpp b/src/customtabwidget.cpp index 6250e93..5b56c13 100644 --- a/src/customtabwidget.cpp +++ b/src/customtabwidget.cpp @@ -271,6 +271,7 @@ void CustomTabWidget::updateTabStyles() " font-weight: 500;" " font-size: 20px;" " border: none;" + " outline: none;" " border-radius: 27px;" " background-color: transparent;" "}" @@ -283,6 +284,7 @@ void CustomTabWidget::updateTabStyles() " font-weight: 500;" " font-size: 20px;" " border: none;" + " outline: none;" " border-radius: 27px;" " background-color: transparent;" "}" diff --git a/src/deviceinfowidget.cpp b/src/deviceinfowidget.cpp index 3a3df60..a04ba6e 100644 --- a/src/deviceinfowidget.cpp +++ b/src/deviceinfowidget.cpp @@ -4,6 +4,7 @@ #include "fileexplorerwidget.h" #include "iDescriptor-ui.h" #include "iDescriptor.h" +#include #include #include #include @@ -53,7 +54,6 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) infoContainer->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); QVBoxLayout *infoLayout = new QVBoxLayout(infoContainer); - // infoLayout->setContentsMargins(15, 15, 15, 15); // infoLayout->setSpacing(10); // Header @@ -77,17 +77,20 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) (1000 * 1000 * 1000)) + " GB"); + diskCapacityLabel->setSizePolicy(QSizePolicy::Maximum, + QSizePolicy::Preferred); diskCapacityLabel->setAttribute(Qt::WA_StyledBackground, true); - diskCapacityLabel->setStyleSheet("background-color: rgba(0, 255, 30, 0.12);" + diskCapacityLabel->setStyleSheet("background-color: rgba(0, 255, 30, 0.5);" "padding: 4px;" - "border-radius: 4px;"); + "border-radius: 13px;"); m_chargingStatusLabel = new QLabel(device->deviceInfo.batteryInfo.isCharging ? "Charging" : "Not Charging"); m_chargingStatusLabel->setStyleSheet( - device->deviceInfo.batteryInfo.isCharging ? "color: green;" - : "color: white;"); + device->deviceInfo.batteryInfo.isCharging + ? QString("color: %1;").arg(COLOR_GREEN.name()) + : "color: white;"); // Create the layout without a parent widget QHBoxLayout *chargingLayout = new QHBoxLayout(); @@ -97,7 +100,7 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) // Create icon label m_lightningIconLabel = new QLabel(); QPixmap lightningIcon(":/icons/MdiLightningBolt.png"); - QPixmap scaledIcon = lightningIcon.scaled(16, 16, Qt::KeepAspectRatio, + QPixmap scaledIcon = lightningIcon.scaled(26, 26, Qt::KeepAspectRatio, Qt::SmoothTransformation); m_lightningIconLabel->setPixmap(scaledIcon); m_batteryWidget = @@ -118,6 +121,7 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) headerLayout->addWidget(devProductType); headerLayout->addWidget(diskCapacityLabel); + headerLayout->addStretch(); // Push items to the left headerLayout->addLayout(chargingLayout); headerLayout->addWidget(m_chargingWattsWithCableTypeLabel); @@ -149,12 +153,16 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) // 3. Create the grid widget (the main content) QWidget *gridWidget = new QWidget(); gridWidget->setObjectName("infoGrid"); - // Set a background color that matches the main window, with rounded corners - gridWidget->setStyleSheet( - "QWidget#infoGrid {" - " background-color: #2e2e2e;" // Match your window background - " border-radius: 8px;" - "}"); + + QPalette palette = qApp->palette(); + QColor background = palette.color(QPalette::Window); + + gridWidget->setStyleSheet("QWidget#infoGrid {" + " background-color: " + + background.name() + + ";" + " border-radius: 8px;" + "}"); // 4. Create the light (top-left) shadow and apply to the grid widget QGraphicsDropShadowEffect *lightShadow = new QGraphicsDropShadowEffect(); @@ -195,17 +203,17 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) switch (device->deviceInfo.activationState) { case DeviceInfo::ActivationState::Activated: stateText = "Activated"; - color = QColor(0, 180, 0); // Green + color = COLOR_GREEN; tooltipText = "Device is activated and ready for use."; break; case DeviceInfo::ActivationState::FactoryActivated: stateText = "Factory Activated"; - color = QColor(255, 140, 0); // Orange + color = COLOR_ORANGE; tooltipText = "Activation is most likely bypassed."; break; default: stateText = "Unactivated"; - color = QColor(220, 0, 0); // Red + color = COLOR_RED; tooltipText = "Device is not activated and requires setup."; break; } @@ -380,12 +388,13 @@ void DeviceInfoWidget::updateChargingStatusIcon() { if (m_device->deviceInfo.batteryInfo.isCharging) { m_chargingStatusLabel->setText("Charging"); - m_chargingStatusLabel->setStyleSheet("color: green;"); + m_chargingStatusLabel->setStyleSheet( + QString("color: %1;").arg(COLOR_GREEN.name())); m_lightningIconLabel->show(); } else { m_chargingStatusLabel->setText("Not Charging"); - m_chargingStatusLabel->setStyleSheet("color: white;"); + m_chargingStatusLabel->setStyleSheet(""); m_lightningIconLabel->hide(); } } \ No newline at end of file diff --git a/src/devicemanagerwidget.cpp b/src/devicemanagerwidget.cpp index a3ebd89..6e32da3 100644 --- a/src/devicemanagerwidget.cpp +++ b/src/devicemanagerwidget.cpp @@ -85,6 +85,8 @@ void DeviceManagerWidget::addDevice(iDescriptorDevice *device) << QString::fromStdString(device->udid); DeviceMenuWidget *deviceWidget = new DeviceMenuWidget(device, this); + deviceWidget->setContentsMargins(35, 15, 35, 15); + QString tabTitle = QString::fromStdString(device->deviceInfo.productType); m_stackedWidget->addWidget(deviceWidget); diff --git a/src/diskusagewidget.cpp b/src/diskusagewidget.cpp index ec502e1..08bce26 100644 --- a/src/diskusagewidget.cpp +++ b/src/diskusagewidget.cpp @@ -1,5 +1,6 @@ #include "diskusagewidget.h" #include "iDescriptor.h" +#include #include #include #include @@ -26,24 +27,25 @@ void DiskUsageWidget::paintEvent(QPaintEvent *event) Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); + QColor textColor = qApp->palette().text().color(); if (m_state == Loading) { - painter.setPen(Qt::black); + painter.setPen(textColor); painter.drawText(rect(), Qt::AlignCenter, "Loading disk usage..."); return; } if (m_state == Error) { - painter.setPen(Qt::black); + painter.setPen(textColor); painter.drawText(rect(), Qt::AlignCenter, "Error: " + m_errorMessage); return; } // Title - painter.setPen(Qt::black); QFont titleFont = font(); titleFont.setBold(true); painter.setFont(titleFont); + painter.setPen(textColor); QRectF titleRect(0, 5, width(), 20); painter.drawText(titleRect, Qt::AlignHCenter | Qt::AlignTop, "Disk Usage"); painter.setFont(font()); // Reset font @@ -91,7 +93,7 @@ void DiskUsageWidget::paintEvent(QPaintEvent *event) drawSegment(m_freeSpace, freeColor); // Legend - painter.setPen(Qt::black); + painter.setPen(textColor); qreal legendY = barRect.bottom() + 15; const int legendBoxSize = 10; const int legendSpacing = 5; @@ -102,6 +104,7 @@ void DiskUsageWidget::paintEvent(QPaintEvent *event) QRectF(currentLegendX, legendY, legendBoxSize, legendBoxSize), color); currentLegendX += legendBoxSize + legendSpacing; + painter.setPen(textColor); QFontMetrics fm(font()); QRect textRect = fm.boundingRect(text); diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h index 2eda016..f10fbbf 100644 --- a/src/iDescriptor-ui.h +++ b/src/iDescriptor-ui.h @@ -2,6 +2,10 @@ #include #include +#define COLOR_GREEN QColor(0, 180, 0) // Green +#define COLOR_ORANGE QColor(255, 140, 0) // Orange +#define COLOR_RED QColor(255, 0, 0) // Red + // A custom QGraphicsView that keeps the content fitted with aspect ratio on // resize class ResponsiveGraphicsView : public QGraphicsView diff --git a/src/ifusediskunmountbutton.cpp b/src/ifusediskunmountbutton.cpp new file mode 100644 index 0000000..ec771b2 --- /dev/null +++ b/src/ifusediskunmountbutton.cpp @@ -0,0 +1,14 @@ +#include "ifusediskunmountbutton.h" +#include +#include + +iFuseDiskUnmountButton::iFuseDiskUnmountButton(const QString &path, + QWidget *parent) + : QPushButton{parent} +{ + setIcon(QIcon(":/icons/ClarityHardDiskSolidAlerted.png")); + setToolTip("Unmount iFuse at " + path); + setFlat(true); + setCursor(Qt::PointingHandCursor); + setFixedSize(24, 24); +} diff --git a/src/ifusediskunmountbutton.h b/src/ifusediskunmountbutton.h new file mode 100644 index 0000000..1e7573b --- /dev/null +++ b/src/ifusediskunmountbutton.h @@ -0,0 +1,16 @@ +#ifndef IFUSEDISKUNMOUNTBUTTON_H +#define IFUSEDISKUNMOUNTBUTTON_H + +#include + +class iFuseDiskUnmountButton : public QPushButton +{ + Q_OBJECT +public: + explicit iFuseDiskUnmountButton(const QString &path, + QWidget *parent = nullptr); + +signals: +}; + +#endif // IFUSEDISKUNMOUNTBUTTON_H diff --git a/src/ifusemanager.cpp b/src/ifusemanager.cpp new file mode 100644 index 0000000..aacfcf5 --- /dev/null +++ b/src/ifusemanager.cpp @@ -0,0 +1,53 @@ +#include "ifusemanager.h" +#include +#include + +QStringList iFuseManager::getMountArg(std::string &udid, QString &path) +{ + return QStringList() << "-u" << QString::fromStdString(udid) << path; +} + +#ifdef Q_OS_LINUX +QList iFuseManager::getMountPoints() +{ + QProcess mountProcess; + mountProcess.start("mount", QStringList() << "-t" + << "fuse.ifuse"); + mountProcess.waitForFinished(); + + QString output = mountProcess.readAllStandardOutput(); + + if (output.trimmed().isEmpty()) { + qDebug() << "[iFuseWidget] No existing ifuse mounts found."; + return {}; + } + + QStringList mountPoints; + QStringList lines = output.split('\n', Qt::SkipEmptyParts); + for (const QString &line : lines) { + // A typical line is: "ifuse on /path/to/mount type fuse.ifuse (...)" + QString mountPath = line.section(" on ", 1).section(" type ", 0, 0); + if (!mountPath.isEmpty()) { + qDebug() << "[iFuseWidget] - Mount point:" << mountPath; + mountPoints.append(mountPath); + } + } + return mountPoints; +} +#endif + +bool iFuseManager::linuxUnmount(const QString &path) +{ + QProcess umountProcess; + umountProcess.start("fusermount", QStringList() << "-u" << path); + umountProcess.waitForFinished(); + + if (umountProcess.exitCode() != 0) { + qWarning() << "[iFuseWidget] Failed to unmount" << path << ":" + << umountProcess.readAllStandardError().trimmed(); + return false; + } + + qDebug() << "[iFuseWidget] Successfully unmounted" << path; + return true; +} \ No newline at end of file diff --git a/src/ifusemanager.h b/src/ifusemanager.h new file mode 100644 index 0000000..5a59ed3 --- /dev/null +++ b/src/ifusemanager.h @@ -0,0 +1,20 @@ +#ifndef IFUSEMANAGER_H +#define IFUSEMANAGER_H + +#include + +class iFuseManager : public QObject +{ + Q_OBJECT +public: + // explicit iFuseManager(QObject *parent = nullptr); + static QList getMountPoints(); +#ifdef Q_OS_LINUX + static QStringList getMountArg(std::string &udid, QString &path); +#endif + // TODO: need to implement a cross-platform mount and unmount function + static bool linuxUnmount(const QString &path); +signals: +}; + +#endif // IFUSEMANAGER_H diff --git a/src/ifusewidget.cpp b/src/ifusewidget.cpp new file mode 100644 index 0000000..705402d --- /dev/null +++ b/src/ifusewidget.cpp @@ -0,0 +1,395 @@ +#include "ifusewidget.h" +#include "clickablelabel.h" +#include "iDescriptor.h" +#include "ifusediskunmountbutton.h" +#include "ifusemanager.h" +#include "mainwindow.h" +#include +#include +#include +#include + +iFuseWidget::iFuseWidget(iDescriptorDevice *device, QWidget *parent) + : QWidget(parent), m_mainLayout(nullptr), m_ifuseProcess(nullptr), + m_device(device) +{ + setupUI(); + updateDeviceComboBox(); + + // Connect to AppContext signals for device changes + connect(AppContext::sharedInstance(), &AppContext::deviceAdded, this, + &iFuseWidget::refreshDevices); + connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, + &iFuseWidget::refreshDevices); +} + +iFuseWidget::~iFuseWidget() +{ + if (m_ifuseProcess && m_ifuseProcess->state() == QProcess::Running) { + m_ifuseProcess->kill(); + m_ifuseProcess->waitForFinished(3000); + } +} + +void iFuseWidget::setupUI() +{ + m_mainLayout = new QVBoxLayout(this); + m_mainLayout->setSpacing(15); + m_mainLayout->setContentsMargins(20, 20, 20, 20); + + // Description label + m_descriptionLabel = new QLabel("This tool allows you to mount your " + "iPhone's disk as a drive on your PC"); + m_descriptionLabel->setWordWrap(true); + m_descriptionLabel->setStyleSheet( + "font-size: 14px; color: #666; margin-bottom: 10px;"); + m_mainLayout->addWidget(m_descriptionLabel); + + // Status label + m_statusLabel = new QLabel(); + m_statusLabel->setWordWrap(true); + m_statusLabel->hide(); + m_statusLabel->setStyleSheet( + "padding: 8px; border-radius: 4px; margin: 5px 0;"); + m_mainLayout->addWidget(m_statusLabel); + + // Device selection + QWidget *deviceWidget = new QWidget(); + QHBoxLayout *deviceLayout = new QHBoxLayout(deviceWidget); + deviceLayout->setContentsMargins(0, 0, 0, 0); + + QLabel *deviceLabel = new QLabel("Select Device:"); + deviceLabel->setMinimumWidth(100); + m_deviceComboBox = new QComboBox(); + m_deviceComboBox->setMinimumHeight(35); + + deviceLayout->addWidget(deviceLabel); + deviceLayout->addWidget(m_deviceComboBox, 1); + m_mainLayout->addWidget(deviceWidget); + + // Mount path selection + QWidget *pathWidget = new QWidget(); + QHBoxLayout *pathLayout = new QHBoxLayout(pathWidget); + pathLayout->setContentsMargins(0, 0, 0, 0); + + m_mountPathLabel = new ClickableLabel(); + m_mountPathLabel->setText("Mount directory will be shown here"); + m_mountPathLabel->setStyleSheet("QLabel { " + "border: 1px solid #ccc; " + "padding: 8px; " + "border-radius: 4px; " + "background-color: #f9f9f9; " + "}" + "QLabel:hover { " + "background-color: #f0f0f0; " + "cursor: pointer; " + "}"); + m_mountPathLabel->setMinimumHeight(35); + + m_folderPickerButton = new QPushButton("Browse..."); + m_folderPickerButton->setMinimumHeight(35); + + pathLayout->addWidget(m_mountPathLabel, 1); + 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_mainLayout->addWidget(m_mountButton); + + // Add stretch to push everything to the top + m_mainLayout->addStretch(); + + // Connect signals + connect(m_folderPickerButton, &QPushButton::clicked, this, + &iFuseWidget::onFolderPickerClicked); + connect(m_mountPathLabel, &ClickableLabel::clicked, this, + &iFuseWidget::onMountPathClicked); + connect(m_mountButton, &QPushButton::clicked, this, + &iFuseWidget::onMountClicked); + + connect(m_deviceComboBox, &QComboBox::currentTextChanged, this, + &iFuseWidget::onDeviceChanged); + + // Set default mount path based on device + if (m_device) { + QString homeDir = + QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + QString productType = + QString::fromStdString(m_device->deviceInfo.productType); + QString defaultMountPath = QDir(homeDir).absoluteFilePath(productType); + m_mountPathLabel->setText(defaultMountPath); + } else { + QString homeDir = + QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + QString defaultMountPath = QDir(homeDir).absoluteFilePath("iPhone"); + m_mountPathLabel->setText(defaultMountPath); + } +} + +void iFuseWidget::updateDeviceComboBox() +{ + m_deviceComboBox->clear(); + + QList devices = + AppContext::sharedInstance()->getAllDevices(); + + if (devices.isEmpty()) { + close(); + return; + } + + m_deviceComboBox->setEnabled(true); + m_mountButton->setEnabled(true); + + for (iDescriptorDevice *device : devices) { + QString displayText = + QString::fromStdString(device->deviceInfo.productType) + " / " + + QString::fromStdString(device->udid); + m_deviceComboBox->addItem(displayText, + QString::fromStdString(device->udid)); + } + + // Try to find and select the device passed to the widget + int deviceIndex = -1; + if (m_device) { + deviceIndex = + m_deviceComboBox->findData(QString::fromStdString(m_device->udid)); + } + + if (deviceIndex != -1) { + // Found the pre-selected device, so select it. + m_deviceComboBox->setCurrentIndex(deviceIndex); + } else if (!devices.isEmpty()) { + // Pre-selected device not found or not provided, so select the first + // one. + m_device = devices.first(); + m_deviceComboBox->setCurrentIndex(0); + } +} + +void iFuseWidget::onFolderPickerClicked() +{ + QString currentPath = m_mountPathLabel->text(); + QString dir = QFileDialog::getExistingDirectory( + this, "Select Mount Directory", currentPath); + if (!dir.isEmpty()) { + m_mountPathLabel->setText(dir); + } +} + +void iFuseWidget::onMountPathClicked() +{ + QString currentPath = m_mountPathLabel->text(); + if (!currentPath.isEmpty() && QDir(currentPath).exists()) { + QDesktopServices::openUrl(QUrl::fromLocalFile(currentPath)); + } +} + +void iFuseWidget::onMountClicked() +{ + if (!validateInputs()) { + return; + } + + // Check if ifuse binary exists + m_ifuseProcess = new QProcess(this); + connect(m_ifuseProcess, + QOverload::of(&QProcess::finished), this, + &iFuseWidget::onProcessFinished); + connect(m_ifuseProcess, &QProcess::errorOccurred, this, + &iFuseWidget::onProcessError); + + // First check if ifuse exists + QProcess checkProcess; + checkProcess.start("which", QStringList() << "ifuse"); + checkProcess.waitForFinished(3000); + + // todo: ship with ifuse binary + if (checkProcess.exitCode() != 0) { + setStatusMessage( + "Error: ifuse binary not found. Please install ifuse first.", true); + return; + } + + // Create the mount directory + QString homeDir = + QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + QString productType = + m_device ? QString::fromStdString(m_device->deviceInfo.productType) + : "iPhone"; + QString fullMountPath = QDir(homeDir).absoluteFilePath(productType); + + QDir dir; + if (!QDir(fullMountPath).exists()) { + if (!dir.mkpath(fullMountPath)) { + setStatusMessage("Error: Failed to create mount directory: " + + fullMountPath, + true); + return; + } + } + + m_currentMountPath = fullMountPath; + + // Get selected device UDID + QString deviceUdid = getSelectedDeviceUdid(); + + setStatusMessage("Mounting device...", false); + m_mountButton->setText("Mounting..."); + m_mountButton->setEnabled(false); + + // Run ifuse command + QStringList arguments; + arguments << "-u" << deviceUdid << fullMountPath; + + m_ifuseProcess->start("ifuse", arguments); +} + +void iFuseWidget::onProcessFinished(int exitCode, + QProcess::ExitStatus exitStatus) +{ + m_mountButton->setText("Mount Device"); + m_mountButton->setEnabled(true); + + if (exitStatus == QProcess::CrashExit) { + setStatusMessage("Error: ifuse process crashed", true); + return; + } + + if (exitCode == 0) { + setStatusMessage( + "Device mounted successfully at: " + m_currentMountPath, false); + + auto *b = new iFuseDiskUnmountButton(m_currentMountPath); + MainWindow::sharedInstance()->statusBar()->addPermanentWidget(b); + + connect(b, &iFuseDiskUnmountButton::clicked, this, [this, b]() { + qDebug() << "Unmounting" << m_currentMountPath; + bool ok = iFuseManager::linuxUnmount(m_currentMountPath); + if (!ok) { + QMessageBox::warning(nullptr, "Unmount Failed", + "Failed to unmount iFuse at " + + m_currentMountPath + + ". Please try again."); + return; + } + MainWindow::sharedInstance()->statusBar()->removeWidget(b); + b->deleteLater(); + }); + // Open the mounted directory + QDesktopServices::openUrl(QUrl::fromLocalFile(m_currentMountPath)); + } else { + QString errorOutput = m_ifuseProcess->readAllStandardError(); + setStatusMessage("Mount failed: " + errorOutput, true); + } + + m_ifuseProcess->deleteLater(); + m_ifuseProcess = nullptr; +} + +void iFuseWidget::onProcessError(QProcess::ProcessError error) +{ + m_mountButton->setText("Mount Device"); + m_mountButton->setEnabled(true); + + QString errorMessage; + switch (error) { + case QProcess::FailedToStart: + errorMessage = "Failed to start ifuse. Make sure it's installed."; + break; + case QProcess::Crashed: + errorMessage = "ifuse process crashed."; + break; + case QProcess::Timedout: + errorMessage = "ifuse process timed out."; + break; + default: + errorMessage = "Unknown error occurred."; + break; + } + + setStatusMessage("Error: " + errorMessage, true); + + if (m_ifuseProcess) { + m_ifuseProcess->deleteLater(); + m_ifuseProcess = nullptr; + } +} + +void iFuseWidget::refreshDevices() { updateDeviceComboBox(); } + +bool iFuseWidget::validateInputs() +{ + if (m_deviceComboBox->currentData().toString().isEmpty()) { + setStatusMessage("Error: No device selected", true); + return false; + } + + return true; +} + +QString iFuseWidget::getSelectedDeviceUdid() +{ + return m_deviceComboBox->currentData().toString(); +} + +void iFuseWidget::setStatusMessage(const QString &message, bool isError) +{ + m_statusLabel->setText(message); + m_statusLabel->show(); + + if (isError) { + m_statusLabel->setStyleSheet( + "background-color: #ffe6e6; color: #d00; border: 1px solid " + "#ffcccc; padding: 8px; border-radius: 4px; margin: 5px 0;"); + } else { + m_statusLabel->setStyleSheet( + "background-color: #e6ffe6; color: #060; border: 1px solid " + "#ccffcc; padding: 8px; border-radius: 4px; margin: 5px 0;"); + } + + // Auto-hide status after 5 seconds for non-error messages + if (!isError) { + QTimer::singleShot(5000, [this]() { m_statusLabel->hide(); }); + } +} + +void iFuseWidget::onDeviceChanged(const QString &text) +{ + QString selectedUdid = m_deviceComboBox->currentData().toString(); + QList devices = + AppContext::sharedInstance()->getAllDevices(); + + for (iDescriptorDevice *device : devices) { + if (QString::fromStdString(device->udid) == selectedUdid) { + m_device = device; + + // Update mount path to reflect new device + QString homeDir = + QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + QString productType = + QString::fromStdString(device->deviceInfo.productType); + QString newMountPath = QDir(homeDir).absoluteFilePath(productType); + m_mountPathLabel->setText(newMountPath); + + break; + } + } +} \ No newline at end of file diff --git a/src/ifusewidget.h b/src/ifusewidget.h new file mode 100644 index 0000000..c62256a --- /dev/null +++ b/src/ifusewidget.h @@ -0,0 +1,61 @@ +#ifndef IFUSEWIDGET_H +#define IFUSEWIDGET_H + +#include "appcontext.h" +#include "clickablelabel.h" +#include "iDescriptor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class iFuseWidget : public QWidget +{ + Q_OBJECT + +public: + explicit iFuseWidget(iDescriptorDevice *device, QWidget *parent = nullptr); + ~iFuseWidget(); + +private slots: + void onFolderPickerClicked(); + void onMountPathClicked(); + void onMountClicked(); + void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + void onProcessError(QProcess::ProcessError error); + void refreshDevices(); + +private: + void setupUI(); + void updateDeviceComboBox(); + bool validateInputs(); + QString getSelectedDeviceUdid(); + void setStatusMessage(const QString &message, bool isError = false); + void onDeviceChanged(const QString &deviceName); + // UI Components + QVBoxLayout *m_mainLayout; + QLabel *m_descriptionLabel; + QLabel *m_statusLabel; + QComboBox *m_deviceComboBox; + ClickableLabel *m_mountPathLabel; + QPushButton *m_folderPickerButton; + QLabel *m_folderNameLabel; + QPushButton *m_mountButton; + iDescriptorDevice *m_device; + + // Data + QString m_selectedPath; + QProcess *m_ifuseProcess; + QString m_currentMountPath; +}; + +#endif // IFUSEWIDGET_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 2b63627..b998ca3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,4 @@ #include "mainwindow.h" - #include int main(int argc, char *argv[]) @@ -10,7 +9,7 @@ int main(int argc, char *argv[]) // QCoreApplication::setOrganizationDomain("iDescriptor.com"); QCoreApplication::setApplicationName("iDescriptor"); - MainWindow w; - w.show(); + MainWindow *w = MainWindow::sharedInstance(); + w->show(); return a.exec(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5867462..fda02e5 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2,6 +2,8 @@ #include "./ui_mainwindow.h" #include "customtabwidget.h" #include "detailwindow.h" +#include "ifusediskunmountbutton.h" +#include "ifusemanager.h" #include "settingswidget.h" #include #include @@ -120,11 +122,16 @@ void handleCallbackRecovery(const irecv_device_event_t *event, void *userData) } irecv_device_event_context_t context; +MainWindow *MainWindow::sharedInstance() +{ + static MainWindow instance; + return &instance; +} + MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); - // Create custom tab widget m_customTabWidget = new CustomTabWidget(this); m_customTabWidget->setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, @@ -217,6 +224,27 @@ MainWindow::MainWindow(QWidget *parent) ui->statusbar->addPermanentWidget(settingsButton); +#ifdef Q_OS_LINUX + QList mounted_iFusePaths = iFuseManager::getMountPoints(); + + for (const QString &path : mounted_iFusePaths) { + auto *p = new iFuseDiskUnmountButton(path); + + ui->statusbar->addPermanentWidget(p); + connect(p, &iFuseDiskUnmountButton::clicked, this, [this, p, path]() { + bool ok = iFuseManager::linuxUnmount(path); + if (!ok) { + QMessageBox::warning(nullptr, "Unmount Failed", + "Failed to unmount iFuse at " + path + + ". Please try again."); + return; + } + ui->statusbar->removeWidget(p); + p->deleteLater(); + }); + } +#endif + irecv_error_t res_recovery = irecv_device_event_subscribe(&context, handleCallbackRecovery, nullptr); @@ -233,6 +261,7 @@ MainWindow::MainWindow(QWidget *parent) void MainWindow::createMenus() { +#ifdef Q_OS_MAC QMenu *actionsMenu = menuBar()->addMenu("&Actions"); // Add a custom "About" action for your app @@ -243,6 +272,7 @@ void MainWindow::createMenus() "A modern device management tool."); }); actionsMenu->addAction(aboutAct); +#endif } void MainWindow::updateNoDevicesConnected() diff --git a/src/mainwindow.h b/src/mainwindow.h index 61f73f7..d8da75e 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -20,6 +20,7 @@ class MainWindow : public QMainWindow Q_OBJECT public: + static MainWindow *sharedInstance(); MainWindow(QWidget *parent = nullptr); ~MainWindow(); void onRecoveryDeviceAdded(QObject *recoveryDeviceInfoObj); diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index c4db591..1159ebd 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -3,6 +3,8 @@ #include "appcontext.h" #include "devdiskimageswidget.h" #include "iDescriptor.h" +#include "ifusewidget.h" +#include "pcfileexplorerwidget.h" #include "querymobilegestaltwidget.h" #include "realtimescreen.h" #include "virtual_location.h" @@ -138,6 +140,14 @@ void ToolboxWidget::setupUI() createToolbox("Developer Disk Images", "Manage developer disk images", "SP_DialogOkButton", false); + QWidget *wirelessImport = createToolbox( + "Wireless File Import", "Import files wirelessly to your iDevice", + "SP_DialogOkButton", false); + + QWidget *mount_iPhone = createToolbox( + "Mount iPhone", "Mount your iPhone's filesystem on your PC", + "SP_DialogOkButton", false); + // Add toolboxes to grid (3 columns) m_gridLayout->addWidget(airplayerBox, 0, 0); m_gridLayout->addWidget(virtualLocationBox, 0, 1); @@ -151,7 +161,8 @@ void ToolboxWidget::setupUI() m_gridLayout->addWidget(enterRecoveryMode, 3, 0); m_gridLayout->addWidget(unmountDevImage, 3, 1); m_gridLayout->addWidget(devDiskImages, 3, 2); - + m_gridLayout->addWidget(wirelessImport, 4, 0); + m_gridLayout->addWidget(mount_iPhone, 4, 1); m_gridLayout->setRowStretch(3, 1); m_scrollArea->setWidget(m_contentWidget); @@ -402,15 +413,19 @@ void ToolboxWidget::onToolboxClicked(const QString &toolName) m_devDiskImagesWidget->raise(); m_devDiskImagesWidget->activateWindow(); } - } else if (toolName == "Touch ID Test") { - // Handle Touch ID test - QMessageBox::information( - this, "Touch ID Test", - "Touch ID test functionality not implemented."); - } else if (toolName == "Face ID Test") { - // Handle Face ID test - QMessageBox::information(this, "Face ID Test", - "Face ID test functionality not implemented."); + } else if (toolName == "Wireless File Import") { + // Handle wireless file import + PCFileExplorerWidget *fileExplorer = new PCFileExplorerWidget(); + fileExplorer->setAttribute(Qt::WA_DeleteOnClose); + fileExplorer->setWindowFlag(Qt::Window); + fileExplorer->resize(800, 600); + fileExplorer->show(); + } else if (toolName == "Mount iPhone") { + iFuseWidget *ifuseWidget = new iFuseWidget(m_currentDevice); + ifuseWidget->setAttribute(Qt::WA_DeleteOnClose); + ifuseWidget->setWindowFlag(Qt::Window); + ifuseWidget->resize(600, 400); + ifuseWidget->show(); } // Implement specific tool functionality here }