From 8d4f4b11f9c8dc2945b310d1166da6db961ea655 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Thu, 9 Oct 2025 21:24:45 -0700 Subject: [PATCH] improve UI styles - Added album path management in PhotoModel for better photo loading. - Updated responsive QLabel to handle scaling more effectively. - Introduced ClickableIconWidget for better icon interaction in the UI. - Added new color definitions for blue and accent blue. - Enhanced the AppTabWidget styles to adapt to dark mode. - Replaced QLineEdit with ZLineEdit for consistent styling. - Improved the SSH terminal widget with better error handling and process management. - Refactored ToolboxWidget methods for device management. - Adjusted margins and styles in various widgets for improved layout. --- CMakeLists.txt | 1 - icons/HugeiconsWrench01.png | Bin 0 -> 16959 bytes icons/IcOutlinePowerSettingsNew.png | Bin 0 -> 14988 bytes icons/IcTwotoneRestartAlt.png | Bin 0 -> 15011 bytes resources.qrc | 3 + src/afcexplorerwidget.cpp | 216 ++++++++--- src/afcexplorerwidget.h | 19 +- src/appswidget.cpp | 3 +- src/batterywidget.cpp | 7 +- src/cableinfowidget.cpp | 8 +- src/core/helpers/is_dark_mode.cpp | 16 + src/core/helpers/safe_afc_read_directory.cpp | 25 +- src/core/services/restart.cpp | 3 - src/core/services/shutdown.cpp | 3 - src/customtabwidget.cpp | 19 +- src/deviceinfowidget.cpp | 57 ++- src/devicesidebarwidget.cpp | 51 +-- src/gallerywidget.cpp | 371 ++++++++++++------- src/gallerywidget.h | 18 + src/iDescriptor-ui.h | 155 +++++++- src/iDescriptor.h | 2 + src/installedappswidget.cpp | 72 ++-- src/installedappswidget.h | 7 +- src/jailbrokenwidget.cpp | 2 + src/photomodel.cpp | 59 ++- src/photomodel.h | 5 + src/querymobilegestaltwidget.cpp | 2 +- src/responsiveqlabel.cpp | 18 +- src/sshterminalwidget.cpp | 222 +++++------ src/toolboxwidget.cpp | 74 +++- src/toolboxwidget.h | 4 +- src/zlineedit.cpp | 36 ++ src/zlineedit.h | 19 + 33 files changed, 1067 insertions(+), 430 deletions(-) create mode 100644 icons/HugeiconsWrench01.png create mode 100644 icons/IcOutlinePowerSettingsNew.png create mode 100644 icons/IcTwotoneRestartAlt.png create mode 100644 src/core/helpers/is_dark_mode.cpp create mode 100644 src/zlineedit.cpp create mode 100644 src/zlineedit.h 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 0000000000000000000000000000000000000000..d2218bdcfbc869266d2ded5d19cf258619e9ba69 GIT binary patch literal 16959 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 E04{WICIA2c literal 0 HcmV?d00001 diff --git a/icons/IcOutlinePowerSettingsNew.png b/icons/IcOutlinePowerSettingsNew.png new file mode 100644 index 0000000000000000000000000000000000000000..f4fddce4a9367fa1e4d3e71754fcce949a596b87 GIT binary patch literal 14988 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 literal 0 HcmV?d00001 diff --git a/icons/IcTwotoneRestartAlt.png b/icons/IcTwotoneRestartAlt.png new file mode 100644 index 0000000000000000000000000000000000000000..3cf2410e5b9d19c6841c202b3f2b1ae1716eb0c7 GIT binary patch literal 15011 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 PgQ$vvrhMsb>qq|=3tpi~ literal 0 HcmV?d00001 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