Refactor app download process and integrate AppStoreManager

- Replaced the direct usage of the Go library with AppStoreManager in AppDownloadBaseDialog.
- Removed the C-style callback for download progress and implemented a lambda function for progress updates.
- Added error handling for AppStoreManager initialization in the download process.
- Updated AppDownloadDialog to use QStandardPaths for the default download directory.
- Created AppStoreManager class to handle account management and app operations.
- Implemented login functionality in LoginDialog using AppStoreManager.
- Added QProcessIndicator for visual feedback during login and app download processes.
- Updated AppsWidget to manage login state and display account information using AppStoreManager.
- Cleaned up unused code and improved UI elements for better user experience.
This commit is contained in:
uncor3
2025-10-02 23:02:04 +00:00
parent f0fede4e81
commit e25f194ee9
12 changed files with 785 additions and 210 deletions
+46 -68
View File
@@ -1,5 +1,5 @@
#include "appdownloadbasedialog.h"
#include "libipatool-go.h"
#include "appstoremanager.h"
#include <QDesktopServices>
#include <QDir>
#include <QFutureWatcher>
@@ -10,23 +10,6 @@
#include <QVBoxLayout>
#include <QtConcurrent/QtConcurrent>
void downloadProgressCallback(long long current, long long total,
void *userData)
{
// Cast the user data back to the dialog instance.
AppDownloadBaseDialog *dialog =
static_cast<AppDownloadBaseDialog *>(userData);
if (dialog) {
int percentage = 0;
if (total > 0) {
percentage = static_cast<int>((current * 100) / total);
}
// Safely call the update method on the GUI thread.
QMetaObject::invokeMethod(dialog, "updateProgressBar",
Qt::QueuedConnection, Q_ARG(int, percentage));
}
}
void AppDownloadBaseDialog::updateProgressBar(int percentage)
{
@@ -82,58 +65,53 @@ void AppDownloadBaseDialog::startDownloadProcess(const QString &bundleId,
if (m_actionButton)
m_actionButton->setEnabled(false);
// // C-style callback function for progress updates from the Go library
// auto progressCallback = [this](long long current, long long total) {
AppStoreManager *manager = AppStoreManager::sharedInstance();
if (!manager) {
QMessageBox::critical(this, "Error",
"Failed to initialize App Store manager.");
reject();
return;
}
// // Use invokeMethod to call a slot on the GUI thread safely
// // QMetaObject::invokeMethod(this, "updateProgressBar",
// // Qt::QueuedConnection, Q_ARG(int,
// // percentage));
// updateProgressBar(percentage);
// };
auto progressCallback = [this](long long current, long long total) {
int percentage = 0;
if (total > 0) {
percentage = static_cast<int>((current * 100) / total);
}
updateProgressBar(percentage);
};
QFuture<int> future =
QtConcurrent::run([bundleId, outputDir, acquireLicense, this]() {
// Call the Go function directly
return IpaToolDownloadApp(
bundleId.toUtf8().data(), outputDir.toUtf8().data(), "",
acquireLicense, downloadProgressCallback, this);
});
QFutureWatcher<int> *watcher = new QFutureWatcher<int>(this);
connect(watcher, &QFutureWatcher<int>::finished, this,
[this, watcher, outputDir]() {
int result = watcher->result();
watcher->deleteLater();
if (result == 0) { // Success
m_progressBar->setValue(100);
if (QMessageBox::Yes ==
QMessageBox::question(
this, "Download Successful",
QString("Successfully downloaded. Would you like "
"to open the output directory: %1?")
.arg(outputDir))) {
QDir dir(outputDir);
if (!dir.exists()) {
QMessageBox::warning(
this, "Directory Not Found",
QString("The directory %1 does not exist.")
.arg(outputDir));
} else {
QDesktopServices::openUrl(
QUrl::fromLocalFile(outputDir));
}
manager->downloadApp(
bundleId, outputDir, "", acquireLicense,
[this, outputDir](int result) {
if (result == 0) { // Success
m_progressBar->setValue(100);
if (QMessageBox::Yes ==
QMessageBox::question(
this, "Download Successful",
QString("Successfully downloaded. Would you like "
"to open the output directory: %1?")
.arg(outputDir))) {
QDir dir(outputDir);
if (!dir.exists()) {
QMessageBox::warning(
this, "Directory Not Found",
QString("The directory %1 does not exist.")
.arg(outputDir));
} else {
QDesktopServices::openUrl(
QUrl::fromLocalFile(outputDir));
}
accept();
} else { // Failure
QMessageBox::critical(
this, "Download Failed",
QString("Failed to download %1. Error code: %2")
.arg(m_appName)
.arg(result));
reject();
}
});
watcher->setFuture(future);
accept();
} else { // Failure
QMessageBox::critical(
this, "Download Failed",
QString("Failed to download %1. Error code: %2")
.arg(m_appName)
.arg(result));
reject();
}
},
progressCallback);
}
+4 -1
View File
@@ -5,6 +5,7 @@
#include <QFileDialog>
#include <QLabel>
#include <QPushButton>
#include <QStandardPaths>
#include <QVBoxLayout>
AppDownloadDialog::AppDownloadDialog(const QString &appName,
@@ -12,7 +13,9 @@ AppDownloadDialog::AppDownloadDialog(const QString &appName,
const QString &description,
QWidget *parent)
: AppDownloadBaseDialog(appName, parent),
m_outputDir(QDir::homePath().append("/Downloads")), m_bundleId(bundleId)
m_outputDir(
QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)),
m_bundleId(bundleId)
{
setWindowTitle("Download " + appName + " IPA");
setModal(true);
+218
View File
@@ -0,0 +1,218 @@
#include "appstoremanager.h"
#include "libipatool-go.h"
#include <QApplication>
#include <QDebug>
#include <QFuture>
#include <QFutureWatcher>
#include <QInputDialog>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QLineEdit>
#include <QtConcurrent/QtConcurrent>
// 2FA callback for login
static char *getAuthCodeCallback()
{
static QByteArray buffer;
QString code;
QMetaObject::invokeMethod(
qApp,
[&]() {
bool ok;
code = QInputDialog::getText(
nullptr, "Two-Factor Authentication",
"Enter the 2FA code:", QLineEdit::Normal, QString(), &ok);
},
Qt::BlockingQueuedConnection);
if (code.isEmpty()) {
return nullptr;
}
buffer = code.toUtf8();
return buffer.data();
}
AppStoreManager *AppStoreManager::sharedInstance()
{
static AppStoreManager instance;
return instance.m_initialized ? &instance : nullptr;
}
AppStoreManager::AppStoreManager(QObject *parent)
: QObject(parent), m_initialized(false)
{
m_initialized = initialize();
}
bool AppStoreManager::initialize()
{
int result = IpaToolInitialize();
if (result != 0) {
qDebug() << "IpaToolInitialize failed with error code:" << result;
return false;
}
qDebug() << "IpaToolInitialize succeeded";
return true;
}
QJsonObject AppStoreManager::getAccountInfo()
{
if (!m_initialized) {
return QJsonObject();
}
char *accountInfoCStr = IpaToolGetAccountInfo();
if (!accountInfoCStr) {
return QJsonObject();
}
QString jsonAccountInfo(accountInfoCStr);
free(accountInfoCStr);
QJsonParseError parseError;
QJsonDocument doc =
QJsonDocument::fromJson(jsonAccountInfo.toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
qDebug() << "JSON parse error:" << parseError.errorString();
return QJsonObject();
}
return doc.object();
}
void AppStoreManager::loginWithCallback(
const QString &email, const QString &password,
std::function<void(bool success, const QJsonObject &accountInfo)> callback)
{
if (!m_initialized) {
callback(false, QJsonObject());
return;
}
QFuture<int> future = QtConcurrent::run([email, password]() {
return IpaToolLoginWithCallback(email.toUtf8().data(),
password.toUtf8().data(),
getAuthCodeCallback);
});
QFutureWatcher<int> *watcher = new QFutureWatcher<int>(this);
connect(watcher, &QFutureWatcher<int>::finished, this,
[this, watcher, callback]() {
int result = watcher->result();
watcher->deleteLater();
bool success = (result == 0);
QJsonObject accountInfo;
if (success) {
accountInfo = getAccountInfo();
emit loginSuccessful(accountInfo);
}
callback(success, accountInfo);
});
watcher->setFuture(future);
}
void AppStoreManager::revokeCredentials()
{
if (!m_initialized) {
return;
}
IpaToolRevokeCredentials();
// todo: should we ?
// could be problematic if user logs in using ipatool
// emit loggedOut(getAccountInfo());
emit loggedOut(QJsonObject());
}
void AppStoreManager::searchApps(
const QString &searchTerm, int limit,
std::function<void(bool success, const QString &results)> callback)
{
if (!m_initialized) {
callback(false, QString());
return;
}
QFuture<QString> future =
QtConcurrent::run([searchTerm, limit]() -> QString {
char *resultsCStr =
IpaToolSearch(searchTerm.toUtf8().data(), limit);
if (!resultsCStr) {
return QString();
}
QString results(resultsCStr);
free(resultsCStr);
return results;
});
QFutureWatcher<QString> *watcher = new QFutureWatcher<QString>(this);
connect(watcher, &QFutureWatcher<QString>::finished, this,
[watcher, callback]() {
QString results = watcher->result();
watcher->deleteLater();
callback(!results.isEmpty(), results);
});
watcher->setFuture(future);
}
void AppStoreManager::downloadApp(
const QString &bundleId, const QString &outputDir, const QString &country,
bool acquireLicense, std::function<void(int result)> callback,
std::function<void(long long current, long long total)> progressCallback)
{
if (!m_initialized) {
callback(-1);
return;
}
// Create a wrapper for the progress callback
void *progressUserData = nullptr;
void (*cProgressCallback)(long long, long long, void *) = nullptr;
if (progressCallback) {
// Store the callback in a way that can be accessed from C
auto *callbackPtr =
new std::function<void(long long, long long)>(progressCallback);
progressUserData = callbackPtr;
cProgressCallback = [](long long current, long long total,
void *userData) {
auto *cb = static_cast<std::function<void(long long, long long)> *>(
userData);
QMetaObject::invokeMethod(
qApp, [cb, current, total]() { (*cb)(current, total); },
Qt::QueuedConnection);
};
}
QFuture<int> future = QtConcurrent::run([bundleId, outputDir, country,
acquireLicense, cProgressCallback,
progressUserData]() {
int result = IpaToolDownloadApp(bundleId.toUtf8().data(),
outputDir.toUtf8().data(),
country.toUtf8().data(), acquireLicense,
cProgressCallback, progressUserData);
return result;
});
QFutureWatcher<int> *watcher = new QFutureWatcher<int>(this);
connect(
watcher, &QFutureWatcher<int>::finished, this,
[watcher, callback, progressUserData]() {
int result = watcher->result();
watcher->deleteLater();
// Clean up progress callback if it was allocated
if (progressUserData) {
delete static_cast<std::function<void(long long, long long)> *>(
progressUserData);
}
callback(result);
});
watcher->setFuture(future);
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef APPSTOREMANAGER_H
#define APPSTOREMANAGER_H
#include <QJsonObject>
#include <QObject>
#include <functional>
class AppStoreManager : public QObject
{
Q_OBJECT
public:
static AppStoreManager *sharedInstance();
// Account management
QJsonObject getAccountInfo();
void loginWithCallback(
const QString &email, const QString &password,
std::function<void(bool success, const QJsonObject &accountInfo)>
callback);
void revokeCredentials();
// App operations
void searchApps(
const QString &searchTerm, int limit,
std::function<void(bool success, const QString &results)> callback);
void downloadApp(const QString &bundleId, const QString &outputDir,
const QString &country, bool acquireLicense,
std::function<void(int result)> callback,
std::function<void(long long current, long long total)>
progressCallback = nullptr);
signals:
void loginSuccessful(const QJsonObject &accountInfo);
void loggedOut(const QJsonObject &accountInfo);
private:
AppStoreManager(QObject *parent = nullptr);
~AppStoreManager() = default;
AppStoreManager(const AppStoreManager &) = delete;
AppStoreManager &operator=(const AppStoreManager &) = delete;
bool m_initialized;
bool initialize();
};
#endif // APPSTOREMANAGER_H
+127 -61
View File
@@ -3,7 +3,7 @@
#include "appdownloadbasedialog.h"
#include "appdownloaddialog.h"
#include "appinstalldialog.h"
#include "libipatool-go.h"
#include "appstoremanager.h"
#include "logindialog.h"
#include <QAction>
#include <QApplication>
@@ -42,7 +42,7 @@
#include <QVBoxLayout>
#include <QWidget>
#include <QtConcurrent/QtConcurrent>
// watch for login and logout events
AppsWidget::AppsWidget(QWidget *parent) : QWidget(parent), m_isLoggedIn(false)
{
// m_searchProcess = new QProcess(this);
@@ -74,54 +74,20 @@ void AppsWidget::setupUI()
m_statusLabel->setStyleSheet("margin-right: 20px;");
// --- Status and Login Button ---
// TODO: need a singleton for IpaTool
int init_result = IpaToolInitialize();
if (init_result != 0) {
qDebug() << "IpaToolInitialize failed with error code:" << init_result;
m_manager = AppStoreManager::sharedInstance();
if (!m_manager) {
qDebug() << "AppStoreManager failed to initialize";
m_statusLabel->setText("Failed to initialize");
m_loginButton = new QPushButton("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 {
qDebug() << "IpaToolInitialize succeeded";
char *accountInfoCStr = IpaToolGetAccountInfo();
if (accountInfoCStr) {
QString jsonAccountInfo(accountInfoCStr);
free(accountInfoCStr);
qDebug() << "Account info JSON:" << jsonAccountInfo;
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(
QByteArray(jsonAccountInfo.toUtf8()), &parseError);
if (parseError.error == QJsonParseError::NoError &&
doc.isObject()) {
QJsonObject jsonObj = doc.object();
if (jsonObj.contains("success") &&
jsonObj.value("success").toBool()) {
if (jsonObj.contains("email")) {
QString email = jsonObj.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");
}
} else {
qDebug() << "JSON parse error:" << parseError.errorString();
m_statusLabel->setText("Not signed in");
}
} else {
m_statusLabel->setText("Not signed in");
}
onAppStoreInitialized(m_manager->getAccountInfo());
}
m_statusLabel->setStyleSheet("font-size: 14px; color: #666;");
m_loginButton = new QPushButton(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_statusLabel->setStyleSheet("font-size: 14px; color: #666;");
mainLayout->addWidget(headerWidget);
@@ -168,7 +134,6 @@ void AppsWidget::setupUI()
m_scrollArea->setWidget(m_contentWidget);
mainLayout->addWidget(m_scrollArea);
// Connections
connect(m_loginButton, &QPushButton::clicked, this,
&AppsWidget::onLoginClicked);
@@ -179,6 +144,34 @@ void AppsWidget::setupUI()
&AppsWidget::performSearch);
connect(m_searchWatcher, &QFutureWatcher<QString>::finished, this,
&AppsWidget::onSearchFinished);
connect(m_manager, &AppStoreManager::loginSuccessful, this,
&AppsWidget::onAppStoreInitialized);
connect(m_manager, &AppStoreManager::loggedOut, this,
&AppsWidget::onAppStoreInitialized);
}
void AppsWidget::onAppStoreInitialized(const QJsonObject &accountInfo)
{
qDebug() << "AppStoreManager initialized successfully";
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 = new QPushButton(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;");
}
void AppsWidget::populateDefaultApps()
@@ -343,17 +336,34 @@ void AppsWidget::onDownloadIpaClicked(const QString &name,
void AppsWidget::onLoginClicked()
{
if (m_isLoggedIn) {
IpaToolRevokeCredentials();
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) {
QString email = dialog.getEmail();
QString password = dialog.getPassword();
// 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...");
}
}
}
}
}
@@ -387,17 +397,73 @@ void AppsWidget::performSearch()
showStatusMessage(QString("Searching for \"%1\"...").arg(searchTerm));
auto searchFn = [searchTerm]() -> QString {
char *resultsCStr = IpaToolSearch(searchTerm.toUtf8().data(), 20);
qDebug() << "Search results C string:" << resultsCStr;
if (!resultsCStr) {
return QString();
}
QString results(resultsCStr);
free(resultsCStr);
return results;
};
m_searchWatcher->setFuture(QtConcurrent::run(searchFn));
AppStoreManager *manager = AppStoreManager::sharedInstance();
if (!manager) {
showStatusMessage("Failed to initialize App Store manager.");
return;
}
manager->searchApps(
searchTerm, 20, [this](bool success, const QString &results) {
if (!success || results.isEmpty()) {
showStatusMessage("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;
showStatusMessage("Failed to parse search results.");
return;
}
QJsonObject rootObj = doc.object();
if (!rootObj.value("success").toBool()) {
QString errorMessage =
rootObj.value("error").toString("Unknown search error.");
showStatusMessage(
QString("Search error: %1").arg(errorMessage));
return;
}
QJsonArray resultsArray = rootObj.value("results").toArray();
if (resultsArray.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 : 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);
});
}
void AppsWidget::onSearchFinished()
+3
View File
@@ -1,6 +1,7 @@
#ifndef APPSWIDGET_H
#define APPSWIDGET_H
#include "appstoremanager.h"
#include <QComboBox>
#include <QDialog>
#include <QFile>
@@ -31,6 +32,7 @@ private slots:
void onSearchTextChanged();
void performSearch();
void onSearchFinished();
void onAppStoreInitialized(const QJsonObject &accountInfo);
private:
void setupUI();
@@ -46,6 +48,7 @@ private:
QPushButton *m_loginButton;
QLabel *m_statusLabel;
bool m_isLoggedIn;
AppStoreManager *m_manager;
// Search
QLineEdit *m_searchEdit;
+3 -1
View File
@@ -1,3 +1,4 @@
#ifdef Q_OS_MACOS
#include "diskusagebar.h"
#include "platform/macos.h"
@@ -61,4 +62,5 @@ void DiskUsageBar::showPopover()
info.percentage = m_percentage;
showPopoverForBarWidget(this, info);
}
}
#endif
+5 -1
View File
@@ -1,3 +1,5 @@
#ifdef Q_OS_MACOS
#ifndef DISKUSAGEBAR_H
#define DISKUSAGEBAR_H
@@ -29,4 +31,6 @@ private:
QTimer *m_hoverTimer;
};
#endif // DISKUSAGEBAR_H
#endif // DISKUSAGEBAR_H
#endif // Q_OS_MACOS
+86 -76
View File
@@ -1,5 +1,6 @@
#include "logindialog.h"
#include "libipatool-go.h"
#include "appstoremanager.h"
#include "qprocessindicator.h"
#include <QApplication>
#include <QDialogButtonBox>
#include <QInputDialog>
@@ -10,28 +11,6 @@
#include <QVBoxLayout>
#include <QtConcurrent/QtConcurrent>
// 2FA callback for login
static char *getAuthCodeCallback()
{
static QByteArray buffer;
QString code;
QMetaObject::invokeMethod(
qApp,
[&]() {
bool ok;
code = QInputDialog::getText(
nullptr, "Two-Factor Authentication",
"Enter the 2FA code:", QLineEdit::Normal, QString(), &ok);
},
Qt::BlockingQueuedConnection);
if (code.isEmpty()) {
return nullptr;
}
buffer = code.toUtf8();
return buffer.data();
}
LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent)
{
setWindowTitle("Login to App Store");
@@ -43,13 +22,6 @@ LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent)
layout->setSpacing(15);
layout->setContentsMargins(20, 20, 20, 20);
// Title
QLabel *titleLabel = new QLabel("Sign in to continue");
titleLabel->setStyleSheet(
"font-size: 18px; font-weight: bold; color: #333;");
titleLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(titleLabel);
// Email
QLabel *emailLabel = new QLabel("Email:");
emailLabel->setStyleSheet("font-size: 14px; color: #555;");
@@ -73,24 +45,66 @@ LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent)
"border-radius: 4px; font-size: 14px;");
layout->addWidget(m_passwordEdit);
// Buttons
QDialogButtonBox *buttonBox =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttonBox->button(QDialogButtonBox::Ok)->setText("Sign In");
buttonBox->setStyleSheet(
// Description
QLabel *descriptionLabel =
new QLabel("Don't worry, your credentials won't be "
"stored, shared anywhere. This App is open-source.");
descriptionLabel->setStyleSheet("font-size: 10px; font-weight: bold;");
descriptionLabel->setAlignment(Qt::AlignLeft);
descriptionLabel->setWordWrap(true); // Add this line
layout->addWidget(descriptionLabel);
// --- Buttons and Indicator ---
// Create a container widget for the sign-in button and the indicator
QWidget *signInContainer = new QWidget(this);
m_signInStackedLayout = new QStackedLayout(signInContainer);
m_signInStackedLayout->setContentsMargins(0, 0, 0, 0);
// Create the actual "Sign In" button
m_signInButton = new QPushButton("Sign In");
m_signInButton->setStyleSheet(
"QPushButton { padding: 8px 16px; font-size: 14px; border-radius: 4px; "
"}"
"QPushButton[text='Sign In'] { background-color: #007AFF; color: "
"white; border: none; }"
"QPushButton[text='Sign In']:hover { background-color: #0056CC; }"
"QPushButton[text='Cancel'] { background-color: #f0f0f0; color: #333; "
"border: 1px solid #ddd; }");
"background-color: #007AFF; color: white; border: none; min-width: "
"80px; }"
"QPushButton:hover { background-color: #0056CC; }");
connect(buttonBox, &QDialogButtonBox::accepted, this, &LoginDialog::signIn);
// connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
// Create the indicator
QWidget *indicatorWidget = new QWidget();
QVBoxLayout *indicatorLayout = new QVBoxLayout(indicatorWidget);
indicatorLayout->setContentsMargins(0, 0, 0, 0);
indicatorLayout->setAlignment(Qt::AlignCenter);
m_indicator = new QProcessIndicator(this);
m_indicator->setType(QProcessIndicator::line_rotate);
m_indicator->setFixedSize(48, 24);
indicatorLayout->addWidget(m_indicator);
layout->addWidget(buttonBox);
// Add button and indicator to the stacked layout
m_signInStackedLayout->addWidget(m_signInButton);
m_signInStackedLayout->addWidget(indicatorWidget);
// Ensure the container has the same size as the button
signInContainer->setFixedSize(m_signInButton->sizeHint());
// Create the "Cancel" button
m_cancelButton = new QPushButton("Cancel");
// add disabled style to cancel button
m_cancelButton->setStyleSheet(
"QPushButton { padding: 8px 16px; font-size: 14px; border-radius: 4px; "
"background-color: #f0f0f0; color: #333; border: 1px solid #ddd; "
"min-width: 80px; }"
"QPushButton:disabled { background-color: #eee; color: #aaa; border: "
"1px solid #ddd; }");
// Layout for the buttons
QHBoxLayout *buttonLayout = new QHBoxLayout();
buttonLayout->addStretch();
buttonLayout->addWidget(m_cancelButton);
buttonLayout->addWidget(signInContainer);
connect(m_signInButton, &QPushButton::clicked, this, &LoginDialog::signIn);
connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject);
layout->addLayout(buttonLayout);
}
QString LoginDialog::getEmail() const { return m_emailEdit->text(); }
@@ -105,37 +119,33 @@ void LoginDialog::signIn()
"Email and password cannot be empty.");
return;
}
QFuture<int> f = QtConcurrent::run([email, password]() {
return IpaToolLoginWithCallback(email.toUtf8().data(),
password.toUtf8().data(),
getAuthCodeCallback);
});
QFutureWatcher<int> *watcher = new QFutureWatcher<int>(this);
connect(watcher, &QFutureWatcher<int>::finished, this, [this, watcher]() {
int result = watcher->future().result();
if (result == 0) {
accept();
} else {
QMessageBox::warning(
this, "Login Failed",
"Login failed. Please check your credentials and 2FA code.");
}
watcher->deleteLater();
});
// int result = IpaToolLoginWithCallback(
// email.toUtf8().data(), password.toUtf8().data(),
// getAuthCodeCallback);
// if (result == 0) {
// accept();
// // m_isLoggedIn = true;
// // m_loginButton->setText("Sign Out");
// // m_statusLabel->setText("Signed in as " + email);
// } else {
// QMessageBox::warning(
// this, "Login Failed",
// "Login failed. Please check your credentials and 2FA code.");
// }
AppStoreManager *manager = AppStoreManager::sharedInstance();
if (!manager) {
QMessageBox::critical(this, "Error",
"Failed to initialize App Store manager.");
return;
}
// Perform login logic here
// Show indicator and disable cancel button
m_signInStackedLayout->setCurrentIndex(1);
m_indicator->start();
m_cancelButton->setEnabled(false);
manager->loginWithCallback(
email, password, [this](bool success, const QJsonObject &accountInfo) {
// Hide indicator and re-enable buttons
m_indicator->stop();
m_signInStackedLayout->setCurrentIndex(0);
m_cancelButton->setEnabled(true);
if (success) {
qDebug() << "Login successful";
accept();
} else {
QMessageBox::warning(this, "Login Failed",
"Login failed. Please check your "
"credentials and 2FA code.");
}
});
}
+13 -2
View File
@@ -1,21 +1,32 @@
#ifndef LOGINDIALOG_H
#define LOGINDIALOG_H
#include "qprocessindicator.h"
#include <QDialog>
#include <QLineEdit>
#include <QPushButton>
#include <QStackedLayout>
class LoginDialog : public QDialog
{
Q_OBJECT
public:
explicit LoginDialog(QWidget *parent = nullptr);
LoginDialog(QWidget *parent = nullptr);
QString getEmail() const;
QString getPassword() const;
private slots:
void signIn();
private:
QLineEdit *m_emailEdit;
QLineEdit *m_passwordEdit;
void signIn();
QPushButton *m_signInButton;
QPushButton *m_cancelButton;
QProcessIndicator *m_indicator;
QStackedLayout *m_signInStackedLayout;
};
#endif // LOGINDIALOG_H
+170
View File
@@ -0,0 +1,170 @@
// https://github.com/raythorn/QProcessIndicator/blob/master/QProcessIndicator/QProcessIndicator.cpp
#include "qprocessindicator.h"
#include <QDebug>
#include <QPoint>
#include <QtGlobal>
#define SPIN_INTERVAL 60
QProcessIndicator::QProcessIndicator(QWidget *parent)
: QWidget(parent), m_type(line_rotate), m_interval(SPIN_INTERVAL),
m_angle(0), m_scale(0.0f), m_color(Qt::black)
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
setFocusPolicy(Qt::NoFocus);
m_timer = new QTimer();
connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
}
QProcessIndicator::~QProcessIndicator()
{
stop();
delete m_timer;
}
void QProcessIndicator::paintEvent(QPaintEvent *e)
{
Q_UNUSED(e)
if (!m_timer->isActive()) {
return;
}
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
switch (m_type) {
case line_rotate:
drawRotateLine(&painter);
break;
case line_scale:
drawScaleLine((&painter));
break;
case ball_rotate:
drawRotateBall(&painter);
break;
}
}
void QProcessIndicator::start() { m_timer->start(m_interval); }
void QProcessIndicator::stop() { m_timer->stop(); }
void QProcessIndicator::onTimeout()
{
switch (m_type) {
case line_rotate:
case ball_rotate:
m_angle = (m_angle + 45) % 360;
break;
case line_scale:
m_scale += 0.1f;
m_scale = m_scale > .5f ? 0.0f : m_scale;
break;
}
update();
}
void QProcessIndicator::drawRotateLine(QPainter *painter)
{
int width = qMin(this->width(), this->height());
int outerRadius = (width - 4) * 0.5f;
int innerRadius = outerRadius * 0.42f;
int capsuleHeight = outerRadius - innerRadius;
int capsuleWidth =
(width > 32) ? capsuleHeight * .32f : capsuleHeight * .40f;
int capsuleRadius = capsuleWidth / 2;
for (int i = 0; i < 8; i++) {
QColor color = m_color;
color.setAlphaF(1.0f - (i / 8.0f));
painter->setPen(Qt::NoPen);
painter->setBrush(color);
painter->save();
painter->translate(rect().center());
painter->rotate(m_angle - i * 45.0f);
painter->drawRoundedRect(-capsuleWidth * 0.5,
-(innerRadius + capsuleHeight), capsuleWidth,
capsuleHeight, capsuleRadius, capsuleRadius);
painter->restore();
}
}
void QProcessIndicator::drawScaleLine(QPainter *painter)
{
int height = qMin(this->width(), this->height());
qreal lineWidth = height * 0.15f;
qreal lineHeight = height * 0.9f;
qreal lineRadius = lineWidth / 2.0f;
qreal lineGap = lineWidth;
qreal margin = (this->width() - lineWidth * 5 - lineGap * 4) / 2.0f;
for (int i = 0; i < 5; i++) {
painter->setPen(Qt::NoPen);
painter->setBrush(m_color);
int tmp = m_scale * 10 + i + 1;
if (tmp > 5) {
tmp = 5 - tmp % 5;
}
qDebug() << tmp;
qreal scale = 0.5f + tmp * 0.1f;
qreal h = lineHeight * scale;
painter->save();
painter->translate(
QPointF(margin + (lineWidth + lineGap) * i, this->height() / 2));
painter->drawRoundedRect(0, -h / 2.0f, lineWidth, h, lineRadius,
lineRadius);
painter->restore();
}
}
void QProcessIndicator::drawRotateBall(QPainter *painter)
{
int width = qMin(this->width(), this->height());
int outerRadius = (width - 4) * 0.5f;
int innerRadius = outerRadius * 0.78f;
int capsuleRadius = (outerRadius - innerRadius) / 2;
for (int i = 0; i < 8; i++) {
QColor color = m_color;
color.setAlphaF(1.0f - (i / 8.0f));
painter->setPen(Qt::NoPen);
painter->setBrush(color);
qreal radius = capsuleRadius * (1.0f - (i / 16.0f));
painter->save();
painter->translate(rect().center());
painter->rotate(m_angle - i * 45.0f);
QPointF centre =
QPointF(-capsuleRadius, -(innerRadius + capsuleRadius));
painter->drawEllipse(centre, radius * 2, radius * 2);
painter->restore();
}
}
+62
View File
@@ -0,0 +1,62 @@
// https://github.com/raythorn/QProcessIndicator/blob/master/QProcessIndicator/QProcessIndicator.h
#ifndef QPROCESSINDICATOR_H
#define QPROCESSINDICATOR_H
#include <QColor>
#include <QPaintEvent>
#include <QPainter>
#include <QTimer>
#include <QWidget>
class QProcessIndicator : public QWidget
{
Q_OBJECT
Q_PROPERTY(int m_type READ type WRITE setType)
Q_PROPERTY(QColor m_color READ color WRITE setColor)
Q_PROPERTY(int m_interval READ interval WRITE setInterval)
public:
QProcessIndicator(QWidget *parent = 0);
~QProcessIndicator();
enum {
line_rotate,
line_scale,
ball_rotate,
};
void paintEvent(QPaintEvent *e);
void start();
void stop();
int type() { return m_type; }
void setType(int type) { m_type = type; }
QColor &color() { return m_color; }
void setColor(QColor &color) { m_color = color; }
int interval() { return m_interval; }
void setInterval(int interval) { m_interval = interval; }
private slots:
void onTimeout();
private:
void drawRotateLine(QPainter *painter);
void drawScaleLine(QPainter *painter);
void drawRotateBall(QPainter *painter);
private:
int m_type;
int m_interval;
QColor m_color;
int m_angle;
qreal m_scale;
QTimer *m_timer;
};
#endif // QPROCESSINDICATOR_H