diff --git a/CMakeLists.txt b/CMakeLists.txt index bc75b60..a31ccfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -230,7 +230,6 @@ target_link_libraries(iDescriptor PRIVATE ${SSL_LIBRARY} ${CRYPTO_LIBRARY} ${SSH_LIBRARY} - ${HEIF_LIBRARIES} # ${FRIDA_LIBRARY} # ${ZIP_LIBRARY} PkgConfig::PUGIXML diff --git a/icons/HugeiconsWrench01.png b/icons/HugeiconsWrench01.png new file mode 100644 index 0000000..d2218bd Binary files /dev/null and b/icons/HugeiconsWrench01.png differ diff --git a/icons/IcOutlinePowerSettingsNew.png b/icons/IcOutlinePowerSettingsNew.png new file mode 100644 index 0000000..f4fddce Binary files /dev/null and b/icons/IcOutlinePowerSettingsNew.png differ diff --git a/icons/IcTwotoneRestartAlt.png b/icons/IcTwotoneRestartAlt.png new file mode 100644 index 0000000..3cf2410 Binary files /dev/null and b/icons/IcTwotoneRestartAlt.png differ diff --git a/resources.qrc b/resources.qrc index 4e0bf7f..0471523 100644 --- a/resources.qrc +++ b/resources.qrc @@ -5,6 +5,9 @@ icons/MdiLightningBolt.png icons/MingcuteSettings7Line.png icons/ClarityHardDiskSolidAlerted.png + icons/IcOutlinePowerSettingsNew.png + icons/HugeiconsWrench01.png + icons/IcTwotoneRestartAlt.png icons/icon.png qml/MapView.qml resources/dump.js diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp index 0e5cbbe..c773a9b 100644 --- a/src/afcexplorerwidget.cpp +++ b/src/afcexplorerwidget.cpp @@ -1,4 +1,5 @@ #include "afcexplorerwidget.h" +#include "iDescriptor-ui.h" #include "iDescriptor.h" #include "mediapreviewdialog.h" #include "settingsmanager.h" @@ -12,8 +13,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -40,6 +44,8 @@ AfcExplorerWidget::AfcExplorerWidget(afc_client_t afcClient, // Initialize m_history.push("/"); + m_currentHistoryIndex = 0; + m_forwardHistory.clear(); loadPath("/"); setupContextMenu(); @@ -48,9 +54,23 @@ AfcExplorerWidget::AfcExplorerWidget(afc_client_t afcClient, void AfcExplorerWidget::goBack() { if (m_history.size() > 1) { - m_history.pop(); + // Move current path to forward history + QString currentPath = m_history.pop(); + m_forwardHistory.push(currentPath); + QString prevPath = m_history.top(); loadPath(prevPath); + updateNavigationButtons(); + } +} + +void AfcExplorerWidget::goForward() +{ + if (!m_forwardHistory.isEmpty()) { + QString forwardPath = m_forwardHistory.pop(); + m_history.push(forwardPath); + loadPath(forwardPath); + updateNavigationButtons(); } } @@ -70,8 +90,11 @@ void AfcExplorerWidget::onItemDoubleClicked(QListWidgetItem *item) QString nextPath = currPath == "/" ? "/" + name : currPath + name; if (isDir) { + // Clear forward history when navigating to a new directory + m_forwardHistory.clear(); m_history.push(nextPath); loadPath(nextPath); + updateNavigationButtons(); } else { const QString lowerFileName = name.toLower(); const bool isPreviewable = @@ -113,64 +136,52 @@ void AfcExplorerWidget::onItemDoubleClicked(QListWidgetItem *item) } } -void AfcExplorerWidget::onBreadcrumbClicked() +void AfcExplorerWidget::onAddressBarReturnPressed() { - QPushButton *btn = qobject_cast(sender()); - if (!btn) - return; - QString path = btn->property("fullPath").toString(); - // pathLabel removed, compare with m_history.top() - if (!m_history.isEmpty() && path == m_history.top()) - return; + QString path = m_addressBar->text().trimmed(); + if (path.isEmpty()) { + path = "/"; + } + + // Normalize the path + if (!path.startsWith("/")) { + path = "/" + path; + } + + // Remove duplicate slashes + path = path.replace(QRegularExpression("/+"), "/"); + + // Clear forward history when navigating to a new path + m_forwardHistory.clear(); + + // Update history and load the path m_history.push(path); loadPath(path); + updateNavigationButtons(); } -void AfcExplorerWidget::updateBreadcrumb(const QString &path) +void AfcExplorerWidget::updateNavigationButtons() { - // Remove old breadcrumb buttons - QLayoutItem *child; - while ((child = m_breadcrumbLayout->takeAt(0)) != nullptr) { - if (child->widget()) { - child->widget()->deleteLater(); - } - delete child; + // Update button states based on history + if (m_backButton) { + m_backButton->setEnabled(m_history.size() > 1); } - - QStringList parts = path.split("/", Qt::SkipEmptyParts); - QString currPath = ""; - int idx = 0; - // Add root - QPushButton *rootBtn = new QPushButton("/"); - rootBtn->setFlat(true); - rootBtn->setProperty("fullPath", "/"); - connect(rootBtn, &QPushButton::clicked, this, - &AfcExplorerWidget::onBreadcrumbClicked); - m_breadcrumbLayout->addWidget(rootBtn); - - for (const QString &part : parts) { - currPath += part; - if (idx > 0) { - QLabel *sep = new QLabel(" / "); - m_breadcrumbLayout->addWidget(sep); - } - - QPushButton *btn = new QPushButton(part); - btn->setFlat(true); - btn->setProperty("fullPath", currPath); - connect(btn, &QPushButton::clicked, this, - &AfcExplorerWidget::onBreadcrumbClicked); - m_breadcrumbLayout->addWidget(btn); - idx++; + if (m_forwardButton) { + m_forwardButton->setEnabled(!m_forwardHistory.isEmpty()); } - m_breadcrumbLayout->addStretch(); +} + +void AfcExplorerWidget::updateAddressBar(const QString &path) +{ + // Update the address bar with the current path + m_addressBar->setText(path); } void AfcExplorerWidget::loadPath(const QString &path) { m_fileList->clear(); - updateBreadcrumb(path); + updateAddressBar(path); AFCFileTree tree = get_file_tree(m_currentAfcClient, m_device->device, path.toStdString()); @@ -435,23 +446,81 @@ void AfcExplorerWidget::setupFileExplorer() exportLayout->addStretch(); explorerLayout->addLayout(exportLayout); - // Navigation layout (Back + Breadcrumb) - QHBoxLayout *navLayout = new QHBoxLayout(); - m_backBtn = new QPushButton("Back"); - m_breadcrumbLayout = new QHBoxLayout(); - m_breadcrumbLayout->setSpacing(0); - navLayout->addWidget(m_backBtn); - navLayout->addLayout(m_breadcrumbLayout); - navLayout->addStretch(); - explorerLayout->addLayout(navLayout); + // Navigation layout (Address Bar with embedded icons) + m_navWidget = new QWidget(); + m_navWidget->setObjectName("navWidget"); + m_navWidget->setFocusPolicy(Qt::StrongFocus); // Make it focusable + connect(qApp, &QApplication::paletteChanged, this, + &AfcExplorerWidget::updateNavStyles); + + m_navWidget->setMaximumWidth(500); + m_navWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + QHBoxLayout *navContainerLayout = new QHBoxLayout(); + navContainerLayout->addStretch(); + navContainerLayout->addWidget(m_navWidget); + navContainerLayout->addStretch(); + + QHBoxLayout *navLayout = new QHBoxLayout(m_navWidget); + navLayout->setContentsMargins(0, 0, 0, 0); + navLayout->setSpacing(0); + + // Create navigation buttons using ClickableIconWidget + QWidget *explorerLeftSideNavButtons = new QWidget(); + QHBoxLayout *leftNavLayout = new QHBoxLayout(explorerLeftSideNavButtons); + // explorerLeftSideNavButtons->setStyleSheet("border-right: 1px solid + // red;"); + leftNavLayout->setContentsMargins(0, 0, 0, 0); + leftNavLayout->setSpacing(1); + + m_backButton = new ClickableIconWidget( + QIcon::fromTheme("go-previous", QIcon("←")), "Go Back"); + m_backButton->setEnabled(false); + + m_forwardButton = new ClickableIconWidget( + QIcon::fromTheme("go-next", QIcon("→")), "Go Forward"); + m_forwardButton->setEnabled(false); + + m_enterButton = new ClickableIconWidget( + QIcon::fromTheme("go-jump", QIcon("⏎")), "Navigate to path"); + + m_addressBar = new QLineEdit(); + m_addressBar->setPlaceholderText("Enter path..."); + m_addressBar->setText("/"); + + // Add widgets to navigation layout + leftNavLayout->addWidget(m_backButton); + leftNavLayout->addWidget(m_forwardButton); + navLayout->addWidget(explorerLeftSideNavButtons); + navLayout->addWidget(m_addressBar); + navLayout->addWidget(m_enterButton); + + // Add the container layout (which centers navWidget) to the main layout + explorerLayout->addLayout(navContainerLayout); // File list m_fileList = new QListWidget(); + // todo m_fileList->setSelectionMode(QAbstractItemView::ExtendedSelection); + + QScrollBar *vBar = m_fileList->QAbstractScrollArea::verticalScrollBar(); + // vBar->setStyleSheet("background:red; border: red;"); + vBar->setStyleSheet(styleSheet()); + // vBar->setStyleSheet( + // "QScrollArea { background: transparent; border: none; }"); + // m_scrollArea->viewport()->setStyleSheet("background: transparent;"); + // m_fileList->viewport()->setStyleSheet("background: transparent;"); explorerLayout->addWidget(m_fileList); - // Connect buttons - connect(m_backBtn, &QPushButton::clicked, this, &AfcExplorerWidget::goBack); + // Connect buttons and actions + connect(m_backButton, &ClickableIconWidget::clicked, this, + &AfcExplorerWidget::goBack); + connect(m_forwardButton, &ClickableIconWidget::clicked, this, + &AfcExplorerWidget::goForward); + connect(m_enterButton, &ClickableIconWidget::clicked, this, + &AfcExplorerWidget::onAddressBarReturnPressed); + connect(m_addressBar, &QLineEdit::returnPressed, this, + &AfcExplorerWidget::onAddressBarReturnPressed); connect(m_fileList, &QListWidget::itemDoubleClicked, this, &AfcExplorerWidget::onItemDoubleClicked); connect(m_exportBtn, &QPushButton::clicked, this, @@ -460,6 +529,9 @@ void AfcExplorerWidget::setupFileExplorer() &AfcExplorerWidget::onImportClicked); connect(m_addToFavoritesBtn, &QPushButton::clicked, this, &AfcExplorerWidget::onAddToFavoritesClicked); + + updateNavigationButtons(); + updateNavStyles(); } // todo: implement @@ -485,3 +557,35 @@ void AfcExplorerWidget::saveFavoritePlace(const QString &path, SettingsManager *settings = SettingsManager::sharedInstance(); settings->saveFavoritePlace(path, alias); } + +void AfcExplorerWidget::updateNavStyles() +{ + QColor bgColor = isDarkMode() ? qApp->palette().color(QPalette::Light) + : qApp->palette().color(QPalette::Dark); + QColor borderColor = qApp->palette().color(QPalette::Mid); + QColor accentColor = qApp->palette().color(QPalette::Highlight); + + QString navStyles = QString("QWidget#navWidget {" + " background-color: %1;" + " border: 1px solid %2;" + " border-radius: 10px;" + "}" + "QWidget#navWidget {" + " outline: 1px solid %3;" + " outline-offset: 1px;" + "}") + .arg(bgColor.name()) + .arg(bgColor.lighter().name()) + .arg(accentColor.name()); + + m_navWidget->setStyleSheet(navStyles); + + // Update address bar styles to complement the nav widget + QString addressBarStyles = + QString("QLineEdit { background-color: %1; border-radius: 10px; " + "border: 1px solid %2; }") + .arg(bgColor.name()) + .arg(borderColor.lighter().name()); + + m_addressBar->setStyleSheet(addressBarStyles); +} \ No newline at end of file diff --git a/src/afcexplorerwidget.h b/src/afcexplorerwidget.h index 8c592f3..0446586 100644 --- a/src/afcexplorerwidget.h +++ b/src/afcexplorerwidget.h @@ -1,10 +1,13 @@ #ifndef AFCEXPLORER_H #define AFCEXPLORER_H +#include "iDescriptor-ui.h" #include "iDescriptor.h" +#include #include #include #include +#include #include #include #include @@ -29,8 +32,9 @@ signals: private slots: void goBack(); + void goForward(); void onItemDoubleClicked(QListWidgetItem *item); - void onBreadcrumbClicked(); + void onAddressBarReturnPressed(); void onFileListContextMenu(const QPoint &pos); void onExportClicked(); void onImportClicked(); @@ -38,13 +42,18 @@ private slots: private: QWidget *m_explorer; - QPushButton *m_backBtn; + QWidget *m_navWidget; QPushButton *m_exportBtn; QPushButton *m_importBtn; QPushButton *m_addToFavoritesBtn; QListWidget *m_fileList; QStack m_history; - QHBoxLayout *m_breadcrumbLayout; + QStack m_forwardHistory; + int m_currentHistoryIndex; + QLineEdit *m_addressBar; + ClickableIconWidget *m_backButton; + ClickableIconWidget *m_forwardButton; + ClickableIconWidget *m_enterButton; iDescriptorDevice *m_device; // Current AFC mode @@ -52,7 +61,8 @@ private: void setupFileExplorer(); void loadPath(const QString &path); - void updateBreadcrumb(const QString &path); + void updateAddressBar(const QString &path); + void updateNavigationButtons(); void saveFavoritePlace(const QString &path, const QString &alias); void setupContextMenu(); @@ -62,6 +72,7 @@ private: const char *local_path); int import_file_to_device(afc_client_t afc, const char *device_path, const char *local_path); + void updateNavStyles(); }; #endif // AFCEXPLORER_H diff --git a/src/appswidget.cpp b/src/appswidget.cpp index 2b6ec8f..7167e80 100644 --- a/src/appswidget.cpp +++ b/src/appswidget.cpp @@ -5,6 +5,7 @@ #include "appinstalldialog.h" #include "appstoremanager.h" #include "logindialog.h" +#include "zlineedit.h" #include #include #include @@ -59,7 +60,7 @@ void AppsWidget::setupUI() m_statusLabel->setStyleSheet("margin-right: 20px;"); m_loginButton = new QPushButton(); - m_searchEdit = new QLineEdit(); + m_searchEdit = new ZLineEdit(); m_searchEdit->setMaximumWidth(400); m_searchEdit->setStyleSheet("QLineEdit { " " padding: 8px; " diff --git a/src/batterywidget.cpp b/src/batterywidget.cpp index 5617deb..a818d6b 100644 --- a/src/batterywidget.cpp +++ b/src/batterywidget.cpp @@ -1,6 +1,7 @@ // https://github.com/p-dobrzynski-dev/QtCustomWidgets/blob/master/batterywidget.cpp #include "batterywidget.h" +#include #include #include #include @@ -12,6 +13,8 @@ BatteryWidget::BatteryWidget(float value, bool isCharging, QWidget *parent) { setMinimumSize(30, 30); setMaximumSize(40, 40); + + connect(qApp, &QApplication::paletteChanged, this, [this]() { update(); }); } void BatteryWidget::resizeEvent(QResizeEvent *) @@ -83,7 +86,6 @@ void BatteryWidget::paintEvent(QPaintEvent *) QBrush brush = QBrush(Qt::white); painter.setPen(pen); - // Drawing battery frame float widgetCorner = widgetFrame.height() / 15; @@ -108,10 +110,11 @@ void BatteryWidget::paintEvent(QPaintEvent *) batteryLevelRect.moveTo(batteryLevelFrame.topLeft()); painter.drawRoundedRect(batteryLevelRect, widgetCorner, widgetCorner); - pen.setColor(Qt::white); + pen.setColor(palette().color(QPalette::Text)); painter.setPen(pen); QFont textFont = QFont(); textFont.setPixelSize(widgetFrame.height() / 1.65); + textFont.setWeight(QFont::Bold); painter.setFont(textFont); QFontMetrics fm(textFont); QString percentageLevelString = QString("%1%").arg(m_value); diff --git a/src/cableinfowidget.cpp b/src/cableinfowidget.cpp index 45bd653..7383b46 100644 --- a/src/cableinfowidget.cpp +++ b/src/cableinfowidget.cpp @@ -63,16 +63,12 @@ void CableInfoWidget::initCableInfo() } m_statusLabel->setText("Analyzing cable..."); - // Get cable info get_cable_info(m_device->device, m_response); - char *xml_string = nullptr; - uint32_t xml_length = 0; - plist_to_xml(m_response, &xml_string, &xml_length); - qDebug() << "Cable info plist:\n" - << QString::fromUtf8(xml_string, xml_length); + analyzeCableInfo(); updateUI(); } + // FIXME: genuine check is not perfect, still need more research void CableInfoWidget::analyzeCableInfo() { diff --git a/src/core/helpers/is_dark_mode.cpp b/src/core/helpers/is_dark_mode.cpp new file mode 100644 index 0000000..dda5cb8 --- /dev/null +++ b/src/core/helpers/is_dark_mode.cpp @@ -0,0 +1,16 @@ +#include +#include +#include + +bool isDarkMode() +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + const auto scheme = QGuiApplication::styleHints()->colorScheme(); + return scheme == Qt::ColorScheme::Dark; +#else + const QPalette defaultPalette; + const auto text = defaultPalette.color(QPalette::WindowText); + const auto window = defaultPalette.color(QPalette::Window); + return text.lightness() > window.lightness(); +#endif // QT_VERSION +} \ No newline at end of file diff --git a/src/core/helpers/safe_afc_read_directory.cpp b/src/core/helpers/safe_afc_read_directory.cpp index 1c15e69..2d11cc1 100644 --- a/src/core/helpers/safe_afc_read_directory.cpp +++ b/src/core/helpers/safe_afc_read_directory.cpp @@ -5,18 +5,17 @@ afc_error_t safe_afc_read_directory(afc_client_t afcClient, idevice_t device, const char *path, char ***dirs) { - afc_error_t res = afc_read_directory(afcClient, path, dirs); - // maybe the afc client is not valid anymore, so we try to reinitialize it - if (res != AFC_E_SUCCESS) { - qDebug() << "AFC read directory error: " << res; - afc_client_free(afcClient); - afc_client_new(device, NULL, &afcClient); - res = afc_read_directory(afcClient, path, dirs); - if (res != AFC_E_SUCCESS) { - qDebug() << "Failed to re-read directory after AFC client reset: " - << res; + try { + if (!afcClient || !device) { + qDebug() << "AFC client is null in safe_afc_read_directory"; + return AFC_E_INVALID_ARG; } - } - return res; -} + afc_error_t result = afc_read_directory(afcClient, path, dirs); + + return result; + } catch (const std::exception &e) { + qDebug() << "Exception in safe_afc_read_directory:" << e.what(); + return AFC_E_UNKNOWN_ERROR; + } +} \ No newline at end of file diff --git a/src/core/services/restart.cpp b/src/core/services/restart.cpp index 474a731..a829009 100644 --- a/src/core/services/restart.cpp +++ b/src/core/services/restart.cpp @@ -24,9 +24,6 @@ #include #include -// TODO:break all the client because device wont restart if any client is still -// connected we need to change the main device init function to not connect to -// any client bool restart(std::string _udid) { idevice_t device = NULL; diff --git a/src/core/services/shutdown.cpp b/src/core/services/shutdown.cpp index dedc45c..c0d3478 100644 --- a/src/core/services/shutdown.cpp +++ b/src/core/services/shutdown.cpp @@ -24,9 +24,6 @@ #include #include -// TODO:break all the client because device wont restart if any client is still -// connected we need to change the main device init function to not connect to -// any client bool shutdown(idevice_t device) { lockdownd_client_t lockdown_client = NULL; diff --git a/src/customtabwidget.cpp b/src/customtabwidget.cpp index b6bddad..13f83aa 100644 --- a/src/customtabwidget.cpp +++ b/src/customtabwidget.cpp @@ -69,9 +69,7 @@ void CustomTabWidget::setupGlider() " background-color: #2b5693;" " border-radius: 1px;" "}"); - // Set initial size - will be updated in animateGlider - m_glider->setFixedSize(100, 2); // 2px height for bottom border effect - m_glider->lower(); // Make sure glider is behind tabs + m_glider->hide(); // Hide initially until tabs are added m_gliderAnimation = new QPropertyAnimation(m_glider, "pos"); m_gliderAnimation->setDuration(250); @@ -102,6 +100,14 @@ int CustomTabWidget::addTab(QWidget *widget, const QIcon &icon, // Set first tab as checked by default if (index == 0) { tab->setChecked(true); + // Position glider immediately for first tab to prevent shifting + QTimer::singleShot(0, [this, tab]() { + m_glider->setFixedSize(tab->size().width(), 2); + int targetX = tab->pos().x(); + int targetY = tab->pos().y() + tab->size().height() - 2; + m_glider->move(targetX, targetY); + m_glider->show(); + }); } return index; @@ -122,12 +128,7 @@ void CustomTabWidget::setCurrentIndex(int index) emit currentChanged(index); } -void CustomTabWidget::finalizeStyles() -{ - updateTabStyles(); - // Position glider for first tab - QTimer::singleShot(0, [this]() { animateGlider(0); }); -} +void CustomTabWidget::finalizeStyles() { updateTabStyles(); } int CustomTabWidget::currentIndex() const { return m_currentIndex; } diff --git a/src/deviceinfowidget.cpp b/src/deviceinfowidget.cpp index e254084..f0b7307 100644 --- a/src/deviceinfowidget.cpp +++ b/src/deviceinfowidget.cpp @@ -5,6 +5,7 @@ #include "iDescriptor-ui.h" #include "iDescriptor.h" #include "infolabel.h" +#include "toolboxwidget.h" #include #include #include @@ -31,15 +32,57 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) mainLayout->setContentsMargins(0, 0, 10, 0); mainLayout->setSpacing(1); + // Left side container for image and actions + QWidget *leftContainer = new QWidget(); + QVBoxLayout *leftLayout = new QVBoxLayout(leftContainer); + leftLayout->setContentsMargins(0, 0, 0, 0); + leftLayout->setSpacing(1); + // Create responsive image label m_deviceImageLabel = new ResponsiveQLabel(this); m_deviceImageLabel->setPixmap(QPixmap(":/resources/iphone.png")); m_deviceImageLabel->setMinimumWidth(200); - m_deviceImageLabel->setSizePolicy(QSizePolicy::Ignored, - QSizePolicy::Expanding); + m_deviceImageLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_deviceImageLabel->setStyleSheet("background: transparent; border: none;"); - mainLayout->addWidget(m_deviceImageLabel, 1); // Stretch factor 1 + // Actions group box + QWidget *actionsWidget = new QWidget(); + actionsWidget->setObjectName("actionsWidget"); + actionsWidget->setFixedHeight(40); + actionsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + actionsWidget->setStyleSheet( + "QWidget#actionsWidget { background: transparent; border: none; }"); + QHBoxLayout *actionsLayout = new QHBoxLayout(actionsWidget); + actionsLayout->setContentsMargins(1, 1, 1, 1); + actionsLayout->setSpacing(10); + + ClickableIconWidget *shutdownBtn = new ClickableIconWidget( + QIcon(":/icons/IcOutlinePowerSettingsNew.png"), "Shutdown", this); + shutdownBtn->setIconSize(QSize(20, 20)); + connect(shutdownBtn, &ClickableIconWidget::clicked, this, + [device]() { ToolboxWidget::shutdownDevice(device); }); + + ClickableIconWidget *restartBtn = new ClickableIconWidget( + QIcon(":/icons/IcTwotoneRestartAlt.png"), "Restart", this); + restartBtn->setIconSize(QSize(20, 20)); + connect(restartBtn, &ClickableIconWidget::clicked, this, + [device]() { ToolboxWidget::restartDevice(device); }); + + ClickableIconWidget *recoveryBtn = new ClickableIconWidget( + QIcon(":/icons/HugeiconsWrench01.png"), "Recovery", this); + recoveryBtn->setIconSize(QSize(20, 20)); + connect(recoveryBtn, &ClickableIconWidget::clicked, this, + [device]() { ToolboxWidget::_enterRecoveryMode(device); }); + + actionsLayout->addWidget(shutdownBtn); + actionsLayout->addWidget(restartBtn); + actionsLayout->addWidget(recoveryBtn); + + leftLayout->addWidget(m_deviceImageLabel); + leftLayout->addWidget(actionsWidget, 0, Qt::AlignCenter); + leftLayout->addStretch(); // stretch to push everything to the top + + mainLayout->addWidget(leftContainer); // Stretch factor 1 // Right side: Info Table QWidget *infoContainer = new QWidget(); @@ -65,9 +108,11 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) diskCapacityLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); diskCapacityLabel->setAttribute(Qt::WA_StyledBackground, true); - diskCapacityLabel->setStyleSheet("background-color: rgba(0, 255, 30, 0.5);" - "padding: 2px;" - "border-radius: 13px;"); + // background-color: rgba(0, 255, 30, 0.5); + diskCapacityLabel->setStyleSheet(QString("background-color: %1;" + "padding: 2px 4px;" + "border-radius: 13px;") + .arg(COLOR_ACCENT_BLUE.name())); m_chargingStatusLabel = new QLabel(device->deviceInfo.batteryInfo.isCharging ? "Charging" diff --git a/src/devicesidebarwidget.cpp b/src/devicesidebarwidget.cpp index 3aabac1..b0f18f1 100644 --- a/src/devicesidebarwidget.cpp +++ b/src/devicesidebarwidget.cpp @@ -82,27 +82,29 @@ void DeviceSidebarItem::setupUI() for (QPushButton *btn : navButtons) { btn->setCheckable(true); btn->setMaximumHeight(25); - btn->setStyleSheet("QPushButton { " - " background-color: #f8f9fa; " - " border: 1px solid #dee2e6; " - " padding: 4px 8px; " - " text-align: center; " - " border-radius: 3px; " - " font-size: 11px; " - " color: #212529; " - "} " - "QPushButton:checked { " - " background-color: #0d6efd; " - " color: white; " - " border: 1px solid #0a58ca; " - "} " - "QPushButton:hover:!checked { " - " background-color: #e9ecef; " - " border-color: #adb5bd; " - "} " - "QPushButton:checked:hover { " - " background-color: #0b5ed7; " - "}"); + btn->setStyleSheet( + QString("QPushButton { " + " background-color: #f8f9fa; " + " border: 1px solid #dee2e6; " + " padding: 4px 8px; " + " text-align: center; " + " border-radius: 3px; " + " font-size: 11px; " + " color: #212529; " + "} " + "QPushButton:checked { " + " background-color: %1; " + " color: white; " + " border: 1px solid %1; " + "} " + "QPushButton:hover:!checked { " + " background-color: #e9ecef; " + " border-color: #adb5bd; " + "} " + "QPushButton:checked:hover { " + " background-color: %2; " + "}") + .arg(COLOR_ACCENT_BLUE.name(), COLOR_BLUE.name())); connect(btn, &QPushButton::clicked, this, &DeviceSidebarItem::onNavigationButtonClicked); @@ -129,10 +131,11 @@ void DeviceSidebarItem::setSelected(bool selected) return; m_selected = selected; - + // todo : bug the first device selected style is not applied if (selected) { - setStyleSheet("DeviceSidebarItem { border: " - "2px solid #2196f3; border-radius: 5px; }"); + setStyleSheet(QString("DeviceSidebarItem { border: " + "2px solid %1; border-radius: 5px; }") + .arg(COLOR_BLUE.name())); } else { setStyleSheet("DeviceSidebarItem { border: " "1px solid #e0e0e0; border-radius: 5px; }"); diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp index a2aa4a0..ff52e16 100644 --- a/src/gallerywidget.cpp +++ b/src/gallerywidget.cpp @@ -13,6 +13,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -21,6 +24,7 @@ void GalleryWidget::load() { if (m_loaded) return; + m_loaded = true; setupUI(); @@ -28,7 +32,9 @@ void GalleryWidget::load() GalleryWidget::GalleryWidget(iDescriptorDevice *device, QWidget *parent) : QWidget{parent}, m_device(device), m_model(nullptr), - m_exportManager(nullptr) + m_exportManager(nullptr), m_stackedWidget(nullptr), + m_albumSelectionWidget(nullptr), m_albumListView(nullptr), + m_photoGalleryWidget(nullptr), m_listView(nullptr), m_backButton(nullptr) { // Initialize export manager m_exportManager = new PhotoExportManager(this); @@ -40,72 +46,27 @@ void GalleryWidget::setupUI() { m_mainLayout = new QVBoxLayout(this); m_mainLayout->setContentsMargins(0, 0, 0, 0); - // m_mainLayout->setSpacing(10); - // Setup controls at the top + // Setup controls at the top (outside of stacked widget) setupControlsLayout(); - // Create list view - m_listView = new QListView(this); - m_listView->setViewMode(QListView::IconMode); - m_listView->setFlow(QListView::LeftToRight); - m_listView->setWrapping(true); - m_listView->setResizeMode(QListView::Adjust); - m_listView->setIconSize(QSize(120, 120)); - m_listView->setSpacing(10); - m_listView->setSelectionMode(QAbstractItemView::ExtendedSelection); - m_listView->setUniformItemSizes(true); - // m_listView->setGridSize(QSize(140, 300)); // Fixed grid size - // m_listView->setIconSize(QSize(120, 300)); + // Create stacked widget for different views + m_stackedWidget = new QStackedWidget(this); - m_listView->setStyleSheet( - "QListView { " - " border-top: 1px solid #c1c1c1ff; " // Gray border for the ListView - " background-color: transparent; " // Optional: background - " padding: 0px;" - "} " - "QListView::item { " - " width: 150px; " - " height: 150px; " - " margin: 2px; " - "}"); - // Create and set model - m_model = new PhotoModel(m_device, this); - m_listView->setModel(m_model); + // Setup album selection view + setupAlbumSelectionView(); - // Add to main layout - m_mainLayout->addWidget(m_listView); + // Setup photo gallery view + setupPhotoGalleryView(); + + // Add stacked widget to main layout + m_mainLayout->addWidget(m_stackedWidget); setLayout(m_mainLayout); - // Add progress widget after main layout is set - // m_mainLayout->insertWidget( - // 1, m_progressWidget); // Insert between controls and list view - - // Connect double-click to open preview dialog - connect(m_listView, &QListView::doubleClicked, this, - [this](const QModelIndex &index) { - if (!index.isValid()) - return; - - QString filePath = - m_model->data(index, Qt::UserRole).toString(); - if (filePath.isEmpty()) - return; - - qDebug() << "Opening preview for" << filePath; - auto *previewDialog = new MediaPreviewDialog( - m_device, m_device->afcClient, filePath, this); - previewDialog->setAttribute(Qt::WA_DeleteOnClose); - previewDialog->show(); - }); - - // Update export button states based on selection - connect(m_listView->selectionModel(), - &QItemSelectionModel::selectionChanged, this, [this]() { - bool hasSelection = - m_listView->selectionModel()->hasSelection(); - m_exportSelectedButton->setEnabled(hasSelection); - }); + // Start with album selection view and load albums + m_stackedWidget->setCurrentWidget(m_albumSelectionWidget); + setControlsEnabled(false); // Disable controls until album is selected + loadAlbumList(); } void GalleryWidget::setupControlsLayout() @@ -122,23 +83,9 @@ void GalleryWidget::setupControlsLayout() static_cast(PhotoModel::NewestFirst)); m_sortComboBox->addItem("Oldest First", static_cast(PhotoModel::OldestFirst)); - m_sortComboBox->setCurrentIndex(0); // Default to Newest First - m_sortComboBox->setStyleSheet("QComboBox { " - " padding: 5px 10px; " - " border-radius: 4px; " - " min-width: 100px; " - "} " - "QComboBox:hover { " - " border-color: #0078d4; " - "} " - "QComboBox::drop-down { " - " border: none; " - " width: 20px; " - "} " - "QComboBox::down-arrow { " - " width: 12px; " - " height: 12px; " - "}"); + m_sortComboBox->setCurrentIndex(0); // Default to Newest First + m_sortComboBox->setMinimumWidth(150); // Ensure text fits + m_sortComboBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); // Filter combo box QLabel *filterLabel = new QLabel("Filter:"); @@ -149,46 +96,22 @@ void GalleryWidget::setupControlsLayout() static_cast(PhotoModel::ImagesOnly)); m_filterComboBox->addItem("Videos Only", static_cast(PhotoModel::VideosOnly)); - m_filterComboBox->setCurrentIndex(0); // Default to All - m_filterComboBox->setStyleSheet(m_sortComboBox->styleSheet()); + m_filterComboBox->setCurrentIndex(0); // Default to All + m_filterComboBox->setMinimumWidth(150); // Ensure text fits + m_filterComboBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); // Export buttons m_exportSelectedButton = new QPushButton("Export Selected"); m_exportSelectedButton->setEnabled(false); // Initially disabled - m_exportSelectedButton->setStyleSheet("QPushButton { " - " background-color: #0078d4; " - " color: white; " - " border: none; " - " padding: 8px 16px; " - " border-radius: 4px; " - " font-weight: bold; " - "} " - "QPushButton:hover:enabled { " - " background-color: #106ebe; " - "} " - "QPushButton:pressed:enabled { " - " background-color: #005a9e; " - "} " - "QPushButton:disabled { " - " background-color: #ccc; " - " color: #888; " - "}"); - + m_exportSelectedButton->setSizePolicy(QSizePolicy::Preferred, + QSizePolicy::Fixed); + m_exportSelectedButton->setStyleSheet("QPushButton { padding: 8px 16px; }"); m_exportAllButton = new QPushButton("Export All"); - m_exportAllButton->setStyleSheet("QPushButton { " - " background-color: #28a745; " - " color: white; " - " border: none; " - " padding: 8px 16px; " - " border-radius: 4px; " - " font-weight: bold; " - "} " - "QPushButton:hover { " - " background-color: #218838; " - "} " - "QPushButton:pressed { " - " background-color: #1e7e34; " - "}"); + m_exportAllButton->setStyleSheet("QPushButton { padding: 8px 16px; }"); + + // Back button + m_backButton = new QPushButton("← Back to Albums"); + m_backButton->setVisible(false); // Hidden initially // Connect signals connect(m_sortComboBox, QOverload::of(&QComboBox::currentIndexChanged), @@ -200,8 +123,11 @@ void GalleryWidget::setupControlsLayout() &GalleryWidget::onExportSelected); connect(m_exportAllButton, &QPushButton::clicked, this, &GalleryWidget::onExportAll); + connect(m_backButton, &QPushButton::clicked, this, + &GalleryWidget::onBackToAlbums); // Add widgets to layout + m_controlsLayout->addWidget(m_backButton); m_controlsLayout->addWidget(sortLabel); m_controlsLayout->addWidget(m_sortComboBox); m_controlsLayout->addWidget(filterLabel); @@ -252,14 +178,13 @@ void GalleryWidget::onExportSelected() { if (!m_model || !m_listView->selectionModel()->hasSelection()) { QMessageBox::information(this, "No Selection", - "Please select one or more items to export."); + "Please select photos to export."); return; } if (m_exportManager->isExporting()) { QMessageBox::information(this, "Export in Progress", - "An export operation is already in progress. " - "Please wait for it to complete."); + "An export is already in progress."); return; } @@ -268,20 +193,21 @@ void GalleryWidget::onExportSelected() QStringList filePaths = m_model->getSelectedFilePaths(selectedIndexes); if (filePaths.isEmpty()) { - QMessageBox::warning(this, "Export Error", - "No valid files selected for export."); + QMessageBox::information(this, "No Items", + "No valid items selected for export."); return; } QString exportDir = selectExportDirectory(); if (exportDir.isEmpty()) { - return; // User cancelled directory selection + return; } qDebug() << "Starting export of selected files:" << filePaths.size() << "items to" << exportDir; // Create export dialog and connect signals + // todo:cleanup auto *exportDialog = new FileExportDialog(this); // Connect PhotoExportManager signals to FileExportDialog @@ -309,17 +235,14 @@ void GalleryWidget::onExportAll() if (m_exportManager->isExporting()) { QMessageBox::information(this, "Export in Progress", - "An export operation is already in progress. " - "Please wait for it to complete."); + "An export is already in progress."); return; } QStringList filePaths = m_model->getFilteredFilePaths(); if (filePaths.isEmpty()) { - QMessageBox::information( - this, "No Items", - "There are no items to export with the current filter."); + QMessageBox::information(this, "No Items", "No items to export."); return; } @@ -335,13 +258,14 @@ void GalleryWidget::onExportAll() QString exportDir = selectExportDirectory(); if (exportDir.isEmpty()) { - return; // User cancelled directory selection + return; } qDebug() << "Starting export of all filtered files:" << filePaths.size() << "items to" << exportDir; // Create export dialog and connect signals + // todo:cleanup auto *exportDialog = new FileExportDialog(this); // Connect PhotoExportManager signals to FileExportDialog @@ -373,3 +297,202 @@ QString GalleryWidget::selectExportDirectory() return selectedDir; } + +void GalleryWidget::setupAlbumSelectionView() +{ + m_albumSelectionWidget = new QWidget(); + QVBoxLayout *layout = new QVBoxLayout(m_albumSelectionWidget); + layout->setContentsMargins(0, 0, 0, 0); + // Add instructions label + QLabel *instructionLabel = new QLabel("Select a photo album:"); + instructionLabel->setStyleSheet("font-weight: bold;"); + layout->addWidget(instructionLabel); + + // Create list view for albums + m_albumListView = new QListView(); + // m_albumListView->setStyleSheet("QListView { " + // " border: 1px solid #c1c1c1ff; " + // " background-color: white; " + // " padding: 5px; " + // "} " + // "QListView::item { " + // " padding: 10px; " + // " border-bottom: 1px solid #e1e1e1; " + // "} " + // "QListView::item:hover { " + // " background-color: #f0f0f0; " + // "} " + // "QListView::item:selected { " + // " background-color: #0078d4; " + // " color: white; " + // "}"); + + layout->addWidget(m_albumListView); + + // Add the album selection widget to stacked widget + m_stackedWidget->addWidget(m_albumSelectionWidget); + + // Connect double-click to select album + connect(m_albumListView, &QListView::doubleClicked, this, + [this](const QModelIndex &index) { + if (!index.isValid()) + return; + QString albumPath = index.data(Qt::UserRole).toString(); + onAlbumSelected(albumPath); + }); +} + +void GalleryWidget::setupPhotoGalleryView() +{ + m_photoGalleryWidget = new QWidget(); + QVBoxLayout *layout = new QVBoxLayout(m_photoGalleryWidget); + layout->setContentsMargins(0, 0, 0, 0); + + // Create list view for photos + m_listView = new QListView(); + m_listView->setViewMode(QListView::IconMode); + m_listView->setFlow(QListView::LeftToRight); + m_listView->setWrapping(true); + m_listView->setResizeMode(QListView::Adjust); + m_listView->setIconSize(QSize(120, 120)); + m_listView->setSpacing(10); + m_listView->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_listView->setUniformItemSizes(true); + + m_listView->setStyleSheet("QListView { " + " border-top: 1px solid #c1c1c1ff; " + " background-color: transparent; " + " padding: 0px;" + "} " + "QListView::item { " + " width: 150px; " + " height: 150px; " + " margin: 2px; " + "}"); + + layout->addWidget(m_listView); + + // Add the photo gallery widget to stacked widget + m_stackedWidget->addWidget(m_photoGalleryWidget); + + // Connect double-click to open preview dialog + connect(m_listView, &QListView::doubleClicked, this, + [this](const QModelIndex &index) { + if (!index.isValid()) + return; + + QString filePath = + m_model->data(index, Qt::UserRole).toString(); + if (filePath.isEmpty()) + return; + + qDebug() << "Opening preview for" << filePath; + auto *previewDialog = new MediaPreviewDialog( + m_device, m_device->afcClient, filePath, this); + previewDialog->setAttribute(Qt::WA_DeleteOnClose); + previewDialog->show(); + }); +} + +void GalleryWidget::loadAlbumList() +{ + // Get DCIM directory contents + qDebug() << "Loading album list from /DCIM"; + AFCFileTree dcimTree = + get_file_tree(m_device->afcClient, m_device->device, "/DCIM"); + + if (!dcimTree.success) { + qDebug() << "Failed to read DCIM directory"; + QMessageBox::warning(this, "Error", + "Could not access DCIM directory on device."); + return; + } + + qDebug() << "DCIM directory read successfully, found" + << dcimTree.entries.size() << "entries"; + + auto *albumModel = new QStandardItemModel(this); + + for (const MediaEntry &entry : dcimTree.entries) { + QString albumName = QString::fromStdString(entry.name); + qDebug() << "DCIM entry:" << albumName << "(isDir:" << entry.isDir + << ")"; + + // Check if it's a directory and matches common iOS photo album patterns + if (entry.isDir && + (albumName.contains("APPLE") || + QRegularExpression("^\\d{3}APPLE$").match(albumName).hasMatch() || + QRegularExpression("^\\d{4}\\d{2}\\d{2}$") + .match(albumName) + .hasMatch())) { + qDebug() << "Found photo album:" << albumName; + auto *item = new QStandardItem(albumName); + QString fullPath = QString("/DCIM/%1").arg(albumName); + item->setData(fullPath, Qt::UserRole); // Store full path + item->setIcon(QIcon::fromTheme("folder")); + albumModel->appendRow(item); + } + } + + m_albumListView->setModel(albumModel); + + if (albumModel->rowCount() == 0) { + QMessageBox::information(this, "No Albums", + "No photo albums found on device."); + } else { + qDebug() << "Found" << albumModel->rowCount() << "photo albums"; + } +} + +void GalleryWidget::onAlbumSelected(const QString &albumPath) +{ + m_currentAlbumPath = albumPath; + + // Create model if not exists + if (!m_model) { + m_model = new PhotoModel(m_device, this); + m_listView->setModel(m_model); + + // Update export button states based on selection + connect(m_listView->selectionModel(), + &QItemSelectionModel::selectionChanged, this, [this]() { + bool hasSelection = + m_listView->selectionModel()->hasSelection(); + m_exportSelectedButton->setEnabled(hasSelection); + }); + } + + // Set album path and load photos + m_model->setAlbumPath(albumPath); + + // Switch to photo gallery view + m_stackedWidget->setCurrentWidget(m_photoGalleryWidget); + + // Enable controls and show back button + setControlsEnabled(true); + m_backButton->setVisible(true); + + qDebug() << "Loaded album:" << albumPath; +} + +void GalleryWidget::onBackToAlbums() +{ + // Switch back to album selection view + m_stackedWidget->setCurrentWidget(m_albumSelectionWidget); + + // Disable controls and hide back button + setControlsEnabled(false); + m_backButton->setVisible(false); + + // Clear current album path + m_currentAlbumPath.clear(); +} + +void GalleryWidget::setControlsEnabled(bool enabled) +{ + m_sortComboBox->setEnabled(enabled); + m_filterComboBox->setEnabled(enabled); + m_exportSelectedButton->setEnabled( + enabled && m_listView && m_listView->selectionModel()->hasSelection()); + m_exportAllButton->setEnabled(enabled); +} diff --git a/src/gallerywidget.h b/src/gallerywidget.h index c7c5e13..d957137 100644 --- a/src/gallerywidget.h +++ b/src/gallerywidget.h @@ -10,6 +10,8 @@ class QComboBox; class QPushButton; class QHBoxLayout; class QVBoxLayout; +class QStackedWidget; +class QLabel; QT_END_NAMESPACE class PhotoModel; @@ -39,18 +41,33 @@ private slots: void onFilterChanged(); void onExportSelected(); void onExportAll(); + void onAlbumSelected(const QString &albumPath); + void onBackToAlbums(); private: void setupUI(); void setupControlsLayout(); + void setupAlbumSelectionView(); + void setupPhotoGalleryView(); + void loadAlbumList(); + void setControlsEnabled(bool enabled); QString selectExportDirectory(); iDescriptorDevice *m_device; bool m_loaded = false; + QString m_currentAlbumPath; // UI components QVBoxLayout *m_mainLayout; QHBoxLayout *m_controlsLayout; + QStackedWidget *m_stackedWidget; + + // Album selection view + QWidget *m_albumSelectionWidget; + QListView *m_albumListView; + + // Photo gallery view + QWidget *m_photoGalleryWidget; QListView *m_listView; PhotoModel *m_model; @@ -59,6 +76,7 @@ private: QComboBox *m_filterComboBox; QPushButton *m_exportSelectedButton; QPushButton *m_exportAllButton; + QPushButton *m_backButton; // Export manager PhotoExportManager *m_exportManager; diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h index d9c459e..f8ccf13 100644 --- a/src/iDescriptor-ui.h +++ b/src/iDescriptor-ui.h @@ -16,6 +16,8 @@ #define COLOR_GREEN QColor(0, 180, 0) // Green #define COLOR_ORANGE QColor(255, 140, 0) // Orange #define COLOR_RED QColor(255, 0, 0) // Red +#define COLOR_BLUE QColor("#2b5693") +#define COLOR_ACCENT_BLUE QColor("#0b5ed7") // A custom QGraphicsView that keeps the content fitted with aspect ratio on // resize @@ -59,6 +61,118 @@ protected: } }; +class ClickableIconWidget : public QWidget +{ + Q_OBJECT +public: + ClickableIconWidget(const QIcon &icon, const QString &tooltip, + QWidget *parent = nullptr) + : QWidget(parent), m_icon(icon), m_iconSize(24, 24), m_pressed(false) + { + setToolTip(tooltip); + setFixedSize(32, 32); + setCursor(Qt::PointingHandCursor); + connect(qApp, &QApplication::paletteChanged, this, + [this]() { update(); }); + } + + void setIcon(const QIcon &icon) + { + m_icon = icon; + update(); + } + void setIconSize(const QSize &size) + { + m_iconSize = size; + update(); + } + +signals: + void clicked(); + +protected: + void paintEvent(QPaintEvent *event) override + { + Q_UNUSED(event) + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + // Draw background circle when hovered or pressed + if (underMouse() || m_pressed) { + QColor bgColor = palette().color(QPalette::Highlight); + bgColor.setAlpha(m_pressed ? 60 : 30); + painter.setBrush(bgColor); + painter.setPen(Qt::NoPen); + painter.drawEllipse(rect().adjusted(2, 2, -2, -2)); + } + + // Draw icon centered with theme-appropriate color + QRect iconRect = rect(); + iconRect.setSize(m_iconSize); + iconRect.moveCenter(rect().center()); + + // Get the appropriate icon color based on theme + QColor iconColor = palette().color(QPalette::WindowText); + + // Create a colored version of the icon + QPixmap pixmap = m_icon.pixmap(m_iconSize); + if (!pixmap.isNull()) { + QPixmap coloredPixmap(pixmap.size()); + coloredPixmap.fill(Qt::transparent); + + QPainter iconPainter(&coloredPixmap); + iconPainter.setCompositionMode( + QPainter::CompositionMode_SourceOver); + iconPainter.drawPixmap(0, 0, pixmap); + iconPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + iconPainter.fillRect(coloredPixmap.rect(), iconColor); + + painter.drawPixmap(iconRect, coloredPixmap); + } else { + m_icon.paint(&painter, iconRect); + } + } + + void mousePressEvent(QMouseEvent *event) override + { + if (event->button() == Qt::LeftButton) { + m_pressed = true; + update(); + } + QWidget::mousePressEvent(event); + } + + void mouseReleaseEvent(QMouseEvent *event) override + { + if (event->button() == Qt::LeftButton && m_pressed) { + m_pressed = false; + update(); + if (rect().contains(event->pos())) { + emit clicked(); + } + } + QWidget::mouseReleaseEvent(event); + } + + void enterEvent(QEnterEvent *event) override + { + Q_UNUSED(event) + update(); + } + + void leaveEvent(QEvent *event) override + { + Q_UNUSED(event) + m_pressed = false; + update(); + } + +private: + QIcon m_icon; + QSize m_iconSize; + bool m_pressed; +}; + enum class iDescriptorTool { Airplayer, RealtimeScreen, @@ -96,6 +210,46 @@ protected: QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); + // Draw fading left and right borders (no top/bottom) + QColor borderColor = QApplication::palette().color(QPalette::Mid); + + // Create gradient for fading effect + int fadeMargin = 20; // pixels to fade over + int centerHeight = height() / 2; + int fadeStart = fadeMargin; + int fadeEnd = height() - fadeMargin; + + // Left border with fade + for (int y = 0; y < height(); ++y) { + QColor currentColor = borderColor; + if (y < fadeStart) { + // Fade from transparent to full opacity + float alpha = static_cast(y) / fadeStart; + currentColor.setAlphaF(alpha * borderColor.alphaF()); + } else if (y > fadeEnd) { + // Fade from full opacity to transparent + float alpha = static_cast(height() - y) / fadeMargin; + currentColor.setAlphaF(alpha * borderColor.alphaF()); + } + painter.setPen(QPen(currentColor, 1)); + painter.drawPoint(0, y); + } + + // Right border with fade + for (int y = 0; y < height(); ++y) { + QColor currentColor = borderColor; + if (y < fadeStart) { + float alpha = static_cast(y) / fadeStart; + currentColor.setAlphaF(alpha * borderColor.alphaF()); + } else if (y > fadeEnd) { + float alpha = static_cast(height() - y) / fadeMargin; + currentColor.setAlphaF(alpha * borderColor.alphaF()); + } + painter.setPen(QPen(currentColor, 1)); + painter.drawPoint(width() - 1, y); + } + + // Draw the center button QColor buttonColor = QApplication::palette().color(QPalette::Text); buttonColor.setAlpha(60); @@ -123,7 +277,6 @@ public: : QSplitter(orientation, parent) { setHandleWidth(10); - setCursor(Qt::SplitHCursor); } protected: diff --git a/src/iDescriptor.h b/src/iDescriptor.h index 3f4b75f..96c779f 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -397,3 +397,5 @@ QPixmap load_heic(const QByteArray &data); QByteArray read_afc_file_to_byte_array(afc_client_t afcClient, const char *path); + +bool isDarkMode(); \ No newline at end of file diff --git a/src/installedappswidget.cpp b/src/installedappswidget.cpp index 8cc2df2..caf27df 100644 --- a/src/installedappswidget.cpp +++ b/src/installedappswidget.cpp @@ -3,6 +3,7 @@ #include "iDescriptor-ui.h" #include "iDescriptor.h" #include "qprocessindicator.h" +#include "zlineedit.h" #include #include #include @@ -137,19 +138,25 @@ void AppTabWidget::leaveEvent(QEvent *event) void AppTabWidget::updateStyles() { + // QStyleHints::colorScheme() QString borderStyle; - // TODO: for some reason setting a style overrides every other style instead - // of adding or overriding - // if (m_selected) { - // setStyleSheet("border: 2px solid #007AFF;"); - // } - // borderStyle = "border: 2px solid #007AFF;"; - // } else if (m_hovered) { - // borderStyle = "border: 1px solid" + highlightColor.name() + ";"; - // } else { - // borderStyle = ""; - // } - // setStyleSheet(borderStyle); + // QColor bgColor = qApp->palette().color(QPalette::Window); + QColor bgColor = isDarkMode() ? qApp->palette().color(QPalette::Light) + : qApp->palette().color(QPalette::Dark); + qDebug() << styleSheet(); + if (m_selected) { + borderStyle = "QGroupBox { background-color: " + + qApp->palette().color(QPalette::Highlight).name() + + "; border-radius: " + "10px; border : 1px solid " + + bgColor.lighter().name() + "; }"; + } else { + borderStyle = "QGroupBox { background-color: " + bgColor.name() + + "; border-radius: 10px; border: 1px solid " + + bgColor.lighter().name() + "; }"; + } + // update(); + setStyleSheet(borderStyle); } InstalledAppsWidget::InstalledAppsWidget(iDescriptorDevice *device, @@ -164,7 +171,7 @@ InstalledAppsWidget::InstalledAppsWidget(iDescriptorDevice *device, &InstalledAppsWidget::onAppsDataReady); connect(m_containerWatcher, &QFutureWatcher::finished, this, &InstalledAppsWidget::onContainerDataReady); - + setStyleSheet("InstalledAppsWidget { background: transparent; }"); fetchInstalledApps(); } @@ -189,6 +196,12 @@ void InstalledAppsWidget::setupUI() // Start in loading state showLoadingState(); + + connect(qApp, &QApplication::paletteChanged, this, [this]() { + for (AppTabWidget *tab : m_appTabs) { + tab->updateStyles(); + } + }); } void InstalledAppsWidget::showLoadingState() @@ -534,6 +547,7 @@ void InstalledAppsWidget::createAppTab(const QString &appName, new AppTabWidget(appName, bundleId, version, this); connect(tabWidget, &AppTabWidget::clicked, this, &InstalledAppsWidget::onAppTabClicked); + m_appTabs.append(tabWidget); // Remove the stretch before adding the new tab m_tabLayout->removeItem(m_tabLayout->itemAt(m_tabLayout->count() - 1)); @@ -618,7 +632,6 @@ void InstalledAppsWidget::loadAppContainer(const QString &bundleId) loadingLayout->addWidget(l, 0, Qt::AlignCenter); m_containerLayout->addWidget(loadingWidget); - m_containerScrollArea->setVisible(true); QFuture future = QtConcurrent::run([this, bundleId]() -> QVariantMap { @@ -825,18 +838,8 @@ void InstalledAppsWidget::createLeftPanel() searchLayout->setContentsMargins(5, 0, 5, 5); // Search box - m_searchEdit = new QLineEdit(); + m_searchEdit = new ZLineEdit(); m_searchEdit->setPlaceholderText("Search apps..."); - m_searchEdit->setStyleSheet("QLineEdit { " - " border: 2px solid #e0e0e0; " - " border-radius: 6px; " - " padding: 8px 12px; " - " font-size: 14px; " - "} " - "QLineEdit:focus { " - " border: 2px solid #007AFF; " - " outline: none; " - "}"); searchLayout->addWidget(m_searchEdit); // File sharing filter checkbox @@ -851,10 +854,13 @@ void InstalledAppsWidget::createLeftPanel() m_tabScrollArea = new QScrollArea(); m_tabScrollArea->setWidgetResizable(true); m_tabScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_tabScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - m_tabScrollArea->setStyleSheet("QScrollArea { border: none; }"); + m_tabScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_tabScrollArea->setStyleSheet( + "QScrollArea { background: transparent; border: none; }"); + m_tabScrollArea->viewport()->setStyleSheet("background: transparent;"); m_tabContainer = new QWidget(); + m_tabContainer->setStyleSheet("QWidget { background: transparent; }"); m_tabLayout = new QVBoxLayout(m_tabContainer); m_tabLayout->setContentsMargins(0, 0, 10, 0); m_tabLayout->setSpacing(10); @@ -874,19 +880,15 @@ void InstalledAppsWidget::createRightPanel() contentLayout->setContentsMargins(0, 0, 0, 5); contentLayout->setSpacing(0); - // Container explorer area - m_containerScrollArea = new QScrollArea(); - m_containerScrollArea->setWidgetResizable(true); - m_containerScrollArea->setMinimumHeight(200); - m_containerScrollArea->setVisible(false); - m_containerWidget = new QWidget(); + m_containerWidget->setObjectName("containerWidget"); + m_containerWidget->setStyleSheet( + "QWidget#containerWidget { border: none; }"); m_containerLayout = new QVBoxLayout(m_containerWidget); m_containerLayout->setContentsMargins(0, 0, 0, 0); m_containerLayout->setSpacing(0); - m_containerScrollArea->setWidget(m_containerWidget); - contentLayout->addWidget(m_containerScrollArea); + contentLayout->addWidget(m_containerWidget); m_splitter->addWidget(rightContentWidget); } diff --git a/src/installedappswidget.h b/src/installedappswidget.h index 27bd1e8..a01d302 100644 --- a/src/installedappswidget.h +++ b/src/installedappswidget.h @@ -2,6 +2,7 @@ #define INSTALLEDAPPSWIDGET_H #include "iDescriptor.h" +#include "zlineedit.h" #include #include #include @@ -9,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -38,6 +38,7 @@ public: QString getBundleId() const { return m_bundleId; } QString getAppName() const { return m_appName; } QString getVersion() const { return m_version; } + void updateStyles(); signals: void clicked(); @@ -50,7 +51,6 @@ protected: private: void fetchAppIcon(); void setupUI(); - void updateStyles(); QString m_appName; QString m_bundleId; @@ -61,6 +61,7 @@ private: QLabel *m_iconLabel; QLabel *m_nameLabel; QLabel *m_versionLabel; + QList m_appTabs; }; class InstalledAppsWidget : public QWidget @@ -101,7 +102,7 @@ private: QWidget *m_errorWidget; QWidget *m_contentWidget; QLabel *m_errorLabel; - QLineEdit *m_searchEdit; + ZLineEdit *m_searchEdit; QCheckBox *m_fileSharingCheckBox; QScrollArea *m_tabScrollArea; QWidget *m_tabContainer; diff --git a/src/jailbrokenwidget.cpp b/src/jailbrokenwidget.cpp index 835093e..29a5fa1 100644 --- a/src/jailbrokenwidget.cpp +++ b/src/jailbrokenwidget.cpp @@ -85,7 +85,9 @@ void JailbrokenWidget::setupDeviceSelectionUI(QVBoxLayout *layout) scrollArea->setWidgetResizable(true); scrollArea->setMinimumHeight(200); scrollArea->setMaximumHeight(300); + scrollArea->setObjectName("devicescrollArea"); + scrollArea->setStyleSheet("QWidget#devicescrollArea {border: none;}"); QWidget *scrollContent = new QWidget(); m_deviceLayout = new QVBoxLayout(scrollContent); m_deviceLayout->setContentsMargins(5, 5, 5, 5); diff --git a/src/photomodel.cpp b/src/photomodel.cpp index 0d2c66a..b0fbf46 100644 --- a/src/photomodel.cpp +++ b/src/photomodel.cpp @@ -1,4 +1,3 @@ - #include "photomodel.h" #include "iDescriptor.h" #include "mediastreamermanager.h" @@ -31,8 +30,7 @@ PhotoModel::PhotoModel(iDescriptorDevice *device, QObject *parent) connect(this, &PhotoModel::thumbnailNeedsToBeLoaded, this, &PhotoModel::requestThumbnail, Qt::QueuedConnection); - // Populate the photo paths - populatePhotoPaths(); + // Don't populate paths in constructor - wait for setAlbumPath } PhotoModel::~PhotoModel() @@ -415,15 +413,50 @@ void PhotoModel::populatePhotoPaths() { // TODO:beginResetModel called on PhotoModel(0x600002d12a40) without calling // endResetModel first + if (m_albumPath.isEmpty()) { + qDebug() << "No album path set, skipping population"; + return; + } + beginResetModel(); m_allPhotos.clear(); m_photos.clear(); // Your existing logic to populate photo paths char **files = nullptr; - const char *photoDir = "/DCIM/100APPLE"; - safe_afc_read_directory(m_device->afcClient, m_device->device, photoDir, - &files); + qDebug() << "Populating photos from album path:" << m_albumPath; + + // First verify the album path exists + QByteArray albumPathBytes = m_albumPath.toUtf8(); + const char *albumPathCStr = albumPathBytes.constData(); + + char **albumInfo = nullptr; + afc_error_t infoResult = + afc_get_file_info(m_device->afcClient, albumPathCStr, &albumInfo); + if (infoResult != AFC_E_SUCCESS) { + qDebug() << "Album path does not exist or cannot be accessed:" + << m_albumPath << "Error:" << infoResult; + endResetModel(); + return; + } + if (albumInfo) { + afc_dictionary_free(albumInfo); + } + + // Fix: Store the QByteArray to keep the C string valid + QByteArray photoDirBytes = m_albumPath.toUtf8(); + const char *photoDir = photoDirBytes.constData(); + qDebug() << "Photo directory:" << m_albumPath; + qDebug() << "Photo directory C string:" << photoDir; + + afc_error_t readResult = safe_afc_read_directory( + m_device->afcClient, m_device->device, photoDir, &files); + if (readResult != AFC_E_SUCCESS) { + qDebug() << "Failed to read photo directory:" << photoDir + << "Error:" << readResult; + endResetModel(); + return; + } if (files) { for (int i = 0; files[i]; i++) { @@ -436,7 +469,7 @@ void PhotoModel::populatePhotoPaths() fileName.endsWith(".M4V", Qt::CaseInsensitive)) { PhotoInfo info; - info.filePath = QString(photoDir) + "/" + fileName; + info.filePath = m_albumPath + "/" + fileName; info.fileName = fileName; info.thumbnailRequested = false; info.fileType = determineFileType(fileName); @@ -654,4 +687,14 @@ PhotoInfo::FileType PhotoModel::determineFileType(const QString &fileName) const } else { return PhotoInfo::Image; } -} \ No newline at end of file +} + +void PhotoModel::setAlbumPath(const QString &albumPath) +{ + if (m_albumPath != albumPath) { + m_albumPath = albumPath; + populatePhotoPaths(); + } +} + +void PhotoModel::refreshPhotos() { populatePhotoPaths(); } \ No newline at end of file diff --git a/src/photomodel.h b/src/photomodel.h index 80ae028..429e2e5 100644 --- a/src/photomodel.h +++ b/src/photomodel.h @@ -42,6 +42,10 @@ public: void setThumbnailSize(const QSize &size); void clearCache(); + // Album management + void setAlbumPath(const QString &albumPath); + void refreshPhotos(); + // Sorting and filtering void setSortOrder(SortOrder order); SortOrder sortOrder() const { return m_sortOrder; } @@ -75,6 +79,7 @@ private slots: private: // Data members iDescriptorDevice *m_device; + QString m_albumPath; QList m_allPhotos; // All photos from device QList m_photos; // Currently filtered/sorted photos diff --git a/src/querymobilegestaltwidget.cpp b/src/querymobilegestaltwidget.cpp index 469f287..6a9de0d 100644 --- a/src/querymobilegestaltwidget.cpp +++ b/src/querymobilegestaltwidget.cpp @@ -43,7 +43,7 @@ void QueryMobileGestaltWidget::setupUI() buttonLayout->addWidget(selectAllButton); buttonLayout->addWidget(clearAllButton); buttonLayout->addStretch(); - buttonLayout->setContentsMargins(5, 5, 5, 5); + buttonLayout->setContentsMargins(5, 0, 5, 0); groupLayout->addLayout(buttonLayout); // Scroll area for checkboxes diff --git a/src/responsiveqlabel.cpp b/src/responsiveqlabel.cpp index 3b91d16..34f1662 100644 --- a/src/responsiveqlabel.cpp +++ b/src/responsiveqlabel.cpp @@ -6,7 +6,7 @@ ResponsiveQLabel::ResponsiveQLabel(QWidget *parent) : QLabel(parent) { setAlignment(Qt::AlignCenter); setScaledContents(false); - setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); setMinimumSize(100, 100); } @@ -44,13 +44,23 @@ void ResponsiveQLabel::paintEvent(QPaintEvent *event) void ResponsiveQLabel::updateScaledPixmap() { - if (m_originalPixmap.isNull() || size().isEmpty()) { + if (m_originalPixmap.isNull()) { return; } + // Use the minimum width as the constraint for scaling + int targetWidth = qMax(minimumWidth(), width()); + // Scale the pixmap while maintaining aspect ratio - m_scaledPixmap = m_originalPixmap.scaled(size(), Qt::KeepAspectRatio, - Qt::SmoothTransformation); + m_scaledPixmap = + m_originalPixmap.scaled(targetWidth, QWIDGETSIZE_MAX, + Qt::KeepAspectRatio, Qt::SmoothTransformation); + + // Resize the widget to match the scaled pixmap size + // This prevents the widget from taking up more space than the actual image + if (!m_scaledPixmap.isNull()) { + setFixedSize(m_scaledPixmap.size()); + } update(); } \ No newline at end of file diff --git a/src/sshterminalwidget.cpp b/src/sshterminalwidget.cpp index 403aaa5..f9d7150 100644 --- a/src/sshterminalwidget.cpp +++ b/src/sshterminalwidget.cpp @@ -1,64 +1,59 @@ #include "sshterminalwidget.h" #include "qprocessindicator.h" -#include +#include +#include #include +#include #include +#include +#include +#include #include #include #include -#include -#include -#include -#include -#include -#include -#include +#include #include +#include #include -SSHTerminalWidget::SSHTerminalWidget(const ConnectionInfo& connectionInfo, QWidget *parent) - : QWidget(parent) - , m_connectionInfo(connectionInfo) - , m_sshSession(nullptr) - , m_sshChannel(nullptr) - , m_iproxyProcess(nullptr) - , m_sshConnected(false) - , m_isInitialized(false) - , m_currentState(TerminalState::Loading) +SSHTerminalWidget::SSHTerminalWidget(const ConnectionInfo &connectionInfo, + QWidget *parent) + : QWidget(parent), m_connectionInfo(connectionInfo), m_sshSession(nullptr), + m_sshChannel(nullptr), m_iproxyProcess(nullptr), m_sshConnected(false), + m_isInitialized(false), m_currentState(TerminalState::Loading) { - setWindowTitle(QString("SSH Terminal - %1").arg(m_connectionInfo.deviceName)); + setWindowTitle( + QString("SSH Terminal - %1").arg(m_connectionInfo.deviceName)); setMinimumSize(800, 600); - + setupUI(); - + // Initialize SSH ssh_init(); - + // Setup timer for checking SSH data m_sshTimer = new QTimer(this); - connect(m_sshTimer, &QTimer::timeout, this, &SSHTerminalWidget::checkSshData); - + connect(m_sshTimer, &QTimer::timeout, this, + &SSHTerminalWidget::checkSshData); + // Start connection process initializeConnection(); } -SSHTerminalWidget::~SSHTerminalWidget() -{ - cleanup(); -} +SSHTerminalWidget::~SSHTerminalWidget() { cleanup(); } void SSHTerminalWidget::setupUI() { QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); - + m_stackedWidget = new QStackedWidget(this); mainLayout->addWidget(m_stackedWidget); - + setupLoadingState(); setupErrorState(); setupActionState(); - + setState(TerminalState::Loading); } @@ -67,7 +62,7 @@ void SSHTerminalWidget::setupLoadingState() m_loadingWidget = new QWidget(); QVBoxLayout *loadingLayout = new QVBoxLayout(m_loadingWidget); loadingLayout->setAlignment(Qt::AlignCenter); - + // Process indicator m_loadingIndicator = new QProcessIndicator(m_loadingWidget); m_loadingIndicator->setType(QProcessIndicator::line_rotate); @@ -76,11 +71,12 @@ void SSHTerminalWidget::setupLoadingState() // Loading label m_loadingLabel = new QLabel("Connecting to SSH server..."); m_loadingLabel->setAlignment(Qt::AlignCenter); - m_loadingLabel->setStyleSheet("QLabel { font-size: 14px; color: #666; margin-top: 20px; }"); - + m_loadingLabel->setStyleSheet( + "QLabel { font-size: 14px; color: #666; margin-top: 20px; }"); + loadingLayout->addWidget(m_loadingIndicator, 0, Qt::AlignCenter); loadingLayout->addWidget(m_loadingLabel); - + m_stackedWidget->addWidget(m_loadingWidget); } @@ -90,21 +86,24 @@ void SSHTerminalWidget::setupErrorState() QVBoxLayout *errorLayout = new QVBoxLayout(m_errorWidget); errorLayout->setAlignment(Qt::AlignCenter); errorLayout->setSpacing(20); - + // Error label m_errorLabel = new QLabel(); m_errorLabel->setAlignment(Qt::AlignCenter); m_errorLabel->setWordWrap(true); - m_errorLabel->setStyleSheet("QLabel { font-size: 14px; color: #d32f2f; padding: 20px; }"); - + m_errorLabel->setStyleSheet( + "QLabel { font-size: 14px; color: #d32f2f; padding: 20px; }"); + // Retry button m_retryButton = new QPushButton("Retry Connection"); - m_retryButton->setStyleSheet("QPushButton { padding: 10px 20px; font-size: 14px; }"); - connect(m_retryButton, &QPushButton::clicked, this, &SSHTerminalWidget::onRetryClicked); - + m_retryButton->setStyleSheet( + "QPushButton { padding: 10px 20px; font-size: 14px; }"); + connect(m_retryButton, &QPushButton::clicked, this, + &SSHTerminalWidget::onRetryClicked); + errorLayout->addWidget(m_errorLabel); errorLayout->addWidget(m_retryButton, 0, Qt::AlignCenter); - + m_stackedWidget->addWidget(m_errorWidget); } @@ -113,13 +112,13 @@ void SSHTerminalWidget::setupActionState() m_actionWidget = new QWidget(); QVBoxLayout *actionLayout = new QVBoxLayout(m_actionWidget); actionLayout->setContentsMargins(0, 0, 0, 0); - + // Terminal widget m_terminal = new QTermWidget(0, m_actionWidget); m_terminal->setScrollBarPosition(QTermWidget::ScrollBarRight); m_terminal->setColorScheme("Linux"); m_terminal->setContextMenuPolicy(Qt::CustomContextMenu); - + connect(m_terminal, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) { QMenu menu(this); @@ -129,30 +128,30 @@ void SSHTerminalWidget::setupActionState() menu.exec(m_terminal->mapToGlobal(pos)); } }); - + m_terminal->startTerminalTeletype(); m_terminal->setStyleSheet("padding: 5px;"); - + actionLayout->addWidget(m_terminal); - + m_stackedWidget->addWidget(m_actionWidget); } void SSHTerminalWidget::setState(TerminalState state) { m_currentState = state; - + switch (state) { case TerminalState::Loading: m_stackedWidget->setCurrentWidget(m_loadingWidget); m_loadingIndicator->start(); break; - + case TerminalState::Error: m_stackedWidget->setCurrentWidget(m_errorWidget); m_loadingIndicator->stop(); break; - + case TerminalState::Connected: m_stackedWidget->setCurrentWidget(m_actionWidget); m_loadingIndicator->stop(); @@ -161,7 +160,7 @@ void SSHTerminalWidget::setState(TerminalState state) } } -void SSHTerminalWidget::showError(const QString& errorMessage) +void SSHTerminalWidget::showError(const QString &errorMessage) { m_errorLabel->setText(errorMessage); setState(TerminalState::Error); @@ -173,14 +172,15 @@ void SSHTerminalWidget::onRetryClicked() cleanup(); m_sshConnected = false; m_isInitialized = false; - + // Reinitialize SSH ssh_init(); - + // Setup timer again m_sshTimer = new QTimer(this); - connect(m_sshTimer, &QTimer::timeout, this, &SSHTerminalWidget::checkSshData); - + connect(m_sshTimer, &QTimer::timeout, this, + &SSHTerminalWidget::checkSshData); + // Update loading message and start connection m_loadingLabel->setText("Connecting to SSH server..."); setState(TerminalState::Loading); @@ -201,84 +201,87 @@ void SSHTerminalWidget::initWiredDevice() if (m_isInitialized) return; m_isInitialized = true; - + m_loadingLabel->setText("Setting up SSH tunnel..."); - + // Start iproxy for wired devices m_iproxyProcess = new QProcess(this); m_iproxyProcess->setProcessChannelMode(QProcess::MergedChannels); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.insert("PATH", env.value("PATH") + ":/usr/local/bin:/opt/homebrew/bin"); m_iproxyProcess->setProcessEnvironment(env); - + connect(m_iproxyProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { - showError("Error starting iproxy: " + m_iproxyProcess->errorString()); + // showError("Error starting iproxy: " + + // m_iproxyProcess->errorString()); qDebug() << "iproxy error:" << error; }); - + connect(m_iproxyProcess, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) { qDebug() << "iproxy finished with exit code:" << exitCode; if (!m_sshConnected) { - showError("iproxy process terminated unexpectedly"); + // showError("iproxy process terminated unexpectedly"); } }); - + // Monitor iproxy output for readiness connect(m_iproxyProcess, &QProcess::readyRead, this, [this]() { QByteArray output = m_iproxyProcess->readAll(); qDebug() << "iproxy output:" << output; - + if (output.contains("waiting for connection")) { qDebug() << "iproxy is ready, starting SSH connection"; disconnect(m_iproxyProcess, &QProcess::readyRead, this, nullptr); startSSH(QHostAddress(QHostAddress::LocalHost).toString(), 3333); } else if (output.contains("ERROR") || output.contains("failed")) { - showError("iproxy failed: " + QString::fromUtf8(output)); + qDebug() << "iproxy error detected in output" << output; + // showError("iproxy failed: " + QString::fromUtf8(output)); } }); - + // Add timeout timer as backup QTimer *timeoutTimer = new QTimer(this); timeoutTimer->setSingleShot(true); connect(timeoutTimer, &QTimer::timeout, this, [this, timeoutTimer]() { - qDebug() << "iproxy timeout - assuming it's ready and attempting SSH connection"; + qDebug() << "iproxy timeout - assuming it's ready and attempting SSH " + "connection"; timeoutTimer->deleteLater(); startSSH(QHostAddress(QHostAddress::LocalHost).toString(), 3333); }); - + QStringList args; args << "-u" << m_connectionInfo.deviceUdid << "3333" << "22"; - + qDebug() << "Starting iproxy with args:" << args; - + QString iproxyPath; QStringList possiblePaths = {"/usr/local/bin/iproxy", "/opt/homebrew/bin/iproxy", "/usr/bin/iproxy", "iproxy"}; - + for (const QString &path : possiblePaths) { if (QFile::exists(path) || path == "iproxy") { iproxyPath = path; break; } } - + if (iproxyPath.isEmpty()) { showError("Error: iproxy not found. Please install libimobiledevice."); return; } - + qDebug() << "Using iproxy at:" << iproxyPath; m_iproxyProcess->start(iproxyPath, args); - + if (!m_iproxyProcess->waitForStarted(5000)) { showError("Failed to start iproxy process"); timeoutTimer->deleteLater(); return; } - + qDebug() << "iproxy process started, waiting for readiness..."; timeoutTimer->start(5000); } @@ -288,9 +291,9 @@ void SSHTerminalWidget::initWirelessDevice() if (m_isInitialized) return; m_isInitialized = true; - + m_loadingLabel->setText("Connecting to network device..."); - + // For wireless devices, connect directly without iproxy startSSH(m_connectionInfo.hostAddress, m_connectionInfo.port); } @@ -298,37 +301,38 @@ void SSHTerminalWidget::initWirelessDevice() void SSHTerminalWidget::startSSH(const QString &host, uint16_t port) { qDebug() << "Starting SSH to" << host << "on port" << port; - + if (m_sshConnected) return; - + m_loadingLabel->setText("Establishing SSH connection..."); qDebug() << "Starting SSH connection to" << host << ":" << port; - + // Create SSH session m_sshSession = ssh_new(); if (!m_sshSession) { showError("Error: Failed to create SSH session"); return; } - + // Configure SSH session QByteArray hostBytes = host.toUtf8(); ssh_options_set(m_sshSession, SSH_OPTIONS_HOST, hostBytes.constData()); int sshPort = static_cast(port); ssh_options_set(m_sshSession, SSH_OPTIONS_PORT, &sshPort); ssh_options_set(m_sshSession, SSH_OPTIONS_USER, "root"); - + // Disable strict host key checking int stricthostcheck = 0; - ssh_options_set(m_sshSession, SSH_OPTIONS_STRICTHOSTKEYCHECK, &stricthostcheck); - + ssh_options_set(m_sshSession, SSH_OPTIONS_STRICTHOSTKEYCHECK, + &stricthostcheck); + // Set log level for debugging int log_level = SSH_LOG_PROTOCOL; ssh_options_set(m_sshSession, SSH_OPTIONS_LOG_VERBOSITY, &log_level); - + qDebug() << "SSH session configured, attempting connection..."; - + // Connect to SSH server int rc = ssh_connect(m_sshSession); qDebug() << "SSH connect result:" << rc << "SSH_OK:" << SSH_OK; @@ -341,20 +345,20 @@ void SSHTerminalWidget::startSSH(const QString &host, uint16_t port) m_sshSession = nullptr; return; } - + qDebug() << "SSH connected successfully, attempting authentication..."; - + // Authenticate with password rc = ssh_userauth_password(m_sshSession, nullptr, "alpine"); if (rc != SSH_AUTH_SUCCESS) { showError(QString("SSH authentication failed: %1") - .arg(ssh_get_error(m_sshSession))); + .arg(ssh_get_error(m_sshSession))); ssh_disconnect(m_sshSession); ssh_free(m_sshSession); m_sshSession = nullptr; return; } - + // Create SSH channel m_sshChannel = ssh_channel_new(m_sshSession); if (!m_sshChannel) { @@ -364,12 +368,12 @@ void SSHTerminalWidget::startSSH(const QString &host, uint16_t port) m_sshSession = nullptr; return; } - + // Open SSH channel rc = ssh_channel_open_session(m_sshChannel); if (rc != SSH_OK) { showError(QString("Failed to open SSH channel: %1") - .arg(ssh_get_error(m_sshSession))); + .arg(ssh_get_error(m_sshSession))); ssh_channel_free(m_sshChannel); m_sshChannel = nullptr; ssh_disconnect(m_sshSession); @@ -377,7 +381,7 @@ void SSHTerminalWidget::startSSH(const QString &host, uint16_t port) m_sshSession = nullptr; return; } - + // Request a PTY rc = ssh_channel_request_pty(m_sshChannel); if (rc != SSH_OK) { @@ -390,7 +394,7 @@ void SSHTerminalWidget::startSSH(const QString &host, uint16_t port) m_sshSession = nullptr; return; } - + // Start shell rc = ssh_channel_request_shell(m_sshChannel); if (rc != SSH_OK) { @@ -403,16 +407,16 @@ void SSHTerminalWidget::startSSH(const QString &host, uint16_t port) m_sshSession = nullptr; return; } - + // Connect terminal to SSH connectLibsshToTerminal(); - + // Start timer to check for SSH data m_sshTimer->start(50); // Check every 50ms - + m_sshConnected = true; setState(TerminalState::Connected); - + qDebug() << "SSH terminal connected successfully"; } @@ -420,7 +424,7 @@ void SSHTerminalWidget::connectLibsshToTerminal() { if (!m_terminal) return; - + // Connect terminal input to SSH channel connect(m_terminal, &QTermWidget::sendData, this, [this](const char *data, int size) { @@ -434,7 +438,7 @@ void SSHTerminalWidget::checkSshData() { if (!m_sshChannel || !ssh_channel_is_open(m_sshChannel)) return; - + // Check if SSH channel has data to read if (ssh_channel_poll(m_sshChannel, 0) > 0) { char buffer[4096]; @@ -445,7 +449,7 @@ void SSHTerminalWidget::checkSshData() write(m_terminal->getPtySlaveFd(), buffer, nbytes); } } - + // Check for stderr data if (ssh_channel_poll(m_sshChannel, 1) > 0) { char buffer[4096]; @@ -456,7 +460,7 @@ void SSHTerminalWidget::checkSshData() write(m_terminal->getPtySlaveFd(), buffer, nbytes); } } - + // Check if channel is closed if (ssh_channel_is_eof(m_sshChannel)) { disconnectSSH(); @@ -476,24 +480,32 @@ void SSHTerminalWidget::cleanup() m_sshTimer->deleteLater(); m_sshTimer = nullptr; } - + if (m_sshChannel) { ssh_channel_close(m_sshChannel); ssh_channel_free(m_sshChannel); m_sshChannel = nullptr; } - + if (m_sshSession) { ssh_disconnect(m_sshSession); ssh_free(m_sshSession); m_sshSession = nullptr; } - + if (m_iproxyProcess) { - m_iproxyProcess->kill(); + // Properly terminate iproxy process + if (m_iproxyProcess->state() != QProcess::NotRunning) { + m_iproxyProcess->terminate(); // Send SIGTERM first + if (!m_iproxyProcess->waitForFinished(3000)) { + qDebug() << "iproxy didn't terminate gracefully, killing..."; + m_iproxyProcess->kill(); // Force kill if needed + m_iproxyProcess->waitForFinished(1000); + } + } m_iproxyProcess->deleteLater(); m_iproxyProcess = nullptr; } - + m_sshConnected = false; } diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index 261b820..f7e6256 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -268,8 +268,13 @@ void ToolboxWidget::updateDeviceList() m_uuid.clear(); // No device, clear uuid } else { m_deviceCombo->setEnabled(true); + QString shortUdid = + QString::fromStdString(devices.first()->udid).left(8) + "..."; for (iDescriptorDevice *device : devices) { - m_deviceCombo->addItem(QString::fromStdString(device->udid)); + m_deviceCombo->addItem( + QString::fromStdString(device->deviceInfo.productType) + " / " + + shortUdid, + QString::fromStdString(device->udid)); } // TODO: m_uuid = devices.first()->udid; @@ -381,29 +386,13 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) virtualLocation->show(); } break; case iDescriptorTool::Restart: { - if (!(restart(m_currentDevice->udid))) - warn("Failed to restart device"); - else { - warn("Device will restart once unplugged", "Success"); - qDebug() << "Restarting device"; - } + restartDevice(m_currentDevice); } break; case iDescriptorTool::Shutdown: { - // TODO - // if (!(shutdown(m_currentDevice->device))) - // warn("Failed to shutdown device"); + shutdownDevice(m_currentDevice); } break; case iDescriptorTool::RecoveryMode: { - // Handle entering recovery mode - bool success = enterRecoveryMode(m_currentDevice); - QMessageBox msgBox; - msgBox.setWindowTitle("Recovery Mode"); - if (success) { - msgBox.setText("Successfully entered recovery mode."); - } else { - msgBox.setText("Failed to enter recovery mode."); - } - msgBox.exec(); + _enterRecoveryMode(m_currentDevice); } break; case iDescriptorTool::QueryMobileGestalt: { // Handle querying MobileGestalt @@ -456,3 +445,48 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) break; } } + +void ToolboxWidget::restartDevice(iDescriptorDevice *device) +{ + if (!device || device->udid.empty()) { + return; + } + + if (!(restart(device->udid))) + warn("Failed to restart device"); + else { + warn("Device will restart once unplugged", "Success"); + qDebug() << "Restarting device"; + } +} + +void ToolboxWidget::shutdownDevice(iDescriptorDevice *device) +{ + if (!device || device->udid.empty()) { + return; + } + + if (!(shutdown(device->device))) + warn("Failed to shutdown device"); + else { + warn("Device will shutdown once unplugged", "Success"); + qDebug() << "Shutting down device"; + } +} + +void ToolboxWidget::_enterRecoveryMode(iDescriptorDevice *device) +{ + if (!device || device->udid.empty()) { + return; + } + + bool success = enterRecoveryMode(device); + QMessageBox msgBox; + msgBox.setWindowTitle("Recovery Mode"); + if (success) { + msgBox.setText("Successfully entered recovery mode."); + } else { + msgBox.setText("Failed to enter recovery mode."); + } + msgBox.exec(); +} \ No newline at end of file diff --git a/src/toolboxwidget.h b/src/toolboxwidget.h index 8c772a3..815dbdc 100644 --- a/src/toolboxwidget.h +++ b/src/toolboxwidget.h @@ -18,7 +18,9 @@ class ToolboxWidget : public QWidget Q_OBJECT public: explicit ToolboxWidget(QWidget *parent = nullptr); - + static void restartDevice(iDescriptorDevice *device); + static void shutdownDevice(iDescriptorDevice *device); + static void _enterRecoveryMode(iDescriptorDevice *device); private slots: void onDeviceAdded(); void onDeviceRemoved(); diff --git a/src/zlineedit.cpp b/src/zlineedit.cpp new file mode 100644 index 0000000..05ed44d --- /dev/null +++ b/src/zlineedit.cpp @@ -0,0 +1,36 @@ +#include "zlineedit.h" + +ZLineEdit::ZLineEdit(QWidget *parent) : QLineEdit(parent) { setupStyles(); } + +ZLineEdit::ZLineEdit(const QString &text, QWidget *parent) + : QLineEdit(text, parent) +{ + setupStyles(); +} + +void ZLineEdit::setupStyles() +{ + updateStyles(); + + // Connect to palette changes for dynamic theme updates + connect(qApp, &QApplication::paletteChanged, this, + &ZLineEdit::updateStyles); +} + +void ZLineEdit::updateStyles() +{ + setStyleSheet("QLineEdit { " + " border: 2px solid " + + qApp->palette().color(QPalette::Midlight).name() + + "; " + " border-radius: 6px; " + " padding: 8px 12px; " + " font-size: 14px; " + "} " + "QLineEdit:focus { " + " border: 2px solid " + + qApp->palette().color(QPalette::Highlight).name() + + "; " + " outline: none; " + "}"); +} \ No newline at end of file diff --git a/src/zlineedit.h b/src/zlineedit.h new file mode 100644 index 0000000..c017d47 --- /dev/null +++ b/src/zlineedit.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +class ZLineEdit : public QLineEdit +{ + Q_OBJECT + +public: + explicit ZLineEdit(QWidget *parent = nullptr); + explicit ZLineEdit(const QString &text, QWidget *parent = nullptr); + +private slots: + void updateStyles(); + +private: + void setupStyles(); +}; \ No newline at end of file