add search functionality and fetch app icons from Apple

This commit is contained in:
uncor3
2025-07-28 13:57:08 +00:00
parent b0e8504bd5
commit 7d7b57ee43
2 changed files with 296 additions and 14 deletions
+284 -14
View File
@@ -3,7 +3,6 @@
#include "appdownloadbasedialog.h"
#include "appdownloaddialog.h"
#include "appinstalldialog.h"
#include "libipatool.h"
#include <QApplication>
#include <QComboBox>
#include <QDialog>
@@ -31,6 +30,75 @@
#include <QVBoxLayout>
#include <QWidget>
#include <QAction>
#include <QBuffer>
#include <QDebug>
#include <QImage>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QUrl>
// Callback: void(QPixmap)
void fetchAppIconFromApple(const QString &bundleId,
std::function<void(const QPixmap &)> callback,
QObject *context)
{
QNetworkAccessManager *manager = new QNetworkAccessManager(context);
QString url =
QString("https://itunes.apple.com/lookup?bundleId=%1").arg(bundleId);
QNetworkReply *reply = manager->get(QNetworkRequest(QUrl(url)));
QObject::connect(
reply, &QNetworkReply::finished, context,
[reply, callback, manager, context]() {
QByteArray data = reply->readAll();
reply->deleteLater();
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
if (parseError.error != QJsonParseError::NoError) {
callback(QPixmap());
manager->deleteLater();
return;
}
QJsonObject obj = doc.object();
QJsonArray results = obj.value("results").toArray();
if (results.isEmpty()) {
callback(QPixmap());
manager->deleteLater();
return;
}
QJsonObject appInfo = results.at(0).toObject();
QString iconUrl = appInfo.value("artworkUrl100").toString();
if (iconUrl.isEmpty()) {
callback(QPixmap());
manager->deleteLater();
return;
}
// Fetch the icon image
QNetworkReply *iconReply =
manager->get(QNetworkRequest(QUrl(iconUrl)));
QObject::connect(iconReply, &QNetworkReply::finished, context,
[iconReply, callback, manager]() {
QByteArray iconData = iconReply->readAll();
iconReply->deleteLater();
QPixmap pixmap;
pixmap.loadFromData(iconData);
callback(pixmap);
manager->deleteLater();
});
});
}
// LoginDialog Implementation
LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent)
{
@@ -97,6 +165,8 @@ QString LoginDialog::getPassword() const { return m_passwordEdit->text(); }
AppsWidget::AppsWidget(QWidget *parent) : QWidget(parent), m_isLoggedIn(false)
{
m_searchProcess = new QProcess(this);
m_debounceTimer = new QTimer(this);
setupUI();
}
@@ -109,8 +179,7 @@ void AppsWidget::setupUI()
// Header with login
QFrame *headerFrame = new QFrame();
headerFrame->setFixedHeight(60);
headerFrame->setStyleSheet(
"background-color: #f8f9fa; border-bottom: 1px solid #dee2e6;");
headerFrame->setStyleSheet("border-bottom: 1px solid #dee2e6;");
QHBoxLayout *headerLayout = new QHBoxLayout(headerFrame);
headerLayout->setContentsMargins(20, 10, 20, 10);
@@ -124,6 +193,7 @@ void AppsWidget::setupUI()
// Create status label first
m_statusLabel = new QLabel("Not signed in");
m_statusLabel->setStyleSheet("margin-right: 20px;");
// --- Status and Login Button ---
// int init_result = IpaToolInitialize();
@@ -199,11 +269,37 @@ void AppsWidget::setupUI()
m_loginButton->setStyleSheet(
"background-color: #007AFF; color: white; border: none; border-radius: "
"4px; padding: 8px 16px; font-size: 14px;");
connect(m_loginButton, &QPushButton::clicked, this,
&AppsWidget::onLoginClicked);
headerLayout->addWidget(m_loginButton);
mainLayout->addWidget(headerFrame);
// --- Search Bar ---
QHBoxLayout *searchContainerLayout = new QHBoxLayout();
searchContainerLayout->setContentsMargins(20, 15, 20, 15);
m_searchEdit = new QLineEdit();
m_searchEdit->setPlaceholderText("Search for apps...");
m_searchEdit->setMaximumWidth(400);
m_searchEdit->setStyleSheet("QLineEdit { "
" padding: 8px; "
" border: 1px solid #ccc; "
" border-radius: 4px; "
" font-size: 14px; "
"}");
QAction *searchAction = m_searchEdit->addAction(
this->style()->standardIcon(QStyle::SP_FileDialogContentsView),
QLineEdit::TrailingPosition);
searchAction->setToolTip("Search");
connect(searchAction, &QAction::triggered, this,
&AppsWidget::performSearch);
searchContainerLayout->addStretch();
searchContainerLayout->addWidget(m_searchEdit);
searchContainerLayout->addStretch();
mainLayout->addLayout(searchContainerLayout);
// --- Status and Login Button ---
// Scroll area for apps
@@ -217,6 +313,32 @@ void AppsWidget::setupUI()
gridLayout->setContentsMargins(20, 20, 20, 20);
gridLayout->setSpacing(20);
populateDefaultApps();
m_scrollArea->setWidget(m_contentWidget);
mainLayout->addWidget(m_scrollArea);
// Connections
connect(m_loginButton, &QPushButton::clicked, this,
&AppsWidget::onLoginClicked);
connect(m_searchEdit, &QLineEdit::textChanged, this,
&AppsWidget::onSearchTextChanged);
m_debounceTimer->setSingleShot(true);
connect(m_debounceTimer, &QTimer::timeout, this,
&AppsWidget::performSearch);
connect(m_searchProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this,
&AppsWidget::onSearchFinished);
}
void AppsWidget::populateDefaultApps()
{
clearAppGrid();
QGridLayout *gridLayout =
qobject_cast<QGridLayout *>(m_contentWidget->layout());
if (!gridLayout)
return;
// Create sample app cards
createAppCard("Instagram", "com.burbn.instagram",
"Photo & Video sharing social network", "", gridLayout, 0, 0);
@@ -238,9 +360,37 @@ void AppsWidget::setupUI()
"Social news aggregation platform", "", gridLayout, 2, 2);
gridLayout->setRowStretch(gridLayout->rowCount(), 1);
}
m_scrollArea->setWidget(m_contentWidget);
mainLayout->addWidget(m_scrollArea);
void AppsWidget::clearAppGrid()
{
QGridLayout *gridLayout =
qobject_cast<QGridLayout *>(m_contentWidget->layout());
if (!gridLayout)
return;
QLayoutItem *item;
while ((item = gridLayout->takeAt(0)) != nullptr) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
}
void AppsWidget::showStatusMessage(const QString &message)
{
clearAppGrid();
QGridLayout *gridLayout =
qobject_cast<QGridLayout *>(m_contentWidget->layout());
if (!gridLayout)
return;
QLabel *statusLabel = new QLabel(message);
statusLabel->setAlignment(Qt::AlignCenter);
statusLabel->setWordWrap(true);
statusLabel->setStyleSheet("font-size: 16px; color: #666;");
gridLayout->addWidget(statusLabel, 0, 0, 1, -1, Qt::AlignCenter);
}
void AppsWidget::createAppCard(const QString &name, const QString &bundleId,
@@ -249,10 +399,16 @@ void AppsWidget::createAppCard(const QString &name, const QString &bundleId,
int row, int col)
{
QFrame *cardFrame = new QFrame();
cardFrame->setObjectName("cardFrame");
cardFrame->setFixedSize(200, 250);
cardFrame->setStyleSheet("QFrame { background-color: white; border: 1px "
"solid #e0e0e0; border-radius: 8px; }"
"QFrame:hover { border-color: #007AFF; }");
cardFrame->setStyleSheet("#cardFrame {"
" border: 1px solid #ddd;"
" border-radius: 8px;"
" background-color: #fff;"
"}"
"#cardFrame:hover {"
" border: 1.5px solid #007AFF;"
"}");
cardFrame->setCursor(Qt::PointingHandCursor);
QVBoxLayout *cardLayout = new QVBoxLayout(cardFrame);
@@ -261,13 +417,36 @@ void AppsWidget::createAppCard(const QString &name, const QString &bundleId,
// App icon
QLabel *iconLabel = new QLabel();
QPixmap icon = QApplication::style()
->standardIcon(QStyle::SP_ComputerIcon)
.pixmap(64, 64);
iconLabel->setPixmap(icon);
QPixmap placeholderIcon = QApplication::style()
->standardIcon(QStyle::SP_ComputerIcon)
.pixmap(64, 64);
iconLabel->setPixmap(placeholderIcon);
iconLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(iconLabel);
fetchAppIconFromApple(
bundleId,
[iconLabel](const QPixmap &pixmap) {
if (!pixmap.isNull()) {
QPixmap scaled =
pixmap.scaled(64, 64, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
QPixmap rounded(64, 64);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, 64, 64), 16, 16);
painter.setClipPath(path);
painter.drawPixmap(0, 0, scaled);
painter.end();
iconLabel->setPixmap(rounded);
}
},
cardFrame);
// App name
QLabel *nameLabel = new QLabel(name);
nameLabel->setStyleSheet(
@@ -352,3 +531,94 @@ void AppsWidget::onAppCardClicked(const QString &appName,
AppInstallDialog dialog(appName, description, this);
dialog.exec();
}
void AppsWidget::onSearchTextChanged() { m_debounceTimer->start(300); }
void AppsWidget::performSearch()
{
if (m_searchProcess->state() == QProcess::Running) {
m_searchProcess->kill();
m_searchProcess->waitForFinished();
}
QString searchTerm = m_searchEdit->text().trimmed();
if (searchTerm.isEmpty()) {
populateDefaultApps();
return;
}
showStatusMessage(QString("Searching for \"%1\"...").arg(searchTerm));
QStringList args;
args << "search" << searchTerm << "--non-interactive"
<< "--keychain-passphrase"
<< "iDescriptor"
<< "--format"
<< "json";
m_searchProcess->start("ipatool", args);
}
void AppsWidget::onSearchFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
QString jsonOutput = m_searchProcess->readAllStandardOutput();
if (jsonOutput.isEmpty() && exitCode != 0) {
QString errorOutput = m_searchProcess->readAllStandardError();
qDebug() << "Search process failed:" << errorOutput;
showStatusMessage(
QString("Search failed: %1")
.arg(errorOutput.isEmpty() ? "Unknown error" : errorOutput));
return;
}
QJsonParseError parseError;
QJsonDocument doc =
QJsonDocument::fromJson(jsonOutput.toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
qDebug() << "JSON parse error:" << parseError.errorString()
<< " on output: " << jsonOutput;
showStatusMessage("Failed to parse search results.");
return;
}
QJsonObject rootObj = doc.object();
if (rootObj.value("level").toString() == "error") {
QString errorMessage =
rootObj.value("error").toString("Unknown search error.");
showStatusMessage(QString("Search error: %1").arg(errorMessage));
return;
}
QJsonArray appsArray = rootObj.value("apps").toArray();
if (appsArray.isEmpty()) {
showStatusMessage("No apps found.");
return;
}
clearAppGrid();
QGridLayout *gridLayout =
qobject_cast<QGridLayout *>(m_contentWidget->layout());
if (!gridLayout)
return;
int row = 0;
int col = 0;
const int maxCols = 3;
for (const QJsonValue &appValue : appsArray) {
QJsonObject appObj = appValue.toObject();
QString name = appObj.value("name").toString();
QString bundleId = appObj.value("bundleID").toString();
QString description = "Version: " + appObj.value("version").toString();
createAppCard(name, bundleId, description, "", gridLayout, row, col);
col++;
if (col >= maxCols) {
col = 0;
row++;
}
}
gridLayout->setRowStretch(gridLayout->rowCount(), 1);
}
+12
View File
@@ -41,17 +41,29 @@ private slots:
void onLoginClicked();
void onAppCardClicked(const QString &appName, const QString &description);
void onDownloadIpaClicked(const QString &name, const QString &bundleId);
void onSearchTextChanged();
void performSearch();
void onSearchFinished(int exitCode, QProcess::ExitStatus exitStatus);
private:
void setupUI();
void createAppCard(const QString &name, const QString &bundleId,
const QString &description, const QString &iconPath,
QGridLayout *gridLayout, int row, int col);
void populateDefaultApps();
void clearAppGrid();
void showStatusMessage(const QString &message);
QScrollArea *m_scrollArea;
QWidget *m_contentWidget;
QPushButton *m_loginButton;
QLabel *m_statusLabel;
bool m_isLoggedIn;
// Search
QLineEdit *m_searchEdit;
QTimer *m_debounceTimer;
QProcess *m_searchProcess;
};
#endif // APPSWIDGET_H