mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-22 03:45:51 +08:00
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:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user