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