mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-22 03:45:51 +08:00
WIP: Implement Installed Apps Widget and AFC2 file explorer
- Added InstalledAppsWidget for displaying installed applications on the device. - Created AppTabWidget for individual app representation with icon and details. - Integrated fetching of app icons from Apple and displaying them in the widget. - Implemented search functionality to filter installed apps. - Added functionality to manage favorite places in SettingsManager. - Introduced methods to save, remove, and retrieve favorite places with proper cleanup of invalid entries. - Enhanced UI with responsive design and improved user interaction. - Added support for AFC2 and improved file exploration capabilities.
This commit is contained in:
@@ -111,6 +111,7 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type,
|
||||
.device = initResult.device,
|
||||
.deviceInfo = initResult.deviceInfo,
|
||||
.afcClient = initResult.afcClient,
|
||||
.afc2Client = initResult.afc2Client,
|
||||
};
|
||||
m_devices[device->udid] = device;
|
||||
if (addType == AddType::Regular)
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPixmap>
|
||||
@@ -45,62 +43,6 @@
|
||||
#include <QWidget>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
// Callback: void(QPixmap)
|
||||
// TODO : move to utils
|
||||
void fetchAppIconFromApple(const QString &bundleId,
|
||||
std::function<void(const QPixmap &)> callback,
|
||||
QObject *context)
|
||||
{
|
||||
QNetworkAccessManager *manager = new QNetworkAccessManager(context);
|
||||
QString url =
|
||||
QString("https://itunes.apple.com/lookup?bundleId=%1").arg(bundleId);
|
||||
|
||||
QNetworkReply *reply = manager->get(QNetworkRequest(QUrl(url)));
|
||||
QObject::connect(
|
||||
reply, &QNetworkReply::finished, context,
|
||||
[reply, callback, manager, context]() {
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
callback(QPixmap());
|
||||
manager->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
QJsonArray results = obj.value("results").toArray();
|
||||
if (results.isEmpty()) {
|
||||
callback(QPixmap());
|
||||
manager->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject appInfo = results.at(0).toObject();
|
||||
QString iconUrl = appInfo.value("artworkUrl100").toString();
|
||||
if (iconUrl.isEmpty()) {
|
||||
callback(QPixmap());
|
||||
manager->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the icon image
|
||||
QNetworkReply *iconReply =
|
||||
manager->get(QNetworkRequest(QUrl(iconUrl)));
|
||||
QObject::connect(iconReply, &QNetworkReply::finished, context,
|
||||
[iconReply, callback, manager]() {
|
||||
QByteArray iconData = iconReply->readAll();
|
||||
iconReply->deleteLater();
|
||||
QPixmap pixmap;
|
||||
pixmap.loadFromData(iconData);
|
||||
callback(pixmap);
|
||||
manager->deleteLater();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
AppsWidget::AppsWidget(QWidget *parent) : QWidget(parent), m_isLoggedIn(false)
|
||||
{
|
||||
// m_searchProcess = new QProcess(this);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPixmap>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
void fetchAppIconFromApple(const QString &bundleId,
|
||||
std::function<void(const QPixmap &)> callback,
|
||||
QObject *context)
|
||||
{
|
||||
QNetworkAccessManager *manager = new QNetworkAccessManager(context);
|
||||
QString url =
|
||||
QString("https://itunes.apple.com/lookup?bundleId=%1").arg(bundleId);
|
||||
|
||||
QNetworkReply *reply = manager->get(QNetworkRequest(QUrl(url)));
|
||||
QObject::connect(
|
||||
reply, &QNetworkReply::finished, context,
|
||||
[reply, callback, manager, context]() {
|
||||
QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
callback(QPixmap());
|
||||
manager->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
QJsonArray results = obj.value("results").toArray();
|
||||
if (results.isEmpty()) {
|
||||
callback(QPixmap());
|
||||
manager->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject appInfo = results.at(0).toObject();
|
||||
QString iconUrl = appInfo.value("artworkUrl100").toString();
|
||||
if (iconUrl.isEmpty()) {
|
||||
callback(QPixmap());
|
||||
manager->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the icon image
|
||||
QNetworkReply *iconReply =
|
||||
manager->get(QNetworkRequest(QUrl(iconUrl)));
|
||||
QObject::connect(iconReply, &QNetworkReply::finished, context,
|
||||
[iconReply, callback, manager]() {
|
||||
QByteArray iconData = iconReply->readAll();
|
||||
iconReply->deleteLater();
|
||||
QPixmap pixmap;
|
||||
pixmap.loadFromData(iconData);
|
||||
callback(pixmap);
|
||||
manager->deleteLater();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "../../iDescriptor.h"
|
||||
#include <QDebug>
|
||||
#include <libimobiledevice/afc.h>
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
#include <libimobiledevice/lockdown.h>
|
||||
|
||||
afc_error_t afc2_client_new(idevice_t device, afc_client_t *afc)
|
||||
{
|
||||
|
||||
lockdownd_service_descriptor_t service = NULL;
|
||||
// TODO: should free service ?
|
||||
lockdownd_client_t client = NULL;
|
||||
|
||||
if (lockdownd_client_new_with_handshake(device, &client, APP_LABEL) !=
|
||||
LOCKDOWN_E_SUCCESS) {
|
||||
qDebug() << "Could not connect to lockdownd";
|
||||
return AFC_E_UNKNOWN_ERROR;
|
||||
}
|
||||
if (lockdownd_start_service(client, AFC2_SERVICE_NAME, &service) !=
|
||||
LOCKDOWN_E_SUCCESS) {
|
||||
qDebug() << "Could not start AFC service";
|
||||
lockdownd_client_free(client);
|
||||
return AFC_E_UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
return afc_client_new(device, service, afc);
|
||||
|
||||
// char **dirs = NULL;
|
||||
// if (afc_read_directory(afc, argv[1], &dirs) == AFC_E_SUCCESS) {
|
||||
// for (int i = 0; dirs[i]; i++) {
|
||||
// printf("Entry: %s\n", dirs[i]);
|
||||
// }
|
||||
// // free(dirs);
|
||||
// }
|
||||
}
|
||||
@@ -209,7 +209,6 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc,
|
||||
/*BatteryInfo*/
|
||||
plist_t diagnostics = nullptr;
|
||||
get_battery_info(rawProductType, result.device, d.is_iPhone, diagnostics);
|
||||
plist_print(diagnostics);
|
||||
|
||||
if (!diagnostics) {
|
||||
qDebug() << "Failed to get diagnostics plist.";
|
||||
@@ -266,7 +265,7 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: need to handle errors and free resources properly
|
||||
// TODO: IDescriptorInitDeviceResult
|
||||
IDescriptorInitDeviceResult init_idescriptor_device(const char *udid)
|
||||
{
|
||||
// TODO:on a broken usb cable this can hang for a long time
|
||||
@@ -281,6 +280,7 @@ IDescriptorInitDeviceResult init_idescriptor_device(const char *udid)
|
||||
lockdownd_error_t ldret = LOCKDOWN_E_UNKNOWN_ERROR;
|
||||
lockdownd_service_descriptor_t lockdownService = nullptr;
|
||||
afc_client_t afcClient = nullptr;
|
||||
afc_client_t afc2Client = nullptr;
|
||||
try {
|
||||
idevice_error_t ret = idevice_new_with_options(&result.device, udid,
|
||||
IDEVICE_LOOKUP_USBMUX);
|
||||
@@ -323,6 +323,20 @@ IDescriptorInitDeviceResult init_idescriptor_device(const char *udid)
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
afc_error_t err = AFC_E_UNKNOWN_ERROR;
|
||||
if ((err = afc2_client_new(result.device, &afc2Client)) !=
|
||||
AFC_E_SUCCESS) {
|
||||
qDebug() << "AFC2 client not available." << "Error:" << err;
|
||||
} else {
|
||||
result.afc2Client = afc2Client;
|
||||
qDebug() << "AFC2 client created successfully.";
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
/* Fine! This only works on Jailbroken and AFC2 tweak installed
|
||||
* devices */
|
||||
}
|
||||
|
||||
pugi::xml_document infoXml;
|
||||
get_device_info_xml(udid, 0, 0, infoXml, client, result.device);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "batterywidget.h"
|
||||
#include "diskusagewidget.h"
|
||||
#include "fileexplorerwidget.h"
|
||||
#include "iDescriptor-ui.h"
|
||||
#include "iDescriptor.h"
|
||||
#include <QDebug>
|
||||
#include <QGraphicsPixmapItem>
|
||||
@@ -20,26 +21,6 @@
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
// A custom QGraphicsView that keeps the content fitted with aspect ratio on
|
||||
// resize
|
||||
class ResponsiveGraphicsView : public QGraphicsView
|
||||
{
|
||||
public:
|
||||
ResponsiveGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr)
|
||||
: QGraphicsView(scene, parent)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override
|
||||
{
|
||||
if (scene() && !scene()->items().isEmpty()) {
|
||||
fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio);
|
||||
}
|
||||
QGraphicsView::resizeEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent)
|
||||
: QWidget(parent), m_device(device)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// #ifndef DEVICELISTENER_H
|
||||
// #define DEVICELISTENER_H
|
||||
// #include <QBluetoothDeviceInfo>
|
||||
// #include <QLowEnergyAdvertisingData>
|
||||
// #include <QLowEnergyController>
|
||||
// #include <QLowEnergyServiceData>
|
||||
// #include <QObject>
|
||||
|
||||
// class DeviceListener : public QObject
|
||||
// {
|
||||
// Q_OBJECT
|
||||
|
||||
// public:
|
||||
// DeviceListener();
|
||||
// ~DeviceListener();
|
||||
|
||||
// private slots:
|
||||
// void clientConnected();
|
||||
// void clientDisconnected();
|
||||
|
||||
// private:
|
||||
// void setupVirtualKeyboard();
|
||||
|
||||
// QLowEnergyAdvertisingData m_advertisingData;
|
||||
// QLowEnergyServiceData m_hidServiceData;
|
||||
|
||||
// QLowEnergyController *m_leController = nullptr;
|
||||
// QLowEnergyService *m_service = nullptr;
|
||||
// QLowEnergyCharacteristic
|
||||
// m_inputReportChar; // Keep a reference to the characteristic
|
||||
// };
|
||||
|
||||
// #endif // DEVICELISTENER_H
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "fileexplorerwidget.h"
|
||||
#include "gallerywidget.h"
|
||||
#include "iDescriptor.h"
|
||||
#include "installedappswidget.h"
|
||||
#include <QDebug>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
@@ -17,19 +18,19 @@ DeviceMenuWidget::DeviceMenuWidget(iDescriptorDevice *device, QWidget *parent)
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
|
||||
mainLayout->addWidget(tabWidget);
|
||||
|
||||
tabWidget->addTab(new DeviceInfoWidget(device, this), "");
|
||||
|
||||
// FIXME:race condition with lockdownd_client_new_with_handshake
|
||||
FileExplorerWidget *explorer = new FileExplorerWidget(device, this);
|
||||
explorer->setMinimumHeight(300);
|
||||
|
||||
tabWidget->addTab(explorer, "");
|
||||
|
||||
GalleryWidget *gallery = new GalleryWidget(device, this);
|
||||
unsigned int galleryIndex = tabWidget->addTab(gallery, "");
|
||||
gallery->setMinimumHeight(300);
|
||||
setLayout(mainLayout);
|
||||
|
||||
tabWidget->addTab(new DeviceInfoWidget(device, this), "");
|
||||
tabWidget->addTab(new InstalledAppsWidget(device, this), "");
|
||||
unsigned int galleryIndex = tabWidget->addTab(gallery, "");
|
||||
tabWidget->addTab(explorer, "");
|
||||
|
||||
// TODO : one time ?
|
||||
connect(tabWidget, &QTabWidget::currentChanged, this,
|
||||
[this, galleryIndex, gallery](int index) {
|
||||
if (index == galleryIndex) {
|
||||
@@ -43,10 +44,12 @@ void DeviceMenuWidget::switchToTab(const QString &tabName)
|
||||
{
|
||||
if (tabName == "Info") {
|
||||
tabWidget->setCurrentIndex(0);
|
||||
} else if (tabName == "Files") {
|
||||
} else if (tabName == "Apps") {
|
||||
tabWidget->setCurrentIndex(1);
|
||||
} else if (tabName == "Gallery") {
|
||||
tabWidget->setCurrentIndex(2);
|
||||
} else if (tabName == "Files") {
|
||||
tabWidget->setCurrentIndex(3);
|
||||
} else {
|
||||
qDebug() << "Tab not found:" << tabName;
|
||||
}
|
||||
|
||||
+233
-40
@@ -1,68 +1,58 @@
|
||||
#include "fileexplorerwidget.h"
|
||||
#include "iDescriptor.h"
|
||||
#include "mediapreviewdialog.h"
|
||||
#include "settingsmanager.h"
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QFileDialog>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QInputDialog>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSplitter>
|
||||
#include <QTreeWidget>
|
||||
#include <QVariant>
|
||||
#include <libimobiledevice/afc.h>
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
|
||||
FileExplorerWidget::FileExplorerWidget(iDescriptorDevice *device,
|
||||
QWidget *parent)
|
||||
: QWidget(parent), device(device)
|
||||
: QWidget(parent), device(device), usingAFC2(false)
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
// Initialize current AFC client to default
|
||||
currentAfcClient = device->afcClient;
|
||||
qDebug() << "AFC2 available:" << (device->afc2Client != nullptr);
|
||||
// Create main splitter
|
||||
mainSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
|
||||
// --- New: Export/Import buttons layout ---
|
||||
QHBoxLayout *exportLayout = new QHBoxLayout();
|
||||
exportBtn = new QPushButton("Export");
|
||||
exportDeleteBtn = new QPushButton("Export & Delete");
|
||||
importBtn = new QPushButton("Import"); // NEW
|
||||
exportLayout->addWidget(exportBtn);
|
||||
exportLayout->addWidget(exportDeleteBtn);
|
||||
exportLayout->addWidget(importBtn); // NEW
|
||||
exportLayout->addStretch();
|
||||
mainLayout->addLayout(exportLayout);
|
||||
// Setup sidebar
|
||||
setupSidebar();
|
||||
|
||||
// --- Navigation layout (Back + Breadcrumb) ---
|
||||
QHBoxLayout *navLayout = new QHBoxLayout();
|
||||
backBtn = new QPushButton("Back");
|
||||
breadcrumbLayout = new QHBoxLayout();
|
||||
breadcrumbLayout->setSpacing(0);
|
||||
navLayout->addWidget(backBtn);
|
||||
navLayout->addLayout(breadcrumbLayout);
|
||||
navLayout->addStretch();
|
||||
mainLayout->addLayout(navLayout);
|
||||
// Setup file explorer
|
||||
setupFileExplorer();
|
||||
|
||||
fileList = new QListWidget();
|
||||
fileList->setSelectionMode(
|
||||
QAbstractItemView::ExtendedSelection); // Enable multi-select
|
||||
mainLayout->addWidget(fileList);
|
||||
|
||||
connect(backBtn, &QPushButton::clicked, this, &FileExplorerWidget::goBack);
|
||||
connect(fileList, &QListWidget::itemDoubleClicked, this,
|
||||
&FileExplorerWidget::onItemDoubleClicked);
|
||||
|
||||
// --- New: Export/Import buttons connections ---
|
||||
connect(exportBtn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onExportClicked);
|
||||
connect(exportDeleteBtn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onExportDeleteClicked);
|
||||
connect(importBtn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onImportClicked); // NEW
|
||||
// Add widgets to splitter
|
||||
mainSplitter->addWidget(sidebarTree);
|
||||
mainSplitter->addWidget(fileExplorerWidget);
|
||||
mainSplitter->setSizes({400, 800});
|
||||
|
||||
// Main layout
|
||||
QHBoxLayout *mainLayout = new QHBoxLayout(this);
|
||||
mainLayout->addWidget(mainSplitter);
|
||||
setLayout(mainLayout);
|
||||
|
||||
// Initialize
|
||||
history.push("/");
|
||||
loadPath("/");
|
||||
|
||||
setupContextMenu();
|
||||
connect(SettingsManager::sharedInstance(),
|
||||
&SettingsManager::favoritePlacesChanged, this,
|
||||
&FileExplorerWidget::refreshFavoritePlaces);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::goBack()
|
||||
@@ -162,7 +152,7 @@ void FileExplorerWidget::loadPath(const QString &path)
|
||||
updateBreadcrumb(path);
|
||||
|
||||
MediaFileTree tree =
|
||||
get_file_tree(device->afcClient, device->device, path.toStdString());
|
||||
get_file_tree(currentAfcClient, device->device, path.toStdString());
|
||||
if (!tree.success) {
|
||||
fileList->addItem("Failed to load directory");
|
||||
return;
|
||||
@@ -283,7 +273,7 @@ void FileExplorerWidget::exportSelectedFile(QListWidgetItem *item,
|
||||
|
||||
// Export file using the validated connections
|
||||
int result =
|
||||
export_file_to_path(device->afcClient, devicePath.toStdString().c_str(),
|
||||
export_file_to_path(currentAfcClient, devicePath.toStdString().c_str(),
|
||||
savePath.toStdString().c_str());
|
||||
|
||||
qDebug() << "Export result:" << result;
|
||||
@@ -366,7 +356,7 @@ void FileExplorerWidget::onImportClicked()
|
||||
for (const QString &localPath : fileNames) {
|
||||
QFileInfo fi(localPath);
|
||||
QString devicePath = currPath + fi.fileName();
|
||||
int result = import_file_to_device(device->afcClient,
|
||||
int result = import_file_to_device(currentAfcClient,
|
||||
devicePath.toStdString().c_str(),
|
||||
localPath.toStdString().c_str());
|
||||
if (result == 0)
|
||||
@@ -416,3 +406,206 @@ int FileExplorerWidget::import_file_to_device(afc_client_t afc,
|
||||
in.close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// useAFC2 ,path,
|
||||
typedef QPair<bool, QString> SidebarItemData;
|
||||
|
||||
void FileExplorerWidget::setupSidebar()
|
||||
{
|
||||
sidebarTree = new QTreeWidget();
|
||||
sidebarTree->setHeaderLabel("Files");
|
||||
sidebarTree->setMinimumWidth(350);
|
||||
sidebarTree->setMaximumWidth(400);
|
||||
|
||||
// AFC Default section
|
||||
afcDefaultItem = new QTreeWidgetItem(sidebarTree);
|
||||
afcDefaultItem->setText(0, "Explorer");
|
||||
afcDefaultItem->setIcon(0, QIcon::fromTheme("folder"));
|
||||
afcDefaultItem->setData(0, Qt::UserRole,
|
||||
QVariant::fromValue(SidebarItemData(false, "/")));
|
||||
afcDefaultItem->setExpanded(true);
|
||||
|
||||
// Add root folder under Default
|
||||
QTreeWidgetItem *rootItem = new QTreeWidgetItem(afcDefaultItem);
|
||||
rootItem->setText(0, "Default");
|
||||
rootItem->setIcon(0, QIcon::fromTheme("folder"));
|
||||
rootItem->setData(0, Qt::UserRole,
|
||||
QVariant::fromValue(SidebarItemData(false, "/")));
|
||||
rootItem->setData(0, Qt::UserRole + 1, QVariant::fromValue(false));
|
||||
|
||||
// AFC2 Jailbroken section
|
||||
afcJailbrokenItem = new QTreeWidgetItem(afcDefaultItem);
|
||||
afcJailbrokenItem->setText(0, "Jailbroken (AFC2)");
|
||||
afcJailbrokenItem->setIcon(0, QIcon::fromTheme("applications-system"));
|
||||
afcJailbrokenItem->setData(0, Qt::UserRole,
|
||||
QVariant::fromValue(SidebarItemData(true, "/")));
|
||||
afcJailbrokenItem->setExpanded(false);
|
||||
|
||||
// Common Places section
|
||||
commonPlacesItem = new QTreeWidgetItem(sidebarTree);
|
||||
commonPlacesItem->setText(0, "Common Places");
|
||||
commonPlacesItem->setIcon(0, QIcon::fromTheme("places-bookmarks"));
|
||||
commonPlacesItem->setData(
|
||||
0, Qt::UserRole,
|
||||
QVariant::fromValue(
|
||||
SidebarItemData(false, "../../../var/mobile/Library/Wallpapers")));
|
||||
commonPlacesItem->setExpanded(true);
|
||||
|
||||
QTreeWidgetItem *wallpapersItem = new QTreeWidgetItem(commonPlacesItem);
|
||||
wallpapersItem->setText(0, "Wallpapers");
|
||||
wallpapersItem->setIcon(0, QIcon::fromTheme("image-x-generic"));
|
||||
wallpapersItem->setData(
|
||||
0, Qt::UserRole,
|
||||
QVariant::fromValue(
|
||||
SidebarItemData(false, "../../../var/mobile/Library/Wallpapers")));
|
||||
wallpapersItem->setData(0, Qt::UserRole + 1,
|
||||
QVariant::fromValue(false)); // Default AFC
|
||||
|
||||
// Favorite Places section
|
||||
favoritePlacesItem = new QTreeWidgetItem(sidebarTree);
|
||||
favoritePlacesItem->setText(0, "Favorite Places");
|
||||
favoritePlacesItem->setIcon(0, QIcon::fromTheme("user-bookmarks"));
|
||||
favoritePlacesItem->setData(
|
||||
// todo:implement
|
||||
0, Qt::UserRole, QVariant::fromValue(SidebarItemData(false, "/")));
|
||||
favoritePlacesItem->setExpanded(true);
|
||||
|
||||
loadFavoritePlaces();
|
||||
|
||||
connect(sidebarTree, &QTreeWidget::itemClicked, this,
|
||||
&FileExplorerWidget::onSidebarItemClicked);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::setupFileExplorer()
|
||||
{
|
||||
fileExplorerWidget = new QWidget();
|
||||
QVBoxLayout *explorerLayout = new QVBoxLayout(fileExplorerWidget);
|
||||
|
||||
// Export/Import buttons layout
|
||||
QHBoxLayout *exportLayout = new QHBoxLayout();
|
||||
exportBtn = new QPushButton("Export");
|
||||
exportDeleteBtn = new QPushButton("Export & Delete");
|
||||
importBtn = new QPushButton("Import");
|
||||
addToFavoritesBtn = new QPushButton("Add to Favorites");
|
||||
exportLayout->addWidget(exportBtn);
|
||||
exportLayout->addWidget(exportDeleteBtn);
|
||||
exportLayout->addWidget(importBtn);
|
||||
exportLayout->addWidget(addToFavoritesBtn);
|
||||
exportLayout->addStretch();
|
||||
explorerLayout->addLayout(exportLayout);
|
||||
|
||||
// Navigation layout (Back + Breadcrumb)
|
||||
QHBoxLayout *navLayout = new QHBoxLayout();
|
||||
backBtn = new QPushButton("Back");
|
||||
breadcrumbLayout = new QHBoxLayout();
|
||||
breadcrumbLayout->setSpacing(0);
|
||||
navLayout->addWidget(backBtn);
|
||||
navLayout->addLayout(breadcrumbLayout);
|
||||
navLayout->addStretch();
|
||||
explorerLayout->addLayout(navLayout);
|
||||
|
||||
// File list
|
||||
fileList = new QListWidget();
|
||||
fileList->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
explorerLayout->addWidget(fileList);
|
||||
|
||||
// Connect buttons
|
||||
connect(backBtn, &QPushButton::clicked, this, &FileExplorerWidget::goBack);
|
||||
connect(fileList, &QListWidget::itemDoubleClicked, this,
|
||||
&FileExplorerWidget::onItemDoubleClicked);
|
||||
connect(exportBtn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onExportClicked);
|
||||
connect(exportDeleteBtn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onExportDeleteClicked);
|
||||
connect(importBtn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onImportClicked);
|
||||
connect(addToFavoritesBtn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onAddToFavoritesClicked);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onSidebarItemClicked(QTreeWidgetItem *item, int column)
|
||||
{
|
||||
Q_UNUSED(column)
|
||||
|
||||
bool useAfc2 = item->data(0, Qt::UserRole).value<SidebarItemData>().first;
|
||||
QString path = item->data(0, Qt::UserRole).value<SidebarItemData>().second;
|
||||
|
||||
// if (itemType == "try_install_afc2") {
|
||||
// onTryInstallAFC2Clicked();
|
||||
// return;
|
||||
// }
|
||||
|
||||
switchToAFC(useAfc2);
|
||||
loadPath(path);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onAddToFavoritesClicked()
|
||||
{
|
||||
QString currentPath = "/";
|
||||
if (!history.isEmpty())
|
||||
currentPath = history.top();
|
||||
|
||||
bool ok;
|
||||
QString alias = QInputDialog::getText(
|
||||
this, "Add to Favorites",
|
||||
"Enter alias for this location:", QLineEdit::Normal, currentPath, &ok);
|
||||
if (ok && !alias.isEmpty()) {
|
||||
saveFavoritePlace(currentPath, alias);
|
||||
refreshFavoritePlaces();
|
||||
}
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onTryInstallAFC2Clicked()
|
||||
{
|
||||
qDebug() << "Clicked on try to install AFC2";
|
||||
}
|
||||
|
||||
void FileExplorerWidget::switchToAFC(bool useAFC2)
|
||||
{
|
||||
if (useAFC2 && device->afc2Client) {
|
||||
usingAFC2 = true;
|
||||
currentAfcClient = device->afc2Client;
|
||||
} else {
|
||||
usingAFC2 = false;
|
||||
currentAfcClient = device->afcClient;
|
||||
}
|
||||
}
|
||||
|
||||
void FileExplorerWidget::loadFavoritePlaces()
|
||||
{
|
||||
SettingsManager *settings = SettingsManager::sharedInstance();
|
||||
QList<QPair<QString, QString>> favorites = settings->getFavoritePlaces();
|
||||
qDebug() << "Loading favorite places:" << favorites.size();
|
||||
for (const auto &favorite : favorites) {
|
||||
QString path = favorite.first;
|
||||
QString alias = favorite.second;
|
||||
|
||||
qDebug() << "Favorite:" << alias << "->" << path;
|
||||
QTreeWidgetItem *favoriteItem = new QTreeWidgetItem(favoritePlacesItem);
|
||||
favoriteItem->setText(0, alias);
|
||||
favoriteItem->setIcon(0, QIcon::fromTheme("folder-favorites"));
|
||||
favoriteItem->setData(
|
||||
0, Qt::UserRole, QVariant::fromValue(SidebarItemData(false, path)));
|
||||
favoriteItem->setData(0, Qt::UserRole + 1,
|
||||
QVariant::fromValue(false)); // Default to AFC
|
||||
}
|
||||
}
|
||||
|
||||
void FileExplorerWidget::saveFavoritePlace(const QString &path,
|
||||
const QString &alias)
|
||||
{
|
||||
qDebug() << "Saving favorite place:" << alias << "->" << path;
|
||||
SettingsManager *settings = SettingsManager::sharedInstance();
|
||||
settings->saveFavoritePlace(path, alias);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::refreshFavoritePlaces()
|
||||
{
|
||||
// Clear existing favorite items
|
||||
while (favoritePlacesItem->childCount() > 0) {
|
||||
delete favoritePlacesItem->takeChild(0);
|
||||
}
|
||||
|
||||
// Reload favorite places
|
||||
loadFavoritePlaces();
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
#include "iDescriptor.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QStack>
|
||||
#include <QString>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libimobiledevice/afc.h>
|
||||
@@ -31,19 +34,42 @@ private slots:
|
||||
void onExportClicked();
|
||||
void onExportDeleteClicked();
|
||||
void onImportClicked();
|
||||
void onSidebarItemClicked(QTreeWidgetItem *item, int column);
|
||||
void onAddToFavoritesClicked();
|
||||
void onTryInstallAFC2Clicked();
|
||||
|
||||
private:
|
||||
QSplitter *mainSplitter;
|
||||
QTreeWidget *sidebarTree;
|
||||
QWidget *fileExplorerWidget;
|
||||
QPushButton *backBtn;
|
||||
QPushButton *exportBtn;
|
||||
QPushButton *exportDeleteBtn;
|
||||
QPushButton *importBtn;
|
||||
QPushButton *addToFavoritesBtn;
|
||||
QListWidget *fileList;
|
||||
QStack<QString> history;
|
||||
QHBoxLayout *breadcrumbLayout;
|
||||
iDescriptorDevice *device;
|
||||
|
||||
// Current AFC mode
|
||||
bool usingAFC2;
|
||||
afc_client_t currentAfcClient;
|
||||
|
||||
// Tree items
|
||||
QTreeWidgetItem *afcDefaultItem;
|
||||
QTreeWidgetItem *afcJailbrokenItem;
|
||||
QTreeWidgetItem *commonPlacesItem;
|
||||
QTreeWidgetItem *favoritePlacesItem;
|
||||
|
||||
void setupSidebar();
|
||||
void setupFileExplorer();
|
||||
void loadPath(const QString &path);
|
||||
void updateBreadcrumb(const QString &path);
|
||||
void loadFavoritePlaces();
|
||||
void saveFavoritePlace(const QString &path, const QString &alias);
|
||||
void refreshFavoritePlaces();
|
||||
void switchToAFC(bool useAFC2);
|
||||
|
||||
void setupContextMenu();
|
||||
void exportSelectedFile(QListWidgetItem *item);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include <QGraphicsView>
|
||||
// A custom QGraphicsView that keeps the content fitted with aspect ratio on
|
||||
// resize
|
||||
class ResponsiveGraphicsView : public QGraphicsView
|
||||
{
|
||||
public:
|
||||
ResponsiveGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr)
|
||||
: QGraphicsView(scene, parent)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override
|
||||
{
|
||||
if (scene() && !scene()->items().isEmpty()) {
|
||||
fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio);
|
||||
}
|
||||
QGraphicsView::resizeEvent(event);
|
||||
}
|
||||
};
|
||||
+10
-2
@@ -15,7 +15,7 @@
|
||||
#define APP_LABEL "iDescriptor"
|
||||
#define APP_VERSION "0.0.1"
|
||||
#define APP_COPYRIGHT "© 2023 Uncore. All rights reserved."
|
||||
|
||||
#define AFC2_SERVICE_NAME "com.apple.afc2"
|
||||
#define RECOVERY_CLIENT_CONNECTION_TRIES 3
|
||||
#define APPLE_VENDOR_ID 0x05ac
|
||||
|
||||
@@ -144,6 +144,7 @@ struct iDescriptorDevice {
|
||||
clients are not long lived, so do not assume this will be valid
|
||||
*/
|
||||
afc_client_t afcClient;
|
||||
afc_client_t afc2Client;
|
||||
bool is_iPhone;
|
||||
};
|
||||
|
||||
@@ -153,6 +154,7 @@ struct IDescriptorInitDeviceResult {
|
||||
idevice_t device;
|
||||
DeviceInfo deviceInfo;
|
||||
afc_client_t afcClient;
|
||||
afc_client_t afc2Client;
|
||||
};
|
||||
|
||||
// Device model identifier to marketing name mapping
|
||||
@@ -440,4 +442,10 @@ void get_battery_info(std::string productType, idevice_t idevice,
|
||||
bool is_iphone, plist_t &diagnostics);
|
||||
|
||||
void parseOldDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d);
|
||||
void parseDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d);
|
||||
void parseDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d);
|
||||
|
||||
void fetchAppIconFromApple(const QString &bundleId,
|
||||
std::function<void(const QPixmap &)> callback,
|
||||
QObject *context);
|
||||
|
||||
afc_error_t afc2_client_new(idevice_t device, afc_client_t *afc);
|
||||
|
||||
@@ -0,0 +1,840 @@
|
||||
#include "installedappswidget.h"
|
||||
#include "iDescriptor.h"
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QEnterEvent>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLineEdit>
|
||||
#include <QStyle>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <libimobiledevice/afc.h>
|
||||
#include <libimobiledevice/house_arrest.h>
|
||||
#include <libimobiledevice/installation_proxy.h>
|
||||
#include <libimobiledevice/lockdown.h>
|
||||
#include <plist/plist.h>
|
||||
|
||||
// AppTabWidget Implementation
|
||||
AppTabWidget::AppTabWidget(const QString &appName, const QString &bundleId,
|
||||
const QString &version, QWidget *parent)
|
||||
: QWidget(parent), m_appName(appName), m_bundleId(bundleId),
|
||||
m_version(version)
|
||||
{
|
||||
setFixedHeight(60);
|
||||
setMinimumWidth(250);
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
|
||||
// Load placeholder icon
|
||||
m_icon = QApplication::style()
|
||||
->standardIcon(QStyle::SP_ComputerIcon)
|
||||
.pixmap(32, 32);
|
||||
|
||||
fetchAppIcon();
|
||||
}
|
||||
|
||||
void AppTabWidget::fetchAppIcon()
|
||||
{
|
||||
fetchAppIconFromApple(
|
||||
m_bundleId,
|
||||
[this](const QPixmap &pixmap) {
|
||||
if (!pixmap.isNull()) {
|
||||
QPixmap scaled =
|
||||
pixmap.scaled(32, 32, Qt::KeepAspectRatioByExpanding,
|
||||
Qt::SmoothTransformation);
|
||||
QPixmap rounded(32, 32);
|
||||
rounded.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&rounded);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(QRectF(0, 0, 32, 32), 8, 8);
|
||||
painter.setClipPath(path);
|
||||
painter.drawPixmap(0, 0, scaled);
|
||||
painter.end();
|
||||
|
||||
m_icon = rounded;
|
||||
update();
|
||||
}
|
||||
},
|
||||
this);
|
||||
}
|
||||
|
||||
void AppTabWidget::setSelected(bool selected)
|
||||
{
|
||||
m_selected = selected;
|
||||
update();
|
||||
}
|
||||
|
||||
void AppTabWidget::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Background
|
||||
QColor bgColor;
|
||||
if (m_selected) {
|
||||
bgColor = QColor(0, 122, 255); // #007AFF
|
||||
} else if (m_hovered) {
|
||||
bgColor = QColor(224, 224, 224); // #e0e0e0
|
||||
} else {
|
||||
bgColor = QColor(245, 245, 245); // #f5f5f5
|
||||
}
|
||||
|
||||
painter.fillRect(rect().adjusted(2, 2, -2, -2), bgColor);
|
||||
|
||||
// Border
|
||||
painter.setPen(QPen(QColor(204, 204, 204), 1)); // #ccc
|
||||
painter.drawRoundedRect(rect().adjusted(2, 2, -2, -2), 4, 4);
|
||||
|
||||
// Icon
|
||||
QRect iconRect(10, 14, 32, 32);
|
||||
painter.drawPixmap(iconRect, m_icon);
|
||||
|
||||
// Text
|
||||
QColor textColor = m_selected ? Qt::white : Qt::black;
|
||||
painter.setPen(textColor);
|
||||
|
||||
QFont font = painter.font();
|
||||
font.setWeight(QFont::Medium);
|
||||
painter.setFont(font);
|
||||
|
||||
QRect textRect(50, 10, width() - 60, 20);
|
||||
QString displayText = m_appName;
|
||||
if (displayText.length() > 20) {
|
||||
displayText = displayText.left(17) + "...";
|
||||
}
|
||||
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, displayText);
|
||||
|
||||
// Version
|
||||
if (!m_version.isEmpty()) {
|
||||
font.setPointSize(font.pointSize() - 1);
|
||||
font.setWeight(QFont::Normal);
|
||||
painter.setFont(font);
|
||||
|
||||
QColor versionColor =
|
||||
m_selected ? QColor(255, 255, 255, 180) : QColor(102, 102, 102);
|
||||
painter.setPen(versionColor);
|
||||
|
||||
QRect versionRect(50, 32, width() - 60, 16);
|
||||
painter.drawText(versionRect, Qt::AlignLeft | Qt::AlignVCenter,
|
||||
m_version);
|
||||
}
|
||||
}
|
||||
|
||||
void AppTabWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
emit clicked();
|
||||
}
|
||||
|
||||
void AppTabWidget::enterEvent(QEnterEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
m_hovered = true;
|
||||
update();
|
||||
}
|
||||
|
||||
void AppTabWidget::leaveEvent(QEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
m_hovered = false;
|
||||
update();
|
||||
}
|
||||
|
||||
InstalledAppsWidget::InstalledAppsWidget(iDescriptorDevice *device,
|
||||
QWidget *parent)
|
||||
: QWidget(parent), m_device(device)
|
||||
{
|
||||
m_watcher = new QFutureWatcher<QVariantMap>(this);
|
||||
m_containerWatcher = new QFutureWatcher<QVariantMap>(this);
|
||||
setupUI();
|
||||
|
||||
connect(m_watcher, &QFutureWatcher<QVariantMap>::finished, this,
|
||||
&InstalledAppsWidget::onAppsDataReady);
|
||||
connect(m_containerWatcher, &QFutureWatcher<QVariantMap>::finished, this,
|
||||
&InstalledAppsWidget::onContainerDataReady);
|
||||
|
||||
fetchInstalledApps();
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::setupUI()
|
||||
{
|
||||
m_mainLayout = new QHBoxLayout(this);
|
||||
m_mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_mainLayout->setSpacing(0);
|
||||
|
||||
// Left side - Custom tab area with scroll
|
||||
QFrame *tabFrame = new QFrame();
|
||||
tabFrame->setMinimumWidth(350);
|
||||
tabFrame->setMaximumWidth(400);
|
||||
tabFrame->setStyleSheet("QFrame { border: 1px solid #ccc; }");
|
||||
|
||||
QVBoxLayout *tabFrameLayout = new QVBoxLayout(tabFrame);
|
||||
tabFrameLayout->setContentsMargins(0, 0, 0, 0);
|
||||
tabFrameLayout->setSpacing(0);
|
||||
|
||||
// Search box at the top
|
||||
QWidget *searchContainer = new QWidget();
|
||||
searchContainer->setFixedHeight(50);
|
||||
QHBoxLayout *searchLayout = new QHBoxLayout(searchContainer);
|
||||
searchLayout->setContentsMargins(10, 10, 10, 10);
|
||||
|
||||
m_searchEdit = new QLineEdit();
|
||||
m_searchEdit->setPlaceholderText("Search apps...");
|
||||
m_searchEdit->setStyleSheet("QLineEdit { "
|
||||
" border: 2px solid #e0e0e0; "
|
||||
" border-radius: 6px; "
|
||||
" padding: 8px 12px; "
|
||||
" font-size: 14px; "
|
||||
" color: #333; "
|
||||
"} "
|
||||
"QLineEdit:focus { "
|
||||
" border: 2px solid #007AFF; "
|
||||
" outline: none; "
|
||||
"} "
|
||||
"QLineEdit::placeholder { "
|
||||
" color: #999; "
|
||||
"}");
|
||||
|
||||
// Add search icon
|
||||
QAction *searchAction = m_searchEdit->addAction(
|
||||
this->style()->standardIcon(QStyle::SP_FileDialogContentsView),
|
||||
QLineEdit::LeadingPosition);
|
||||
searchAction->setToolTip("Search");
|
||||
|
||||
searchLayout->addWidget(m_searchEdit);
|
||||
tabFrameLayout->addWidget(searchContainer);
|
||||
|
||||
// Add a separator line
|
||||
QFrame *separator = new QFrame();
|
||||
separator->setFrameShape(QFrame::HLine);
|
||||
separator->setFrameShadow(QFrame::Sunken);
|
||||
separator->setStyleSheet("QFrame { color: #e0e0e0; }");
|
||||
tabFrameLayout->addWidget(separator);
|
||||
|
||||
m_tabScrollArea = new QScrollArea();
|
||||
m_tabScrollArea->setWidgetResizable(true);
|
||||
m_tabScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_tabScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_tabScrollArea->setStyleSheet("QScrollArea { border: none; }");
|
||||
|
||||
m_tabContainer = new QWidget();
|
||||
// m_tabContainer->setStyleSheet("");
|
||||
m_tabLayout = new QVBoxLayout(m_tabContainer);
|
||||
m_tabLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_tabLayout->setSpacing(0);
|
||||
m_tabLayout->addStretch(); // Push tabs to top
|
||||
|
||||
m_tabScrollArea->setWidget(m_tabContainer);
|
||||
tabFrameLayout->addWidget(m_tabScrollArea);
|
||||
|
||||
// Right side - Content area
|
||||
m_contentWidget = new QWidget();
|
||||
m_contentWidget->setStyleSheet("border: 1px solid #ccc;");
|
||||
|
||||
QVBoxLayout *contentLayout = new QVBoxLayout(m_contentWidget);
|
||||
contentLayout->setContentsMargins(20, 20, 20, 20);
|
||||
contentLayout->setSpacing(15);
|
||||
|
||||
m_contentLabel = new QLabel("Select an app to view details");
|
||||
m_contentLabel->setAlignment(Qt::AlignCenter);
|
||||
m_contentLabel->setStyleSheet("font-size: 16px; color: #666;");
|
||||
contentLayout->addWidget(m_contentLabel);
|
||||
|
||||
// Container explorer area
|
||||
QLabel *containerTitle = new QLabel("App Container:");
|
||||
containerTitle->setStyleSheet(
|
||||
"font-size: 14px; font-weight: bold; color: #333;");
|
||||
containerTitle->setVisible(false);
|
||||
contentLayout->addWidget(containerTitle);
|
||||
|
||||
m_containerScrollArea = new QScrollArea();
|
||||
m_containerScrollArea->setWidgetResizable(true);
|
||||
m_containerScrollArea->setMinimumHeight(200);
|
||||
m_containerScrollArea->setStyleSheet(
|
||||
"QScrollArea { border: 1px solid #ddd; border-radius: 4px; }");
|
||||
m_containerScrollArea->setVisible(false);
|
||||
|
||||
m_containerWidget = new QWidget();
|
||||
m_containerLayout = new QVBoxLayout(m_containerWidget);
|
||||
m_containerLayout->setContentsMargins(10, 10, 10, 10);
|
||||
m_containerLayout->setSpacing(5);
|
||||
|
||||
m_containerScrollArea->setWidget(m_containerWidget);
|
||||
contentLayout->addWidget(m_containerScrollArea);
|
||||
|
||||
// Progress bar for loading
|
||||
m_progressBar = new QProgressBar();
|
||||
m_progressBar->setRange(0, 0); // Indeterminate progress
|
||||
m_progressBar->setVisible(false);
|
||||
contentLayout->addWidget(m_progressBar);
|
||||
|
||||
m_mainLayout->addWidget(tabFrame);
|
||||
m_mainLayout->addWidget(m_contentWidget, 1); // Give content area more space
|
||||
|
||||
// Connect search functionality
|
||||
connect(m_searchEdit, &QLineEdit::textChanged, this,
|
||||
&InstalledAppsWidget::filterApps);
|
||||
|
||||
showLoadingState();
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::showLoadingState()
|
||||
{
|
||||
m_contentLabel->setText("Loading installed apps...");
|
||||
m_progressBar->setVisible(true);
|
||||
|
||||
// Clear existing tabs
|
||||
qDeleteAll(m_appTabs);
|
||||
m_appTabs.clear();
|
||||
m_selectedTab = nullptr;
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::showErrorState(const QString &error)
|
||||
{
|
||||
m_contentLabel->setText(QString("Error loading apps: %1").arg(error));
|
||||
m_progressBar->setVisible(false);
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::fetchInstalledApps()
|
||||
{
|
||||
if (!m_device || !m_device->device) {
|
||||
showErrorState("Invalid device");
|
||||
return;
|
||||
}
|
||||
|
||||
QFuture<QVariantMap> future = QtConcurrent::run([this]() -> QVariantMap {
|
||||
QVariantMap result;
|
||||
QVariantList apps;
|
||||
|
||||
instproxy_client_t instproxy = nullptr;
|
||||
lockdownd_client_t lockdownClient = nullptr;
|
||||
lockdownd_service_descriptor_t lockdowndService = nullptr;
|
||||
|
||||
try {
|
||||
if (lockdownd_client_new_with_handshake(
|
||||
m_device->device, &lockdownClient, APP_LABEL) !=
|
||||
LOCKDOWN_E_SUCCESS) {
|
||||
result["error"] = "Could not connect to lockdown service";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (lockdownd_start_service(
|
||||
lockdownClient, "com.apple.mobile.installation_proxy",
|
||||
&lockdowndService) != LOCKDOWN_E_SUCCESS) {
|
||||
result["error"] = "Could not start installation proxy service";
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (instproxy_client_new(m_device->device, lockdowndService,
|
||||
&instproxy) != INSTPROXY_E_SUCCESS) {
|
||||
result["error"] = "Could not connect to installation proxy";
|
||||
lockdownd_service_descriptor_free(lockdowndService);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
lockdownd_service_descriptor_free(lockdowndService);
|
||||
lockdowndService = nullptr;
|
||||
|
||||
// Get both User and System apps
|
||||
QStringList appTypes = {"User", "System"};
|
||||
|
||||
for (const QString &appType : appTypes) {
|
||||
plist_t client_opts = plist_new_dict();
|
||||
plist_dict_set_item(
|
||||
client_opts, "ApplicationType",
|
||||
plist_new_string(appType.toUtf8().constData()));
|
||||
|
||||
plist_t return_attrs = plist_new_array();
|
||||
plist_array_append_item(return_attrs,
|
||||
plist_new_string("CFBundleIdentifier"));
|
||||
plist_array_append_item(
|
||||
return_attrs, plist_new_string("CFBundleDisplayName"));
|
||||
plist_array_append_item(
|
||||
return_attrs,
|
||||
plist_new_string("CFBundleShortVersionString"));
|
||||
plist_array_append_item(return_attrs,
|
||||
plist_new_string("CFBundleVersion"));
|
||||
|
||||
plist_dict_set_item(client_opts, "ReturnAttributes",
|
||||
return_attrs);
|
||||
|
||||
plist_t apps_plist = nullptr;
|
||||
if (instproxy_browse(instproxy, client_opts, &apps_plist) ==
|
||||
INSTPROXY_E_SUCCESS &&
|
||||
apps_plist) {
|
||||
if (plist_get_node_type(apps_plist) == PLIST_ARRAY) {
|
||||
for (uint32_t i = 0;
|
||||
i < plist_array_get_size(apps_plist); i++) {
|
||||
plist_t app_info =
|
||||
plist_array_get_item(apps_plist, i);
|
||||
if (!app_info)
|
||||
continue;
|
||||
|
||||
QVariantMap appData;
|
||||
|
||||
// Get bundle identifier
|
||||
plist_t bundle_id = plist_dict_get_item(
|
||||
app_info, "CFBundleIdentifier");
|
||||
if (bundle_id && plist_get_node_type(bundle_id) ==
|
||||
PLIST_STRING) {
|
||||
char *bundle_id_str = nullptr;
|
||||
plist_get_string_val(bundle_id, &bundle_id_str);
|
||||
if (bundle_id_str) {
|
||||
appData["bundleId"] =
|
||||
QString(bundle_id_str);
|
||||
free(bundle_id_str);
|
||||
}
|
||||
}
|
||||
|
||||
// Get display name
|
||||
plist_t display_name = plist_dict_get_item(
|
||||
app_info, "CFBundleDisplayName");
|
||||
if (display_name &&
|
||||
plist_get_node_type(display_name) ==
|
||||
PLIST_STRING) {
|
||||
char *display_name_str = nullptr;
|
||||
plist_get_string_val(display_name,
|
||||
&display_name_str);
|
||||
if (display_name_str) {
|
||||
appData["displayName"] =
|
||||
QString(display_name_str);
|
||||
free(display_name_str);
|
||||
}
|
||||
}
|
||||
|
||||
// Get version
|
||||
plist_t version = plist_dict_get_item(
|
||||
app_info, "CFBundleShortVersionString");
|
||||
if (version &&
|
||||
plist_get_node_type(version) == PLIST_STRING) {
|
||||
char *version_str = nullptr;
|
||||
plist_get_string_val(version, &version_str);
|
||||
if (version_str) {
|
||||
appData["version"] = QString(version_str);
|
||||
free(version_str);
|
||||
}
|
||||
}
|
||||
|
||||
appData["type"] = appType;
|
||||
|
||||
if (!appData["bundleId"].toString().isEmpty()) {
|
||||
apps.append(appData);
|
||||
}
|
||||
}
|
||||
}
|
||||
plist_free(apps_plist);
|
||||
}
|
||||
plist_free(client_opts);
|
||||
}
|
||||
|
||||
instproxy_client_free(instproxy);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
|
||||
result["apps"] = apps;
|
||||
result["success"] = true;
|
||||
|
||||
} catch (const std::exception &e) {
|
||||
if (instproxy)
|
||||
instproxy_client_free(instproxy);
|
||||
if (lockdownClient)
|
||||
lockdownd_client_free(lockdownClient);
|
||||
if (lockdowndService)
|
||||
lockdownd_service_descriptor_free(lockdowndService);
|
||||
|
||||
result["error"] = QString("Exception: %1").arg(e.what());
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
m_watcher->setFuture(future);
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::onAppsDataReady()
|
||||
{
|
||||
QVariantMap result = m_watcher->result();
|
||||
m_progressBar->setVisible(false);
|
||||
|
||||
if (!result.value("success", false).toBool()) {
|
||||
showErrorState(result.value("error", "Unknown error").toString());
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantList apps = result.value("apps").toList();
|
||||
if (apps.isEmpty()) {
|
||||
m_contentLabel->setText("No apps found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort apps by display name
|
||||
std::sort(apps.begin(), apps.end(),
|
||||
[](const QVariant &a, const QVariant &b) {
|
||||
QString nameA = a.toMap().value("displayName").toString();
|
||||
QString nameB = b.toMap().value("displayName").toString();
|
||||
if (nameA.isEmpty())
|
||||
nameA = a.toMap().value("bundleId").toString();
|
||||
if (nameB.isEmpty())
|
||||
nameB = b.toMap().value("bundleId").toString();
|
||||
return nameA.compare(nameB, Qt::CaseInsensitive) < 0;
|
||||
});
|
||||
|
||||
// Clear existing tabs
|
||||
qDeleteAll(m_appTabs);
|
||||
m_appTabs.clear();
|
||||
m_selectedTab = nullptr;
|
||||
|
||||
// Create tabs for each app
|
||||
for (const QVariant &appVariant : apps) {
|
||||
QVariantMap appData = appVariant.toMap();
|
||||
QString displayName = appData.value("displayName").toString();
|
||||
QString bundleId = appData.value("bundleId").toString();
|
||||
QString version = appData.value("version").toString();
|
||||
QString appType = appData.value("type").toString();
|
||||
|
||||
if (displayName.isEmpty()) {
|
||||
displayName = bundleId;
|
||||
}
|
||||
|
||||
// Create tab name with type indicator
|
||||
QString tabName = displayName;
|
||||
if (appType == "System") {
|
||||
tabName += " (System)";
|
||||
}
|
||||
|
||||
createAppTab(tabName, bundleId, version);
|
||||
}
|
||||
|
||||
m_contentLabel->setText(
|
||||
QString("Found %1 installed apps").arg(apps.count()));
|
||||
|
||||
// Select first tab if available
|
||||
if (!m_appTabs.isEmpty()) {
|
||||
selectAppTab(m_appTabs.first());
|
||||
}
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::createAppTab(const QString &appName,
|
||||
const QString &bundleId,
|
||||
const QString &version)
|
||||
{
|
||||
AppTabWidget *tabWidget =
|
||||
new AppTabWidget(appName, bundleId, version, this);
|
||||
connect(tabWidget, &AppTabWidget::clicked, this,
|
||||
&InstalledAppsWidget::onAppTabClicked);
|
||||
|
||||
// Remove the stretch before adding the new tab
|
||||
m_tabLayout->removeItem(m_tabLayout->itemAt(m_tabLayout->count() - 1));
|
||||
|
||||
m_tabLayout->addWidget(tabWidget);
|
||||
m_tabLayout->addStretch(); // Add stretch back at the end
|
||||
|
||||
m_appTabs.append(tabWidget);
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::onAppTabClicked()
|
||||
{
|
||||
AppTabWidget *clickedTab = qobject_cast<AppTabWidget *>(sender());
|
||||
if (clickedTab) {
|
||||
selectAppTab(clickedTab);
|
||||
}
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::selectAppTab(AppTabWidget *tab)
|
||||
{
|
||||
// Deselect previous tab
|
||||
if (m_selectedTab) {
|
||||
m_selectedTab->setSelected(false);
|
||||
}
|
||||
|
||||
// Select new tab
|
||||
m_selectedTab = tab;
|
||||
tab->setSelected(true);
|
||||
|
||||
// Update content
|
||||
QString bundleId = tab->getBundleId();
|
||||
QString version = tab->getVersion();
|
||||
QString appName = tab->getAppName();
|
||||
|
||||
// Remove the (System) suffix for display
|
||||
QString displayName = appName;
|
||||
displayName.remove(" (System)");
|
||||
|
||||
QString content = QString("<h2>%1</h2>"
|
||||
"<p><b>Bundle ID:</b> %2</p>"
|
||||
"<p><b>Version:</b> %3</p>"
|
||||
"<hr>")
|
||||
.arg(displayName, bundleId,
|
||||
version.isEmpty() ? "Unknown" : version);
|
||||
|
||||
m_contentLabel->setText(content);
|
||||
m_contentLabel->setTextFormat(Qt::RichText);
|
||||
m_contentLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||
m_contentLabel->setWordWrap(true);
|
||||
|
||||
// Load app container data
|
||||
loadAppContainer(bundleId);
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::filterApps(const QString &searchText)
|
||||
{
|
||||
QString lowerSearchText = searchText.toLower();
|
||||
|
||||
for (AppTabWidget *tab : m_appTabs) {
|
||||
bool shouldShow = false;
|
||||
|
||||
if (lowerSearchText.isEmpty()) {
|
||||
shouldShow = true;
|
||||
} else {
|
||||
// Search in app name and bundle ID
|
||||
QString appName = tab->getAppName().toLower();
|
||||
QString bundleId = tab->getBundleId().toLower();
|
||||
|
||||
shouldShow = appName.contains(lowerSearchText) ||
|
||||
bundleId.contains(lowerSearchText);
|
||||
}
|
||||
|
||||
tab->setVisible(shouldShow);
|
||||
}
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::loadAppContainer(const QString &bundleId)
|
||||
{
|
||||
if (!m_device || !m_device->device) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous container data
|
||||
QLayoutItem *item;
|
||||
while ((item = m_containerLayout->takeAt(0)) != nullptr) {
|
||||
if (item->widget()) {
|
||||
item->widget()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
QLabel *loadingLabel = new QLabel("Loading app container...");
|
||||
loadingLabel->setStyleSheet("color: #666; font-style: italic;");
|
||||
m_containerLayout->addWidget(loadingLabel);
|
||||
m_containerScrollArea->setVisible(true);
|
||||
|
||||
// Find container title and make it visible
|
||||
QVBoxLayout *contentLayout =
|
||||
qobject_cast<QVBoxLayout *>(m_contentWidget->layout());
|
||||
if (contentLayout && contentLayout->count() > 1) {
|
||||
QLayoutItem *titleItem = contentLayout->itemAt(1);
|
||||
if (titleItem && titleItem->widget()) {
|
||||
titleItem->widget()->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
QFuture<QVariantMap> future = QtConcurrent::run([this, bundleId]()
|
||||
-> QVariantMap {
|
||||
QVariantMap result;
|
||||
|
||||
afc_client_t afcClient = nullptr;
|
||||
lockdownd_client_t lockdownClient = nullptr;
|
||||
lockdownd_service_descriptor_t lockdowndService = nullptr;
|
||||
house_arrest_client_t houseArrestClient = nullptr;
|
||||
|
||||
try {
|
||||
if (lockdownd_client_new_with_handshake(
|
||||
m_device->device, &lockdownClient, APP_LABEL) !=
|
||||
LOCKDOWN_E_SUCCESS) {
|
||||
result["error"] = "Could not connect to lockdown service";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (lockdownd_start_service(
|
||||
lockdownClient, "com.apple.mobile.house_arrest",
|
||||
&lockdowndService) != LOCKDOWN_E_SUCCESS) {
|
||||
result["error"] = "Could not start house arrest service";
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (house_arrest_client_new(m_device->device, lockdowndService,
|
||||
&houseArrestClient) !=
|
||||
HOUSE_ARREST_E_SUCCESS) {
|
||||
result["error"] = "Could not connect to house arrest";
|
||||
lockdownd_service_descriptor_free(lockdowndService);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
lockdownd_service_descriptor_free(lockdowndService);
|
||||
lockdowndService = nullptr;
|
||||
|
||||
// Send vendor container command
|
||||
if (house_arrest_send_command(houseArrestClient, "VendContainer",
|
||||
bundleId.toUtf8().constData()) !=
|
||||
HOUSE_ARREST_E_SUCCESS) {
|
||||
result["error"] = "Could not send VendContainer command";
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get result
|
||||
plist_t dict = nullptr;
|
||||
if (house_arrest_get_result(houseArrestClient, &dict) !=
|
||||
HOUSE_ARREST_E_SUCCESS ||
|
||||
!dict) {
|
||||
result["error"] = "App container not available for this app";
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check for error in response
|
||||
plist_t error_node = plist_dict_get_item(dict, "Error");
|
||||
if (error_node) {
|
||||
char *error_str = nullptr;
|
||||
plist_get_string_val(error_node, &error_str);
|
||||
if (error_str) {
|
||||
result["error"] =
|
||||
QString("Container access denied: %1").arg(error_str);
|
||||
free(error_str);
|
||||
} else {
|
||||
result["error"] = "Container access denied";
|
||||
}
|
||||
plist_free(dict);
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
plist_free(dict);
|
||||
|
||||
// Get AFC client for file access
|
||||
if (afc_client_new_from_house_arrest_client(
|
||||
houseArrestClient, &afcClient) != AFC_E_SUCCESS) {
|
||||
result["error"] =
|
||||
"Could not create AFC client for app container";
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
// List root directory contents
|
||||
char **list = nullptr;
|
||||
if (afc_read_directory(afcClient, "/", &list) != AFC_E_SUCCESS) {
|
||||
result["error"] = "Could not read app container directory";
|
||||
afc_client_free(afcClient);
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList files;
|
||||
if (list) {
|
||||
for (int i = 0; list[i]; i++) {
|
||||
QString fileName = QString::fromUtf8(list[i]);
|
||||
if (fileName != "." && fileName != "..") {
|
||||
files.append(fileName);
|
||||
}
|
||||
}
|
||||
afc_dictionary_free(list);
|
||||
}
|
||||
|
||||
result["files"] = files;
|
||||
result["success"] = true;
|
||||
|
||||
afc_client_free(afcClient);
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
|
||||
} catch (const std::exception &e) {
|
||||
if (afcClient)
|
||||
afc_client_free(afcClient);
|
||||
if (houseArrestClient)
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
if (lockdownClient)
|
||||
lockdownd_client_free(lockdownClient);
|
||||
if (lockdowndService)
|
||||
lockdownd_service_descriptor_free(lockdowndService);
|
||||
|
||||
result["error"] = QString("Exception: %1").arg(e.what());
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
m_containerWatcher->setFuture(future);
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::onContainerDataReady()
|
||||
{
|
||||
QVariantMap result = m_containerWatcher->result();
|
||||
|
||||
// Clear loading state
|
||||
QLayoutItem *item;
|
||||
while ((item = m_containerLayout->takeAt(0)) != nullptr) {
|
||||
if (item->widget()) {
|
||||
item->widget()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
if (!result.value("success", false).toBool()) {
|
||||
QLabel *errorLabel = new QLabel("No data available for this app");
|
||||
errorLabel->setStyleSheet(
|
||||
"color: #999; font-style: italic; text-align: center;");
|
||||
errorLabel->setAlignment(Qt::AlignCenter);
|
||||
m_containerLayout->addWidget(errorLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList files = result.value("files").toStringList();
|
||||
if (files.isEmpty()) {
|
||||
QLabel *emptyLabel = new QLabel("App container is empty");
|
||||
emptyLabel->setStyleSheet("color: #999; font-style: italic;");
|
||||
m_containerLayout->addWidget(emptyLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort files/directories
|
||||
files.sort();
|
||||
|
||||
// Add files/directories to the container view
|
||||
for (const QString &fileName : files) {
|
||||
QLabel *fileLabel = new QLabel();
|
||||
|
||||
// Determine if it's likely a directory (simple heuristic)
|
||||
QString displayText = fileName;
|
||||
QIcon icon;
|
||||
if (!fileName.contains('.') || fileName.endsWith("/")) {
|
||||
icon = this->style()->standardIcon(QStyle::SP_DirIcon);
|
||||
displayText = "📁 " + fileName;
|
||||
} else {
|
||||
icon = this->style()->standardIcon(QStyle::SP_FileIcon);
|
||||
displayText = "📄 " + fileName;
|
||||
}
|
||||
|
||||
fileLabel->setText(displayText);
|
||||
fileLabel->setStyleSheet("QLabel { "
|
||||
" padding: 4px 8px; "
|
||||
" border: 1px solid transparent; "
|
||||
" border-radius: 3px; "
|
||||
" font-family: monospace; "
|
||||
" font-size: 13px; "
|
||||
"} "
|
||||
"QLabel:hover { "
|
||||
" background-color: #f0f0f0; "
|
||||
" border: 1px solid #ddd; "
|
||||
"}");
|
||||
fileLabel->setWordWrap(true);
|
||||
|
||||
m_containerLayout->addWidget(fileLabel);
|
||||
}
|
||||
|
||||
// Add stretch to push items to top
|
||||
m_containerLayout->addStretch();
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#ifndef INSTALLEDAPPSWIDGET_H
|
||||
#define INSTALLEDAPPSWIDGET_H
|
||||
|
||||
#include "iDescriptor.h"
|
||||
#include <QEnterEvent>
|
||||
#include <QFrame>
|
||||
#include <QFutureWatcher>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPixmap>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
// Custom App Tab Widget
|
||||
class AppTabWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AppTabWidget(const QString &appName, const QString &bundleId,
|
||||
const QString &version, QWidget *parent = nullptr);
|
||||
|
||||
void setSelected(bool selected);
|
||||
bool isSelected() const { return m_selected; }
|
||||
|
||||
QString getBundleId() const { return m_bundleId; }
|
||||
QString getAppName() const { return m_appName; }
|
||||
QString getVersion() const { return m_version; }
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
// TODO override?
|
||||
void enterEvent(QEnterEvent *event);
|
||||
void leaveEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
void fetchAppIcon();
|
||||
|
||||
QString m_appName;
|
||||
QString m_bundleId;
|
||||
QString m_version;
|
||||
QPixmap m_icon;
|
||||
bool m_selected = false;
|
||||
bool m_hovered = false;
|
||||
};
|
||||
|
||||
class InstalledAppsWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InstalledAppsWidget(iDescriptorDevice *device,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onAppsDataReady();
|
||||
void onAppTabClicked();
|
||||
void onContainerDataReady();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void fetchInstalledApps();
|
||||
void createAppTab(const QString &appName, const QString &bundleId,
|
||||
const QString &version);
|
||||
void showLoadingState();
|
||||
void showErrorState(const QString &error);
|
||||
void selectAppTab(AppTabWidget *tab);
|
||||
void filterApps(const QString &searchText);
|
||||
void loadAppContainer(const QString &bundleId);
|
||||
|
||||
iDescriptorDevice *m_device;
|
||||
QHBoxLayout *m_mainLayout;
|
||||
QLineEdit *m_searchEdit;
|
||||
QScrollArea *m_tabScrollArea;
|
||||
QWidget *m_tabContainer;
|
||||
QVBoxLayout *m_tabLayout;
|
||||
QWidget *m_contentWidget;
|
||||
QLabel *m_contentLabel;
|
||||
QProgressBar *m_progressBar;
|
||||
QScrollArea *m_containerScrollArea;
|
||||
QWidget *m_containerWidget;
|
||||
QVBoxLayout *m_containerLayout;
|
||||
QFutureWatcher<QVariantMap> *m_watcher;
|
||||
QFutureWatcher<QVariantMap> *m_containerWatcher;
|
||||
|
||||
// App data storage
|
||||
QList<AppTabWidget *> m_appTabs;
|
||||
AppTabWidget *m_selectedTab = nullptr;
|
||||
};
|
||||
|
||||
#endif // INSTALLEDAPPSWIDGET_H
|
||||
+126
-1
@@ -1,4 +1,5 @@
|
||||
#include "settingsmanager.h"
|
||||
#include <QDebug>
|
||||
#include <QSettings>
|
||||
|
||||
#define DEFAULT_DEVDISKIMGPATH "./devdiskimages"
|
||||
@@ -11,12 +12,136 @@ SettingsManager *SettingsManager::sharedInstance()
|
||||
|
||||
SettingsManager::SettingsManager(QObject *parent) : QObject{parent}
|
||||
{
|
||||
|
||||
m_settings = new QSettings(this);
|
||||
|
||||
// Clean up any invalid favorite places on startup
|
||||
cleanupFavoritePlaces();
|
||||
}
|
||||
|
||||
QString SettingsManager::devdiskimgpath() const
|
||||
{
|
||||
return m_settings->value("devdiskimgpath", DEFAULT_DEVDISKIMGPATH)
|
||||
.toString();
|
||||
}
|
||||
|
||||
void SettingsManager::saveFavoritePlace(const QString &path,
|
||||
const QString &alias)
|
||||
{
|
||||
if (path.isEmpty() || alias.isEmpty()) {
|
||||
qDebug() << "Cannot save favorite place: path or alias is empty";
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a key that encodes the path properly
|
||||
QString key =
|
||||
"favorite_places/" + QString::fromLatin1(path.toUtf8().toBase64());
|
||||
m_settings->setValue(key, QStringList() << path << alias);
|
||||
m_settings->sync();
|
||||
|
||||
qDebug() << "Saved favorite place:" << alias << "(" << path << ")";
|
||||
emit favoritePlacesChanged();
|
||||
}
|
||||
|
||||
void SettingsManager::removeFavoritePlace(const QString &path)
|
||||
{
|
||||
// Use the same encoding as in saveFavoritePlace
|
||||
QString key =
|
||||
"favorite_places/" + QString::fromLatin1(path.toUtf8().toBase64());
|
||||
if (m_settings->contains(key)) {
|
||||
m_settings->remove(key);
|
||||
m_settings->sync();
|
||||
qDebug() << "Removed favorite place:" << path;
|
||||
emit favoritePlacesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QList<QPair<QString, QString>> SettingsManager::getFavoritePlaces() const
|
||||
{
|
||||
QList<QPair<QString, QString>> favorites;
|
||||
|
||||
// Get all keys that start with "favorite_places/"
|
||||
QStringList allKeys = m_settings->allKeys();
|
||||
QStringList favoriteKeys = allKeys.filter("favorite_places/");
|
||||
|
||||
qDebug() << "Found favorite keys:" << favoriteKeys;
|
||||
|
||||
for (const QString &key : favoriteKeys) {
|
||||
QStringList value = m_settings->value(key).toStringList();
|
||||
if (value.size() >= 2) {
|
||||
QString path = value[0];
|
||||
QString alias = value[1];
|
||||
if (!path.isEmpty() && !alias.isEmpty()) {
|
||||
favorites.append(qMakePair(path, alias));
|
||||
qDebug() << "Loaded favorite:" << alias << "->" << path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by alias for consistent ordering
|
||||
std::sort(
|
||||
favorites.begin(), favorites.end(),
|
||||
[](const QPair<QString, QString> &a, const QPair<QString, QString> &b) {
|
||||
return a.second.toLower() < b.second.toLower();
|
||||
});
|
||||
|
||||
return favorites;
|
||||
}
|
||||
|
||||
bool SettingsManager::isFavoritePlace(const QString &path) const
|
||||
{
|
||||
QString key =
|
||||
"favorite_places/" + QString::fromLatin1(path.toUtf8().toBase64());
|
||||
return m_settings->contains(key);
|
||||
}
|
||||
|
||||
QString SettingsManager::getFavoritePlaceAlias(const QString &path) const
|
||||
{
|
||||
QString key =
|
||||
"favorite_places/" + QString::fromLatin1(path.toUtf8().toBase64());
|
||||
QStringList value = m_settings->value(key).toStringList();
|
||||
if (value.size() >= 2) {
|
||||
return value[1]; // Return alias
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void SettingsManager::clearFavoritePlaces()
|
||||
{
|
||||
// Get all keys that start with "favorite_places/" and remove them
|
||||
QStringList allKeys = m_settings->allKeys();
|
||||
QStringList favoriteKeys = allKeys.filter("favorite_places/");
|
||||
|
||||
for (const QString &key : favoriteKeys) {
|
||||
m_settings->remove(key);
|
||||
}
|
||||
|
||||
m_settings->sync();
|
||||
|
||||
qDebug() << "Cleared all favorite places";
|
||||
emit favoritePlacesChanged();
|
||||
}
|
||||
|
||||
void SettingsManager::cleanupFavoritePlaces()
|
||||
{
|
||||
// Get all keys that start with "favorite_places/" and clean them up
|
||||
QStringList allKeys = m_settings->allKeys();
|
||||
QStringList favoriteKeys = allKeys.filter("favorite_places/");
|
||||
QStringList keysToRemove;
|
||||
|
||||
for (const QString &key : favoriteKeys) {
|
||||
QStringList value = m_settings->value(key).toStringList();
|
||||
if (value.size() < 2 || value[0].isEmpty() || value[1].isEmpty()) {
|
||||
keysToRemove.append(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &key : keysToRemove) {
|
||||
qDebug() << "Removing invalid favorite place key:" << key;
|
||||
m_settings->remove(key);
|
||||
}
|
||||
|
||||
if (!keysToRemove.isEmpty()) {
|
||||
m_settings->sync();
|
||||
emit favoritePlacesChanged();
|
||||
}
|
||||
}
|
||||
+22
-4
@@ -2,22 +2,40 @@
|
||||
#define SETTINGSMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
class SettingsManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SettingsManager(QObject *parent = nullptr);
|
||||
static SettingsManager *sharedInstance();
|
||||
|
||||
signals:
|
||||
|
||||
public slots:
|
||||
// Existing methods
|
||||
QString devdiskimgpath() const;
|
||||
|
||||
// Favorite Places API
|
||||
void saveFavoritePlace(const QString &path, const QString &alias);
|
||||
void removeFavoritePlace(const QString &path);
|
||||
QList<QPair<QString, QString>>
|
||||
getFavoritePlaces() const; // Returns (path, alias) pairs
|
||||
bool isFavoritePlace(const QString &path) const;
|
||||
QString getFavoritePlaceAlias(const QString &path) const;
|
||||
void clearFavoritePlaces();
|
||||
|
||||
signals:
|
||||
void favoritePlacesChanged();
|
||||
|
||||
private:
|
||||
explicit SettingsManager(QObject *parent = nullptr);
|
||||
QSettings *m_settings;
|
||||
|
||||
void cleanupFavoritePlaces();
|
||||
|
||||
static const QString FAVORITE_PREFIX;
|
||||
};
|
||||
|
||||
#endif // SETTINGSMANAGER_H
|
||||
|
||||
Reference in New Issue
Block a user