From 8f095aab8908025b5172a1e3fe90f2a4ec526df6 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Tue, 30 Sep 2025 04:45:15 +0000 Subject: [PATCH] implement ifuse for Linux - Updated CMakeLists.txt to include platform-specific source files for macOS. - Added new icon for disk unmount button. - Modified resources.qrc to include the new icon. - Implemented iFuse disk unmount button and manager classes for handling iFuse operations on Linux. - Created iFuseWidget for managing iPhone disk mounting, including UI and process handling. - Integrated iFuse functionality into the main application, allowing users to mount and unmount iPhone disks. - Enhanced DeviceInfoWidget and other UI components for better user experience. - Added support for displaying mounted iFuse paths in the status bar. --- CMakeLists.txt | 24 +- icons/ClarityHardDiskSolidAlerted.png | Bin 0 -> 11688 bytes resources.qrc | 3 +- src/airplaywindow.cpp | 17 +- src/airplaywindow.h | 2 + src/customtabwidget.cpp | 2 + src/deviceinfowidget.cpp | 43 +-- src/devicemanagerwidget.cpp | 2 + src/diskusagewidget.cpp | 11 +- src/iDescriptor-ui.h | 4 + src/ifusediskunmountbutton.cpp | 14 + src/ifusediskunmountbutton.h | 16 ++ src/ifusemanager.cpp | 53 ++++ src/ifusemanager.h | 20 ++ src/ifusewidget.cpp | 395 ++++++++++++++++++++++++++ src/ifusewidget.h | 61 ++++ src/main.cpp | 5 +- src/mainwindow.cpp | 32 ++- src/mainwindow.h | 1 + src/toolboxwidget.cpp | 35 ++- 20 files changed, 693 insertions(+), 47 deletions(-) create mode 100644 icons/ClarityHardDiskSolidAlerted.png create mode 100644 src/ifusediskunmountbutton.cpp create mode 100644 src/ifusediskunmountbutton.h create mode 100644 src/ifusemanager.cpp create mode 100644 src/ifusemanager.h create mode 100644 src/ifusewidget.cpp create mode 100644 src/ifusewidget.h 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 0000000000000000000000000000000000000000..f6af879810dc9bd72059a0bfd49989b2ecc1a674 GIT binary patch literal 11688 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 JwhsBy{{ZA&PYVD5 literal 0 HcmV?d00001 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 }