mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
840 lines
30 KiB
C++
840 lines
30 KiB
C++
/*
|
|
* iDescriptor: A free and open-source idevice management tool.
|
|
*
|
|
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "appswidget.h"
|
|
#include "appcontext.h"
|
|
#include "appdownloadbasedialog.h"
|
|
#include "appdownloaddialog.h"
|
|
#include "appinstalldialog.h"
|
|
#include "appstoremanager.h"
|
|
#include "creddialog.h"
|
|
#include "iDescriptor-ui.h"
|
|
#include "iDescriptor.h"
|
|
#include "keychaindialog.h"
|
|
#include "logindialog.h"
|
|
#include "mainwindow.h"
|
|
#include "settingsmanager.h"
|
|
#include "sponsorwidget.h"
|
|
#include "zlineedit.h"
|
|
#include <QApplication>
|
|
#include <QComboBox>
|
|
#include <QDebug>
|
|
#include <QDesktopServices>
|
|
#include <QFile>
|
|
#include <QFileDialog>
|
|
#include <QGridLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QIcon>
|
|
#include <QImage>
|
|
#include <QInputDialog>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QMessageBox>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QPixmap>
|
|
#include <QProgressBar>
|
|
#include <QPushButton>
|
|
#include <QScrollArea>
|
|
#include <QStyle>
|
|
#include <QTimer>
|
|
#include <QVBoxLayout>
|
|
#include <QWidget>
|
|
#include <QtConcurrent/QtConcurrent>
|
|
|
|
// FIXME: we dont watch for login and logout events because there is no such api
|
|
AppsWidget *AppsWidget::sharedInstance()
|
|
{
|
|
static AppsWidget *instance = new AppsWidget();
|
|
return instance;
|
|
}
|
|
|
|
AppsWidget::AppsWidget(QWidget *parent) : QWidget(parent), m_isLoggedIn(false)
|
|
{
|
|
m_debounceTimer = new QTimer(this);
|
|
setupUI();
|
|
}
|
|
|
|
void AppsWidget::setupUI()
|
|
{
|
|
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
mainLayout->setSpacing(0);
|
|
// Header with login
|
|
m_networkManager = new QNetworkAccessManager(this);
|
|
|
|
QWidget *headerWidget = new QWidget();
|
|
headerWidget->setFixedHeight(60);
|
|
headerWidget->setStyleSheet("border-bottom: 1px solid #363d32;");
|
|
|
|
QHBoxLayout *headerLayout = new QHBoxLayout(headerWidget);
|
|
headerLayout->setContentsMargins(20, 10, 20, 10);
|
|
|
|
// Create status label first
|
|
m_statusLabel = new QLabel("Not signed in");
|
|
m_statusLabel->setStyleSheet("margin-right: 20px;");
|
|
|
|
m_loginButton = new QPushButton();
|
|
m_searchEdit = new ZLineEdit();
|
|
m_searchEdit->setMaximumWidth(350);
|
|
|
|
// --- Status and Login Button ---
|
|
m_manager = AppStoreManager::sharedInstance();
|
|
|
|
m_statusLabel->setStyleSheet("font-size: 14px; color: #666;");
|
|
|
|
mainLayout->addWidget(headerWidget);
|
|
|
|
static ZIcon searchIcon(":/resources/icons/MdiLightMagnify.png");
|
|
m_searchAction = m_searchEdit->addAction(
|
|
searchIcon.getThemedPixmap(QSize(16, 16), palette()),
|
|
QLineEdit::TrailingPosition);
|
|
m_searchAction->setToolTip("Search");
|
|
connect(m_searchAction, &QAction::triggered, this,
|
|
&AppsWidget::performSearch);
|
|
|
|
// Update search icon when theme changes
|
|
connect(qApp, &QApplication::paletteChanged, this, [this]() {
|
|
m_searchAction->setIcon(
|
|
searchIcon.getThemedPixmap(QSize(16, 16), palette()));
|
|
});
|
|
|
|
headerLayout->addWidget(m_searchEdit);
|
|
headerLayout->addStretch();
|
|
headerLayout->addWidget(m_statusLabel);
|
|
headerLayout->addWidget(m_loginButton);
|
|
|
|
// Stacked widget for different pages
|
|
m_stackedWidget = new QStackedWidget();
|
|
setupDefaultAppsPage();
|
|
setupLoadingPage();
|
|
setupErrorPage();
|
|
|
|
mainLayout->addWidget(m_stackedWidget);
|
|
|
|
// Show default apps initially
|
|
showLoading("Loading apps...");
|
|
// 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_manager, &AppStoreManager::loginSuccessful, this,
|
|
&AppsWidget::onAppStoreInitialized);
|
|
connect(m_manager, &AppStoreManager::loggedOut, this,
|
|
&AppsWidget::onAppStoreInitialized);
|
|
}
|
|
|
|
void AppsWidget::init()
|
|
{
|
|
QUrl sponsorsUrl(SPONSORS_JSON_URL);
|
|
QNetworkRequest request(sponsorsUrl);
|
|
QNetworkReply *reply = m_networkManager->get(request);
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
|
try {
|
|
if (reply->error() == QNetworkReply::NoError) {
|
|
QByteArray responseData = reply->readAll();
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
|
|
if (jsonDoc.isNull() || !jsonDoc.isObject()) {
|
|
qDebug() << "Failed to parse sponsors JSON";
|
|
showDefaultApps();
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
|
|
QJsonObject rootObj = jsonDoc.object();
|
|
QJsonObject versioned = getVersionedConfig(rootObj);
|
|
|
|
if (versioned.isEmpty()) {
|
|
qDebug() << "No sponsor configuration found for version"
|
|
<< APP_VERSION << "or default.";
|
|
showDefaultApps();
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
|
|
QJsonObject sponsorObj = versioned["sponsors"].toObject();
|
|
QJsonObject platinumObj = sponsorObj["platinum"].toObject();
|
|
QJsonObject goldObj = sponsorObj["gold"].toObject();
|
|
QJsonObject silverObj = sponsorObj["silver"].toObject();
|
|
QJsonObject bronzeObj = sponsorObj["bronze"].toObject();
|
|
|
|
m_platinumSponsors = platinumObj["members"].toArray();
|
|
m_goldSponsors = goldObj["members"].toArray();
|
|
m_silverSponsors = silverObj["members"].toArray();
|
|
m_bronzeSponsors = bronzeObj["members"].toArray();
|
|
|
|
if (!m_platinumSponsors.isEmpty()) {
|
|
qDebug() << "Platinum Sponsors found";
|
|
}
|
|
}
|
|
qDebug() << "Sponsors fetch completed";
|
|
reply->deleteLater();
|
|
QTimer::singleShot(0, this, &AppsWidget::handleInit);
|
|
} catch (...) {
|
|
qDebug() << "Exception occurred while processing sponsors";
|
|
reply->deleteLater();
|
|
QTimer::singleShot(0, this, &AppsWidget::handleInit);
|
|
}
|
|
});
|
|
}
|
|
|
|
void AppsWidget::handleInit()
|
|
{
|
|
if (!m_manager) {
|
|
qDebug() << "AppStoreManager failed to initialize";
|
|
m_statusLabel->setText("Failed to initialize");
|
|
m_loginButton->setText("Failed to initialize");
|
|
m_loginButton->setEnabled(false);
|
|
m_loginButton->setStyleSheet(
|
|
"background-color: #ccc; color: #666; "
|
|
"border: "
|
|
"none; border-radius: "
|
|
"4px; padding: 8px 16px; font-size: 14px;");
|
|
return;
|
|
}
|
|
/*
|
|
FIXME: ipatoolinitialze still uses the secure backends
|
|
when if the user rejects it, the moment he/she tries to sign in
|
|
prompt(keychain or secret-service whatever the backend is) will be seen
|
|
again
|
|
*/
|
|
if (!SettingsManager::sharedInstance()->useUnsecureBackend() &&
|
|
SettingsManager::sharedInstance()->showKeychainDialog()) {
|
|
#ifdef __APPLE__
|
|
KeychainDialog dialog(this);
|
|
if (dialog.exec() == QDialog::Rejected) {
|
|
// pass empty QJsonObject to skip signing in
|
|
onAppStoreInitialized(QJsonObject());
|
|
showDefaultApps();
|
|
return;
|
|
}
|
|
// windows doesn't show any keychain dialog
|
|
#elif __linux__
|
|
CredDialog dialog(this);
|
|
if (dialog.exec() == QDialog::Rejected) {
|
|
// pass empty QJsonObject to skip signing in
|
|
onAppStoreInitialized(QJsonObject());
|
|
showDefaultApps();
|
|
return;
|
|
}
|
|
#endif
|
|
}
|
|
onAppStoreInitialized(m_manager->getAccountInfo());
|
|
showDefaultApps();
|
|
}
|
|
|
|
void AppsWidget::onAppStoreInitialized(const QJsonObject &accountInfo)
|
|
{
|
|
if (accountInfo.contains("success") &&
|
|
accountInfo.value("success").toBool()) {
|
|
if (accountInfo.contains("email")) {
|
|
QString email = accountInfo.value("email").toString();
|
|
m_statusLabel->setText("Signed in as " + email);
|
|
m_isLoggedIn = true;
|
|
m_searchEdit->setDisabled(false);
|
|
} else {
|
|
m_statusLabel->setText("Not signed in");
|
|
m_searchEdit->setDisabled(true);
|
|
}
|
|
} else {
|
|
m_searchEdit->setDisabled(true);
|
|
m_statusLabel->setText("Not signed in");
|
|
}
|
|
|
|
m_loginButton->setText(m_isLoggedIn ? "Sign Out" : "Sign In");
|
|
m_loginButton->setStyleSheet(
|
|
"background-color: #007AFF; color: white; border: none; "
|
|
"border-radius: "
|
|
"4px; padding: 8px 16px; font-size: 14px;");
|
|
m_searchEdit->setPlaceholderText(m_isLoggedIn ? "Search for apps..."
|
|
: "Sign in to search");
|
|
}
|
|
|
|
void AppsWidget::setupDefaultAppsPage()
|
|
{
|
|
m_defaultAppsPage = new QWidget();
|
|
|
|
// Scroll area for apps
|
|
m_scrollArea = new QScrollArea();
|
|
m_scrollArea->setWidgetResizable(true);
|
|
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
m_scrollArea->setStyleSheet(
|
|
"QScrollArea { background: transparent; border: none; }");
|
|
m_scrollArea->viewport()->setStyleSheet("background: transparent;");
|
|
|
|
m_contentWidget = new QWidget();
|
|
QGridLayout *gridLayout = new QGridLayout(m_contentWidget);
|
|
gridLayout->setContentsMargins(20, 20, 20, 20);
|
|
gridLayout->setSpacing(20);
|
|
|
|
m_scrollArea->setWidget(m_contentWidget);
|
|
|
|
QVBoxLayout *pageLayout = new QVBoxLayout(m_defaultAppsPage);
|
|
pageLayout->setContentsMargins(0, 0, 0, 0);
|
|
pageLayout->addWidget(m_scrollArea);
|
|
|
|
m_stackedWidget->addWidget(m_defaultAppsPage);
|
|
}
|
|
|
|
void AppsWidget::setupLoadingPage()
|
|
{
|
|
m_loadingPage = new QWidget();
|
|
|
|
QVBoxLayout *loadingLayout = new QVBoxLayout(m_loadingPage);
|
|
loadingLayout->setAlignment(Qt::AlignCenter);
|
|
|
|
m_loadingIndicator = new QProcessIndicator();
|
|
m_loadingIndicator->setType(QProcessIndicator::line_rotate);
|
|
m_loadingIndicator->setFixedSize(64, 32);
|
|
|
|
m_loadingLabel = new QLabel("Loading...");
|
|
m_loadingLabel->setAlignment(Qt::AlignCenter);
|
|
m_loadingLabel->setStyleSheet(
|
|
"font-size: 16px; color: #666; margin-top: 20px;");
|
|
|
|
loadingLayout->addWidget(m_loadingIndicator, 0, Qt::AlignCenter);
|
|
loadingLayout->addWidget(m_loadingLabel, 0, Qt::AlignCenter);
|
|
|
|
m_stackedWidget->addWidget(m_loadingPage);
|
|
}
|
|
|
|
void AppsWidget::setupErrorPage()
|
|
{
|
|
m_errorPage = new QWidget();
|
|
|
|
QVBoxLayout *errorLayout = new QVBoxLayout(m_errorPage);
|
|
errorLayout->setAlignment(Qt::AlignCenter);
|
|
|
|
m_errorLabel = new QLabel("Error occurred");
|
|
m_errorLabel->setAlignment(Qt::AlignCenter);
|
|
m_errorLabel->setWordWrap(true);
|
|
m_errorLabel->setStyleSheet("font-size: 16px; color: #666;");
|
|
|
|
errorLayout->addWidget(m_errorLabel, 0, Qt::AlignCenter);
|
|
|
|
m_stackedWidget->addWidget(m_errorPage);
|
|
}
|
|
|
|
void AppsWidget::showDefaultApps()
|
|
{
|
|
clearAppGrid();
|
|
populateDefaultApps();
|
|
m_stackedWidget->setCurrentWidget(m_defaultAppsPage);
|
|
}
|
|
|
|
void AppsWidget::showLoading(const QString &message)
|
|
{
|
|
m_loadingLabel->setText(message);
|
|
m_loadingIndicator->start();
|
|
m_stackedWidget->setCurrentWidget(m_loadingPage);
|
|
}
|
|
|
|
void AppsWidget::showError(const QString &message)
|
|
{
|
|
m_loadingIndicator->stop();
|
|
m_errorLabel->setText(message);
|
|
m_stackedWidget->setCurrentWidget(m_errorPage);
|
|
}
|
|
|
|
void AppsWidget::populateDefaultApps()
|
|
{
|
|
QGridLayout *gridLayout =
|
|
qobject_cast<QGridLayout *>(m_contentWidget->layout());
|
|
if (!gridLayout)
|
|
return;
|
|
|
|
int row = 0;
|
|
int col = 0;
|
|
const int maxCols = 3;
|
|
|
|
auto advanceGridPos = [&]() {
|
|
col++;
|
|
if (col >= maxCols) {
|
|
col = 0;
|
|
row++;
|
|
}
|
|
};
|
|
|
|
for (const QJsonValue &sponsorValue : m_platinumSponsors) {
|
|
QJsonObject sponsorObj = sponsorValue.toObject();
|
|
QString name = sponsorObj.value("name").toString();
|
|
QString bundleId = sponsorObj.value("bundleId").toString();
|
|
QString logoUrl = sponsorObj.value("logo").toString();
|
|
QString description = sponsorObj.value("description").toString();
|
|
QString url = sponsorObj.value("url").toString();
|
|
bool useBundleIdForIcon =
|
|
sponsorObj.value("useBundleIdForIcon").toBool(true);
|
|
createAppCard(name, bundleId, description, logoUrl, url, gridLayout,
|
|
row, col, useBundleIdForIcon,
|
|
SponsorType(SponsorType::Platinum));
|
|
advanceGridPos();
|
|
}
|
|
|
|
for (const QJsonValue &sponsorValue : m_goldSponsors) {
|
|
QJsonObject sponsorObj = sponsorValue.toObject();
|
|
QString name = sponsorObj.value("name").toString();
|
|
QString bundleId = sponsorObj.value("bundleId").toString();
|
|
QString description = sponsorObj.value("description").toString();
|
|
QString logoUrl = sponsorObj.value("logo").toString();
|
|
QString url = sponsorObj.value("url").toString();
|
|
bool useBundleIdForIcon =
|
|
sponsorObj.value("useBundleIdForIcon").toBool(true);
|
|
createAppCard(name, bundleId, description, logoUrl, url, gridLayout,
|
|
row, col, useBundleIdForIcon,
|
|
SponsorType(SponsorType::Gold));
|
|
advanceGridPos();
|
|
}
|
|
|
|
for (const QJsonValue &sponsorValue : m_silverSponsors) {
|
|
QJsonObject sponsorObj = sponsorValue.toObject();
|
|
QString name = sponsorObj.value("name").toString();
|
|
QString bundleId = sponsorObj.value("bundleId").toString();
|
|
QString description = sponsorObj.value("description").toString();
|
|
QString url = sponsorObj.value("url").toString();
|
|
QString logoUrl = sponsorObj.value("logo").toString();
|
|
bool useBundleIdForIcon =
|
|
sponsorObj.value("useBundleIdForIcon").toBool(true);
|
|
|
|
createAppCard(name, bundleId, description, logoUrl, url, gridLayout,
|
|
row, col, useBundleIdForIcon,
|
|
SponsorType(SponsorType::Silver));
|
|
advanceGridPos();
|
|
}
|
|
|
|
for (const QJsonValue &sponsorValue : m_bronzeSponsors) {
|
|
QJsonObject sponsorObj = sponsorValue.toObject();
|
|
QString name = sponsorObj.value("name").toString();
|
|
QString bundleId = sponsorObj.value("bundleId").toString();
|
|
QString description = sponsorObj.value("description").toString();
|
|
QString url = sponsorObj.value("url").toString();
|
|
QString logoUrl = sponsorObj.value("logo").toString();
|
|
|
|
bool useBundleIdForIcon =
|
|
sponsorObj.value("useBundleIdForIcon").toBool(true);
|
|
createAppCard(name, bundleId, description, logoUrl, url, gridLayout,
|
|
row, col, useBundleIdForIcon,
|
|
SponsorType(SponsorType::Bronze));
|
|
advanceGridPos();
|
|
}
|
|
|
|
if (m_platinumSponsors.empty() && m_goldSponsors.empty()) {
|
|
createSponsorCard(gridLayout, row, col);
|
|
advanceGridPos();
|
|
}
|
|
|
|
createAppCard("Instagram", "com.burbn.instagram",
|
|
"Photo & Video sharing social network", "", "", gridLayout,
|
|
row, col);
|
|
advanceGridPos();
|
|
createAppCard("Spotify", "com.spotify.client",
|
|
"Music streaming and podcast platform", "", "", gridLayout,
|
|
row, col);
|
|
advanceGridPos();
|
|
createAppCard("YouTube", "com.google.ios.youtube",
|
|
"Video sharing and streaming platform", "", "", gridLayout,
|
|
row, col);
|
|
advanceGridPos();
|
|
createAppCard("X", "com.atebits.Tweetie2", "Social media and microblogging",
|
|
"", "", gridLayout, row, col);
|
|
advanceGridPos();
|
|
createAppCard("TikTok", "com.zhiliaoapp.musically",
|
|
"Short-form video hosting service", "", "", gridLayout, row,
|
|
col);
|
|
advanceGridPos();
|
|
createAppCard("Twitch", "tv.twitch", "Live streaming platform", "", "",
|
|
gridLayout, row, col);
|
|
advanceGridPos();
|
|
createAppCard("Telegram", "ph.telegra.Telegraph",
|
|
"Cloud-based instant messaging", "", "", gridLayout, row,
|
|
col);
|
|
advanceGridPos();
|
|
createAppCard("Reddit", "com.reddit.Reddit",
|
|
"Social news aggregation platform", "", "", gridLayout, row,
|
|
col);
|
|
advanceGridPos();
|
|
|
|
gridLayout->setRowStretch(gridLayout->rowCount(), 1);
|
|
}
|
|
|
|
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::createSponsorCard(QGridLayout *gridLayout, int row, int col)
|
|
{
|
|
if (!gridLayout)
|
|
return;
|
|
|
|
ClickableWidget *sponsorCard = new ClickableWidget();
|
|
sponsorCard->setStyleSheet("border: 1px solid #ddd; border-radius: 8px;");
|
|
sponsorCard->setCursor(Qt::PointingHandCursor);
|
|
connect(sponsorCard, &ClickableWidget::clicked, this, [this]() {
|
|
auto sWidget = new SponsorWidget();
|
|
sWidget->setAttribute(Qt::WA_DeleteOnClose);
|
|
sWidget->show();
|
|
});
|
|
QVBoxLayout *sponsorLayout = new QVBoxLayout(sponsorCard);
|
|
sponsorLayout->setContentsMargins(12, 12, 12, 12);
|
|
sponsorLayout->setSpacing(8);
|
|
|
|
QLabel *sponsorLabel = new QLabel("Become a Sponsor!");
|
|
sponsorLabel->setAlignment(Qt::AlignCenter);
|
|
sponsorLabel->setStyleSheet("font-size: 14px; font-weight: bold;");
|
|
sponsorLayout->addWidget(sponsorLabel);
|
|
|
|
gridLayout->addWidget(sponsorCard, row, col);
|
|
}
|
|
|
|
void AppsWidget::createAppCard(
|
|
const QString &name, const QString &bundleId, const QString &description,
|
|
const QString &logoUrl, const QString &websiteUrl, QGridLayout *gridLayout,
|
|
int row, int col, bool useBundleIdForIcon, const SponsorType &sponsorType)
|
|
{
|
|
QWidget *cardWidget = new QWidget();
|
|
|
|
QHBoxLayout *cardLayout = new QHBoxLayout(cardWidget);
|
|
cardLayout->setContentsMargins(15, 15, 15, 15);
|
|
cardLayout->setSpacing(10);
|
|
|
|
// App icon
|
|
QLabel *iconLabel = new QLabel();
|
|
QPointer<QLabel> safeIconLabel = iconLabel;
|
|
QPixmap placeholderIcon = QApplication::style()
|
|
->standardIcon(QStyle::SP_ComputerIcon)
|
|
.pixmap(64, 64);
|
|
iconLabel->setPixmap(placeholderIcon);
|
|
iconLabel->setAlignment(Qt::AlignCenter);
|
|
cardLayout->addWidget(iconLabel);
|
|
|
|
if (!logoUrl.isEmpty() && !useBundleIdForIcon) {
|
|
QUrl url(logoUrl);
|
|
QNetworkRequest request(url);
|
|
QNetworkReply *reply = m_networkManager->get(request);
|
|
connect(
|
|
reply, &QNetworkReply::finished, this, [reply, safeIconLabel]() {
|
|
if (reply->error() == QNetworkReply::NoError && safeIconLabel) {
|
|
QByteArray data = reply->readAll();
|
|
QPixmap pixmap;
|
|
if (pixmap.loadFromData(data)) {
|
|
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();
|
|
|
|
safeIconLabel->setPixmap(rounded);
|
|
}
|
|
}
|
|
reply->deleteLater();
|
|
});
|
|
connect(iconLabel, &QObject::destroyed, reply, &QNetworkReply::abort);
|
|
} else if (!bundleId.isEmpty()) {
|
|
fetchAppIconFromApple(
|
|
m_networkManager, bundleId, [safeIconLabel](const QPixmap &pixmap) {
|
|
// Check if iconLabel still exists
|
|
if (safeIconLabel && !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();
|
|
|
|
safeIconLabel->setPixmap(rounded);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Vertical layout for name and description
|
|
QVBoxLayout *textLayout = new QVBoxLayout();
|
|
|
|
// App name with sponsor indicator
|
|
QHBoxLayout *nameLayout = new QHBoxLayout();
|
|
QLabel *nameLabel = new QLabel(name);
|
|
nameLabel->setStyleSheet("font-size: 16px;");
|
|
nameLabel->setWordWrap(true);
|
|
nameLayout->addWidget(nameLabel);
|
|
|
|
// Add sponsor type indicator
|
|
if (!sponsorType.isEmpty()) {
|
|
QLabel *sponsorLabel = new QLabel(sponsorType.name);
|
|
sponsorLabel->setStyleSheet(QString("font-size: 10px; "
|
|
"font-weight: bold; "
|
|
"color: #333; "
|
|
"background-color: %2; "
|
|
"border-radius: 4px; "
|
|
"padding: 2px 6px; "
|
|
"margin-left: 8px;")
|
|
.arg(sponsorType.color));
|
|
sponsorLabel->setFixedHeight(16);
|
|
sponsorLabel->setAlignment(Qt::AlignCenter);
|
|
nameLayout->addWidget(sponsorLabel);
|
|
}
|
|
|
|
nameLayout->addStretch();
|
|
textLayout->addLayout(nameLayout);
|
|
|
|
// App description
|
|
QLabel *descLabel = new QLabel(description);
|
|
descLabel->setStyleSheet("font-size: 12px; color: #666;");
|
|
descLabel->setAlignment(Qt::AlignLeft);
|
|
descLabel->setWordWrap(true);
|
|
textLayout->addWidget(descLabel);
|
|
|
|
cardLayout->addLayout(textLayout);
|
|
|
|
QVBoxLayout *buttonsLayout = new QVBoxLayout();
|
|
|
|
// Install button placeholder
|
|
if (!bundleId.isEmpty()) {
|
|
ZLabel *installLabel = new ZLabel("Install");
|
|
installLabel->setAlignment(Qt::AlignCenter);
|
|
installLabel->setStyleSheet(
|
|
"font-size: 12px; color: #007AFF; font-weight: "
|
|
"bold; background-color: transparent;");
|
|
installLabel->setCursor(Qt::PointingHandCursor);
|
|
installLabel->setFixedHeight(30);
|
|
|
|
buttonsLayout->addStretch();
|
|
buttonsLayout->addWidget(installLabel);
|
|
connect(installLabel, &ZLabel::clicked, this,
|
|
[this, name, bundleId, description]() {
|
|
onAppCardClicked(name, bundleId, description);
|
|
});
|
|
}
|
|
if (websiteUrl.isEmpty()) {
|
|
ZLabel *downloadIpaLabel = new ZLabel("Download IPA");
|
|
downloadIpaLabel->setAlignment(Qt::AlignCenter);
|
|
downloadIpaLabel->setStyleSheet("font-size: 12px; font-weight: "
|
|
"bold; background-color: transparent;");
|
|
downloadIpaLabel->setCursor(Qt::PointingHandCursor);
|
|
|
|
connect(
|
|
downloadIpaLabel, &ZLabel::clicked, this,
|
|
[this, name, bundleId]() { onDownloadIpaClicked(name, bundleId); });
|
|
|
|
buttonsLayout->addWidget(downloadIpaLabel);
|
|
} else {
|
|
ZLabel *websiteLabel = new ZLabel("Website");
|
|
websiteLabel->setStyleSheet("font-size: 12px; font-weight: "
|
|
"bold; background-color: transparent;");
|
|
websiteLabel->setAlignment(Qt::AlignCenter);
|
|
websiteLabel->setCursor(Qt::PointingHandCursor);
|
|
|
|
connect(websiteLabel, &ZLabel::clicked, this, [this, websiteUrl]() {
|
|
QDesktopServices::openUrl(QUrl(websiteUrl));
|
|
});
|
|
buttonsLayout->addWidget(websiteLabel);
|
|
}
|
|
|
|
buttonsLayout->addStretch();
|
|
|
|
cardLayout->addLayout(buttonsLayout);
|
|
gridLayout->addWidget(cardWidget, row, col);
|
|
}
|
|
void AppsWidget::onDownloadIpaClicked(const QString &name,
|
|
const QString &bundleId)
|
|
{
|
|
if (!m_isLoggedIn) {
|
|
QMessageBox::information(this, "Sign In Required",
|
|
"Please sign in to download IPA files.");
|
|
return;
|
|
}
|
|
QString description = "Download the IPA file for " + name;
|
|
AppDownloadDialog dialog(name, bundleId, description, this);
|
|
dialog.exec();
|
|
}
|
|
|
|
void AppsWidget::onLoginClicked()
|
|
{
|
|
if (m_isLoggedIn) {
|
|
AppStoreManager *manager = AppStoreManager::sharedInstance();
|
|
if (manager) {
|
|
manager->revokeCredentials();
|
|
}
|
|
m_isLoggedIn = false;
|
|
m_loginButton->setText("Sign In");
|
|
m_statusLabel->setText("Not signed in");
|
|
m_searchEdit->setPlaceholderText("Sign in to search");
|
|
return;
|
|
}
|
|
|
|
LoginDialog dialog(this);
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
// Login was successful, update UI
|
|
AppStoreManager *manager = AppStoreManager::sharedInstance();
|
|
if (manager) {
|
|
QJsonObject accountInfo = manager->getAccountInfo();
|
|
if (accountInfo.contains("success") &&
|
|
accountInfo.value("success").toBool()) {
|
|
if (accountInfo.contains("email")) {
|
|
QString email = accountInfo.value("email").toString();
|
|
m_statusLabel->setText("Signed in as " + email);
|
|
m_isLoggedIn = true;
|
|
m_loginButton->setText("Sign Out");
|
|
m_searchEdit->setPlaceholderText("Search for apps...");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AppsWidget::onAppCardClicked(const QString &appName,
|
|
const QString &bundleId,
|
|
const QString &description)
|
|
{
|
|
if (!m_isLoggedIn) {
|
|
QMessageBox::information(this, "Sign In Required",
|
|
"Please sign in to install apps.");
|
|
return;
|
|
}
|
|
|
|
AppInstallDialog dialog(appName, description, bundleId, this);
|
|
dialog.exec();
|
|
}
|
|
|
|
void AppsWidget::onSearchTextChanged() { m_debounceTimer->start(300); }
|
|
|
|
void AppsWidget::performSearch()
|
|
{
|
|
QString searchTerm = m_searchEdit->text().trimmed();
|
|
if (searchTerm.isEmpty()) {
|
|
showDefaultApps();
|
|
return;
|
|
}
|
|
|
|
showLoading(QString("Searching for \"%1\"...").arg(searchTerm));
|
|
|
|
AppStoreManager *manager = AppStoreManager::sharedInstance();
|
|
if (!manager) {
|
|
showError("Failed to initialize App Store manager.");
|
|
return;
|
|
}
|
|
|
|
manager->searchApps(searchTerm, 20,
|
|
[this](bool success, const QString &results) {
|
|
onSearchFinished(success, results);
|
|
});
|
|
}
|
|
|
|
void AppsWidget::onSearchFinished(bool success, const QString &results)
|
|
{
|
|
// FIXME: cancel fetch instead of just ignoring results
|
|
QString searchTerm = m_searchEdit->text().trimmed();
|
|
if (searchTerm.isEmpty()) {
|
|
showDefaultApps();
|
|
return;
|
|
}
|
|
|
|
if (!success || results.isEmpty()) {
|
|
showError("No apps found or search failed.");
|
|
return;
|
|
}
|
|
|
|
QJsonParseError parseError;
|
|
QJsonDocument doc = QJsonDocument::fromJson(results.toUtf8(), &parseError);
|
|
|
|
if (parseError.error != QJsonParseError::NoError) {
|
|
qDebug() << "JSON parse error:" << parseError.errorString()
|
|
<< " on output: " << results;
|
|
showError("Failed to parse search results.");
|
|
return;
|
|
}
|
|
|
|
qDebug() << "Search results:" << doc;
|
|
QJsonObject rootObj = doc.object();
|
|
if (!rootObj.value("success").toBool()) {
|
|
QString errorMessage =
|
|
rootObj.value("error").toString("Unknown search error.");
|
|
showError(QString("Search error: %1").arg(errorMessage));
|
|
return;
|
|
}
|
|
|
|
QJsonArray resultsArray = rootObj.value("results").toArray();
|
|
if (resultsArray.isEmpty()) {
|
|
showError("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 : resultsArray) {
|
|
QJsonObject appObj = appValue.toObject();
|
|
QString name = appObj.value("trackName").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);
|
|
m_stackedWidget->setCurrentWidget(m_defaultAppsPage);
|
|
}
|