mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-22 03:45:51 +08:00
464 lines
14 KiB
C++
464 lines
14 KiB
C++
#include "fileexplorerwidget.h"
|
|
#include "./core/services/get-media.cpp"
|
|
#include "iDescriptor.h"
|
|
#include <QDebug>
|
|
#include <QDesktopServices>
|
|
#include <QFileDialog>
|
|
#include <QHBoxLayout>
|
|
#include <QIcon>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QSignalBlocker>
|
|
#include <QVariant>
|
|
#include <libimobiledevice/afc.h>
|
|
#include <libimobiledevice/libimobiledevice.h>
|
|
|
|
bool FileExplorerWidget::ensureConnection()
|
|
{
|
|
|
|
// Validate all required connections
|
|
if (!device->device) {
|
|
qDebug() << "Failed to connect to device";
|
|
QMessageBox::warning(this, "Error", "Device connection lost");
|
|
return false;
|
|
}
|
|
|
|
lockdownd_error_t ldret = LOCKDOWN_E_UNKNOWN_ERROR;
|
|
|
|
if (LOCKDOWN_E_SUCCESS != (ldret = lockdownd_client_new_with_handshake(
|
|
device->device, &client, APP_LABEL))) {
|
|
return false; // Failed to create lockdown client
|
|
// result.error = ldret;
|
|
qDebug() << "In fileexplorer Failed to create lockdown client: "
|
|
<< ldret;
|
|
// idevice_free(result.device);
|
|
// return result;
|
|
}
|
|
// if (!lockdownService) {
|
|
// qDebug() << "Failed to connect to lockdown service";
|
|
// QMessageBox::warning(this, "Error", "Lockdown service unavailable");
|
|
|
|
// Try to reinitialize the AFC service
|
|
if (lockdownd_start_service(client, "com.apple.afc", &lockdownService) !=
|
|
LOCKDOWN_E_SUCCESS) {
|
|
qDebug() << "Failed to restart AFC service";
|
|
QMessageBox::warning(this, "Error", "Could not restart AFC service");
|
|
return false;
|
|
}
|
|
|
|
if (afc_client_new(device->device, lockdownService, &afcClient) !=
|
|
AFC_E_SUCCESS) {
|
|
qDebug() << "Failed to create new AFC client";
|
|
lockdownd_service_descriptor_free(lockdownService);
|
|
QMessageBox::warning(this, "Error", "Could not create AFC client");
|
|
return false;
|
|
}
|
|
|
|
qDebug() << "Successfully reinitialized AFC service";
|
|
// }
|
|
return true;
|
|
}
|
|
|
|
FileExplorerWidget::FileExplorerWidget(iDescriptorDevice *device,
|
|
QWidget *parent)
|
|
: QWidget(parent), device(device)
|
|
{
|
|
// Debug: log devices vector
|
|
if (!ensureConnection()) {
|
|
qDebug() << "Failed to ensure connection in FileExplorerWidget";
|
|
return;
|
|
}
|
|
|
|
QVBoxLayout *mainLayout = new QVBoxLayout(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);
|
|
|
|
// --- 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);
|
|
|
|
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
|
|
|
|
setLayout(mainLayout);
|
|
history.push("/");
|
|
loadPath("/");
|
|
|
|
setupContextMenu();
|
|
}
|
|
|
|
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 {
|
|
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 =
|
|
getMediaFileTree(afcClient, lockdownService, 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(afcClient, 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;
|
|
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;
|
|
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);
|
|
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(afcClient, 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;
|
|
}
|