Files
iDescriptor/src/appswidget.cpp
T
uncor3 b15b205f52 add initial recovery device support for new UI & update styling
- Added QStackedWidget to manage different states in InstalledAppsWidget.
- Created separate loading, error, and content widgets for better UI management.
- Updated LoginDialog title and adjusted styles for labels.
- Enhanced MainWindow by removing unused code and improving device handling.
- Updated QueryMobileGestaltWidget UI for better clarity and usability.
- Removed deprecated screenshot handling code in RealtimeScreen.
2025-10-06 15:30:47 -07:00

520 lines
17 KiB
C++

#include "appswidget.h"
#include "appcontext.h"
#include "appdownloadbasedialog.h"
#include "appdownloaddialog.h"
#include "appinstalldialog.h"
#include "appstoremanager.h"
#include "logindialog.h"
#include <QApplication>
#include <QComboBox>
#include <QDebug>
#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 <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QProgressBar>
#include <QPushButton>
#include <QScrollArea>
#include <QStyle>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
#include <QtConcurrent/QtConcurrent>
// watch for login and logout events
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
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 QLineEdit();
m_searchEdit->setMaximumWidth(400);
m_searchEdit->setStyleSheet("QLineEdit { "
" padding: 8px; "
" border: 1px solid #ccc; "
" border-radius: 4px; "
" font-size: 14px; "
"}");
// --- Status and Login Button ---
m_manager = AppStoreManager::sharedInstance();
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;");
} else {
onAppStoreInitialized(m_manager->getAccountInfo());
}
m_statusLabel->setStyleSheet("font-size: 14px; color: #666;");
mainLayout->addWidget(headerWidget);
QAction *searchAction = m_searchEdit->addAction(
this->style()->standardIcon(QStyle::SP_FileDialogContentsView),
QLineEdit::TrailingPosition);
searchAction->setToolTip("Search");
connect(searchAction, &QAction::triggered, this,
&AppsWidget::performSearch);
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
showDefaultApps();
// 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::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;
} else {
m_statusLabel->setText("Not signed in");
}
} else {
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;
// Create sample app cards
createAppCard("Instagram", "com.burbn.instagram",
"Photo & Video sharing social network", "", gridLayout, 0, 0);
createAppCard("WhatsApp", "net.whatsapp.WhatsApp",
"Free messaging and video calling", "", gridLayout, 0, 1);
createAppCard("Spotify", "com.spotify.client",
"Music streaming and podcast platform", "", gridLayout, 0, 2);
createAppCard("YouTube", "com.google.ios.youtube",
"Video sharing and streaming platform", "", gridLayout, 1, 0);
createAppCard("X", "com.atebits.Tweetie2", "Social media and microblogging",
"", gridLayout, 1, 1);
createAppCard("TikTok", "com.zhiliaoapp.musically",
"Short-form video hosting service", "", gridLayout, 1, 2);
createAppCard("Twitch", "tv.twitch", "Live streaming platform", "",
gridLayout, 2, 0);
createAppCard("Telegram", "ph.telegra.Telegraph",
"Cloud-based instant messaging", "", gridLayout, 2, 1);
createAppCard("Reddit", "com.reddit.Reddit",
"Social news aggregation platform", "", gridLayout, 2, 2);
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::showStatusMessage(const QString &message)
// {
// showError(message);
// }
void AppsWidget::createAppCard(const QString &name, const QString &bundleId,
const QString &description,
const QString &iconPath, QGridLayout *gridLayout,
int row, int col)
{
QWidget *cardWidget = new QWidget();
// cardWidget->setFixedSize(200, 250);
cardWidget->setCursor(Qt::PointingHandCursor);
QHBoxLayout *cardLayout = new QHBoxLayout(cardWidget);
cardLayout->setContentsMargins(15, 15, 15, 15);
cardLayout->setSpacing(10);
// App icon
QLabel *iconLabel = new QLabel();
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);
}
},
cardWidget);
// Vertical layout for name and description
QVBoxLayout *textLayout = new QVBoxLayout();
// App name
QLabel *nameLabel = new QLabel(name);
nameLabel->setStyleSheet("font-size: 16px;");
nameLabel->setAlignment(Qt::AlignCenter);
nameLabel->setWordWrap(true);
textLayout->addWidget(nameLabel);
// App description
QLabel *descLabel = new QLabel(description);
descLabel->setStyleSheet("font-size: 12px; color: #666;");
descLabel->setAlignment(Qt::AlignCenter);
descLabel->setWordWrap(true);
textLayout->addWidget(descLabel);
cardLayout->addStretch();
cardLayout->addLayout(textLayout);
QVBoxLayout *buttonsLayout = new QVBoxLayout();
// Install button placeholder
QPushButton *installLabel = new QPushButton("Install");
QPushButton *downloadIpaLabel = new QPushButton("Download IPA");
installLabel->setStyleSheet(
"font-size: 12px; color: #007AFF; font-weight: bold;");
installLabel->setCursor(Qt::PointingHandCursor);
installLabel->setFixedHeight(30);
// installLabel->setAlignment(Qt::AlignCenter);
connect(
installLabel, &QPushButton::clicked, this,
[this, name, description]() { onAppCardClicked(name, description); });
connect(downloadIpaLabel, &QPushButton::clicked, this,
[this, name, bundleId]() { onDownloadIpaClicked(name, bundleId); });
buttonsLayout->addWidget(installLabel);
buttonsLayout->addWidget(downloadIpaLabel);
cardLayout->addLayout(buttonsLayout);
gridLayout->addWidget(cardWidget, row, col);
}
void AppsWidget::onDownloadIpaClicked(const QString &name,
const QString &bundleId)
{
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 &description)
{
if (!m_isLoggedIn) {
QMessageBox::information(this, "Sign In Required",
"Please sign in to install apps.");
return;
}
AppInstallDialog dialog(appName, description, 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;
}
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);
}