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.
This commit is contained in:
uncor3
2025-10-09 21:24:45 -07:00
parent 777ea21a00
commit 8d4f4b11f9
33 changed files with 1067 additions and 430 deletions
-1
View File
@@ -230,7 +230,6 @@ target_link_libraries(iDescriptor PRIVATE
${SSL_LIBRARY}
${CRYPTO_LIBRARY}
${SSH_LIBRARY}
${HEIF_LIBRARIES}
# ${FRIDA_LIBRARY}
# ${ZIP_LIBRARY}
PkgConfig::PUGIXML
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+3
View File
@@ -5,6 +5,9 @@
<file>icons/MdiLightningBolt.png</file>
<file>icons/MingcuteSettings7Line.png</file>
<file>icons/ClarityHardDiskSolidAlerted.png</file>
<file>icons/IcOutlinePowerSettingsNew.png</file>
<file>icons/HugeiconsWrench01.png</file>
<file>icons/IcTwotoneRestartAlt.png</file>
<file>icons/icon.png</file>
<file>qml/MapView.qml</file>
<file>resources/dump.js</file>
+160 -56
View File
@@ -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 <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QRegularExpression>
#include <QScrollBar>
#include <QSignalBlocker>
#include <QSplitter>
#include <QStyle>
#include <QTemporaryDir>
#include <QTreeWidget>
#include <QVariant>
@@ -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<QPushButton *>(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);
}
+15 -4
View File
@@ -1,10 +1,13 @@
#ifndef AFCEXPLORER_H
#define AFCEXPLORER_H
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include <QAction>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QPushButton>
@@ -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<QString> m_history;
QHBoxLayout *m_breadcrumbLayout;
QStack<QString> 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
+2 -1
View File
@@ -5,6 +5,7 @@
#include "appinstalldialog.h"
#include "appstoremanager.h"
#include "logindialog.h"
#include "zlineedit.h"
#include <QApplication>
#include <QComboBox>
#include <QDebug>
@@ -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; "
+5 -2
View File
@@ -1,6 +1,7 @@
// https://github.com/p-dobrzynski-dev/QtCustomWidgets/blob/master/batterywidget.cpp
#include "batterywidget.h"
#include <QApplication>
#include <QDebug>
#include <QFontMetrics>
#include <QPainter>
@@ -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);
+2 -6
View File
@@ -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()
{
+16
View File
@@ -0,0 +1,16 @@
#include <QGuiApplication>
#include <QPalette>
#include <QStyleHints>
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
}
+12 -13
View File
@@ -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;
}
}
-3
View File
@@ -24,9 +24,6 @@
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
// 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;
-3
View File
@@ -24,9 +24,6 @@
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
// 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;
+10 -9
View File
@@ -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; }
+51 -6
View File
@@ -5,6 +5,7 @@
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "infolabel.h"
#include "toolboxwidget.h"
#include <QApplication>
#include <QDebug>
#include <QGraphicsDropShadowEffect>
@@ -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"
+27 -24
View File
@@ -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; }");
+247 -124
View File
@@ -13,6 +13,9 @@
#include <QListView>
#include <QMessageBox>
#include <QPushButton>
#include <QRegularExpression>
#include <QStackedWidget>
#include <QStandardItemModel>
#include <QStandardPaths>
#include <QVBoxLayout>
#include <QtConcurrent/QtConcurrent>
@@ -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<int>(PhotoModel::NewestFirst));
m_sortComboBox->addItem("Oldest First",
static_cast<int>(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<int>(PhotoModel::ImagesOnly));
m_filterComboBox->addItem("Videos Only",
static_cast<int>(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<int>::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);
}
+18
View File
@@ -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;
+154 -1
View File
@@ -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<float>(y) / fadeStart;
currentColor.setAlphaF(alpha * borderColor.alphaF());
} else if (y > fadeEnd) {
// Fade from full opacity to transparent
float alpha = static_cast<float>(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<float>(y) / fadeStart;
currentColor.setAlphaF(alpha * borderColor.alphaF());
} else if (y > fadeEnd) {
float alpha = static_cast<float>(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:
+2
View File
@@ -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();
+37 -35
View File
@@ -3,6 +3,7 @@
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "qprocessindicator.h"
#include "zlineedit.h"
#include <QAction>
#include <QApplication>
#include <QDebug>
@@ -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<QVariantMap>::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<QVariantMap> 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);
}
+4 -3
View File
@@ -2,6 +2,7 @@
#define INSTALLEDAPPSWIDGET_H
#include "iDescriptor.h"
#include "zlineedit.h"
#include <QCheckBox>
#include <QEnterEvent>
#include <QFrame>
@@ -9,7 +10,6 @@
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QPainter>
@@ -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<AppTabWidget *> 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;
+2
View File
@@ -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);
+51 -8
View File
@@ -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;
}
}
}
void PhotoModel::setAlbumPath(const QString &albumPath)
{
if (m_albumPath != albumPath) {
m_albumPath = albumPath;
populatePhotoPaths();
}
}
void PhotoModel::refreshPhotos() { populatePhotoPaths(); }
+5
View File
@@ -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<PhotoInfo> m_allPhotos; // All photos from device
QList<PhotoInfo> m_photos; // Currently filtered/sorted photos
+1 -1
View File
@@ -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
+14 -4
View File
@@ -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();
}
+117 -105
View File
@@ -1,64 +1,59 @@
#include "sshterminalwidget.h"
#include "qprocessindicator.h"
#include <QVBoxLayout>
#include <QDebug>
#include <QFile>
#include <QHBoxLayout>
#include <QHostAddress>
#include <QLabel>
#include <QMenu>
#include <QProcess>
#include <QProcessEnvironment>
#include <QPushButton>
#include <QStackedWidget>
#include <QTimer>
#include <QProcess>
#include <QProcessEnvironment>
#include <QHostAddress>
#include <QFile>
#include <QDebug>
#include <QMenu>
#include <qtermwidget6/qtermwidget.h>
#include <QVBoxLayout>
#include <libssh/libssh.h>
#include <qtermwidget6/qtermwidget.h>
#include <unistd.h>
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<int>(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;
}
+54 -20
View File
@@ -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();
}
+3 -1
View File
@@ -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();
+36
View File
@@ -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; "
"}");
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <QApplication>
#include <QLineEdit>
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();
};