mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
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:
@@ -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 |
@@ -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
@@ -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
@@ -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
@@ -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; "
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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(); }
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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; "
|
||||
"}");
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
Reference in New Issue
Block a user