integrate ZUpdater & refactor device management and UI components

- Integrated ZUpdater to handle auto updating
- Updated DeviceManagerWidget to improve device selection logic and ensure current device is set correctly when devices are added or removed.
- Enhanced FileExplorerWidget to reset the view when sidebar items are clicked.
- Changed ZIconWidget to inherit from QAbstractButton for better button behavior and removed unnecessary mouse event handling.
- Updated iDescriptor to include device version parsing and improved device version retrieval logic.
- Refactored iFuseDiskUnmountButton and iFuseDiskUnmountButton to use ZIconWidget for a consistent UI.
- Improved iFuseWidget to handle device selection more robustly and update UI accordingly.
- Added SponsorAppCard and SponsorWidget to promote sponsorship within the app.
- Updated ToolboxWidget to streamline device selection and toolbox functionality.
- General code cleanup and comments for better maintainability.
This commit is contained in:
uncor3
2025-11-01 23:31:59 +00:00
parent 4fc2b49251
commit 3ae707b754
41 changed files with 1553 additions and 445 deletions
+386 -67
View File
@@ -6,10 +6,12 @@
#include "appstoremanager.h"
#include "iDescriptor-ui.h"
#include "logindialog.h"
#include "sponsorwidget.h"
#include "zlineedit.h"
#include <QApplication>
#include <QComboBox>
#include <QDebug>
#include <QDesktopServices>
#include <QFile>
#include <QFileDialog>
#include <QGridLayout>
@@ -23,11 +25,15 @@
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QProgressBar>
#include <QPushButton>
#include <QRegularExpression>
#include <QScrollArea>
#include <QStyle>
#include <QTimer>
@@ -35,7 +41,90 @@
#include <QWidget>
#include <QtConcurrent/QtConcurrent>
// Helper struct for semantic version comparison
struct AppVersion {
int major = 0;
int minor = 0;
int patch = 0;
static AppVersion fromString(QString versionString)
{
// Keep only digits and dots for comparison
versionString.remove(QRegularExpression("[^\\d.]"));
AppVersion v;
QStringList parts = versionString.split('.');
if (parts.size() > 0)
v.major = parts[0].toInt();
if (parts.size() > 1)
v.minor = parts[1].toInt();
if (parts.size() > 2)
v.patch = parts[2].toInt();
return v;
}
bool operator<(const AppVersion &other) const
{
if (major != other.major)
return major < other.major;
if (minor != other.minor)
return minor < other.minor;
return patch < other.patch;
}
bool operator==(const AppVersion &other) const
{
return major == other.major && minor == other.minor &&
patch == other.patch;
}
bool operator>(const AppVersion &other) const
{
return !(*this < other || *this == other);
}
bool operator<=(const AppVersion &other) const
{
return (*this < other || *this == other);
}
bool operator>=(const AppVersion &other) const { return !(*this < other); }
};
// Checks if the current app version matches a given version condition
bool versionMatches(const QString &currentVersionStr,
const QString &conditionStr)
{
AppVersion currentVersion = AppVersion::fromString(currentVersionStr);
AppVersion conditionVersion = AppVersion::fromString(conditionStr);
if (conditionStr.startsWith("<="))
return currentVersion <= conditionVersion;
if (conditionStr.startsWith(">="))
return currentVersion >= conditionVersion;
if (conditionStr.startsWith("<"))
return currentVersion < conditionVersion;
if (conditionStr.startsWith(">"))
return currentVersion > conditionVersion;
// Exact match
return currentVersion == conditionVersion;
}
QJsonObject getVersionedConfig(const QJsonObject &rootObj)
{
QStringList keys = rootObj.keys();
for (const QString &key : keys) {
if (versionMatches(APP_VERSION, key)) {
return rootObj[key].toObject();
}
}
}
// watch for login and logout events
AppsWidget *AppsWidget::sharedInstance()
{
static AppsWidget instance;
return &instance;
}
AppsWidget::AppsWidget(QWidget *parent) : QWidget(parent), m_isLoggedIn(false)
{
m_debounceTimer = new QTimer(this);
@@ -47,8 +136,9 @@ void AppsWidget::setupUI()
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Header with login
m_networkManager = new QNetworkAccessManager(this);
QWidget *headerWidget = new QWidget();
headerWidget->setFixedHeight(60);
headerWidget->setStyleSheet("border-bottom: 1px solid #363d32;");
@@ -110,7 +200,7 @@ void AppsWidget::setupUI()
mainLayout->addWidget(m_stackedWidget);
// Show default apps initially
showDefaultApps();
showLoading("Loading apps...");
// Connections
connect(m_loginButton, &QPushButton::clicked, this,
&AppsWidget::onLoginClicked);
@@ -123,6 +213,60 @@ void AppsWidget::setupUI()
&AppsWidget::onAppStoreInitialized);
connect(m_manager, &AppStoreManager::loggedOut, this,
&AppsWidget::onAppStoreInitialized);
// fetch sponsors
QUrl sponsorsUrl("http://localhost:5173/sponsors.json");
QNetworkRequest request(sponsorsUrl);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
try {
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
if (jsonDoc.isNull() || !jsonDoc.isObject()) {
qDebug() << "Failed to parse sponsors JSON";
showDefaultApps();
reply->deleteLater();
return;
}
QJsonObject rootObj = jsonDoc.object();
QJsonObject versioned = getVersionedConfig(rootObj);
if (versioned.isEmpty()) {
qDebug() << "No sponsor configuration found for version"
<< APP_VERSION << "or default.";
showDefaultApps();
reply->deleteLater();
return;
}
QJsonObject sponsorObj = versioned["sponsors"].toObject();
QJsonObject platinumObj = sponsorObj["platinum"].toObject();
QJsonObject goldObj = sponsorObj["gold"].toObject();
QJsonObject silverObj = sponsorObj["silver"].toObject();
QJsonObject bronzeObj = sponsorObj["bronze"].toObject();
// Store the platinum members to be used when populating the
// grid
m_platinumSponsors = platinumObj["members"].toArray();
m_goldSponsors = goldObj["members"].toArray();
m_silverSponsors = silverObj["members"].toArray();
m_bronzeSponsors = bronzeObj["members"].toArray();
if (!m_platinumSponsors.isEmpty()) {
qDebug() << "Platinum Sponsors found";
}
}
qDebug() << "Sponsors fetch completed";
showDefaultApps();
reply->deleteLater();
} catch (...) {
qDebug() << "Exception occurred while processing sponsors";
showDefaultApps();
reply->deleteLater();
}
});
}
void AppsWidget::onAppStoreInitialized(const QJsonObject &accountInfo)
@@ -242,26 +386,102 @@ void AppsWidget::populateDefaultApps()
if (!gridLayout)
return;
// Create sample app cards
createAppCard("Instagram", "com.burbn.instagram",
"Photo & Video sharing social network", "", gridLayout, 0, 0);
createAppCard("WhatsApp", "net.whatsapp.WhatsApp",
"Free messaging and video calling", "", gridLayout, 0, 1);
createAppCard("Spotify", "com.spotify.client",
"Music streaming and podcast platform", "", gridLayout, 0, 2);
createAppCard("YouTube", "com.google.ios.youtube",
"Video sharing and streaming platform", "", gridLayout, 1, 0);
createAppCard("X", "com.atebits.Tweetie2", "Social media and microblogging",
"", gridLayout, 1, 1);
createAppCard("TikTok", "com.zhiliaoapp.musically",
"Short-form video hosting service", "", gridLayout, 1, 2);
createAppCard("Twitch", "tv.twitch", "Live streaming platform", "",
gridLayout, 2, 0);
createAppCard("Telegram", "ph.telegra.Telegraph",
"Cloud-based instant messaging", "", gridLayout, 2, 1);
createAppCard("Reddit", "com.reddit.Reddit",
"Social news aggregation platform", "", gridLayout, 2, 2);
int row = 0;
int col = 0;
const int maxCols = 3;
// Helper lambda to advance the grid position
auto advanceGridPos = [&]() {
col++;
if (col >= maxCols) {
col = 0;
row++;
}
};
for (const QJsonValue &sponsorValue : m_platinumSponsors) {
QJsonObject sponsorObj = sponsorValue.toObject();
QString name = sponsorObj.value("name").toString();
QString bundleId = sponsorObj.value("bundleId").toString();
QString logoUrl = sponsorObj.value("logo").toString();
QString description = sponsorObj.value("description").toString();
QString url = sponsorObj.value("url").toString();
createAppCard(name, bundleId, description, logoUrl, url, gridLayout,
row, col, SponsorType(SponsorType::Platinum));
advanceGridPos();
}
for (const QJsonValue &sponsorValue : m_goldSponsors) {
QJsonObject sponsorObj = sponsorValue.toObject();
QString name = sponsorObj.value("name").toString();
QString bundleId = sponsorObj.value("bundleId").toString();
QString description = sponsorObj.value("description").toString();
QString logoUrl = sponsorObj.value("logo").toString();
QString url = sponsorObj.value("url").toString();
createAppCard(name, bundleId, description, logoUrl, url, gridLayout,
row, col, SponsorType(SponsorType::Gold));
advanceGridPos();
}
if (m_platinumSponsors.empty() && m_goldSponsors.empty()) {
createSponsorCard(gridLayout, row, col);
advanceGridPos();
}
createAppCard("Instagram", "com.burbn.instagram",
"Photo & Video sharing social network", "", "", gridLayout,
row, col);
advanceGridPos();
createAppCard("Spotify", "com.spotify.client",
"Music streaming and podcast platform", "", "", gridLayout,
row, col);
advanceGridPos();
createAppCard("YouTube", "com.google.ios.youtube",
"Video sharing and streaming platform", "", "", gridLayout,
row, col);
advanceGridPos();
createAppCard("X", "com.atebits.Tweetie2", "Social media and microblogging",
"", "", gridLayout, row, col);
advanceGridPos();
createAppCard("TikTok", "com.zhiliaoapp.musically",
"Short-form video hosting service", "", "", gridLayout, row,
col);
advanceGridPos();
createAppCard("Twitch", "tv.twitch", "Live streaming platform", "", "",
gridLayout, row, col);
advanceGridPos();
createAppCard("Telegram", "ph.telegra.Telegraph",
"Cloud-based instant messaging", "", "", gridLayout, row,
col);
advanceGridPos();
createAppCard("Reddit", "com.reddit.Reddit",
"Social news aggregation platform", "", "", gridLayout, row,
col);
advanceGridPos();
for (const QJsonValue &sponsorValue : m_silverSponsors) {
QJsonObject sponsorObj = sponsorValue.toObject();
QString name = sponsorObj.value("name").toString();
QString bundleId = sponsorObj.value("bundleId").toString();
QString description = sponsorObj.value("description").toString();
QString url = sponsorObj.value("url").toString();
createAppCard(name, bundleId, description, "", url, gridLayout, row,
col, SponsorType(SponsorType::Silver));
advanceGridPos();
}
for (const QJsonValue &sponsorValue : m_bronzeSponsors) {
QJsonObject sponsorObj = sponsorValue.toObject();
QString name = sponsorObj.value("name").toString();
QString bundleId = sponsorObj.value("bundleId").toString();
QString description = sponsorObj.value("description").toString();
QString url = sponsorObj.value("url").toString();
createAppCard(name, bundleId, description, "", url, gridLayout, row,
col, SponsorType(SponsorType::Bronze));
advanceGridPos();
}
gridLayout->setRowStretch(gridLayout->rowCount(), 1);
}
@@ -281,10 +501,37 @@ void AppsWidget::clearAppGrid()
}
}
void AppsWidget::createSponsorCard(QGridLayout *gridLayout, int row, int col)
{
if (!gridLayout)
return;
ClickableWidget *sponsorCard = new ClickableWidget();
sponsorCard->setStyleSheet("border: 1px solid #ddd; border-radius: 8px;");
sponsorCard->setCursor(Qt::PointingHandCursor);
connect(sponsorCard, &ClickableWidget::clicked, this, [this]() {
auto sWidget = new SponsorWidget();
sWidget->setAttribute(Qt::WA_DeleteOnClose);
sWidget->show();
});
QVBoxLayout *sponsorLayout = new QVBoxLayout(sponsorCard);
sponsorLayout->setContentsMargins(12, 12, 12, 12);
sponsorLayout->setSpacing(8);
QLabel *sponsorLabel = new QLabel("Become a Sponsor!");
sponsorLabel->setAlignment(Qt::AlignCenter);
sponsorLabel->setStyleSheet("font-size: 14px; font-weight: bold;");
sponsorLayout->addWidget(sponsorLabel);
gridLayout->addWidget(sponsorCard, row, col);
}
void AppsWidget::createAppCard(const QString &name, const QString &bundleId,
const QString &description,
const QString &iconPath, QGridLayout *gridLayout,
int row, int col)
const QString &logoUrl,
const QString &websiteUrl,
QGridLayout *gridLayout, int row, int col,
const SponsorType &sponsorType)
{
QWidget *cardWidget = new QWidget();
@@ -301,74 +548,146 @@ void AppsWidget::createAppCard(const QString &name, const QString &bundleId,
iconLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(iconLabel);
fetchAppIconFromApple(
bundleId,
[iconLabel](const QPixmap &pixmap) {
if (!pixmap.isNull()) {
QPixmap scaled =
pixmap.scaled(64, 64, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
QPixmap rounded(64, 64);
rounded.fill(Qt::transparent);
// If logoUrl is provided and bundleId is empty, use the logo directly
if (!logoUrl.isEmpty()) {
QUrl url(logoUrl);
QNetworkRequest request(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, [reply, iconLabel]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
QPixmap pixmap;
if (pixmap.loadFromData(data)) {
QPixmap scaled =
pixmap.scaled(64, 64, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
QPixmap rounded(64, 64);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, 64, 64), 16, 16);
painter.setClipPath(path);
painter.drawPixmap(0, 0, scaled);
painter.end();
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, 64, 64), 16, 16);
painter.setClipPath(path);
painter.drawPixmap(0, 0, scaled);
painter.end();
iconLabel->setPixmap(rounded);
iconLabel->setPixmap(rounded);
}
}
},
cardWidget);
reply->deleteLater();
});
} else if (!bundleId.isEmpty()) {
// Use Apple's API for app icons
fetchAppIconFromApple(
bundleId,
[iconLabel](const QPixmap &pixmap) {
if (!pixmap.isNull()) {
QPixmap scaled =
pixmap.scaled(64, 64, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
QPixmap rounded(64, 64);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, 64, 64), 16, 16);
painter.setClipPath(path);
painter.drawPixmap(0, 0, scaled);
painter.end();
iconLabel->setPixmap(rounded);
}
},
cardWidget);
}
// Vertical layout for name and description
QVBoxLayout *textLayout = new QVBoxLayout();
// App name
// App name with sponsor indicator
QHBoxLayout *nameLayout = new QHBoxLayout();
QLabel *nameLabel = new QLabel(name);
nameLabel->setStyleSheet("font-size: 16px;");
nameLabel->setAlignment(Qt::AlignCenter);
nameLabel->setWordWrap(true);
textLayout->addWidget(nameLabel);
nameLayout->addWidget(nameLabel);
// Add sponsor type indicator
if (!sponsorType.isEmpty()) {
QLabel *sponsorLabel = new QLabel(sponsorType.name);
QString textColor = (sponsorType.level == SponsorType::Platinum ||
sponsorType.level == SponsorType::Silver)
? "#333"
: "white";
sponsorLabel->setStyleSheet(QString("font-size: 10px; "
"font-weight: bold; "
"color: %1; "
"background-color: %2; "
"border-radius: 4px; "
"padding: 2px 6px; "
"margin-left: 8px;")
.arg(textColor, sponsorType.color));
sponsorLabel->setAlignment(Qt::AlignCenter);
nameLayout->addWidget(sponsorLabel);
}
nameLayout->addStretch();
textLayout->addLayout(nameLayout);
// App description
QLabel *descLabel = new QLabel(description);
descLabel->setStyleSheet("font-size: 12px; color: #666;");
descLabel->setAlignment(Qt::AlignCenter);
descLabel->setAlignment(Qt::AlignLeft);
descLabel->setWordWrap(true);
textLayout->addWidget(descLabel);
cardLayout->addStretch();
cardLayout->addLayout(textLayout);
QVBoxLayout *buttonsLayout = new QVBoxLayout();
// Install button placeholder
ZLabel *installLabel = new ZLabel("Install");
installLabel->setAlignment(Qt::AlignCenter);
installLabel->setStyleSheet("font-size: 12px; color: #007AFF; font-weight: "
"bold; background-color: transparent;");
installLabel->setCursor(Qt::PointingHandCursor);
installLabel->setFixedHeight(30);
if (!bundleId.isEmpty()) {
ZLabel *installLabel = new ZLabel("Install");
installLabel->setAlignment(Qt::AlignCenter);
installLabel->setStyleSheet(
"font-size: 12px; color: #007AFF; font-weight: "
"bold; background-color: transparent;");
installLabel->setCursor(Qt::PointingHandCursor);
installLabel->setFixedHeight(30);
ZLabel *downloadIpaLabel = new ZLabel("Download IPA");
downloadIpaLabel->setAlignment(Qt::AlignCenter);
downloadIpaLabel->setCursor(Qt::PointingHandCursor);
buttonsLayout->addStretch();
buttonsLayout->addWidget(installLabel);
connect(installLabel, &ZLabel::clicked, this,
[this, name, bundleId, description]() {
onAppCardClicked(name, bundleId, description);
});
}
if (websiteUrl.isEmpty()) {
ZLabel *downloadIpaLabel = new ZLabel("Download IPA");
downloadIpaLabel->setAlignment(Qt::AlignCenter);
downloadIpaLabel->setStyleSheet("font-size: 12px; font-weight: "
"bold; background-color: transparent;");
downloadIpaLabel->setCursor(Qt::PointingHandCursor);
connect(installLabel, &ZLabel::clicked, this,
[this, name, bundleId, description]() {
onAppCardClicked(name, bundleId, description);
});
connect(downloadIpaLabel, &ZLabel::clicked, this,
connect(
downloadIpaLabel, &ZLabel::clicked, this,
[this, name, bundleId]() { onDownloadIpaClicked(name, bundleId); });
buttonsLayout->addStretch();
buttonsLayout->addWidget(installLabel);
buttonsLayout->addWidget(downloadIpaLabel);
buttonsLayout->addWidget(downloadIpaLabel);
} else {
ZLabel *websiteLabel = new ZLabel("Website");
websiteLabel->setStyleSheet("font-size: 12px; font-weight: "
"bold; background-color: transparent;");
websiteLabel->setAlignment(Qt::AlignCenter);
websiteLabel->setCursor(Qt::PointingHandCursor);
connect(websiteLabel, &ZLabel::clicked, this, [this, websiteUrl]() {
QDesktopServices::openUrl(QUrl(websiteUrl));
});
buttonsLayout->addWidget(websiteLabel);
}
buttonsLayout->addStretch();
cardLayout->addLayout(buttonsLayout);
@@ -434,7 +753,6 @@ void AppsWidget::onSearchTextChanged() { m_debounceTimer->start(300); }
void AppsWidget::performSearch()
{
QString searchTerm = m_searchEdit->text().trimmed();
if (searchTerm.isEmpty()) {
showDefaultApps();
@@ -510,7 +828,8 @@ void AppsWidget::onSearchFinished(bool success, const QString &results)
QString bundleId = appObj.value("bundleId").toString();
QString description = "Version: " + appObj.value("version").toString();
createAppCard(name, bundleId, description, "", gridLayout, row, col);
createAppCard(name, bundleId, description, "", "", gridLayout, row,
col);
col++;
if (col >= maxCols) {