mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
abstract file explorer logic into AfcExplorerWidget
This commit is contained in:
@@ -0,0 +1,516 @@
|
||||
#include "afcexplorerwidget.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>
|
||||
|
||||
AfcExplorerWidget::AfcExplorerWidget(afc_client_t afcClient,
|
||||
std::function<void()> onClientInvalidCb,
|
||||
iDescriptorDevice *device, QWidget *parent)
|
||||
: QWidget(parent), m_currentAfcClient(afcClient), m_device(device)
|
||||
{
|
||||
// Initialize current AFC client to default
|
||||
m_currentAfcClient = afcClient;
|
||||
|
||||
// Setup file explorer
|
||||
setupFileExplorer();
|
||||
|
||||
// Main layout
|
||||
QHBoxLayout *mainLayout = new QHBoxLayout(this);
|
||||
setLayout(mainLayout);
|
||||
mainLayout->addWidget(m_explorer);
|
||||
|
||||
// Initialize
|
||||
m_history.push("/");
|
||||
loadPath("/");
|
||||
|
||||
setupContextMenu();
|
||||
connect(SettingsManager::sharedInstance(),
|
||||
&SettingsManager::favoritePlacesChanged, this,
|
||||
&AfcExplorerWidget::refreshFavoritePlaces);
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::goBack()
|
||||
{
|
||||
if (m_history.size() > 1) {
|
||||
m_history.pop();
|
||||
QString prevPath = m_history.top();
|
||||
loadPath(prevPath);
|
||||
}
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::onItemDoubleClicked(QListWidgetItem *item)
|
||||
{
|
||||
QVariant data = item->data(Qt::UserRole);
|
||||
bool isDir = data.toBool();
|
||||
QString name = item->text();
|
||||
|
||||
// Use breadcrumb to get current path
|
||||
QString currPath = "/";
|
||||
if (!m_history.isEmpty())
|
||||
currPath = m_history.top();
|
||||
|
||||
if (!currPath.endsWith("/"))
|
||||
currPath += "/";
|
||||
QString nextPath = currPath == "/" ? "/" + name : currPath + name;
|
||||
|
||||
if (isDir) {
|
||||
m_history.push(nextPath);
|
||||
loadPath(nextPath);
|
||||
} else {
|
||||
auto *previewDialog = new MediaPreviewDialog(m_device, nextPath, this);
|
||||
previewDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
previewDialog->show();
|
||||
// TODO: we need this ?
|
||||
emit fileSelected(nextPath);
|
||||
}
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::onBreadcrumbClicked()
|
||||
{
|
||||
QPushButton *btn = qobject_cast<QPushButton *>(sender());
|
||||
if (!btn)
|
||||
return;
|
||||
QString path = btn->property("fullPath").toString();
|
||||
// pathLabel removed, compare with m_history.top()
|
||||
if (!m_history.isEmpty() && path == m_history.top())
|
||||
return;
|
||||
m_history.push(path);
|
||||
loadPath(path);
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::updateBreadcrumb(const QString &path)
|
||||
{
|
||||
// Remove old breadcrumb buttons
|
||||
QLayoutItem *child;
|
||||
while ((child = m_breadcrumbLayout->takeAt(0)) != nullptr) {
|
||||
if (child->widget()) {
|
||||
child->widget()->deleteLater();
|
||||
}
|
||||
delete child;
|
||||
}
|
||||
|
||||
QStringList parts = path.split("/", Qt::SkipEmptyParts);
|
||||
QString currPath = "";
|
||||
int idx = 0;
|
||||
// Add root
|
||||
QPushButton *rootBtn = new QPushButton("/");
|
||||
rootBtn->setFlat(true);
|
||||
rootBtn->setProperty("fullPath", "/");
|
||||
connect(rootBtn, &QPushButton::clicked, this,
|
||||
&AfcExplorerWidget::onBreadcrumbClicked);
|
||||
m_breadcrumbLayout->addWidget(rootBtn);
|
||||
|
||||
for (const QString &part : parts) {
|
||||
currPath += part;
|
||||
if (idx > 0) {
|
||||
QLabel *sep = new QLabel(" / ");
|
||||
m_breadcrumbLayout->addWidget(sep);
|
||||
}
|
||||
|
||||
QPushButton *btn = new QPushButton(part);
|
||||
btn->setFlat(true);
|
||||
btn->setProperty("fullPath", currPath);
|
||||
connect(btn, &QPushButton::clicked, this,
|
||||
&AfcExplorerWidget::onBreadcrumbClicked);
|
||||
m_breadcrumbLayout->addWidget(btn);
|
||||
idx++;
|
||||
}
|
||||
m_breadcrumbLayout->addStretch();
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::loadPath(const QString &path)
|
||||
{
|
||||
m_fileList->clear();
|
||||
// pathLabel->setText(path); // removed
|
||||
|
||||
updateBreadcrumb(path);
|
||||
|
||||
MediaFileTree tree =
|
||||
get_file_tree(m_currentAfcClient, m_device->device, path.toStdString());
|
||||
if (!tree.success) {
|
||||
m_fileList->addItem("Failed to load directory");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &entry : tree.entries) {
|
||||
QListWidgetItem *item =
|
||||
new QListWidgetItem(QString::fromStdString(entry.name));
|
||||
item->setData(Qt::UserRole, entry.isDir);
|
||||
if (entry.isDir)
|
||||
item->setIcon(QIcon::fromTheme("folder"));
|
||||
else
|
||||
item->setIcon(QIcon::fromTheme("text-x-generic"));
|
||||
m_fileList->addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::setupContextMenu()
|
||||
{
|
||||
m_fileList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_fileList, &QListWidget::customContextMenuRequested, this,
|
||||
&AfcExplorerWidget::onFileListContextMenu);
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::onFileListContextMenu(const QPoint &pos)
|
||||
{
|
||||
QListWidgetItem *item = m_fileList->itemAt(pos);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
bool isDir = item->data(Qt::UserRole).toBool();
|
||||
if (isDir)
|
||||
return; // Only export files
|
||||
|
||||
QMenu menu;
|
||||
QAction *exportAction = menu.addAction("Export");
|
||||
QAction *selectedAction =
|
||||
menu.exec(m_fileList->viewport()->mapToGlobal(pos));
|
||||
if (selectedAction == exportAction) {
|
||||
QList<QListWidgetItem *> selectedItems = m_fileList->selectedItems();
|
||||
QList<QListWidgetItem *> filesToExport;
|
||||
if (selectedItems.isEmpty())
|
||||
filesToExport.append(item); // fallback: just the clicked one
|
||||
else {
|
||||
for (QListWidgetItem *selItem : selectedItems) {
|
||||
if (!selItem->data(Qt::UserRole).toBool())
|
||||
filesToExport.append(selItem);
|
||||
}
|
||||
}
|
||||
if (filesToExport.isEmpty())
|
||||
return;
|
||||
QString dir =
|
||||
QFileDialog::getExistingDirectory(this, "Select Export Directory");
|
||||
if (dir.isEmpty())
|
||||
return;
|
||||
for (QListWidgetItem *selItem : filesToExport) {
|
||||
exportSelectedFile(selItem, dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::onExportClicked()
|
||||
{
|
||||
QList<QListWidgetItem *> selectedItems = m_fileList->selectedItems();
|
||||
if (selectedItems.isEmpty())
|
||||
return;
|
||||
|
||||
// Only files (not directories)
|
||||
QList<QListWidgetItem *> filesToExport;
|
||||
for (QListWidgetItem *item : selectedItems) {
|
||||
if (!item->data(Qt::UserRole).toBool())
|
||||
filesToExport.append(item);
|
||||
}
|
||||
if (filesToExport.isEmpty())
|
||||
return;
|
||||
|
||||
// Ask user for a directory to save all files
|
||||
QString dir =
|
||||
QFileDialog::getExistingDirectory(this, "Select Export Directory");
|
||||
if (dir.isEmpty())
|
||||
return;
|
||||
|
||||
for (QListWidgetItem *item : filesToExport) {
|
||||
exportSelectedFile(item, dir);
|
||||
}
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::onExportDeleteClicked()
|
||||
{
|
||||
// Placeholder for future implementation
|
||||
QList<QListWidgetItem *> selectedItems = m_fileList->selectedItems();
|
||||
if (selectedItems.isEmpty())
|
||||
return;
|
||||
// TODO: Implement export & delete logic
|
||||
return;
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::exportSelectedFile(QListWidgetItem *item,
|
||||
const QString &directory)
|
||||
{
|
||||
QString fileName = item->text();
|
||||
QString currPath = "/";
|
||||
if (!m_history.isEmpty())
|
||||
currPath = m_history.top();
|
||||
if (!currPath.endsWith("/"))
|
||||
currPath += "/";
|
||||
qDebug() << "Current path:" << currPath;
|
||||
QString devicePath = currPath == "/" ? "/" + fileName : currPath + fileName;
|
||||
qDebug() << "Exporting file:" << devicePath;
|
||||
|
||||
// Save to selected directory
|
||||
QString savePath = directory + "/" + fileName;
|
||||
|
||||
// Get device info and check connections
|
||||
qDebug() << "Using device index:" << m_device->udid.c_str();
|
||||
qDebug() << "Device UDID:" << QString::fromStdString(m_device->udid);
|
||||
qDebug() << "Device Product Type:"
|
||||
<< QString::fromStdString(m_device->deviceInfo.productType);
|
||||
|
||||
// Export file using the validated connections
|
||||
int result = export_file_to_path(m_currentAfcClient,
|
||||
devicePath.toStdString().c_str(),
|
||||
savePath.toStdString().c_str());
|
||||
|
||||
qDebug() << "Export result:" << result;
|
||||
|
||||
if (result == 0) {
|
||||
qDebug() << "Exported" << devicePath << "to" << savePath;
|
||||
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
this, "Export Successful",
|
||||
"File exported successfully. Would you like to see the directory?",
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
if (reply == QMessageBox::Yes) {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(directory));
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Failed to export" << devicePath;
|
||||
QMessageBox::warning(this, "Export Failed",
|
||||
"Failed to export the file from the device");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to export to a specific local path
|
||||
int AfcExplorerWidget::export_file_to_path(afc_client_t afc,
|
||||
const char *device_path,
|
||||
const char *local_path)
|
||||
{
|
||||
uint64_t handle = 0;
|
||||
// TODO: implement safe_afc_file_open
|
||||
if (afc_file_open(afc, device_path, AFC_FOPEN_RDONLY, &handle) !=
|
||||
AFC_E_SUCCESS) {
|
||||
qDebug() << "Failed to open file on device:" << device_path;
|
||||
return -1;
|
||||
}
|
||||
FILE *out = fopen(local_path, "wb");
|
||||
if (!out) {
|
||||
qDebug() << "Failed to open local file:" << local_path;
|
||||
afc_file_close(afc, handle);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char buffer[4096];
|
||||
uint32_t bytes_read = 0;
|
||||
// TODO: implement safe_afc_file_read
|
||||
while (afc_file_read(afc, handle, buffer, sizeof(buffer), &bytes_read) ==
|
||||
AFC_E_SUCCESS &&
|
||||
bytes_read > 0) {
|
||||
fwrite(buffer, 1, bytes_read, out);
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
// TODO: implement safe_afc_file_close
|
||||
afc_file_close(afc, handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::onImportClicked()
|
||||
{
|
||||
// TODO: check devices
|
||||
|
||||
// Select one or more files to import
|
||||
QStringList fileNames = QFileDialog::getOpenFileNames(this, "Import Files");
|
||||
if (fileNames.isEmpty())
|
||||
return;
|
||||
|
||||
// Use current breadcrumb directory as target
|
||||
QString currPath = "/";
|
||||
if (!m_history.isEmpty())
|
||||
currPath = m_history.top();
|
||||
if (!currPath.endsWith("/"))
|
||||
currPath += "/";
|
||||
|
||||
// if (!device || !client || !serviceDesc)
|
||||
// {
|
||||
// qDebug() << "Failed to connect to device or lockdown service";
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Import each file
|
||||
for (const QString &localPath : fileNames) {
|
||||
QFileInfo fi(localPath);
|
||||
QString devicePath = currPath + fi.fileName();
|
||||
int result = import_file_to_device(m_currentAfcClient,
|
||||
devicePath.toStdString().c_str(),
|
||||
localPath.toStdString().c_str());
|
||||
if (result == 0)
|
||||
qDebug() << "Imported" << localPath << "to" << devicePath;
|
||||
else
|
||||
qDebug() << "Failed to import" << localPath;
|
||||
}
|
||||
|
||||
// Refresh file list
|
||||
loadPath(currPath);
|
||||
}
|
||||
|
||||
// Helper function to import a file from a local path to the device
|
||||
int AfcExplorerWidget::import_file_to_device(afc_client_t afc,
|
||||
const char *device_path,
|
||||
const char *local_path)
|
||||
{
|
||||
QFile in(local_path);
|
||||
if (!in.open(QIODevice::ReadOnly)) {
|
||||
qDebug() << "Failed to open local file for import:" << local_path;
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t handle = 0;
|
||||
if (afc_file_open(afc, device_path, AFC_FOPEN_WRONLY, &handle) !=
|
||||
AFC_E_SUCCESS) {
|
||||
qDebug() << "Failed to open file on device for writing:" << device_path;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char buffer[4096];
|
||||
qint64 bytesRead;
|
||||
while ((bytesRead = in.read(buffer, sizeof(buffer))) > 0) {
|
||||
uint32_t bytesWritten = 0;
|
||||
if (afc_file_write(afc, handle, buffer,
|
||||
static_cast<uint32_t>(bytesRead),
|
||||
&bytesWritten) != AFC_E_SUCCESS ||
|
||||
bytesWritten != bytesRead) {
|
||||
qDebug() << "Failed to write to device file:" << device_path;
|
||||
afc_file_close(afc, handle);
|
||||
in.close();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
afc_file_close(afc, handle);
|
||||
in.close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// useAFC2 ,path,
|
||||
typedef QPair<bool, QString> SidebarItemData;
|
||||
|
||||
void AfcExplorerWidget::setupFileExplorer()
|
||||
{
|
||||
m_explorer = new QWidget();
|
||||
QVBoxLayout *explorerLayout = new QVBoxLayout(m_explorer);
|
||||
|
||||
// Export/Import buttons layout
|
||||
QHBoxLayout *exportLayout = new QHBoxLayout();
|
||||
m_exportBtn = new QPushButton("Export");
|
||||
m_exportDeleteBtn = new QPushButton("Export & Delete");
|
||||
m_importBtn = new QPushButton("Import");
|
||||
m_addToFavoritesBtn = new QPushButton("Add to Favorites");
|
||||
exportLayout->addWidget(m_exportBtn);
|
||||
exportLayout->addWidget(m_exportDeleteBtn);
|
||||
exportLayout->addWidget(m_importBtn);
|
||||
exportLayout->addWidget(m_addToFavoritesBtn);
|
||||
exportLayout->addStretch();
|
||||
explorerLayout->addLayout(exportLayout);
|
||||
|
||||
// Navigation layout (Back + Breadcrumb)
|
||||
QHBoxLayout *navLayout = new QHBoxLayout();
|
||||
m_backBtn = new QPushButton("Back");
|
||||
m_breadcrumbLayout = new QHBoxLayout();
|
||||
m_breadcrumbLayout->setSpacing(0);
|
||||
navLayout->addWidget(m_backBtn);
|
||||
navLayout->addLayout(m_breadcrumbLayout);
|
||||
navLayout->addStretch();
|
||||
explorerLayout->addLayout(navLayout);
|
||||
|
||||
// File list
|
||||
m_fileList = new QListWidget();
|
||||
m_fileList->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
explorerLayout->addWidget(m_fileList);
|
||||
|
||||
// Connect buttons
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &AfcExplorerWidget::goBack);
|
||||
connect(m_fileList, &QListWidget::itemDoubleClicked, this,
|
||||
&AfcExplorerWidget::onItemDoubleClicked);
|
||||
connect(m_exportBtn, &QPushButton::clicked, this,
|
||||
&AfcExplorerWidget::onExportClicked);
|
||||
connect(m_exportDeleteBtn, &QPushButton::clicked, this,
|
||||
&AfcExplorerWidget::onExportDeleteClicked);
|
||||
connect(m_importBtn, &QPushButton::clicked, this,
|
||||
&AfcExplorerWidget::onImportClicked);
|
||||
connect(m_addToFavoritesBtn, &QPushButton::clicked, this,
|
||||
&AfcExplorerWidget::onAddToFavoritesClicked);
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::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 AfcExplorerWidget::onAddToFavoritesClicked()
|
||||
{
|
||||
QString currentPath = "/";
|
||||
if (!m_history.isEmpty())
|
||||
currentPath = m_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 AfcExplorerWidget::onTryInstallAFC2Clicked()
|
||||
{
|
||||
qDebug() << "Clicked on try to install AFC2";
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::switchToAFC(bool useAFC2)
|
||||
{
|
||||
if (useAFC2 && m_device->afc2Client) {
|
||||
m_usingAFC2 = true;
|
||||
m_currentAfcClient = m_device->afc2Client;
|
||||
} else {
|
||||
m_usingAFC2 = false;
|
||||
m_currentAfcClient = m_device->afcClient;
|
||||
}
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::saveFavoritePlace(const QString &path,
|
||||
const QString &alias)
|
||||
{
|
||||
qDebug() << "Saving favorite place:" << alias << "->" << path;
|
||||
SettingsManager *settings = SettingsManager::sharedInstance();
|
||||
settings->saveFavoritePlace(path, alias);
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::refreshFavoritePlaces()
|
||||
{
|
||||
// // Clear existing favorite items
|
||||
// while (m_favoritePlacesItem->childCount() > 0) {
|
||||
// delete m_favoritePlacesItem->takeChild(0);
|
||||
// }
|
||||
|
||||
// // Reload favorite places
|
||||
// loadFavoritePlaces();
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#ifndef AFCEXPLORER_H
|
||||
#define AFCEXPLORER_H
|
||||
|
||||
#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>
|
||||
|
||||
class AfcExplorerWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AfcExplorerWidget(
|
||||
afc_client_t afcClient = nullptr,
|
||||
std::function<void()> onClientInvalidCb = nullptr,
|
||||
iDescriptorDevice *device = nullptr, QWidget *parent = nullptr);
|
||||
signals:
|
||||
void fileSelected(const QString &filePath);
|
||||
|
||||
private slots:
|
||||
void goBack();
|
||||
void onItemDoubleClicked(QListWidgetItem *item);
|
||||
void onBreadcrumbClicked();
|
||||
void onFileListContextMenu(const QPoint &pos);
|
||||
void onExportClicked();
|
||||
void onExportDeleteClicked();
|
||||
void onImportClicked();
|
||||
void onSidebarItemClicked(QTreeWidgetItem *item, int column);
|
||||
void onAddToFavoritesClicked();
|
||||
void onTryInstallAFC2Clicked();
|
||||
|
||||
private:
|
||||
QWidget *m_explorer;
|
||||
QPushButton *m_backBtn;
|
||||
QPushButton *m_exportBtn;
|
||||
QPushButton *m_exportDeleteBtn;
|
||||
QPushButton *m_importBtn;
|
||||
QPushButton *m_addToFavoritesBtn;
|
||||
QListWidget *m_fileList;
|
||||
QStack<QString> m_history;
|
||||
QHBoxLayout *m_breadcrumbLayout;
|
||||
iDescriptorDevice *m_device;
|
||||
|
||||
// Current AFC mode
|
||||
bool m_usingAFC2;
|
||||
afc_client_t m_currentAfcClient;
|
||||
|
||||
void setupFileExplorer();
|
||||
void loadPath(const QString &path);
|
||||
void updateBreadcrumb(const QString &path);
|
||||
void saveFavoritePlace(const QString &path, const QString &alias);
|
||||
void refreshFavoritePlaces();
|
||||
void switchToAFC(bool useAFC2);
|
||||
|
||||
void setupContextMenu();
|
||||
void exportSelectedFile(QListWidgetItem *item);
|
||||
void exportSelectedFile(QListWidgetItem *item, const QString &directory);
|
||||
int export_file_to_path(afc_client_t afc, const char *device_path,
|
||||
const char *local_path);
|
||||
int import_file_to_device(afc_client_t afc, const char *device_path,
|
||||
const char *local_path);
|
||||
};
|
||||
|
||||
#endif // AFCEXPLORER_H
|
||||
+44
-525
@@ -1,4 +1,5 @@
|
||||
#include "fileexplorerwidget.h"
|
||||
#include "afcexplorerwidget.h"
|
||||
#include "iDescriptor.h"
|
||||
#include "mediapreviewdialog.h"
|
||||
#include "settingsmanager.h"
|
||||
@@ -23,410 +24,41 @@ FileExplorerWidget::FileExplorerWidget(iDescriptorDevice *device,
|
||||
QWidget *parent)
|
||||
: QWidget(parent), device(device), usingAFC2(false)
|
||||
{
|
||||
// Initialize current AFC client to default
|
||||
currentAfcClient = device->afcClient;
|
||||
qDebug() << "AFC2 available:" << (device->afc2Client != nullptr);
|
||||
// Create main splitter
|
||||
mainSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
|
||||
// Setup sidebar
|
||||
setupSidebar();
|
||||
|
||||
// Setup file explorer
|
||||
setupFileExplorer();
|
||||
|
||||
// Add widgets to splitter
|
||||
mainSplitter->addWidget(sidebarTree);
|
||||
mainSplitter->addWidget(fileExplorerWidget);
|
||||
mainSplitter->setSizes({400, 800});
|
||||
|
||||
m_mainSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
// Main layout
|
||||
QHBoxLayout *mainLayout = new QHBoxLayout(this);
|
||||
mainLayout->addWidget(mainSplitter);
|
||||
mainLayout->addWidget(m_mainSplitter);
|
||||
|
||||
setupSidebar();
|
||||
|
||||
// Add widgets to splitter
|
||||
m_mainSplitter->addWidget(m_sidebarTree);
|
||||
m_mainSplitter->addWidget(
|
||||
new AfcExplorerWidget(device->afcClient, nullptr, device));
|
||||
m_mainSplitter->setSizes({400, 800});
|
||||
setLayout(mainLayout);
|
||||
|
||||
// Initialize
|
||||
history.push("/");
|
||||
loadPath("/");
|
||||
|
||||
setupContextMenu();
|
||||
connect(SettingsManager::sharedInstance(),
|
||||
&SettingsManager::favoritePlacesChanged, this,
|
||||
&FileExplorerWidget::refreshFavoritePlaces);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::goBack()
|
||||
{
|
||||
if (history.size() > 1) {
|
||||
history.pop();
|
||||
QString prevPath = history.top();
|
||||
loadPath(prevPath);
|
||||
}
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onItemDoubleClicked(QListWidgetItem *item)
|
||||
{
|
||||
QVariant data = item->data(Qt::UserRole);
|
||||
bool isDir = data.toBool();
|
||||
QString name = item->text();
|
||||
|
||||
// Use breadcrumb to get current path
|
||||
QString currPath = "/";
|
||||
if (!history.isEmpty())
|
||||
currPath = history.top();
|
||||
|
||||
if (!currPath.endsWith("/"))
|
||||
currPath += "/";
|
||||
QString nextPath = currPath == "/" ? "/" + name : currPath + name;
|
||||
|
||||
if (isDir) {
|
||||
history.push(nextPath);
|
||||
loadPath(nextPath);
|
||||
} else {
|
||||
auto *previewDialog = new MediaPreviewDialog(device, nextPath, this);
|
||||
previewDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
previewDialog->show();
|
||||
// TODO: we need this ?
|
||||
emit fileSelected(nextPath);
|
||||
}
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onBreadcrumbClicked()
|
||||
{
|
||||
QPushButton *btn = qobject_cast<QPushButton *>(sender());
|
||||
if (!btn)
|
||||
return;
|
||||
QString path = btn->property("fullPath").toString();
|
||||
// pathLabel removed, compare with history.top()
|
||||
if (!history.isEmpty() && path == history.top())
|
||||
return;
|
||||
history.push(path);
|
||||
loadPath(path);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::updateBreadcrumb(const QString &path)
|
||||
{
|
||||
// Remove old breadcrumb buttons
|
||||
QLayoutItem *child;
|
||||
while ((child = breadcrumbLayout->takeAt(0)) != nullptr) {
|
||||
if (child->widget()) {
|
||||
child->widget()->deleteLater();
|
||||
}
|
||||
delete child;
|
||||
}
|
||||
|
||||
QStringList parts = path.split("/", Qt::SkipEmptyParts);
|
||||
QString currPath = "";
|
||||
int idx = 0;
|
||||
// Add root
|
||||
QPushButton *rootBtn = new QPushButton("/");
|
||||
rootBtn->setFlat(true);
|
||||
rootBtn->setProperty("fullPath", "/");
|
||||
connect(rootBtn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onBreadcrumbClicked);
|
||||
breadcrumbLayout->addWidget(rootBtn);
|
||||
|
||||
for (const QString &part : parts) {
|
||||
currPath += part;
|
||||
if (idx > 0) {
|
||||
QLabel *sep = new QLabel(" / ");
|
||||
breadcrumbLayout->addWidget(sep);
|
||||
}
|
||||
|
||||
QPushButton *btn = new QPushButton(part);
|
||||
btn->setFlat(true);
|
||||
btn->setProperty("fullPath", currPath);
|
||||
connect(btn, &QPushButton::clicked, this,
|
||||
&FileExplorerWidget::onBreadcrumbClicked);
|
||||
breadcrumbLayout->addWidget(btn);
|
||||
idx++;
|
||||
}
|
||||
breadcrumbLayout->addStretch();
|
||||
}
|
||||
|
||||
void FileExplorerWidget::loadPath(const QString &path)
|
||||
{
|
||||
fileList->clear();
|
||||
// pathLabel->setText(path); // removed
|
||||
|
||||
updateBreadcrumb(path);
|
||||
|
||||
MediaFileTree tree =
|
||||
get_file_tree(currentAfcClient, device->device, path.toStdString());
|
||||
if (!tree.success) {
|
||||
fileList->addItem("Failed to load directory");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &entry : tree.entries) {
|
||||
QListWidgetItem *item =
|
||||
new QListWidgetItem(QString::fromStdString(entry.name));
|
||||
item->setData(Qt::UserRole, entry.isDir);
|
||||
if (entry.isDir)
|
||||
item->setIcon(QIcon::fromTheme("folder"));
|
||||
else
|
||||
item->setIcon(QIcon::fromTheme("text-x-generic"));
|
||||
fileList->addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
void FileExplorerWidget::setupContextMenu()
|
||||
{
|
||||
fileList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(fileList, &QListWidget::customContextMenuRequested, this,
|
||||
&FileExplorerWidget::onFileListContextMenu);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onFileListContextMenu(const QPoint &pos)
|
||||
{
|
||||
QListWidgetItem *item = fileList->itemAt(pos);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
bool isDir = item->data(Qt::UserRole).toBool();
|
||||
if (isDir)
|
||||
return; // Only export files
|
||||
|
||||
QMenu menu;
|
||||
QAction *exportAction = menu.addAction("Export");
|
||||
QAction *selectedAction = menu.exec(fileList->viewport()->mapToGlobal(pos));
|
||||
if (selectedAction == exportAction) {
|
||||
QList<QListWidgetItem *> selectedItems = fileList->selectedItems();
|
||||
QList<QListWidgetItem *> filesToExport;
|
||||
if (selectedItems.isEmpty())
|
||||
filesToExport.append(item); // fallback: just the clicked one
|
||||
else {
|
||||
for (QListWidgetItem *selItem : selectedItems) {
|
||||
if (!selItem->data(Qt::UserRole).toBool())
|
||||
filesToExport.append(selItem);
|
||||
}
|
||||
}
|
||||
if (filesToExport.isEmpty())
|
||||
return;
|
||||
QString dir =
|
||||
QFileDialog::getExistingDirectory(this, "Select Export Directory");
|
||||
if (dir.isEmpty())
|
||||
return;
|
||||
for (QListWidgetItem *selItem : filesToExport) {
|
||||
exportSelectedFile(selItem, dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onExportClicked()
|
||||
{
|
||||
QList<QListWidgetItem *> selectedItems = fileList->selectedItems();
|
||||
if (selectedItems.isEmpty())
|
||||
return;
|
||||
|
||||
// Only files (not directories)
|
||||
QList<QListWidgetItem *> filesToExport;
|
||||
for (QListWidgetItem *item : selectedItems) {
|
||||
if (!item->data(Qt::UserRole).toBool())
|
||||
filesToExport.append(item);
|
||||
}
|
||||
if (filesToExport.isEmpty())
|
||||
return;
|
||||
|
||||
// Ask user for a directory to save all files
|
||||
QString dir =
|
||||
QFileDialog::getExistingDirectory(this, "Select Export Directory");
|
||||
if (dir.isEmpty())
|
||||
return;
|
||||
|
||||
for (QListWidgetItem *item : filesToExport) {
|
||||
exportSelectedFile(item, dir);
|
||||
}
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onExportDeleteClicked()
|
||||
{
|
||||
// Placeholder for future implementation
|
||||
QList<QListWidgetItem *> selectedItems = fileList->selectedItems();
|
||||
if (selectedItems.isEmpty())
|
||||
return;
|
||||
// TODO: Implement export & delete logic
|
||||
return;
|
||||
}
|
||||
|
||||
void FileExplorerWidget::exportSelectedFile(QListWidgetItem *item,
|
||||
const QString &directory)
|
||||
{
|
||||
QString fileName = item->text();
|
||||
QString currPath = "/";
|
||||
if (!history.isEmpty())
|
||||
currPath = history.top();
|
||||
if (!currPath.endsWith("/"))
|
||||
currPath += "/";
|
||||
qDebug() << "Current path:" << currPath;
|
||||
QString devicePath = currPath == "/" ? "/" + fileName : currPath + fileName;
|
||||
qDebug() << "Exporting file:" << devicePath;
|
||||
|
||||
// Save to selected directory
|
||||
QString savePath = directory + "/" + fileName;
|
||||
|
||||
// Get device info and check connections
|
||||
qDebug() << "Using device index:" << device->udid.c_str();
|
||||
qDebug() << "Device UDID:" << QString::fromStdString(device->udid);
|
||||
qDebug() << "Device Product Type:"
|
||||
<< QString::fromStdString(device->deviceInfo.productType);
|
||||
|
||||
// Export file using the validated connections
|
||||
int result =
|
||||
export_file_to_path(currentAfcClient, devicePath.toStdString().c_str(),
|
||||
savePath.toStdString().c_str());
|
||||
|
||||
qDebug() << "Export result:" << result;
|
||||
|
||||
if (result == 0) {
|
||||
qDebug() << "Exported" << devicePath << "to" << savePath;
|
||||
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
this, "Export Successful",
|
||||
"File exported successfully. Would you like to see the directory?",
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
if (reply == QMessageBox::Yes) {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(directory));
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Failed to export" << devicePath;
|
||||
QMessageBox::warning(this, "Export Failed",
|
||||
"Failed to export the file from the device");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to export to a specific local path
|
||||
int FileExplorerWidget::export_file_to_path(afc_client_t afc,
|
||||
const char *device_path,
|
||||
const char *local_path)
|
||||
{
|
||||
uint64_t handle = 0;
|
||||
// TODO: implement safe_afc_file_open
|
||||
if (afc_file_open(afc, device_path, AFC_FOPEN_RDONLY, &handle) !=
|
||||
AFC_E_SUCCESS) {
|
||||
qDebug() << "Failed to open file on device:" << device_path;
|
||||
return -1;
|
||||
}
|
||||
FILE *out = fopen(local_path, "wb");
|
||||
if (!out) {
|
||||
qDebug() << "Failed to open local file:" << local_path;
|
||||
afc_file_close(afc, handle);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char buffer[4096];
|
||||
uint32_t bytes_read = 0;
|
||||
// TODO: implement safe_afc_file_read
|
||||
while (afc_file_read(afc, handle, buffer, sizeof(buffer), &bytes_read) ==
|
||||
AFC_E_SUCCESS &&
|
||||
bytes_read > 0) {
|
||||
fwrite(buffer, 1, bytes_read, out);
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
// TODO: implement safe_afc_file_close
|
||||
afc_file_close(afc, handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void FileExplorerWidget::onImportClicked()
|
||||
{
|
||||
// TODO: check devices
|
||||
|
||||
// Select one or more files to import
|
||||
QStringList fileNames = QFileDialog::getOpenFileNames(this, "Import Files");
|
||||
if (fileNames.isEmpty())
|
||||
return;
|
||||
|
||||
// Use current breadcrumb directory as target
|
||||
QString currPath = "/";
|
||||
if (!history.isEmpty())
|
||||
currPath = history.top();
|
||||
if (!currPath.endsWith("/"))
|
||||
currPath += "/";
|
||||
|
||||
// if (!device || !client || !serviceDesc)
|
||||
// {
|
||||
// qDebug() << "Failed to connect to device or lockdown service";
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Import each file
|
||||
for (const QString &localPath : fileNames) {
|
||||
QFileInfo fi(localPath);
|
||||
QString devicePath = currPath + fi.fileName();
|
||||
int result = import_file_to_device(currentAfcClient,
|
||||
devicePath.toStdString().c_str(),
|
||||
localPath.toStdString().c_str());
|
||||
if (result == 0)
|
||||
qDebug() << "Imported" << localPath << "to" << devicePath;
|
||||
else
|
||||
qDebug() << "Failed to import" << localPath;
|
||||
}
|
||||
|
||||
// Refresh file list
|
||||
loadPath(currPath);
|
||||
}
|
||||
|
||||
// Helper function to import a file from a local path to the device
|
||||
int FileExplorerWidget::import_file_to_device(afc_client_t afc,
|
||||
const char *device_path,
|
||||
const char *local_path)
|
||||
{
|
||||
QFile in(local_path);
|
||||
if (!in.open(QIODevice::ReadOnly)) {
|
||||
qDebug() << "Failed to open local file for import:" << local_path;
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t handle = 0;
|
||||
if (afc_file_open(afc, device_path, AFC_FOPEN_WRONLY, &handle) !=
|
||||
AFC_E_SUCCESS) {
|
||||
qDebug() << "Failed to open file on device for writing:" << device_path;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char buffer[4096];
|
||||
qint64 bytesRead;
|
||||
while ((bytesRead = in.read(buffer, sizeof(buffer))) > 0) {
|
||||
uint32_t bytesWritten = 0;
|
||||
if (afc_file_write(afc, handle, buffer,
|
||||
static_cast<uint32_t>(bytesRead),
|
||||
&bytesWritten) != AFC_E_SUCCESS ||
|
||||
bytesWritten != bytesRead) {
|
||||
qDebug() << "Failed to write to device file:" << device_path;
|
||||
afc_file_close(afc, handle);
|
||||
in.close();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
afc_file_close(afc, handle);
|
||||
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);
|
||||
m_sidebarTree = new QTreeWidget();
|
||||
m_sidebarTree->setHeaderLabel("Files");
|
||||
m_sidebarTree->setMinimumWidth(350);
|
||||
m_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);
|
||||
m_afcDefaultItem = new QTreeWidgetItem(m_sidebarTree);
|
||||
m_afcDefaultItem->setText(0, "Explorer");
|
||||
m_afcDefaultItem->setIcon(0, QIcon::fromTheme("folder"));
|
||||
m_afcDefaultItem->setData(0, Qt::UserRole,
|
||||
QVariant::fromValue(SidebarItemData(false, "/")));
|
||||
m_afcDefaultItem->setExpanded(true);
|
||||
|
||||
// Add root folder under Default
|
||||
QTreeWidgetItem *rootItem = new QTreeWidgetItem(afcDefaultItem);
|
||||
QTreeWidgetItem *rootItem = new QTreeWidgetItem(m_afcDefaultItem);
|
||||
rootItem->setText(0, "Default");
|
||||
rootItem->setIcon(0, QIcon::fromTheme("folder"));
|
||||
rootItem->setData(0, Qt::UserRole,
|
||||
@@ -434,24 +66,24 @@ void FileExplorerWidget::setupSidebar()
|
||||
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);
|
||||
m_afcJailbrokenItem = new QTreeWidgetItem(m_afcDefaultItem);
|
||||
m_afcJailbrokenItem->setText(0, "Jailbroken (AFC2)");
|
||||
m_afcJailbrokenItem->setIcon(0, QIcon::fromTheme("applications-system"));
|
||||
m_afcJailbrokenItem->setData(
|
||||
0, Qt::UserRole, QVariant::fromValue(SidebarItemData(true, "/")));
|
||||
m_afcJailbrokenItem->setExpanded(false);
|
||||
|
||||
// Common Places section
|
||||
commonPlacesItem = new QTreeWidgetItem(sidebarTree);
|
||||
commonPlacesItem->setText(0, "Common Places");
|
||||
commonPlacesItem->setIcon(0, QIcon::fromTheme("places-bookmarks"));
|
||||
commonPlacesItem->setData(
|
||||
m_commonPlacesItem = new QTreeWidgetItem(m_sidebarTree);
|
||||
m_commonPlacesItem->setText(0, "Common Places");
|
||||
m_commonPlacesItem->setIcon(0, QIcon::fromTheme("places-bookmarks"));
|
||||
m_commonPlacesItem->setData(
|
||||
0, Qt::UserRole,
|
||||
QVariant::fromValue(
|
||||
SidebarItemData(false, "../../../var/mobile/Library/Wallpapers")));
|
||||
commonPlacesItem->setExpanded(true);
|
||||
m_commonPlacesItem->setExpanded(true);
|
||||
|
||||
QTreeWidgetItem *wallpapersItem = new QTreeWidgetItem(commonPlacesItem);
|
||||
QTreeWidgetItem *wallpapersItem = new QTreeWidgetItem(m_commonPlacesItem);
|
||||
wallpapersItem->setText(0, "Wallpapers");
|
||||
wallpapersItem->setIcon(0, QIcon::fromTheme("image-x-generic"));
|
||||
wallpapersItem->setData(
|
||||
@@ -462,113 +94,18 @@ void FileExplorerWidget::setupSidebar()
|
||||
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(
|
||||
m_favoritePlacesItem = new QTreeWidgetItem(m_sidebarTree);
|
||||
m_favoritePlacesItem->setText(0, "Favorite Places");
|
||||
m_favoritePlacesItem->setIcon(0, QIcon::fromTheme("user-bookmarks"));
|
||||
m_favoritePlacesItem->setData(
|
||||
// todo:implement
|
||||
0, Qt::UserRole, QVariant::fromValue(SidebarItemData(false, "/")));
|
||||
favoritePlacesItem->setExpanded(true);
|
||||
m_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;
|
||||
}
|
||||
// connect(m_sidebarTree, &QTreeWidget::itemClicked, this,
|
||||
// &FileExplorerWidget::onSidebarItemClicked);
|
||||
}
|
||||
|
||||
void FileExplorerWidget::loadFavoritePlaces()
|
||||
@@ -581,7 +118,8 @@ void FileExplorerWidget::loadFavoritePlaces()
|
||||
QString alias = favorite.second;
|
||||
|
||||
qDebug() << "Favorite:" << alias << "->" << path;
|
||||
QTreeWidgetItem *favoriteItem = new QTreeWidgetItem(favoritePlacesItem);
|
||||
QTreeWidgetItem *favoriteItem =
|
||||
new QTreeWidgetItem(m_favoritePlacesItem);
|
||||
favoriteItem->setText(0, alias);
|
||||
favoriteItem->setIcon(0, QIcon::fromTheme("folder-favorites"));
|
||||
favoriteItem->setData(
|
||||
@@ -589,23 +127,4 @@ void FileExplorerWidget::loadFavoritePlaces()
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -23,61 +23,21 @@ public:
|
||||
explicit FileExplorerWidget(iDescriptorDevice *device,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void fileSelected(const QString &filePath);
|
||||
|
||||
private slots:
|
||||
void goBack();
|
||||
void onItemDoubleClicked(QListWidgetItem *item);
|
||||
void onBreadcrumbClicked();
|
||||
void onFileListContextMenu(const QPoint &pos);
|
||||
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;
|
||||
QSplitter *m_mainSplitter;
|
||||
afc_client_t currentAfcClient;
|
||||
QTreeWidget *m_sidebarTree;
|
||||
iDescriptorDevice *device;
|
||||
bool usingAFC2;
|
||||
|
||||
// Tree items
|
||||
QTreeWidgetItem *afcDefaultItem;
|
||||
QTreeWidgetItem *afcJailbrokenItem;
|
||||
QTreeWidgetItem *commonPlacesItem;
|
||||
QTreeWidgetItem *favoritePlacesItem;
|
||||
QTreeWidgetItem *m_afcDefaultItem;
|
||||
QTreeWidgetItem *m_afcJailbrokenItem;
|
||||
QTreeWidgetItem *m_commonPlacesItem;
|
||||
QTreeWidgetItem *m_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);
|
||||
void exportSelectedFile(QListWidgetItem *item, const QString &directory);
|
||||
int export_file_to_path(afc_client_t afc, const char *device_path,
|
||||
const char *local_path);
|
||||
int import_file_to_device(afc_client_t afc, const char *device_path,
|
||||
const char *local_path);
|
||||
};
|
||||
|
||||
#endif // FILEEXPLORERWIDGET_H
|
||||
|
||||
+80
-46
@@ -1,4 +1,5 @@
|
||||
#include "installedappswidget.h"
|
||||
#include "afcexplorerwidget.h"
|
||||
#include "iDescriptor.h"
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
@@ -207,6 +208,17 @@ void InstalledAppsWidget::setupUI()
|
||||
searchAction->setToolTip("Search");
|
||||
|
||||
searchLayout->addWidget(m_searchEdit);
|
||||
|
||||
// Add checkbox for file sharing filter
|
||||
m_fileSharingCheckBox = new QCheckBox("File Sharing Enabled");
|
||||
m_fileSharingCheckBox->setChecked(true); // Default enabled
|
||||
m_fileSharingCheckBox->setStyleSheet("QCheckBox { "
|
||||
" font-size: 12px; "
|
||||
" color: #333; "
|
||||
" margin-left: 5px; "
|
||||
"}");
|
||||
searchLayout->addWidget(m_fileSharingCheckBox);
|
||||
|
||||
tabFrameLayout->addWidget(searchContainer);
|
||||
|
||||
// Add a separator line
|
||||
@@ -280,6 +292,10 @@ void InstalledAppsWidget::setupUI()
|
||||
connect(m_searchEdit, &QLineEdit::textChanged, this,
|
||||
&InstalledAppsWidget::filterApps);
|
||||
|
||||
// Connect file sharing filter
|
||||
connect(m_fileSharingCheckBox, &QCheckBox::toggled, this,
|
||||
&InstalledAppsWidget::onFileSharingFilterChanged);
|
||||
|
||||
showLoadingState();
|
||||
}
|
||||
|
||||
@@ -361,6 +377,8 @@ void InstalledAppsWidget::fetchInstalledApps()
|
||||
plist_new_string("CFBundleShortVersionString"));
|
||||
plist_array_append_item(return_attrs,
|
||||
plist_new_string("CFBundleVersion"));
|
||||
plist_array_append_item(
|
||||
return_attrs, plist_new_string("UIFileSharingEnabled"));
|
||||
|
||||
plist_dict_set_item(client_opts, "ReturnAttributes",
|
||||
return_attrs);
|
||||
@@ -422,6 +440,21 @@ void InstalledAppsWidget::fetchInstalledApps()
|
||||
}
|
||||
}
|
||||
|
||||
// Get file sharing enabled status
|
||||
plist_t file_sharing = plist_dict_get_item(
|
||||
app_info, "UIFileSharingEnabled");
|
||||
if (file_sharing &&
|
||||
plist_get_node_type(file_sharing) ==
|
||||
PLIST_BOOLEAN) {
|
||||
uint8_t file_sharing_enabled = 0;
|
||||
plist_get_bool_val(file_sharing,
|
||||
&file_sharing_enabled);
|
||||
appData["fileSharingEnabled"] =
|
||||
(file_sharing_enabled != 0);
|
||||
} else {
|
||||
appData["fileSharingEnabled"] = false;
|
||||
}
|
||||
|
||||
appData["type"] = appType;
|
||||
|
||||
if (!appData["bundleId"].toString().isEmpty()) {
|
||||
@@ -497,6 +530,13 @@ void InstalledAppsWidget::onAppsDataReady()
|
||||
QString bundleId = appData.value("bundleId").toString();
|
||||
QString version = appData.value("version").toString();
|
||||
QString appType = appData.value("type").toString();
|
||||
bool fileSharingEnabled =
|
||||
appData.value("fileSharingEnabled", false).toBool();
|
||||
|
||||
// Filter by file sharing status if checkbox is checked
|
||||
if (m_fileSharingCheckBox->isChecked() && !fileSharingEnabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (displayName.isEmpty()) {
|
||||
displayName = bundleId;
|
||||
@@ -673,10 +713,10 @@ void InstalledAppsWidget::loadAppContainer(const QString &bundleId)
|
||||
lockdowndService = nullptr;
|
||||
|
||||
// Send vendor container command
|
||||
if (house_arrest_send_command(houseArrestClient, "VendContainer",
|
||||
if (house_arrest_send_command(houseArrestClient, "VendDocuments",
|
||||
bundleId.toUtf8().constData()) !=
|
||||
HOUSE_ARREST_E_SUCCESS) {
|
||||
result["error"] = "Could not send VendContainer command";
|
||||
result["error"] = "Could not send VendDocuments command";
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
return result;
|
||||
@@ -743,12 +783,17 @@ void InstalledAppsWidget::loadAppContainer(const QString &bundleId)
|
||||
}
|
||||
afc_dictionary_free(list);
|
||||
}
|
||||
|
||||
qDebug() << "App container files:" << files;
|
||||
result["files"] = files;
|
||||
result["afcClient"] =
|
||||
QVariant::fromValue(reinterpret_cast<void *>(afcClient));
|
||||
result["houseArrestClient"] = QVariant::fromValue(
|
||||
reinterpret_cast<void *>(houseArrestClient));
|
||||
result["success"] = true;
|
||||
|
||||
afc_client_free(afcClient);
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
// Don't free the clients here - they will be used by
|
||||
// AfcExplorerWidget afc_client_free(afcClient);
|
||||
// house_arrest_client_free(houseArrestClient);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
|
||||
} catch (const std::exception &e) {
|
||||
@@ -792,49 +837,38 @@ void InstalledAppsWidget::onContainerDataReady()
|
||||
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);
|
||||
// Get the AFC clients from the result
|
||||
afc_client_t afcClient = reinterpret_cast<afc_client_t>(
|
||||
result.value("afcClient").value<void *>());
|
||||
house_arrest_client_t houseArrestClient =
|
||||
reinterpret_cast<house_arrest_client_t>(
|
||||
result.value("houseArrestClient").value<void *>());
|
||||
|
||||
if (!afcClient) {
|
||||
QLabel *errorLabel =
|
||||
new QLabel("Failed to get AFC client for app container");
|
||||
errorLabel->setStyleSheet("color: #999; font-style: italic;");
|
||||
m_containerLayout->addWidget(errorLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort files/directories
|
||||
files.sort();
|
||||
// Create AfcExplorerWidget with the house arrest AFC client
|
||||
AfcExplorerWidget *explorer = new AfcExplorerWidget(
|
||||
afcClient,
|
||||
[houseArrestClient]() {
|
||||
// Cleanup callback when client becomes invalid
|
||||
if (houseArrestClient) {
|
||||
house_arrest_client_free(houseArrestClient);
|
||||
}
|
||||
},
|
||||
m_device, this);
|
||||
|
||||
// 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();
|
||||
m_containerLayout->addWidget(explorer);
|
||||
}
|
||||
|
||||
void InstalledAppsWidget::onFileSharingFilterChanged(bool enabled)
|
||||
{
|
||||
Q_UNUSED(enabled)
|
||||
// Refresh the apps list when filter changes
|
||||
fetchInstalledApps();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define INSTALLEDAPPSWIDGET_H
|
||||
|
||||
#include "iDescriptor.h"
|
||||
#include <QCheckBox>
|
||||
#include <QEnterEvent>
|
||||
#include <QFrame>
|
||||
#include <QFutureWatcher>
|
||||
@@ -68,6 +69,7 @@ private slots:
|
||||
void onAppsDataReady();
|
||||
void onAppTabClicked();
|
||||
void onContainerDataReady();
|
||||
void onFileSharingFilterChanged(bool enabled);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
@@ -83,6 +85,7 @@ private:
|
||||
iDescriptorDevice *m_device;
|
||||
QHBoxLayout *m_mainLayout;
|
||||
QLineEdit *m_searchEdit;
|
||||
QCheckBox *m_fileSharingCheckBox;
|
||||
QScrollArea *m_tabScrollArea;
|
||||
QWidget *m_tabContainer;
|
||||
QVBoxLayout *m_tabLayout;
|
||||
|
||||
+8
-8
@@ -180,14 +180,14 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
m_customTabWidget->addTab(jailbrokenWidget, "Jailbroken");
|
||||
m_customTabWidget->finalizeStyles();
|
||||
|
||||
connect(
|
||||
m_customTabWidget, &CustomTabWidget::currentChanged, this,
|
||||
[this, jailbrokenWidget](int index) {
|
||||
if (index == 3) { // Jailbroken tab
|
||||
jailbrokenWidget->initWidget();
|
||||
}
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
// connect(
|
||||
// m_customTabWidget, &CustomTabWidget::currentChanged, this,
|
||||
// [this, jailbrokenWidget](int index) {
|
||||
// if (index == 3) { // Jailbroken tab
|
||||
// jailbrokenWidget->initWidget();
|
||||
// }
|
||||
// },
|
||||
// Qt::SingleShotConnection);
|
||||
|
||||
// settings button
|
||||
QPushButton *settingsButton = new QPushButton();
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include <QWheelEvent>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QtGlobal>
|
||||
|
||||
// todo : need to pass afc as well
|
||||
MediaPreviewDialog::MediaPreviewDialog(iDescriptorDevice *device,
|
||||
const QString &filePath, QWidget *parent)
|
||||
: QDialog(parent), m_device(device), m_filePath(filePath),
|
||||
@@ -164,7 +164,14 @@ void MediaPreviewDialog::setupVideoView()
|
||||
&MediaPreviewDialog::onMediaPlayerPositionChanged);
|
||||
connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this,
|
||||
&MediaPreviewDialog::onMediaPlayerStateChanged);
|
||||
|
||||
connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this,
|
||||
[this](QMediaPlayer::Error error, const QString &errorString) {
|
||||
qDebug() << "MediaPlayer Error:" << error << errorString;
|
||||
m_statusLabel->setText("Error: " + errorString);
|
||||
m_loadingLabel->setText("Error: " + errorString);
|
||||
m_loadingLabel->show();
|
||||
m_videoWidget->hide();
|
||||
});
|
||||
// Setup progress timer for smooth updates
|
||||
m_progressTimer = new QTimer(this);
|
||||
connect(m_progressTimer, &QTimer::timeout, this,
|
||||
|
||||
Reference in New Issue
Block a user