From e25f194ee9a06c10af7b89778571c7c34ccb6718 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Thu, 2 Oct 2025 23:02:04 +0000 Subject: [PATCH] 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. --- src/appdownloadbasedialog.cpp | 114 +++++++----------- src/appdownloaddialog.cpp | 5 +- src/appstoremanager.cpp | 218 ++++++++++++++++++++++++++++++++++ src/appstoremanager.h | 48 ++++++++ src/appswidget.cpp | 188 +++++++++++++++++++---------- src/appswidget.h | 3 + src/diskusagebar.cpp | 4 +- src/diskusagebar.h | 6 +- src/logindialog.cpp | 162 +++++++++++++------------ src/logindialog.h | 15 ++- src/qprocessindicator.cpp | 170 ++++++++++++++++++++++++++ src/qprocessindicator.h | 62 ++++++++++ 12 files changed, 785 insertions(+), 210 deletions(-) create mode 100644 src/appstoremanager.cpp create mode 100644 src/appstoremanager.h create mode 100644 src/qprocessindicator.cpp create mode 100644 src/qprocessindicator.h diff --git a/src/appdownloadbasedialog.cpp b/src/appdownloadbasedialog.cpp index 84b80a4..d0a5400 100644 --- a/src/appdownloadbasedialog.cpp +++ b/src/appdownloadbasedialog.cpp @@ -1,5 +1,5 @@ #include "appdownloadbasedialog.h" -#include "libipatool-go.h" +#include "appstoremanager.h" #include #include #include @@ -10,23 +10,6 @@ #include #include -void downloadProgressCallback(long long current, long long total, - void *userData) -{ - // Cast the user data back to the dialog instance. - AppDownloadBaseDialog *dialog = - static_cast(userData); - if (dialog) { - int percentage = 0; - if (total > 0) { - percentage = static_cast((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((current * 100) / total); + } + updateProgressBar(percentage); + }; - QFuture future = - QtConcurrent::run([bundleId, outputDir, acquireLicense, this]() { - // Call the Go function directly - return IpaToolDownloadApp( - bundleId.toUtf8().data(), outputDir.toUtf8().data(), "", - acquireLicense, downloadProgressCallback, this); - }); - - QFutureWatcher *watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::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); } \ No newline at end of file diff --git a/src/appdownloaddialog.cpp b/src/appdownloaddialog.cpp index 2fd02ba..a8f2301 100644 --- a/src/appdownloaddialog.cpp +++ b/src/appdownloaddialog.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include 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); diff --git a/src/appstoremanager.cpp b/src/appstoremanager.cpp new file mode 100644 index 0000000..3187d18 --- /dev/null +++ b/src/appstoremanager.cpp @@ -0,0 +1,218 @@ +#include "appstoremanager.h" +#include "libipatool-go.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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 callback) +{ + if (!m_initialized) { + callback(false, QJsonObject()); + return; + } + + QFuture future = QtConcurrent::run([email, password]() { + return IpaToolLoginWithCallback(email.toUtf8().data(), + password.toUtf8().data(), + getAuthCodeCallback); + }); + + QFutureWatcher *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::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 callback) +{ + if (!m_initialized) { + callback(false, QString()); + return; + } + + QFuture 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 *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::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 callback, + std::function 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(progressCallback); + progressUserData = callbackPtr; + + cProgressCallback = [](long long current, long long total, + void *userData) { + auto *cb = static_cast *>( + userData); + QMetaObject::invokeMethod( + qApp, [cb, current, total]() { (*cb)(current, total); }, + Qt::QueuedConnection); + }; + } + + QFuture 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 *watcher = new QFutureWatcher(this); + connect( + watcher, &QFutureWatcher::finished, this, + [watcher, callback, progressUserData]() { + int result = watcher->result(); + watcher->deleteLater(); + + // Clean up progress callback if it was allocated + if (progressUserData) { + delete static_cast *>( + progressUserData); + } + + callback(result); + }); + watcher->setFuture(future); +} \ No newline at end of file diff --git a/src/appstoremanager.h b/src/appstoremanager.h new file mode 100644 index 0000000..35b33ca --- /dev/null +++ b/src/appstoremanager.h @@ -0,0 +1,48 @@ +#ifndef APPSTOREMANAGER_H +#define APPSTOREMANAGER_H + +#include +#include +#include + +class AppStoreManager : public QObject +{ + Q_OBJECT + +public: + static AppStoreManager *sharedInstance(); + + // Account management + QJsonObject getAccountInfo(); + void loginWithCallback( + const QString &email, const QString &password, + std::function + callback); + void revokeCredentials(); + + // App operations + void searchApps( + const QString &searchTerm, int limit, + std::function callback); + void downloadApp(const QString &bundleId, const QString &outputDir, + const QString &country, bool acquireLicense, + std::function callback, + std::function + 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 \ No newline at end of file diff --git a/src/appswidget.cpp b/src/appswidget.cpp index ee2cd0c..fe894fd 100644 --- a/src/appswidget.cpp +++ b/src/appswidget.cpp @@ -3,7 +3,7 @@ #include "appdownloadbasedialog.h" #include "appdownloaddialog.h" #include "appinstalldialog.h" -#include "libipatool-go.h" +#include "appstoremanager.h" #include "logindialog.h" #include #include @@ -42,7 +42,7 @@ #include #include #include - +// 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::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(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() diff --git a/src/appswidget.h b/src/appswidget.h index 1378c18..45018a5 100644 --- a/src/appswidget.h +++ b/src/appswidget.h @@ -1,6 +1,7 @@ #ifndef APPSWIDGET_H #define APPSWIDGET_H +#include "appstoremanager.h" #include #include #include @@ -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; diff --git a/src/diskusagebar.cpp b/src/diskusagebar.cpp index 3839dee..d062e8d 100644 --- a/src/diskusagebar.cpp +++ b/src/diskusagebar.cpp @@ -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); -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/diskusagebar.h b/src/diskusagebar.h index 9f903d2..bc8ac9e 100644 --- a/src/diskusagebar.h +++ b/src/diskusagebar.h @@ -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 \ No newline at end of file +#endif // DISKUSAGEBAR_H + +#endif // Q_OS_MACOS \ No newline at end of file diff --git a/src/logindialog.cpp b/src/logindialog.cpp index b2a7fed..b9eb22a 100644 --- a/src/logindialog.cpp +++ b/src/logindialog.cpp @@ -1,5 +1,6 @@ #include "logindialog.h" -#include "libipatool-go.h" +#include "appstoremanager.h" +#include "qprocessindicator.h" #include #include #include @@ -10,28 +11,6 @@ #include #include -// 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 f = QtConcurrent::run([email, password]() { - return IpaToolLoginWithCallback(email.toUtf8().data(), - password.toUtf8().data(), - getAuthCodeCallback); - }); - QFutureWatcher *watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::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."); + } + }); } \ No newline at end of file diff --git a/src/logindialog.h b/src/logindialog.h index a6b640a..28afc20 100644 --- a/src/logindialog.h +++ b/src/logindialog.h @@ -1,21 +1,32 @@ #ifndef LOGINDIALOG_H #define LOGINDIALOG_H +#include "qprocessindicator.h" #include #include +#include +#include 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 diff --git a/src/qprocessindicator.cpp b/src/qprocessindicator.cpp new file mode 100644 index 0000000..be6a101 --- /dev/null +++ b/src/qprocessindicator.cpp @@ -0,0 +1,170 @@ +// https://github.com/raythorn/QProcessIndicator/blob/master/QProcessIndicator/QProcessIndicator.cpp +#include "qprocessindicator.h" + +#include +#include +#include + +#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(); + } +} \ No newline at end of file diff --git a/src/qprocessindicator.h b/src/qprocessindicator.h new file mode 100644 index 0000000..bc3d2ca --- /dev/null +++ b/src/qprocessindicator.h @@ -0,0 +1,62 @@ +// https://github.com/raythorn/QProcessIndicator/blob/master/QProcessIndicator/QProcessIndicator.h +#ifndef QPROCESSINDICATOR_H +#define QPROCESSINDICATOR_H + +#include +#include +#include +#include +#include + +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 \ No newline at end of file