diff --git a/resources.qrc b/resources.qrc
index 7bc83ba..0024a9c 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -35,6 +35,8 @@
resources/icons/ClarityEyeLine.png
resources/icons/MaterialSymbolsLightImageOutlineSharp.png
resources/icons/MaterialSymbolsFolder.png
+ resources/icons/QlementineIconsWireless116.png
+ resources/icons/UimProcess.png
qml/MapView.qml
resources/iphone.png
resources/ios-wallpapers/iphone-ios4.png
diff --git a/resources/icons/QlementineIconsWireless116.png b/resources/icons/QlementineIconsWireless116.png
new file mode 100644
index 0000000..0fa302d
Binary files /dev/null and b/resources/icons/QlementineIconsWireless116.png differ
diff --git a/resources/icons/UimProcess.png b/resources/icons/UimProcess.png
new file mode 100644
index 0000000..0ff6cff
Binary files /dev/null and b/resources/icons/UimProcess.png differ
diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp
index 386d796..9d49691 100644
--- a/src/afcexplorerwidget.cpp
+++ b/src/afcexplorerwidget.cpp
@@ -291,11 +291,14 @@ void AfcExplorerWidget::onFileListContextMenu(const QPoint &pos)
if (!currPath.endsWith("/"))
currPath += "/";
+ // FIXME: index
+ int index = 0;
for (QListWidgetItem *selItem : filesToExport) {
QString fileName = selItem->text();
QString devicePath =
currPath == "/" ? "/" + fileName : currPath + fileName;
- exportItems.append(ExportItem(devicePath, fileName));
+ exportItems.append(ExportItem(devicePath, fileName, index));
+ index++;
}
// Start export with singleton - manager will show its own dialog
@@ -346,11 +349,13 @@ void AfcExplorerWidget::onExportClicked()
if (!currPath.endsWith("/"))
currPath += "/";
+ int index = 0;
for (QListWidgetItem *item : filesToExport) {
QString fileName = item->text();
QString devicePath =
currPath == "/" ? "/" + fileName : currPath + fileName;
- exportItems.append(ExportItem(devicePath, fileName));
+ exportItems.append(ExportItem(devicePath, fileName, index));
+ index++;
}
// Start export with singleton - manager will show its own dialog
@@ -869,4 +874,4 @@ void AfcExplorerWidget::goUp()
// Add the new path to history and load it
m_history.push(parentPath);
loadPath(parentPath);
-}
\ No newline at end of file
+}
diff --git a/src/appcontext.cpp b/src/appcontext.cpp
index 13b41eb..970f8eb 100644
--- a/src/appcontext.cpp
+++ b/src/appcontext.cpp
@@ -66,24 +66,20 @@ AppContext::AppContext(QObject *parent) : QObject{parent}
const std::string wifiMacAddress =
PlistNavigator(fileData)["WiFiMACAddress"].getString();
// plist_free(fileData);
- qDebug() << "Found pairing file for MAC"
- << QString::fromStdString(wifiMacAddress);
bool isCompatible = !wifiMacAddress.empty();
// TODO: !important invalidate old pairing files
- // libimobiledevice does not append WIFIMACAddress to the pairing file
+ // sometimes there is no WiFiMACAddress
if (!isCompatible) {
continue;
}
+ qDebug() << "Found pairing file for MAC"
+ << QString::fromStdString(wifiMacAddress);
- IdevicePairingFile *pairing_file = nullptr;
- idevice_pairing_file_read(
- lockdowndir.filePath(fileName).toUtf8().constData(), &pairing_file);
- if (pairing_file) {
- qDebug() << "Caching pairing file for MAC"
- << QString::fromStdString(wifiMacAddress);
- m_pairingFileCache[QString::fromStdString(wifiMacAddress)] =
- pairing_file;
- }
+ qDebug() << "Caching pairing file for MAC"
+ << QString::fromStdString(wifiMacAddress) << "Local Path"
+ << lockdowndir.filePath(fileName);
+ m_pairingFileCache[QString::fromStdString(wifiMacAddress)] =
+ lockdowndir.filePath(fileName);
}
}
@@ -99,14 +95,24 @@ void AppContext::addDevice(QString udid,
addType, wifiMacAddress,
initResult]() {
if (addType == AddType::UpgradeToWireless) {
- const IdevicePairingFile *pairingFile =
- getCachedPairingFile(udid);
- if (!pairingFile) {
+ // udid is mac address here
+ const QString _pairingFilePath = getCachedPairingFile(udid);
+
+ if (_pairingFilePath.isEmpty()) {
+ qDebug() << "Cannot upgrade to wireless, no cached pairing "
+ "file for"
+ << udid;
+ return;
+ }
+
+ QFile pairingFilePath(_pairingFilePath);
+ if (!pairingFilePath.exists()) {
qDebug()
<< "Cannot upgrade to wireless, no pairing file for"
<< udid;
return;
}
+ pairingFilePath.close();
QList networkDevices =
NetworkDeviceManager::sharedInstance()
@@ -121,15 +127,8 @@ void AppContext::addDevice(QString udid,
if (it != networkDevices.constEnd()) {
- IdevicePairingFile *pairing_file = nullptr;
- idevice_pairing_file_read(
- QString("/var/lib/lockdown/%1.plist")
- .arg(udid)
- .toUtf8()
- .constData(),
- &pairing_file);
*initResult = init_idescriptor_device(
- udid, {it->address, pairing_file});
+ udid, {it->address, pairingFilePath.fileName()});
} else {
qDebug() << "No network device found with MAC address:"
<< wifiMacAddress;
@@ -137,19 +136,29 @@ void AppContext::addDevice(QString udid,
}
} else if (addType == AddType::Wireless) {
// FIXME: its not udid here its macAddress
- const IdevicePairingFile *pairingFile =
- getCachedPairingFile(udid);
- if (!pairingFile) {
- qDebug() << "Cannot initialize wireless device, no pairing "
+ const QString _pairingFilePath = getCachedPairingFile(udid);
+
+ if (_pairingFilePath.isEmpty()) {
+ qDebug() << "Cannot upgrade to wireless, no cached pairing "
"file for"
<< udid;
return;
}
+ QFile pairingFilePath(_pairingFilePath);
+ if (!pairingFilePath.exists()) {
+ qDebug()
+ << "Cannot upgrade to wireless, no pairing file for"
+ << udid;
+ return;
+ }
+ pairingFilePath.close();
+
QList networkDevices =
NetworkDeviceManager::sharedInstance()
->m_networkProvider->getNetworkDevices();
+ // todo : retry logic if not found
auto it = std::find_if(
networkDevices.constBegin(), networkDevices.constEnd(),
[wifiMacAddress](const NetworkDevice &device) {
@@ -159,7 +168,7 @@ void AppContext::addDevice(QString udid,
if (it != networkDevices.constEnd()) {
*initResult = init_idescriptor_device(
- udid, {it->address, pairingFile});
+ udid, {it->address, pairingFilePath.fileName()});
} else {
qDebug() << "No network device found with MAC address:"
<< wifiMacAddress;
@@ -473,14 +482,13 @@ AppContext::getDeviceByMacAddress(const QString &macAddress) const
}
void AppContext::cachePairingFile(const QString &udid,
- IdevicePairingFile *pairingFile)
+ const QString &pairingFilePath)
{
- m_pairingFileCache.insert(udid, pairingFile);
+ m_pairingFileCache.insert(udid, pairingFilePath);
}
-const IdevicePairingFile *
-AppContext::getCachedPairingFile(const QString &udid) const
+const QString AppContext::getCachedPairingFile(const QString &udid) const
{
- const IdevicePairingFile *pairingFile = nullptr;
+ QString pairingFile;
// Retrieve the pairing file from the cache
if (m_pairingFileCache.contains(udid)) {
@@ -489,3 +497,8 @@ AppContext::getCachedPairingFile(const QString &udid) const
return pairingFile;
}
+
+void AppContext::heartbeatFailed(const QString &macAddress, int tries)
+{
+ emit deviceHeartbeatFailed(macAddress, tries);
+}
\ No newline at end of file
diff --git a/src/appcontext.h b/src/appcontext.h
index c4d5200..49d2584 100644
--- a/src/appcontext.h
+++ b/src/appcontext.h
@@ -34,8 +34,9 @@ public:
QList getAllDevices();
explicit AppContext(QObject *parent = nullptr);
bool noDevicesConnected() const;
- void cachePairingFile(const QString &udid, IdevicePairingFile *pairingFile);
- const IdevicePairingFile *getCachedPairingFile(const QString &udid) const;
+ // QMap
+ void cachePairingFile(const QString &udid, const QString &pairingFilePath);
+ const QString getCachedPairingFile(const QString &udid) const;
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// QList getAllRecoveryDevices();
@@ -55,8 +56,7 @@ private:
// #endif
QStringList m_pendingDevices;
DeviceSelection m_currentSelection = DeviceSelection("");
- // FIXME: QString can be macAddress or udid - both works fine for now
- QMap m_pairingFileCache;
+ QMap m_pairingFileCache;
signals:
void deviceAdded(iDescriptorDevice *device);
void deviceRemoved(const std::string &udid, const std::string &macAddress);
@@ -79,11 +79,14 @@ signals:
*/
void deviceChange();
void currentDeviceSelectionChanged(const DeviceSelection &selection);
+ void deviceHeartbeatFailed(const QString &macAddress, int tries);
public slots:
void removeDevice(QString udid);
void addDevice(QString udid,
DeviceMonitorThread::IdeviceConnectionType connType,
AddType addType, QString wifiMacAddress = QString());
+ void heartbeatFailed(const QString &macAddress, int tries);
+ // void heartbeatThreadExited(const QString &macAddress);
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void addRecoveryDevice(uint64_t ecid);
void removeRecoveryDevice(uint64_t ecid);
diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp
index bfc94d7..b502eee 100644
--- a/src/core/services/init_device.cpp
+++ b/src/core/services/init_device.cpp
@@ -24,16 +24,15 @@
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
#include "libirecovery.h"
#endif
-#include
-#include
-
#include "../../heartbeat.h"
+#include
#include
#include
-#include
-
#include
+#include
+#include
#include
+
std::string safeGetXML(const char *key, pugi::xml_node dict)
{
for (pugi::xml_node child = dict.first_child(); child;
@@ -365,15 +364,12 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc,
}
}
-// FIXME:spawn on a new thread?
-// wireless connections sometimes take more than 10sec to connect
-// and ofc it freezes the ui
-// TODO:idevice_start_session ?
iDescriptorInitDeviceResult
-init_idescriptor_device(const QString &udid, WirelessInitArgs wirelessArgs)
+init_idescriptor_device(const QString &udid,
+ const WirelessInitArgs &wirelessArgs)
{
const bool isWireless =
- !wirelessArgs.ip.isEmpty() && wirelessArgs.pairing_file;
+ !wirelessArgs.ip.isEmpty() && !wirelessArgs.pairing_file.isEmpty();
qDebug() << "Initializing iDescriptor device with UDID: " << udid
<< (isWireless ? "over wireless" : "over USB");
@@ -391,16 +387,13 @@ init_idescriptor_device(const QString &udid, WirelessInitArgs wirelessArgs)
IdevicePairingFile *pairing_file = nullptr;
IdeviceHandle *deviceHandle = nullptr;
HeartbeatClientHandle *heartbeat = nullptr;
- HeartBeatThread *heartbeatThread = nullptr;
+ HeartbeatThread *heartbeatThread = nullptr;
ImageMounterHandle *image_mounter = nullptr;
DiagnosticsRelayClientHandle *diagnostics_relay = nullptr;
ScreenshotrClientHandle *screenshotr_client = nullptr;
LocationSimulationHandle *location_simulation = nullptr;
- // FIXME: remove debug
- std::stringstream ss;
plist_t val = nullptr;
- // 1. Connect to usbmuxd
IdeviceFfiError *err =
idevice_usbmuxd_new_default_connection(0, &usbmuxd_conn);
if (err) {
@@ -410,7 +403,6 @@ init_idescriptor_device(const QString &udid, WirelessInitArgs wirelessArgs)
}
}
- // 2. Create default address handle
err = idevice_usbmuxd_default_addr_new(&addr_handle);
if (err) {
qDebug() << "Failed to create address handle";
@@ -422,16 +414,20 @@ init_idescriptor_device(const QString &udid, WirelessInitArgs wirelessArgs)
struct sockaddr_in addr_in;
memset(&addr_in, 0, sizeof(addr_in));
addr_in.sin_family = AF_INET;
- addr_in.sin_port = htons(0); // Port doesn't matter for provider
+ addr_in.sin_port = htons(0);
inet_pton(AF_INET, wirelessArgs.ip.toUtf8().constData(),
&addr_in.sin_addr);
- // IdevicePairingFile *pairing_file = nullptr;
- // idevice_pairing_file_read(
- // wirelessArgs.pairing_file.toUtf8().constData(), &pairing_file);
+ err = idevice_pairing_file_read(
+ wirelessArgs.pairing_file.toUtf8().constData(), &pairing_file);
+ if (err) {
+ qDebug() << "Failed to read pairing file";
+ goto cleanup;
+ }
+
err = idevice_tcp_provider_new(
(const idevice_sockaddr *)&addr_in,
- const_cast(wirelessArgs.pairing_file),
- APP_LABEL, &provider);
+ const_cast(pairing_file), APP_LABEL,
+ &provider);
if (err) {
qDebug() << "Failed to create wireless provider";
goto cleanup;
@@ -441,7 +437,8 @@ init_idescriptor_device(const QString &udid, WirelessInitArgs wirelessArgs)
qDebug() << "Failed to start Heartbeat service";
goto cleanup;
}
- heartbeatThread = new HeartBeatThread(heartbeat);
+ // udid is mac address here for wireless
+ heartbeatThread = new HeartbeatThread(heartbeat, udid);
heartbeatThread->start();
while (!heartbeatThread->initialCompleted()) {
@@ -539,16 +536,7 @@ init_idescriptor_device(const QString &udid, WirelessInitArgs wirelessArgs)
// goto cleanup;
// }
get_device_info_xml(udid.toUtf8().constData(), lockdown, infoXml);
- // infoXml.print(ss, " "); // " " for indentation
- // qDebug().noquote() << "--- Full Device Info XML ---"
- // << QString::fromStdString(ss.str());
- // Received plist: {
- // Domain: "com.apple.mobile.wireless_lockdown",
- // Key: "EnableWifiConnections",
- // Request: "GetValue",
- // Value: true
- // }
lockdownd_get_value(lockdown, "EnableWifiConnections",
"com.apple.mobile.wireless_lockdown", &val);
if (val)
@@ -564,10 +552,14 @@ init_idescriptor_device(const QString &udid, WirelessInitArgs wirelessArgs)
result.diagRelay = std::make_shared(
DiagnosticsRelay::adopt(diagnostics_relay));
result.locationSimulation = location_simulation;
- AppContext::sharedInstance()->cachePairingFile(udid, pairing_file);
+ // TODO cache pairing file path
result.deviceInfo.isWireless = isWireless;
fullDeviceInfo(infoXml, afc_client, result.diagRelay.get(), result);
-
+ ::QObject::connect(heartbeatThread, &HeartbeatThread::heartbeatFailed,
+ AppContext::sharedInstance(),
+ &AppContext::heartbeatFailed);
+ ::QObject::connect(heartbeatThread, &HeartbeatThread::heartbeatThreadExited,
+ AppContext::sharedInstance(), &AppContext::removeDevice);
cleanup:
// Cleanup on error
// FIXME: implement proper cleanup
diff --git a/src/devicemanagerwidget.cpp b/src/devicemanagerwidget.cpp
index 9a9accd..aedfe34 100644
--- a/src/devicemanagerwidget.cpp
+++ b/src/devicemanagerwidget.cpp
@@ -145,8 +145,9 @@ void DeviceManagerWidget::addDevice(iDescriptorDevice *device)
QString tabTitle = QString::fromStdString(device->deviceInfo.productType);
m_stackedWidget->addWidget(deviceWidget);
- m_deviceWidgets[device->udid] =
- std::pair{deviceWidget, m_sidebar->addDevice(tabTitle, device->udid)};
+ m_deviceWidgets[device->udid] = std::pair{
+ deviceWidget, m_sidebar->addDevice(tabTitle, device->udid,
+ device->deviceInfo.isWireless)};
}
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
diff --git a/src/devicesidebarwidget.cpp b/src/devicesidebarwidget.cpp
index d3afda0..028bd79 100644
--- a/src/devicesidebarwidget.cpp
+++ b/src/devicesidebarwidget.cpp
@@ -26,9 +26,10 @@
// DeviceSidebarItem Implementation
DeviceSidebarItem::DeviceSidebarItem(const QString &deviceName,
- const std::string &uuid, QWidget *parent)
+ const std::string &uuid, bool isWireless,
+ QWidget *parent)
: QFrame(parent), m_deviceName(deviceName), m_uuid(uuid), m_selected(false),
- m_collapsed(false)
+ m_wireless(isWireless), m_collapsed(false)
{
setupUI();
setFrameStyle(QFrame::StyledPanel);
@@ -51,10 +52,20 @@ void DeviceSidebarItem::setupUI()
[this]() { emit deviceSelected(m_uuid); });
// Device name label
+ QHBoxLayout *nameLayout = new QHBoxLayout();
+ nameLayout->setContentsMargins(0, 0, 0, 0);
m_deviceLabel = new QLabel(m_deviceName);
m_deviceLabel->setStyleSheet("QLabel { font-weight: bold; }");
m_deviceLabel->setWordWrap(true);
- headerLayout->addWidget(m_deviceLabel);
+ nameLayout->addWidget(m_deviceLabel);
+ if (m_wireless) {
+ auto wirelessIcon = new ZIconLabel(
+ QIcon(":/resources/icons/QlementineIconsWireless116.png"),
+ "Wireless", this);
+ nameLayout->setSpacing(5);
+ nameLayout->addWidget(wirelessIcon);
+ }
+ headerLayout->addLayout(nameLayout);
// Toggle button
m_toggleButton = new QPushButton();
@@ -292,9 +303,11 @@ DeviceSidebarWidget::DeviceSidebarWidget(QWidget *parent)
}
DeviceSidebarItem *DeviceSidebarWidget::addDevice(const QString &deviceName,
- const std::string &uuid)
+ const std::string &uuid,
+ bool isWireless)
{
- DeviceSidebarItem *item = new DeviceSidebarItem(deviceName, uuid, this);
+ DeviceSidebarItem *item =
+ new DeviceSidebarItem(deviceName, uuid, isWireless, this);
// Connect to unified handler
connect(item, &DeviceSidebarItem::deviceSelected, this,
diff --git a/src/devicesidebarwidget.h b/src/devicesidebarwidget.h
index fe3ade1..846faa3 100644
--- a/src/devicesidebarwidget.h
+++ b/src/devicesidebarwidget.h
@@ -36,7 +36,7 @@ class DeviceSidebarItem : public QFrame
public:
explicit DeviceSidebarItem(const QString &deviceName,
- const std::string &uuid,
+ const std::string &uuid, bool isWireless,
QWidget *parent = nullptr);
const std::string &getDeviceUuid() const;
@@ -67,6 +67,7 @@ private:
QWidget *m_optionsWidget;
QPushButton *m_toggleButton;
QLabel *m_deviceLabel;
+ bool m_wireless;
// Navigation buttons
QPushButton *m_infoButton;
@@ -164,7 +165,7 @@ public:
// Unified interface
DeviceSidebarItem *addDevice(const QString &deviceName,
- const std::string &uuid);
+ const std::string &uuid, bool isWireless);
DevicePendingSidebarItem *addPendingDevice(const QString &uuid);
RecoveryDeviceSidebarItem *addRecoveryDevice(uint64_t ecid);
diff --git a/src/exportmanager.cpp b/src/exportmanager.cpp
index adef72b..9422558 100644
--- a/src/exportmanager.cpp
+++ b/src/exportmanager.cpp
@@ -18,8 +18,9 @@
*/
#include "exportmanager.h"
-#include "exportprogressdialog.h"
#include "servicemanager.h"
+#include "statusballoon.h"
+#include
#include
#include
#include
@@ -32,29 +33,23 @@ ExportManager *ExportManager::sharedInstance()
static ExportManager self;
return &self;
}
+// TODO: unfinished
-ExportManager::ExportManager(QObject *parent) : QObject(parent)
-{
- // The singleton now creates and owns the dialog.
- // No parent is passed, so it's a top-level window.
- m_exportProgressDialog = new ExportProgressDialog(this, nullptr);
-}
+ExportManager::ExportManager(QObject *parent) : QObject(parent) {}
ExportManager::~ExportManager()
{
// Cancel all active jobs
QMutexLocker locker(&m_jobsMutex);
- for (auto jobPtr : m_activeJobs) {
- jobPtr->cancelRequested = true;
- if (jobPtr->watcher) {
- jobPtr->watcher->cancel();
- jobPtr->watcher->waitForFinished();
- }
- delete jobPtr;
- }
+ // for (auto jobPtr : m_activeJobs) {
+ // jobPtr->cancelRequested = true;
+ // if (jobPtr->watcher) {
+ // jobPtr->watcher->cancel();
+ // jobPtr->watcher->waitForFinished();
+ // }
+ // // delete jobPtr;
+ // }
m_activeJobs.clear();
-
- // The dialog will be deleted automatically due to parent-child relationship
}
QUuid ExportManager::startExport(iDescriptorDevice *device,
@@ -62,6 +57,8 @@ QUuid ExportManager::startExport(iDescriptorDevice *device,
const QString &destinationPath,
std::optional altAfc)
{
+ qDebug() << "startExport() entry - items:" << items.size()
+ << "dest:" << destinationPath;
if (!device || !device->mutex) {
qWarning() << "Invalid device provided to ExportManager";
return QUuid();
@@ -89,32 +86,30 @@ QUuid ExportManager::startExport(iDescriptorDevice *device,
job->items = items;
job->destinationPath = destinationPath;
job->altAfc = altAfc;
- job->watcher = new QFutureWatcher(this);
- const QUuid jobId = job->jobId;
+ // fixme : pass ExportJob
+ job->statusBalloonProcessId =
+ StatusBalloon::sharedInstance()->startExportProcess(
+ QString("Exporting %1 items").arg(items.size()), items.size(),
+ destinationPath);
- connect(job->watcher, &QFutureWatcher::finished, this,
- [this, jobId]() { cleanupJob(jobId); });
+ // Use ExportManager's own jobId for its internal tracking and signals
+ const QUuid managerJobId = job->jobId;
+
+ // todo:cleanupJob ?
+ // connect(job->watcher, &QFutureWatcher::finished, this,
+ // [this, managerJobId]() { cleanupJob(managerJobId); });
// Store job before starting
{
QMutexLocker locker(&m_jobsMutex);
- m_activeJobs[jobId] = job;
+ m_activeJobs[managerJobId] = job;
}
- emit exportStarted(jobId, items.size(), destinationPath);
-
- // The manager now shows its own dialog
- m_exportProgressDialog->showForJob(jobId);
-
- ExportJob *jobPtr = m_activeJobs[jobId];
- jobPtr->future =
- QtConcurrent::run([this, jobPtr]() { executeExportJob(jobPtr); });
- jobPtr->watcher->setFuture(jobPtr->future);
-
- qDebug() << "Started export job" << jobId << "for" << items.size()
+ m_exportThread->executeExportJob(job);
+ qDebug() << "Started export job" << managerJobId << "for" << items.size()
<< "items";
- return jobId;
+ return managerJobId;
}
void ExportManager::cancelExport(const QUuid &jobId)
@@ -139,198 +134,7 @@ bool ExportManager::isJobRunning(const QUuid &jobId) const
return m_activeJobs.contains(jobId);
}
-void ExportManager::executeExportJob(ExportJob *job)
-{
- ExportJobSummary summary;
- summary.jobId = job->jobId;
- summary.totalItems = job->items.size();
- summary.destinationPath = job->destinationPath;
-
- qDebug() << "Executing export job" << job->jobId << "with"
- << job->items.size() << "items";
-
- for (int i = 0; i < job->items.size(); ++i) {
- // Check for cancellation
- if (job->cancelRequested.load()) {
- summary.wasCancelled = true;
- qDebug() << "Export job" << job->jobId << "was cancelled";
- emit exportCancelled(job->jobId);
- return;
- }
-
- const ExportItem &item = job->items.at(i);
-
- emit exportProgress(job->jobId, i + 1, job->items.size(),
- item.suggestedFileName);
-
- ExportResult result =
- exportSingleItem(job->device, item, job->destinationPath,
- job->altAfc, job->cancelRequested, job->jobId);
-
- if (result.success) {
- summary.successfulItems++;
- summary.totalBytesTransferred += result.bytesTransferred;
- } else {
- summary.failedItems++;
- }
-
- emit itemExported(job->jobId, result);
-
- // Check for cancellation again after potentially long file operation
- if (job->cancelRequested.load()) {
- summary.wasCancelled = true;
- qDebug() << "Export job" << job->jobId
- << "was cancelled during execution";
- emit exportCancelled(job->jobId);
- return;
- }
- }
-
- qDebug() << "Export job" << job->jobId
- << "completed - Success:" << summary.successfulItems
- << "Failed:" << summary.failedItems
- << "Bytes:" << summary.totalBytesTransferred;
-
- emit exportFinished(job->jobId, summary);
-}
-// TODO: implement
-ExportResult ExportManager::exportSingleItem(
- iDescriptorDevice *device, const ExportItem &item,
- const QString &destinationDir, std::optional altAfc,
- std::atomic &cancelRequested, const QUuid &jobId)
-{
- ExportResult result;
- result.sourceFilePath = item.sourcePathOnDevice;
-
- // // Generate output path
- // QString outputPath =
- // QDir(destinationDir).filePath(item.suggestedFileName); outputPath =
- // generateUniqueOutputPath(outputPath); result.outputFilePath = outputPath;
-
- // // Get file size first
- // char **info = nullptr;
- // afc_error_t infoResult = ServiceManager::safeAfcGetFileInfo(
- // device, item.sourcePathOnDevice.toUtf8().constData(), &info, altAfc);
-
- // qint64 totalFileSize = 0;
- // if (infoResult == AFC_E_SUCCESS && info) {
- // for (int i = 0; info[i]; i += 2) {
- // if (strcmp(info[i], "st_size") == 0) {
- // totalFileSize = QString::fromUtf8(info[i + 1]).toLongLong();
- // break;
- // }
- // }
- // afc_dictionary_free(info);
- // }
-
- // // Open file on device
- // uint64_t handle = 0;
- // afc_error_t openResult = ServiceManager::safeAfcFileOpen(
- // device, item.sourcePathOnDevice.toUtf8().constData(),
- // AFC_FOPEN_RDONLY, &handle, altAfc);
-
- // if (openResult != AFC_E_SUCCESS) {
- // result.errorMessage =
- // QString("Failed to open file on device: %1 (AFC error: %2)")
- // .arg(item.sourcePathOnDevice)
- // .arg(static_cast(openResult));
- // return result;
- // }
-
- // // Open local output file
- // QFile outputFile(outputPath);
- // if (!outputFile.open(QIODevice::WriteOnly)) {
- // result.errorMessage = QString("Failed to create local file: %1 (%2)")
- // .arg(outputPath)
- // .arg(outputFile.errorString());
- // ServiceManager::safeAfcFileClose(device, handle, altAfc);
- // return result;
- // }
-
- // char buffer[8192];
- // uint32_t bytesRead = 0;
- // qint64 totalBytes = 0;
-
- // while (true) {
- // // Check for cancellation during file copy
- // if (cancelRequested.load()) {
- // outputFile.close();
- // outputFile.remove(); // Clean up partial file
- // ServiceManager::safeAfcFileClose(device, handle, altAfc);
- // result.errorMessage = "Export cancelled by user";
- // return result;
- // }
-
- // afc_error_t readResult = ServiceManager::safeAfcFileRead(
- // device, handle, buffer, sizeof(buffer), &bytesRead, altAfc);
-
- // if (readResult != AFC_E_SUCCESS || bytesRead == 0) {
- // break; // End of file or error
- // }
-
- // qint64 bytesWritten = outputFile.write(buffer, bytesRead);
- // if (bytesWritten != bytesRead) {
- // result.errorMessage =
- // QString("Write error: only wrote %1 of %2 bytes")
- // .arg(bytesWritten)
- // .arg(bytesRead);
- // outputFile.close();
- // outputFile.remove(); // Clean up partial file
- // ServiceManager::safeAfcFileClose(device, handle, altAfc);
- // return result;
- // }
-
- // totalBytes += bytesRead;
-
- // // Emit progress update every 64KB or at end of file
- // if (totalBytes % (64 * 1024) == 0 || totalBytes == totalFileSize) {
- // emit fileTransferProgress(jobId, item.suggestedFileName,
- // totalBytes,
- // totalFileSize);
- // }
- // }
-
- // // Clean up
- // outputFile.close();
- // ServiceManager::safeAfcFileClose(device, handle, altAfc);
-
- // if (totalBytes == 0) {
- // result.errorMessage = "No data read from device file";
- // outputFile.remove(); // Clean up empty file
- // return result;
- // }
-
- result.success = true;
- // result.bytesTransferred = totalBytes;
- return result;
-}
-
-QString ExportManager::generateUniqueOutputPath(const QString &basePath) const
-{
- if (!QFile::exists(basePath)) {
- return basePath;
- }
-
- QFileInfo fileInfo(basePath);
- QString baseName = fileInfo.completeBaseName();
- QString suffix = fileInfo.suffix();
- QString directory = fileInfo.absolutePath();
-
- int counter = 1;
- QString uniquePath;
-
- do {
- QString newName = QString("%1_%2").arg(baseName).arg(counter);
- if (!suffix.isEmpty()) {
- newName += "." + suffix;
- }
- uniquePath = QDir(directory).filePath(newName);
- counter++;
- } while (QFile::exists(uniquePath) && counter < 10000);
-
- return uniquePath;
-}
-
+// TODO: is not being used ?
QString ExportManager::extractFileName(const QString &devicePath) const
{
int lastSlash = devicePath.lastIndexOf('/');
@@ -344,13 +148,13 @@ void ExportManager::cleanupJob(const QUuid &jobId)
{
QMutexLocker locker(&m_jobsMutex);
auto it = m_activeJobs.find(jobId);
- if (it != m_activeJobs.end()) {
- if (it.value()->watcher) {
- it.value()->watcher->deleteLater();
- }
+ // if (it != m_activeJobs.end()) {
+ // if (it.value()->watcher) {
+ // it.value()->watcher->deleteLater();
+ // }
- delete it.value();
- m_activeJobs.erase(it);
- qDebug() << "Cleaned up export job" << jobId;
- }
-}
+ // // delete it.value();
+ // m_activeJobs.erase(it);
+ // qDebug() << "Cleaned up export job" << jobId;
+ // }
+}
\ No newline at end of file
diff --git a/src/exportmanager.h b/src/exportmanager.h
index 7594a18..4926435 100644
--- a/src/exportmanager.h
+++ b/src/exportmanager.h
@@ -20,6 +20,7 @@
#ifndef EXPORTMANAGER_H
#define EXPORTMANAGER_H
+#include "exportmanagerthread.h"
#include "iDescriptor.h"
#include
#include
@@ -35,35 +36,6 @@
// Forward declaration
class ExportProgressDialog;
-struct ExportItem {
- QString sourcePathOnDevice;
- QString suggestedFileName;
-
- ExportItem() = default;
- ExportItem(const QString &sourcePath, const QString &fileName)
- : sourcePathOnDevice(sourcePath), suggestedFileName(fileName)
- {
- }
-};
-
-struct ExportResult {
- QString sourceFilePath;
- QString outputFilePath;
- bool success = false;
- QString errorMessage;
- qint64 bytesTransferred = 0;
-};
-
-struct ExportJobSummary {
- QUuid jobId;
- int totalItems = 0;
- int successfulItems = 0;
- int failedItems = 0;
- qint64 totalBytesTransferred = 0;
- QString destinationPath;
- bool wasCancelled = false;
-};
-
class ExportManager : public QObject
{
Q_OBJECT
@@ -85,7 +57,10 @@ public:
bool isExporting() const;
bool isJobRunning(const QUuid &jobId) const;
+ static QString generateUniqueOutputPath(const QString &basePath);
+ // todo: should we delete this in ~ExportManager?
+ ExportManagerThread *m_exportThread = new ExportManagerThread(this);
signals:
void exportStarted(const QUuid &jobId, int totalItems,
@@ -108,28 +83,8 @@ private:
explicit ExportManager(QObject *parent = nullptr);
~ExportManager();
- struct ExportJob {
- QUuid jobId;
- iDescriptorDevice *device = nullptr;
- QList items;
- QString destinationPath;
- std::optional altAfc;
- std::atomic cancelRequested{false};
- QFuture future;
- QFutureWatcher *watcher = nullptr;
- };
-
void executeExportJob(ExportJob *job);
- ExportResult exportSingleItem(iDescriptorDevice *device,
- const ExportItem &item,
- const QString &destinationDir,
- std::optional altAfc,
- std::atomic &cancelRequested,
- const QUuid &jobId);
-
- QString generateUniqueOutputPath(const QString &basePath) const;
-
QString extractFileName(const QString &devicePath) const;
void cleanupJob(const QUuid &jobId);
diff --git a/src/exportmanagerthread.cpp b/src/exportmanagerthread.cpp
new file mode 100644
index 0000000..278f709
--- /dev/null
+++ b/src/exportmanagerthread.cpp
@@ -0,0 +1,166 @@
+
+#include "exportmanagerthread.h"
+#include "iDescriptor.h"
+#include "servicemanager.h"
+#include
+#include
+#include
+#include
+
+// TODO: unfinished
+void ExportManagerThread::executeExportJob(ExportJob *job)
+{
+ // FIXME: limit to 1 at a time
+ QtConcurrent::run([this, job]() { executeExportJobInternal(job); });
+}
+
+void ExportManagerThread::executeExportJobInternal(ExportJob *job)
+{
+ qDebug() << "Worker thread started for export job" << job->jobId;
+ ExportJobSummary summary;
+ summary.jobId = job->jobId;
+ summary.totalItems = job->items.size();
+ summary.destinationPath = job->destinationPath;
+
+ qDebug() << "Executing export job" << job->jobId << "with"
+ << job->items.size() << "items";
+
+ for (int i = 0; i < job->items.size(); ++i) {
+ // todo:Check for cancellation
+ // if (job->cancelRequested.load() ||
+ // balloon->isCancelRequested(
+ // job->statusBalloonProcessId)) { // Use
+ // // statusBalloonProcessId
+ // summary.wasCancelled = true;
+ // qDebug() << "Export job" << job->jobId << "was cancelled";
+
+ // emit exportCancelled(job->jobId);
+ // return;
+ // }
+
+ const ExportItem &item = job->items.at(i);
+
+ // emit exportProgress(job->jobId, i + 1, job->items.size(),
+ // item.suggestedFileName);
+
+ ExportResult result = exportSingleItem(
+ job->device, item, job->destinationPath, job->altAfc,
+ job->cancelRequested, job->statusBalloonProcessId);
+ if (result.success) {
+ summary.successfulItems++;
+ summary.totalBytesTransferred += result.bytesTransferred;
+ } else {
+ summary.failedItems++;
+ }
+
+ emit itemExported(job->jobId, result);
+
+ // // Check for cancellation again after potentially long file
+ // // operation
+ // if (job->cancelRequested.load() ||
+ // balloon->isCancelRequested(
+ // job->statusBalloonProcessId)) { // Use
+ // // statusBalloonProcessId
+ // summary.wasCancelled = true;
+ // qDebug() << "Export job" << job->jobId
+ // << "was cancelled during execution";
+
+ // QMetaObject::invokeMethod(
+ // QCoreApplication::instance(),
+ // [balloon,2
+ // id =
+ // job->statusBalloonProcessId]() { // Use
+ // //
+ // statusBalloonProcessId
+ // balloon->markProcessCancelled(id);
+ // },
+ // Qt::QueuedConnection);
+
+ // emit exportCancelled(job->jobId);
+ // return;
+ // }
+ }
+
+ qDebug() << "Export job" << job->jobId
+ << "completed - Success:" << summary.successfulItems
+ << "Failed:" << summary.failedItems
+ << "Bytes:" << summary.totalBytesTransferred;
+
+ emit exportFinished(job->jobId, summary);
+}
+
+ExportResult ExportManagerThread::exportSingleItem(
+ iDescriptorDevice *device, const ExportItem &item,
+ const QString &destinationDir, std::optional altAfc,
+ std::atomic &cancelRequested,
+ const QUuid &statusBalloonProcessId) // Change parameter name and type
+{
+ ExportResult result;
+ result.sourceFilePath = item.sourcePathOnDevice;
+
+ // Generate output path
+ QString outputPath = QDir(destinationDir).filePath(item.suggestedFileName);
+ // todo problem
+ outputPath = generateUniqueOutputPath(outputPath);
+ result.outputFilePath = outputPath;
+
+ // Progress callback
+ const QString ¤tFile = item.suggestedFileName;
+ int fileIndex = item.itemIndex;
+ auto progressCallback =
+ [this, statusBalloonProcessId, fileIndex,
+ currentFile](qint64 transferred, // Use statusBalloonProcessId
+ qint64 total) {
+ qDebug() << "Export progress callback for" << fileIndex
+ << "- transferred:" << transferred << "total:" << total;
+ emit fileTransferProgress(statusBalloonProcessId, fileIndex,
+ currentFile, transferred, total);
+ };
+
+ qDebug() << "About to export file from device:" << item.sourcePathOnDevice
+ << "to" << outputPath;
+ // Export file using ServiceManager
+ IdeviceFfiError *err = ServiceManager::exportFileToPath(
+ device, item.sourcePathOnDevice.toUtf8().constData(),
+ outputPath.toUtf8().constData(), progressCallback, &cancelRequested);
+
+ if (err != nullptr) {
+ result.errorMessage =
+ QString("Failed to export file: %1").arg(err->message);
+ qDebug() << result.errorMessage;
+ idevice_error_free(err);
+ return result;
+ }
+
+ // Get file size for statistics
+ QFileInfo fileInfo(outputPath);
+ result.bytesTransferred = fileInfo.size();
+ result.success = true;
+
+ return result;
+}
+QString ExportManagerThread::generateUniqueOutputPath(const QString &basePath)
+{
+ if (!QFile::exists(basePath)) {
+ return basePath;
+ }
+
+ QFileInfo fileInfo(basePath);
+ QString baseName = fileInfo.completeBaseName();
+ QString suffix = fileInfo.suffix();
+ QString directory = fileInfo.absolutePath();
+
+ int counter = 1;
+ QString uniquePath;
+
+ do {
+ QString newName = QString("%1_%2").arg(baseName).arg(counter);
+ if (!suffix.isEmpty()) {
+ newName += "." + suffix;
+ }
+ uniquePath = QDir(directory).filePath(newName);
+ counter++;
+ } while (QFile::exists(uniquePath) && counter < 10000);
+
+ return uniquePath;
+}
\ No newline at end of file
diff --git a/src/exportmanagerthread.h b/src/exportmanagerthread.h
new file mode 100644
index 0000000..4e3c5b4
--- /dev/null
+++ b/src/exportmanagerthread.h
@@ -0,0 +1,40 @@
+#ifndef EXPORTMANAGERTHREAD_H
+#define EXPORTMANAGERTHREAD_H
+#include "iDescriptor.h"
+#include "servicemanager.h"
+#include
+#include
+#include
+
+class ExportManager;
+
+using namespace IdeviceFFI;
+
+class ExportManagerThread : public QObject
+{
+ Q_OBJECT
+public:
+ ExportManagerThread(QObject *parent = nullptr) : QObject(parent) {}
+
+ void executeExportJob(ExportJob *job);
+ ExportResult exportSingleItem(iDescriptorDevice *device,
+ const ExportItem &item,
+ const QString &destinationDir,
+ std::optional altAfc,
+ std::atomic &cancelRequested,
+ const QUuid &statusBalloonProcessId);
+
+private:
+ void executeExportJobInternal(ExportJob *job);
+ QString generateUniqueOutputPath(const QString &basePath);
+signals:
+ void exportProgress(const QUuid &jobId, int currentItem, int totalItems,
+ const QString ¤tFileName);
+ void fileTransferProgress(const QUuid &jobId, int fileIndex,
+ const QString ¤tFile,
+ qint64 bytesTransferred, qint64 totalFileSize);
+ void itemExported(const QUuid &jobId, const ExportResult &result);
+ void exportFinished(const QUuid &jobId, const ExportJobSummary &summary);
+ void exportCancelled(const QUuid &jobId);
+};
+#endif // EXPORTMANAGERTHREAD_H
diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp
index f0759ee..c855088 100644
--- a/src/gallerywidget.cpp
+++ b/src/gallerywidget.cpp
@@ -18,7 +18,7 @@
*/
#include "gallerywidget.h"
-// #include "exportmanager.h"
+#include "exportmanager.h"
#include "iDescriptor.h"
#include "mediapreviewdialog.h"
#include "photomodel.h"
@@ -42,6 +42,7 @@
#include
#include
+// todo: dont load paths on main thread, handle
/*
FIXME: this needs to be refactored once we
figure out how to query Photos.sqlite
@@ -85,7 +86,7 @@ GalleryWidget::GalleryWidget(iDescriptorDevice *device, QWidget *parent)
m_retryButton = new QPushButton("Retry", this);
connect(m_retryButton, &QPushButton::clicked, this, [this]() {
m_stackedWidget->setCurrentWidget(m_loadingWidget);
- QTimer::singleShot(100, this, &GalleryWidget::loadAlbumList);
+ QTimer::singleShot(100, this, &GalleryWidget::reload);
});
errorLayout->addWidget(m_retryButton, 0, Qt::AlignCenter);
m_errorWidget = new QWidget();
@@ -95,6 +96,13 @@ GalleryWidget::GalleryWidget(iDescriptorDevice *device, QWidget *parent)
m_stackedWidget->setCurrentWidget(m_loadingWidget);
setControlsEnabled(false); // Disable controls until album is selected
}
+
+void GalleryWidget::reload()
+{
+ m_loaded = false;
+ load();
+}
+
/*Load is called when the tab is active*/
void GalleryWidget::load()
{
@@ -102,8 +110,16 @@ void GalleryWidget::load()
return;
m_loaded = true;
+ qDebug() << "Before reading DCIM directory";
- loadAlbumList();
+ auto *watcher = new QFutureWatcher(this);
+ auto future = ServiceManager::getFileTreeAsync(m_device, "/DCIM", true);
+ watcher->setFuture(future);
+
+ connect(watcher, &QFutureWatcher::finished, [this, watcher]() {
+ watcher->deleteLater();
+ loadAlbumList(watcher->result());
+ });
}
void GalleryWidget::setupControlsLayout()
@@ -112,6 +128,8 @@ void GalleryWidget::setupControlsLayout()
m_controlsLayout->setSpacing(5);
m_controlsLayout->setContentsMargins(7, 7, 7, 7);
+ m_importButton = new QPushButton("Import");
+
// Sort order combo box
QLabel *sortLabel = new QLabel("Sort:");
sortLabel->setStyleSheet("font-weight: bold;");
@@ -128,13 +146,13 @@ void GalleryWidget::setupControlsLayout()
QLabel *filterLabel = new QLabel("Filter:");
filterLabel->setStyleSheet("font-weight: bold;");
m_filterComboBox = new QComboBox();
- m_filterComboBox->addItem("All Media",
- static_cast(PhotoModel::ImagesOnly));
+ m_filterComboBox->addItem("All Media", static_cast(PhotoModel::All));
m_filterComboBox->addItem("Images Only",
static_cast(PhotoModel::ImagesOnly));
m_filterComboBox->addItem("Videos Only",
static_cast(PhotoModel::VideosOnly));
- m_filterComboBox->setCurrentIndex(2); // Default to All
+ m_filterComboBox->setCurrentIndex(
+ static_cast(PhotoModel::All)); // Default to All
m_filterComboBox->setMinimumWidth(100); // Ensure text fits
m_filterComboBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
@@ -164,6 +182,7 @@ void GalleryWidget::setupControlsLayout()
// Add widgets to layout
m_controlsLayout->addWidget(m_backButton);
+ m_controlsLayout->addWidget(m_importButton);
m_controlsLayout->addWidget(sortLabel);
m_controlsLayout->addWidget(m_sortComboBox);
m_controlsLayout->addWidget(filterLabel);
@@ -222,39 +241,42 @@ void GalleryWidget::onExportSelected()
return;
}
- // if (ExportManager::sharedInstance()->isExporting()) {
- // QMessageBox::information(this, "Export in Progress",
- // "An export is already in progress.");
- // return;
- // }
+ if (ExportManager::sharedInstance()->isExporting()) {
+ QMessageBox::information(this, "Export in Progress",
+ "An export is already in progress.");
+ return;
+ }
- // QModelIndexList selectedIndexes =
- // m_listView->selectionModel()->selectedIndexes();
- // QStringList filePaths = m_model->getSelectedFilePaths(selectedIndexes);
+ QModelIndexList selectedIndexes =
+ m_listView->selectionModel()->selectedIndexes();
+ QStringList filePaths = m_model->getSelectedFilePaths(selectedIndexes);
- // if (filePaths.isEmpty()) {
- // QMessageBox::information(this, "No Items",
- // "No valid items selected for export.");
- // return;
- // }
+ if (filePaths.isEmpty()) {
+ QMessageBox::information(this, "No Items",
+ "No valid items selected for export.");
+ return;
+ }
- // QString exportDir = selectExportDirectory();
- // if (exportDir.isEmpty()) {
- // return;
- // }
+ QString exportDir = selectExportDirectory();
+ if (exportDir.isEmpty()) {
+ return;
+ }
- // // Convert QStringList to QList
- // QList exportItems;
- // for (const QString &filePath : filePaths) {
- // QString fileName = filePath.split('/').last();
- // exportItems.append(ExportItem(filePath, fileName));
- // }
+ // Convert QStringList to QList
+ QList exportItems;
+ // FIXME: index
+ int index = 0;
+ for (const QString &filePath : filePaths) {
+ QString fileName = filePath.split('/').last();
+ exportItems.append(ExportItem(filePath, fileName, index));
+ ++index;
+ }
- // qDebug() << "Starting export of selected files:" << exportItems.size()
- // << "items to" << exportDir;
+ qDebug() << "Starting export of selected files:" << exportItems.size()
+ << "items to" << exportDir;
- // ExportManager::sharedInstance()->startExport(m_device, exportItems,
- // exportDir);
+ ExportManager::sharedInstance()->startExport(m_device, exportItems,
+ exportDir);
}
void GalleryWidget::onExportAll()
@@ -419,12 +441,8 @@ void GalleryWidget::setupPhotoGalleryView()
&GalleryWidget::onPhotoContextMenu);
}
-void GalleryWidget::loadAlbumList()
+void GalleryWidget::loadAlbumList(const AFCFileTree &dcimTree)
{
- qDebug() << "Before reading DCIM directory";
- AFCFileTree dcimTree =
- ServiceManager::safeGetFileTree(m_device, "/DCIM", true);
-
if (!dcimTree.success) {
qDebug() << "Failed to read DCIM directory";
m_stackedWidget->setCurrentWidget(m_errorWidget);
@@ -433,8 +451,6 @@ void GalleryWidget::loadAlbumList()
return;
}
- m_stackedWidget->setCurrentWidget(m_albumSelectionWidget);
-
qDebug() << "DCIM directory read successfully, found"
<< dcimTree.entries.size() << "entries";
@@ -464,6 +480,7 @@ void GalleryWidget::loadAlbumList()
}
m_albumListView->setModel(albumModel);
+ m_stackedWidget->setCurrentWidget(m_albumSelectionWidget);
}
void GalleryWidget::onAlbumSelected(const QString &albumPath)
@@ -484,6 +501,8 @@ void GalleryWidget::onAlbumSelected(const QString &albumPath)
});
}
+ connect(m_model, &PhotoModel::thumbnailNeedsToBeLoaded, m_model,
+ &PhotoModel::requestThumbnail, Qt::QueuedConnection);
// Set album path and load photos
m_model->setAlbumPath(albumPath);
@@ -497,9 +516,16 @@ void GalleryWidget::onAlbumSelected(const QString &albumPath)
void GalleryWidget::onBackToAlbums()
{
+ if (m_model) {
+ disconnect(m_model, &PhotoModel::thumbnailNeedsToBeLoaded, m_model,
+ &PhotoModel::requestThumbnail);
+ }
+
// Switch back to album selection view
m_stackedWidget->setCurrentWidget(m_albumSelectionWidget);
- m_model->clear();
+ if (m_model) {
+ m_model->clear();
+ }
// Disable controls and hide back button
setControlsEnabled(false);
@@ -566,6 +592,7 @@ QIcon GalleryWidget::loadAlbumThumbnail(const QString &albumPath)
if (firstImagePath.endsWith(".HEIC", Qt::CaseInsensitive)) {
qDebug() << "Loading HEIC thumbnail from:" << firstImagePath;
+ // FIXME: move to servicemanager
thumbnail = load_heic(imageData);
} else {
// Load regular image formats
diff --git a/src/gallerywidget.h b/src/gallerywidget.h
index d8073e0..d1976ee 100644
--- a/src/gallerywidget.h
+++ b/src/gallerywidget.h
@@ -58,10 +58,11 @@ private slots:
void onBackToAlbums();
private:
+ void reload();
void setupControlsLayout();
void setupAlbumSelectionView();
void setupPhotoGalleryView();
- void loadAlbumList();
+ void loadAlbumList(const AFCFileTree &dcimTree);
void setControlsEnabled(bool enabled);
QString selectExportDirectory();
QIcon loadAlbumThumbnail(const QString &albumPath);
@@ -80,7 +81,7 @@ private:
ZLoadingWidget *m_loadingWidget;
QWidget *m_errorWidget;
QPushButton *m_retryButton;
-
+ QPushButton *m_importButton;
// Album selection view
QWidget *m_albumSelectionWidget;
QListView *m_albumListView;
diff --git a/src/heartbeat.h b/src/heartbeat.h
index 7026d4b..5874a76 100644
--- a/src/heartbeat.h
+++ b/src/heartbeat.h
@@ -1,26 +1,19 @@
#ifndef HEARTBEATTHREAD_H
#define HEARTBEATTHREAD_H
+#include "iDescriptor.h"
#include
#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
using namespace IdeviceFFI;
-class HeartBeatThread : public QThread
+class HeartbeatThread : public QThread
{
Q_OBJECT
public:
- HeartBeatThread(HeartbeatClientHandle *heartbeat, QObject *parent = nullptr)
- : QThread(parent), m_hb(Heartbeat::adopt(heartbeat))
+ HeartbeatThread(HeartbeatClientHandle *heartbeat, QString macAddress,
+ QObject *parent = nullptr)
+ : QThread(parent), m_hb(Heartbeat::adopt(heartbeat)),
+ m_macAddress(macAddress)
{
}
@@ -28,7 +21,6 @@ public:
{
qDebug() << "Heartbeat thread started";
try {
- // Start with initial interval (15 seconds as per the tool example)
u_int64_t interval = 15;
while (!isInterruptionRequested()) {
@@ -38,7 +30,19 @@ public:
qDebug()
<< "Failed to get marco:"
<< QString::fromStdString(result.unwrap_err().message);
- break;
+ m_tries++;
+ emit heartbeatFailed(m_macAddress, m_tries);
+ if (m_tries >= HEARTBEAT_RETRY_LIMIT) {
+ qDebug()
+ << "Maximum heartbeat retries reached, exiting for "
+ "device"
+ << m_macAddress;
+ emit heartbeatThreadExited(m_macAddress);
+ break;
+ }
+ // If get_marco failed, skip the rest of this iteration
+ // and try again with the current interval.
+ continue;
}
// 2. Get the new interval from device
@@ -51,15 +55,31 @@ public:
qDebug() << "Failed to send polo:"
<< QString::fromStdString(
polo_result.unwrap_err().message);
- break;
+ m_tries++;
+ emit heartbeatFailed(m_macAddress, m_tries);
+ if (m_tries >= HEARTBEAT_RETRY_LIMIT) {
+ qDebug() << "Maximum heartbeat retries reached, "
+ "exiting for "
+ "device"
+ << m_macAddress;
+ emit heartbeatThreadExited(m_macAddress);
+ break;
+ }
+ // If send_polo failed, skip the rest of this iteration
+ // and try again with the current interval.
+ continue;
}
- qDebug() << "Sent polo successfully";
- interval += 5;
- m_initialCompleted = true;
+ // If both marco and polo succeeded:
+ qDebug() << "Sent polo successfully";
+ interval += 5; // Increment interval for the next cycle
+ m_initialCompleted = true; // Mark as initially completed after
+ // first successful full cycle
}
} catch (const std::exception &e) {
qDebug() << "Heartbeat error:" << e.what();
+
+ emit heartbeatThreadExited(m_macAddress);
}
}
@@ -68,5 +88,11 @@ public:
private:
Heartbeat m_hb;
bool m_initialCompleted = false;
+ QString m_macAddress;
+ unsigned int m_tries = 0;
+
+signals:
+ void heartbeatFailed(const QString &macAddress, unsigned int tries = 0);
+ void heartbeatThreadExited(const QString &macAddress);
};
#endif // HEARTBEATTHREAD_H
\ No newline at end of file
diff --git a/src/iDescriptor.h b/src/iDescriptor.h
index 9409e02..be62981 100644
--- a/src/iDescriptor.h
+++ b/src/iDescriptor.h
@@ -32,6 +32,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -70,6 +71,16 @@
#define NotFoundErrorCode -14
#define DISK_IMAGE_TYPE_DEVELOPER "Developer"
+#define HEARTBEAT_RETRY_LIMIT 2
+
+#ifdef __linux__
+#define LOCKDOWN_PATH "/var/lib/lockdown"
+#elif __APPLE__
+#define LOCKDOWN_PATH "/var/db/lockdown"
+#else
+#define LOCKDOWN_PATH ""
+#endif
+
struct BatteryInfo {
QString health;
uint64_t cycleCount;
@@ -412,12 +423,12 @@ void get_device_info_xml(const char *udid, LockdowndClientHandle *client,
pugi::xml_document &infoXml);
struct WirelessInitArgs {
- QString ip;
- const IdevicePairingFile *pairing_file;
+ const QString ip;
+ const QString pairing_file;
};
iDescriptorInitDeviceResult
init_idescriptor_device(const QString &udid,
- WirelessInitArgs wirelessArgs = {nullptr, nullptr});
+ const WirelessInitArgs &wirelessArgs = {"", ""});
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// iDescriptorInitDeviceResultRecovery
@@ -669,4 +680,45 @@ inline int read_file(const char *filename, uint8_t **data, size_t *length)
fclose(file);
return 1;
-}
\ No newline at end of file
+}
+
+struct ExportItem {
+ QString sourcePathOnDevice;
+ QString suggestedFileName;
+ int itemIndex = -1;
+
+ ExportItem() = default;
+ ExportItem(const QString &sourcePath, const QString &fileName, int index)
+ : sourcePathOnDevice(sourcePath), suggestedFileName(fileName),
+ itemIndex(index)
+ {
+ }
+};
+
+struct ExportResult {
+ QString sourceFilePath;
+ QString outputFilePath;
+ bool success = false;
+ QString errorMessage;
+ qint64 bytesTransferred = 0;
+};
+
+struct ExportJobSummary {
+ QUuid jobId;
+ int totalItems = 0;
+ int successfulItems = 0;
+ int failedItems = 0;
+ qint64 totalBytesTransferred = 0;
+ QString destinationPath;
+ bool wasCancelled = false;
+};
+
+struct ExportJob {
+ QUuid jobId;
+ iDescriptorDevice *device = nullptr;
+ QList items;
+ QString destinationPath;
+ std::optional altAfc;
+ std::atomic cancelRequested{false};
+ QUuid statusBalloonProcessId;
+};
\ No newline at end of file
diff --git a/src/livescreenwidget.cpp b/src/livescreenwidget.cpp
index 16870c4..30f6aee 100644
--- a/src/livescreenwidget.cpp
+++ b/src/livescreenwidget.cpp
@@ -228,6 +228,7 @@ void LiveScreenWidget::updateScreenshot()
// return;
// }
qDebug() << "Updating screenshot...";
+ // FIXME: move to services
try {
// TakeScreenshotResult result = take_screenshot(m_shotrClient);
ScreenshotData screenshot;
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 7c876a7..f2608a0 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -44,8 +44,10 @@
#include "appcontext.h"
#include "settingsmanager.h"
// #include "devicemonitor.h"
+#include "Toast.h"
#include "networkdevicemanager.h"
#include "networkdeviceswidget.h"
+#include "statusballoon.h"
#include
#include
#include
@@ -205,6 +207,12 @@ MainWindow::MainWindow(QWidget *parent)
"QLabel:hover { background-color : #13131319; }");
ui->statusbar->addWidget(m_connectedDeviceCountLabel);
+ // TODO: implement downloads/uploads progress stuff
+
+ StatusBalloon *statusBalloon = StatusBalloon::sharedInstance();
+
+ ui->statusbar->addWidget(statusBalloon->getButton());
+
ui->statusbar->setContentsMargins(0, 0, 0, 0);
QLabel *appVersionLabel = new QLabel(QString("v%1").arg(APP_VERSION));
appVersionLabel->setContentsMargins(5, 0, 5, 0);
@@ -410,33 +418,16 @@ MainWindow::MainWindow(QWidget *parent)
connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this,
[](const std::string &udid, const std::string &wifiMacAddress) {
- const IdevicePairingFile *pairingFile =
- AppContext::sharedInstance()->getCachedPairingFile(
- QString::fromStdString(udid));
-
- if (pairingFile) {
- // qDebug() << "Device removed, pairing file for UDID"
- // << QString::fromStdString(udid) << "MAC"
- // << QString::fromStdString(wifiMacAddress)
- // << "exists in cache.";
- // try to upgrade device to wireless if possible
- qDebug()
- << "Upgrading device to wireless connection for UDID"
- << QString::fromStdString(udid);
- QMetaObject::invokeMethod(
- AppContext::sharedInstance(), "addDevice",
- Qt::QueuedConnection,
- Q_ARG(QString, QString::fromStdString(udid)),
- Q_ARG(DeviceMonitorThread::IdeviceConnectionType,
- DeviceMonitorThread::CONNECTION_NETWORK),
- Q_ARG(AddType, AddType::UpgradeToWireless),
- Q_ARG(QString, QString::fromStdString(wifiMacAddress)));
-
- } else {
- qDebug()
- << "Device removed, no cached pairing file for UDID"
- << QString::fromStdString(udid);
- }
+ qDebug() << "Upgrading device to wireless connection for UDID"
+ << QString::fromStdString(udid);
+ QMetaObject::invokeMethod(
+ AppContext::sharedInstance(), "addDevice",
+ Qt::QueuedConnection,
+ Q_ARG(QString, QString::fromStdString(udid)),
+ Q_ARG(DeviceMonitorThread::IdeviceConnectionType,
+ DeviceMonitorThread::CONNECTION_NETWORK),
+ Q_ARG(AddType, AddType::UpgradeToWireless),
+ Q_ARG(QString, QString::fromStdString(wifiMacAddress)));
});
connect(NetworkDeviceManager::sharedInstance(),
@@ -462,18 +453,6 @@ MainWindow::MainWindow(QWidget *parent)
<< device.macAddress;
return;
}
-
- const IdevicePairingFile *pairingFile =
- AppContext::sharedInstance()->getCachedPairingFile(
- device.macAddress);
-
- if (!pairingFile) {
- qDebug() << "No cached pairing file for network device MAC:"
- << device.macAddress
- << "Cannot add as wireless device.";
- return;
- }
-
qDebug() << "Trying to add network device with MAC:"
<< device.macAddress;
@@ -488,6 +467,21 @@ MainWindow::MainWindow(QWidget *parent)
// Handle network device addition if needed
});
+ connect(AppContext::sharedInstance(), &AppContext::deviceHeartbeatFailed,
+ this, [this](const QString &macAddress, int tries) {
+ Toast *toast = new Toast(this);
+ toast->setAttribute(Qt::WA_DeleteOnClose);
+ toast->setDuration(8000); // Hide after 8 seconds
+ toast->setTitle("Heartbeat failed");
+ toast->setText(
+ QString("Heartbeat failed for device with MAC %1. "
+ "Number of failed attempts: %2")
+ .arg(macAddress)
+ .arg(tries));
+ toast->setPosition(ToastPosition::BOTTOM_MIDDLE);
+ toast->show();
+ });
+
// NetworkDevicesWidget *m_networkDevicesWidget = new
// NetworkDevicesWidget();
// m_networkDevicesWidget->setAttribute(Qt::WA_DeleteOnClose);
diff --git a/src/photomodel.cpp b/src/photomodel.cpp
index 74dfed4..f5304f9 100644
--- a/src/photomodel.cpp
+++ b/src/photomodel.cpp
@@ -40,9 +40,8 @@ extern "C" {
#include
#include
}
+// todo implement std::priority_queue with thread pool
-// Limit concurrent video thumbnail generation to 2 to prevent resource
-// exhaustion
QSemaphore PhotoModel::m_videoThumbnailSemaphore(4);
PhotoModel::PhotoModel(iDescriptorDevice *device, FilterType filterType,
@@ -59,17 +58,27 @@ PhotoModel::PhotoModel(iDescriptorDevice *device, FilterType filterType,
void PhotoModel::clear()
{
+ blockSignals(true);
+
// Clean up any active watchers
for (auto *watcher : m_activeLoaders.values()) {
if (watcher) {
+ watcher->disconnect();
watcher->cancel();
- watcher->waitForFinished();
+ // watcher->waitForFinished();
watcher->deleteLater();
}
}
m_activeLoaders.clear();
m_loadingPaths.clear();
m_thumbnailCache.clear();
+
+ beginResetModel();
+ m_photos.clear();
+ m_allPhotos.clear();
+ endResetModel();
+
+ blockSignals(false);
}
PhotoModel::~PhotoModel()
@@ -448,18 +457,15 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const
return info.filePath;
case Qt::DecorationRole: {
- qDebug() << "DecorationRole requested for index:" << index.row();
// Check memory cache first
if (QPixmap *cached = m_thumbnailCache.object(info.filePath)) {
- qDebug() << "Cache HIT for:" << info.fileName;
return QIcon(*cached);
}
// Prevent duplicate requests
if (m_loadingPaths.contains(info.filePath) ||
m_activeLoaders.contains(info.filePath)) {
- qDebug() << "Already loading:" << info.fileName;
// Return appropriate placeholder based on file type
if (info.fileName.endsWith(".MOV", Qt::CaseInsensitive) ||
info.fileName.endsWith(".MP4", Qt::CaseInsensitive) ||
@@ -473,7 +479,6 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const
// Start async loading for both images and videos
if (!m_loadingPaths.contains(info.filePath)) {
- qDebug() << "Starting load for:" << info.fileName;
emit const_cast(this)->thumbnailNeedsToBeLoaded(
index.row());
}
@@ -516,7 +521,6 @@ void PhotoModel::requestThumbnail(int index)
connect(watcher, &QFutureWatcher::finished, this,
[this, watcher, filePath = info.filePath]() {
- qDebug() << "Thumbnail load finished for:" << filePath;
QPixmap thumbnail = watcher->result();
m_loadingPaths.remove(filePath);
@@ -549,16 +553,12 @@ void PhotoModel::requestThumbnail(int index)
if (isVideo) {
future = QtConcurrent::run([this, info]() {
// Acquire semaphore FIRST to limit concurrent video processing
- qDebug() << "Waiting for semaphore for:" << info.fileName;
m_videoThumbnailSemaphore.acquire();
- qDebug() << "Acquired semaphore for:" << info.fileName;
// Generate video thumbnail using FFmpeg directly (no QMediaPlayer)
QPixmap thumbnail = generateVideoThumbnailFFmpeg(
m_device, info.filePath, m_thumbnailSize);
- // Release semaphore
- qDebug() << "Releasing semaphore for:" << info.fileName;
m_videoThumbnailSemaphore.release();
return thumbnail;
});
@@ -587,7 +587,6 @@ QPixmap PhotoModel::loadThumbnailFromDevice(iDescriptorDevice *device,
}
if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) {
- qDebug() << "Loading HEIC image from data for:" << filePath;
QPixmap img = load_heic(imageData);
return img.isNull() ? QPixmap()
: img.scaled(size, Qt::KeepAspectRatio,
@@ -872,13 +871,11 @@ PhotoInfo::FileType PhotoModel::determineFileType(const QString &fileName) const
void PhotoModel::setAlbumPath(const QString &albumPath)
{
- if (m_albumPath != albumPath) {
- qDebug() << "Setting new album path:" << albumPath;
- clear();
+ qDebug() << "Setting new album path:" << albumPath;
+ clear();
- m_albumPath = albumPath;
- populatePhotoPaths();
- }
+ m_albumPath = albumPath;
+ populatePhotoPaths();
}
void PhotoModel::refreshPhotos() { populatePhotoPaths(); }
\ No newline at end of file
diff --git a/src/photomodel.h b/src/photomodel.h
index bc9866f..015501e 100644
--- a/src/photomodel.h
+++ b/src/photomodel.h
@@ -90,7 +90,7 @@ signals:
void thumbnailNeedsToBeLoaded(int index);
void exportRequested(const QStringList &filePaths);
-private slots:
+public slots:
void requestThumbnail(int index);
private:
diff --git a/src/qballoontip.cpp b/src/qballoontip.cpp
index 815afcb..1ec676a 100644
--- a/src/qballoontip.cpp
+++ b/src/qballoontip.cpp
@@ -36,7 +36,7 @@ void QBalloonTip::hideBalloon()
if (!theSolitaryBalloonTip)
return;
theSolitaryBalloonTip->hide();
- delete theSolitaryBalloonTip;
+ // delete theSolitaryBalloonTip;
theSolitaryBalloonTip = nullptr;
}
@@ -52,12 +52,17 @@ bool QBalloonTip::isBalloonVisible() { return theSolitaryBalloonTip; }
QBalloonTip::QBalloonTip(const QIcon &icon, const QString &title,
const QString &message, QWidget *widget)
- : QWidget(nullptr, Qt::ToolTip), widget(widget), showArrow(true)
+ : QWidget(widget ? widget->window() : QApplication::activeWindow(),
+ Qt::ToolTip),
+ widget(widget), showArrow(true)
{
- setAttribute(Qt::WA_DeleteOnClose);
+ // setAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_TranslucentBackground);
if (widget) {
connect(widget, &QWidget::destroyed, this, &QBalloonTip::close);
+ } else if (QApplication::activeWindow()) {
+ connect(QApplication::activeWindow(), &QWidget::destroyed, this,
+ &QBalloonTip::close);
}
// Add drop shadow effect
@@ -68,49 +73,50 @@ QBalloonTip::QBalloonTip(const QIcon &icon, const QString &title,
shadowEffect->setOffset(0, 5);
setGraphicsEffect(shadowEffect);
- QLabel *titleLabel = new QLabel;
- titleLabel->installEventFilter(this);
- titleLabel->setText(title);
- QFont f = titleLabel->font();
- f.setBold(true);
- titleLabel->setFont(f);
- titleLabel->setTextFormat(Qt::PlainText); // to maintain compat with windows
+ // QLabel *titleLabel = new QLabel;
+ // titleLabel->installEventFilter(this);
+ // titleLabel->setText(title);
+ // QFont f = titleLabel->font();
+ // f.setBold(true);
+ // titleLabel->setFont(f);
+ // titleLabel->setTextFormat(Qt::PlainText); // to maintain compat with
+ // windows
- const int iconSize = 18;
- const int closeButtonSize = 15;
+ // const int iconSize = 18;
+ // const int closeButtonSize = 15;
- QPushButton *closeButton = new QPushButton;
- closeButton->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton));
- closeButton->setIconSize(QSize(closeButtonSize, closeButtonSize));
- closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
- closeButton->setFixedSize(closeButtonSize, closeButtonSize);
- QObject::connect(closeButton, SIGNAL(clicked()), this, SLOT(close()));
+ // QPushButton *closeButton = new QPushButton;
+ // closeButton->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton));
+ // closeButton->setIconSize(QSize(closeButtonSize, closeButtonSize));
+ // closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ // closeButton->setFixedSize(closeButtonSize, closeButtonSize);
+ // QObject::connect(closeButton, SIGNAL(clicked()), this, SLOT(close()));
- QLabel *msgLabel = new QLabel;
- msgLabel->installEventFilter(this);
- msgLabel->setText(message);
- msgLabel->setTextFormat(Qt::PlainText);
- msgLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
+ // QLabel *msgLabel = new QLabel;
+ // msgLabel->installEventFilter(this);
+ // msgLabel->setText(message);
+ // msgLabel->setTextFormat(Qt::PlainText);
+ // msgLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
- QGridLayout *layout = new QGridLayout;
- if (!icon.isNull()) {
- QLabel *iconLabel = new QLabel;
- iconLabel->setPixmap(
- icon.pixmap(QSize(iconSize, iconSize), devicePixelRatio()));
- iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
- iconLabel->setMargin(2);
- layout->addWidget(iconLabel, 0, 0);
- layout->addWidget(titleLabel, 0, 1);
- } else {
- layout->addWidget(titleLabel, 0, 0, 1, 2);
- }
+ // QGridLayout *layout = new QGridLayout;
+ // if (!icon.isNull()) {
+ // QLabel *iconLabel = new QLabel;
+ // iconLabel->setPixmap(
+ // icon.pixmap(QSize(iconSize, iconSize), devicePixelRatio()));
+ // iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ // iconLabel->setMargin(2);
+ // layout->addWidget(iconLabel, 0, 0);
+ // layout->addWidget(titleLabel, 0, 1);
+ // } else {
+ // layout->addWidget(titleLabel, 0, 0, 1, 2);
+ // }
- layout->addWidget(closeButton, 0, 2);
+ // layout->addWidget(closeButton, 0, 2);
- layout->addWidget(msgLabel, 1, 0, 1, 3);
- layout->setSizeConstraint(QLayout::SetFixedSize);
- layout->setContentsMargins(3, 3, 3, 3);
- setLayout(layout);
+ // layout->addWidget(msgLabel, 1, 0, 1, 3);
+ // layout->setSizeConstraint(QLayout::SetFixedSize);
+ // layout->setContentsMargins(3, 3, 3, 3);
+ // setLayout(layout);
}
QBalloonTip::~QBalloonTip() { theSolitaryBalloonTip = nullptr; }
diff --git a/src/qballoontip.h b/src/qballoontip.h
index 2c53374..ea932e7 100644
--- a/src/qballoontip.h
+++ b/src/qballoontip.h
@@ -10,24 +10,21 @@ class QBalloonTip : public QWidget
{
Q_OBJECT
public:
- static void showBalloon(const QIcon &icon, const QString &title,
- const QString &msg, QWidget *widget,
- const QPoint &pos, int timeout,
- bool showArrow = true);
- static void hideBalloon();
- static bool isBalloonVisible();
- static void updateBalloonPosition(const QPoint &pos);
-
-private:
- QBalloonTip(const QIcon &icon, const QString &title, const QString &msg,
- QWidget *widget);
- ~QBalloonTip();
+ explicit QBalloonTip(const QIcon &icon, const QString &title,
+ const QString &msg, QWidget *widget);
+ void hideBalloon();
+ bool isBalloonVisible();
+ void updateBalloonPosition(const QPoint &pos);
+ void showBalloon(const QIcon &icon, const QString &title,
+ const QString &msg, QWidget *widget, const QPoint &pos,
+ int timeout, bool showArrow = true);
void balloon(const QPoint &, int, bool);
signals:
void messageClicked();
protected:
+ ~QBalloonTip();
void paintEvent(QPaintEvent *) override;
void resizeEvent(QResizeEvent *) override;
void mousePressEvent(QMouseEvent *e) override;
diff --git a/src/servicemanager.cpp b/src/servicemanager.cpp
index f85d454..f814586 100644
--- a/src/servicemanager.cpp
+++ b/src/servicemanager.cpp
@@ -19,6 +19,7 @@
#include "servicemanager.h"
#include "iDescriptor.h"
+#include
IdeviceFfiError *
ServiceManager::safeAfcReadDirectory(const iDescriptorDevice *device,
@@ -139,6 +140,15 @@ AFCFileTree ServiceManager::safeGetFileTree(const iDescriptorDevice *device,
});
}
+QFuture
+ServiceManager::getFileTreeAsync(const iDescriptorDevice *device,
+ const std::string &path, bool checkDir)
+{
+ return QtConcurrent::run([device, path, checkDir]() {
+ return get_file_tree(device, checkDir, path);
+ });
+}
+
MountedImageInfo
ServiceManager::getMountedImage(const iDescriptorDevice *device)
{
@@ -194,4 +204,119 @@ bool ServiceManager::enableWirelessConnections(const iDescriptorDevice *device)
plist_free(value);
return success;
});
-}
\ No newline at end of file
+}
+
+IdeviceFfiError *ServiceManager::exportFileToPath(
+ const iDescriptorDevice *device, const char *device_path,
+ const char *local_path,
+ std::function progressCallback,
+ std::atomic *cancelRequested)
+{
+ qDebug()
+ << "[serviceManager::exportFileToPath] Exporting file from device path:"
+ << device_path << "to local path:" << local_path;
+ return executeOperation(
+ device,
+ [device, device_path, local_path, progressCallback,
+ cancelRequested]() -> IdeviceFfiError * {
+ AfcFileHandle *afcHandle = nullptr;
+ qDebug() << "Opening file on device:" << device_path;
+ IdeviceFfiError *err_open = safeAfcFileOpen(
+ device, device_path, AfcFopenMode::AfcRdOnly, &afcHandle);
+
+ if (err_open != nullptr) {
+ qDebug() << "Failed to open file on device:" << device_path
+ << "Error Code:" << err_open->code
+ << "Message:" << err_open->message;
+ return err_open;
+ }
+ qDebug() << "File opened on device successfully";
+
+ FILE *out = fopen(local_path, "wb");
+ if (!out) {
+ qDebug() << "Failed to open local file:" << local_path;
+ IdeviceFfiError *err_close =
+ safeAfcFileClose(device, afcHandle);
+ if (err_close != nullptr) {
+ // idevice_error_free(err_close);
+ }
+ return new IdeviceFfiError{1, "FAILED_TO_OPEN_LOCAL_FILE"};
+ }
+ qDebug() << "Local file opened successfully";
+
+ const size_t CHUNK_SIZE = 256 * 1024; // 256KB chunks
+ uint8_t *chunkData = nullptr;
+ size_t bytesRead = 0;
+ qint64 totalBytesRead = 0;
+
+ // Get file size for progress
+ AfcFileInfo fileInfo;
+ IdeviceFfiError *info_err =
+ safeAfcGetFileInfo(device, device_path, &fileInfo);
+ qint64 totalFileSize = 0;
+ if (info_err == nullptr) {
+ totalFileSize = fileInfo.size;
+ // afc_file_info_free(&fileInfo);
+ } else {
+ // idevice_error_free(info_err);
+ }
+
+ IdeviceFfiError *read_err = nullptr;
+ // Read file in chunks
+ while (true) {
+ std::this_thread::sleep_for(std::chrono::seconds(2));
+ // Check for cancellation
+ if (cancelRequested && cancelRequested->load()) {
+ fclose(out);
+ safeAfcFileClose(device, afcHandle);
+ return new IdeviceFfiError{1, "OPERATION_CANCELLED"};
+ }
+
+ read_err = safeAfcFileRead(device, afcHandle, &chunkData,
+ CHUNK_SIZE, &bytesRead);
+
+ if (read_err != nullptr) {
+ qDebug() << "Error reading file:" << read_err->message;
+ fclose(out);
+ safeAfcFileClose(device, afcHandle);
+ return read_err;
+ }
+
+ if (bytesRead == 0) {
+ // End of file reached
+ break;
+ }
+
+ // Write chunk to local file
+ size_t written = fwrite(chunkData, 1, bytesRead, out);
+
+ // Free the memory allocated by afc_file_read
+ afc_file_read_data_free(chunkData, bytesRead);
+ chunkData = nullptr;
+
+ if (written != bytesRead) {
+ qDebug() << "Failed to write all bytes to local file";
+ fclose(out);
+ safeAfcFileClose(device, afcHandle);
+ return new IdeviceFfiError{1, "WRITE_ERROR"};
+ }
+
+ totalBytesRead += bytesRead;
+
+ // Report progress
+ if (progressCallback) {
+ progressCallback(totalBytesRead, totalFileSize);
+ }
+ }
+
+ fclose(out);
+
+ IdeviceFfiError *err_close = safeAfcFileClose(device, afcHandle);
+ if (err_close != nullptr) {
+ qDebug() << "Failed to close AFC file:" << err_close->message;
+ return err_close;
+ }
+
+ return nullptr; // Success
+ });
+}
diff --git a/src/servicemanager.h b/src/servicemanager.h
index d8a8821..695684e 100644
--- a/src/servicemanager.h
+++ b/src/servicemanager.h
@@ -22,6 +22,7 @@
#include "iDescriptor.h"
#include
+#include
#include
#include
#include
@@ -199,6 +200,7 @@ public:
// altAfc was explicitly provided but is null, which is an
// invalid state.
qDebug() << "[executeAfcClientOperation] altAfc is null";
+ // c string is not safe in IdeviceFfiError ?
return new IdeviceFfiError{1, "ALT_AFC_CLIENT_IS_NULL"};
}
@@ -248,6 +250,9 @@ public:
const char *path);
static AFCFileTree safeGetFileTree(const iDescriptorDevice *device,
const std::string &path, bool checkDir);
+ static QFuture
+ getFileTreeAsync(const iDescriptorDevice *device, const std::string &path,
+ bool checkDir);
static MountedImageInfo getMountedImage(const iDescriptorDevice *device);
static IdeviceFfiError *mountImage(const iDescriptorDevice *device,
const char *image_file,
@@ -259,6 +264,13 @@ public:
const char *filePath,
const char *fileName);
static bool enableWirelessConnections(const iDescriptorDevice *device);
+
+ // File export operations
+ static IdeviceFfiError *exportFileToPath(
+ const iDescriptorDevice *device, const char *device_path,
+ const char *local_path,
+ std::function progressCallback = nullptr,
+ std::atomic *cancelRequested = nullptr);
};
#endif // SERVICEMANAGER_H
diff --git a/src/statusballoon.cpp b/src/statusballoon.cpp
new file mode 100644
index 0000000..2245a6c
--- /dev/null
+++ b/src/statusballoon.cpp
@@ -0,0 +1,643 @@
+#include "statusballoon.h"
+#include "exportmanager.h"
+#include "exportmanagerthread.h"
+#include "iDescriptor.h"
+#include "qballoontip.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+Process::Process(QWidget *parent) : QWidget(parent) {}
+
+StatusBalloon *StatusBalloon::sharedInstance()
+{
+ static StatusBalloon instance;
+ return &instance;
+}
+
+StatusBalloon::StatusBalloon(QWidget *parent)
+ : QBalloonTip(QIcon(), "", "", parent)
+{
+ setMinimumHeight(300);
+ setMinimumWidth(300);
+ // Create main layout
+ m_mainLayout = new QVBoxLayout();
+ m_mainLayout->setSpacing(8);
+ m_mainLayout->setContentsMargins(12, 12, 12, 12);
+
+ // Header label
+ m_headerLabel = new QLabel("Processes");
+ QFont headerFont = m_headerLabel->font();
+ headerFont.setPointSize(headerFont.pointSize() + 2);
+ headerFont.setBold(true);
+ m_headerLabel->setFont(headerFont);
+ m_mainLayout->addWidget(m_headerLabel);
+
+ // Container for processes
+ m_processesContainer = new QWidget();
+ m_processesLayout = new QVBoxLayout(m_processesContainer);
+ m_processesLayout->setSpacing(12);
+ m_processesLayout->setContentsMargins(0, 0, 0, 0);
+ m_mainLayout->addWidget(m_processesContainer);
+
+ setLayout(m_mainLayout);
+ connect(m_button, &ZIconWidget::clicked, this, &StatusBalloon::showBalloon);
+ connectExportThreadSignals();
+}
+
+void StatusBalloon::connectExportThreadSignals()
+{
+ ExportManager *exportManager = ExportManager::sharedInstance();
+
+ connect(exportManager->m_exportThread, &ExportManagerThread::exportFinished,
+ this, &StatusBalloon::onExportFinished);
+
+ connect(exportManager->m_exportThread, &ExportManagerThread::itemExported,
+ this, &StatusBalloon::onItemExported);
+
+ connect(exportManager->m_exportThread,
+ &ExportManagerThread::fileTransferProgress, this,
+ &StatusBalloon::onFileTransferProgress);
+ QTimer::singleShot(0, this, [this]() {
+ // test
+ startExportProcess("Test Export Process", 10, "/path/to/destination");
+ });
+}
+
+void StatusBalloon::onFileTransferProgress(const QUuid &processId,
+ int currentItem,
+ const QString ¤tFile,
+ qint64 bytesTransferred,
+ qint64 totalBytes)
+{
+ qDebug() << "StatusBalloon::updateProcessProgress entry:" << processId
+ << currentItem << currentFile << bytesTransferred << totalBytes;
+ QMutexLocker locker(&m_processesMutex);
+
+ if (!m_processes.contains(processId)) {
+ qDebug() << "StatusBalloon::updateProcessProgress: unknown processId"
+ << processId;
+ return;
+ }
+
+ ProcessItem *item = m_processes[processId];
+ item->completedItems = currentItem;
+ item->currentFile = currentFile;
+ item->transferredBytes = bytesTransferred;
+ item->totalBytes = totalBytes;
+
+ if (!item->processWidget)
+ qDebug()
+ << "StatusBalloon::updateProcessProgress: no widget for processId"
+ << processId;
+
+ // Update status label
+ QString statusText;
+ if (item->status == ProcessStatus::Running) {
+ if (!item->currentFile.isEmpty()) {
+ statusText = item->currentFile;
+ } else {
+ statusText = "Processing...";
+ }
+ } else if (item->status == ProcessStatus::Completed) {
+ statusText = "Completed successfully";
+ } else if (item->status == ProcessStatus::Failed) {
+ statusText = "Failed";
+ } else if (item->status == ProcessStatus::Cancelled) {
+ statusText = "Cancelled";
+ }
+ item->statusLabel->setText(statusText);
+
+ // Update progress bar
+ // progess should be based on exported bytes vs total bytes of the current
+ // file
+ if (item->totalItems > 0) {
+ int progress = (item->transferredBytes * 100) / item->totalBytes;
+ item->progressBar->setValue(progress);
+ }
+
+ // Update stats
+ QString statsText = QString("%1 of %2 items")
+ .arg(item->completedItems)
+ .arg(item->totalItems);
+ if (item->failedItems > 0) {
+ statsText += QString(" • %1 failed").arg(item->failedItems);
+ }
+
+ if (item->status == ProcessStatus::Running && item->transferredBytes > 0) {
+ // Calculate transfer rate
+ QDateTime now = QDateTime::currentDateTime();
+ qint64 elapsed = m_lastUpdateTime[item->processId].msecsTo(now);
+ if (elapsed > 0) {
+ qint64 bytesDiff = item->transferredBytes -
+ m_lastBytesTransferred[item->processId];
+ qint64 bytesPerSecond = (bytesDiff * 1000) / elapsed;
+ if (bytesPerSecond > 0) {
+ statsText += " • " + formatTransferRate(bytesPerSecond);
+ }
+ m_lastBytesTransferred[item->processId] = item->transferredBytes;
+ m_lastUpdateTime[item->processId] = now;
+ }
+ }
+
+ item->statsLabel->setText(statsText);
+
+ // Update buttons
+ if (item->status == ProcessStatus::Running) {
+ item->cancelButton->setVisible(true);
+ item->actionButton->setVisible(false);
+ } else {
+ item->cancelButton->setVisible(false);
+ if (item->type == ProcessType::Export &&
+ item->status == ProcessStatus::Completed) {
+ item->actionButton->setVisible(true);
+ }
+ }
+}
+
+// todo fix these
+// StatusBalloon::onItemExported entry:
+// QUuid("{9bd97848-cb52-4ef8-93c1-a1d1c285e6a3}") Success: true
+// StatusBalloon::onItemExported: unknown processId
+// QUuid("{9bd97848-cb52-4ef8-93c1-a1d1c285e6a3}")
+// StatusBalloon::onExportFinished entry:
+// QUuid("{9bd97848-cb52-4ef8-93c1-a1d1c285e6a3}") WasCancelled: false
+// StatusBalloon::onExportFinished: unknown processId
+// QUuid("{9bd97848-cb52-4ef8-93c1-a1d1c285e6a3}")
+
+void StatusBalloon::onExportFinished(const QUuid &processId,
+ const ExportJobSummary &summary)
+{
+ qDebug() << "StatusBalloon::onExportFinished entry:" << processId
+ << "WasCancelled:" << summary.wasCancelled;
+ QMutexLocker locker(&m_processesMutex);
+ if (!m_processes.contains(processId)) {
+ qDebug() << "StatusBalloon::onExportFinished: unknown processId"
+ << processId;
+ return;
+ }
+
+ // todo: handle failed ?
+ ProcessItem *item = m_processes[processId];
+ if (summary.wasCancelled) {
+ item->status = ProcessStatus::Cancelled;
+ } else {
+ item->status = ProcessStatus::Completed;
+ }
+ item->endTime = QDateTime::currentDateTime();
+
+ updateUI();
+}
+
+void StatusBalloon::onItemExported(const QUuid &processId,
+ const ExportResult &result)
+{
+ qDebug() << "StatusBalloon::onItemExported entry:" << processId
+ << "Success:" << result.success;
+ QMutexLocker locker(&m_processesMutex);
+
+ if (!m_processes.contains(processId)) {
+ qDebug() << "StatusBalloon::onItemExported: unknown processId"
+ << processId;
+ return;
+ }
+
+ ProcessItem *item = m_processes[processId];
+ if (result.success) {
+ item->completedItems += 1;
+ } else {
+ item->failedItems += 1;
+ }
+
+ updateUI();
+}
+
+QUuid StatusBalloon::startExportProcess(const QString &title, int totalItems,
+ const QString &destinationPath)
+{
+ qDebug() << "StatusBalloon::startExportProcess entry:" << title
+ << totalItems << destinationPath;
+
+ // allocate item first so it can be used after unlocking
+ auto *item = new ProcessItem();
+ item->processId = QUuid::createUuid();
+ item->type = ProcessType::Export;
+ item->status = ProcessStatus::Running;
+ item->title = title;
+ item->totalItems = totalItems;
+ item->completedItems = 0;
+ item->failedItems = 0;
+ item->totalBytes = 0;
+ item->transferredBytes = 0;
+ item->startTime = QDateTime::currentDateTime();
+ item->destinationPath = destinationPath;
+
+ { // scope the lock only for shared-state mutation
+ QMutexLocker locker(&m_processesMutex);
+ m_processes[item->processId] = item;
+ m_currentProcessId = item->processId;
+ m_lastBytesTransferred[item->processId] = 0;
+ m_lastUpdateTime[item->processId] = QDateTime::currentDateTime();
+ } // mutex released here
+
+ // UI work must run without holding m_processesMutex to avoid re-locking
+ // deadlock
+ createProcessWidget(item);
+ updateUI();
+
+ return item->processId;
+}
+
+QUuid StatusBalloon::startUploadProcess(const QString &title, int totalItems)
+{
+ // allocate item first
+ auto *item = new ProcessItem();
+ item->processId = QUuid::createUuid();
+ item->type = ProcessType::Upload;
+ item->status = ProcessStatus::Running;
+ item->title = title;
+ item->totalItems = totalItems;
+ item->completedItems = 0;
+ item->failedItems = 0;
+ item->totalBytes = 0;
+ item->transferredBytes = 0;
+ item->startTime = QDateTime::currentDateTime();
+
+ { // scope the lock only for shared-state mutation
+ QMutexLocker locker(&m_processesMutex);
+ m_processes[item->processId] = item;
+ m_currentProcessId = item->processId;
+ m_lastBytesTransferred[item->processId] = 0;
+ m_lastUpdateTime[item->processId] = QDateTime::currentDateTime();
+ } // mutex released here
+
+ createProcessWidget(item);
+ updateUI();
+
+ return item->processId;
+}
+
+void StatusBalloon::createProcessWidget(ProcessItem *item)
+{
+ item->processWidget = new QWidget();
+ auto *layout = new QVBoxLayout(item->processWidget);
+ layout->setSpacing(6);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ // Title
+ item->titleLabel = new QLabel(item->title);
+ QFont titleFont = item->titleLabel->font();
+ titleFont.setBold(true);
+ item->titleLabel->setFont(titleFont);
+ layout->addWidget(item->titleLabel);
+
+ // Status
+ item->statusLabel = new QLabel("Starting...");
+ layout->addWidget(item->statusLabel);
+
+ // Progress bar
+ item->progressBar = new QProgressBar();
+ item->progressBar->setRange(0, 100);
+ item->progressBar->setValue(0);
+ item->progressBar->setTextVisible(true); // show text for debugging
+ item->progressBar->setFixedHeight(12); // make it visible
+ layout->addWidget(item->progressBar);
+
+ // Stats
+ item->statsLabel = new QLabel();
+ QFont statsFont = item->statsLabel->font();
+ statsFont.setPointSize(statsFont.pointSize() - 1);
+ item->statsLabel->setFont(statsFont);
+ layout->addWidget(item->statsLabel);
+
+ // Buttons layout
+ auto *buttonsLayout = new QHBoxLayout();
+ buttonsLayout->setSpacing(6);
+
+ // Action button (Open Folder for export, hidden initially)
+ item->actionButton = new QPushButton();
+ item->actionButton->setVisible(false);
+ if (item->type == ProcessType::Export) {
+ item->actionButton->setText("Open Folder");
+ connect(item->actionButton, &QPushButton::clicked, this,
+ &StatusBalloon::onOpenFolderClicked);
+ }
+ buttonsLayout->addWidget(item->actionButton);
+
+ buttonsLayout->addStretch();
+
+ // Cancel button
+ item->cancelButton = new QPushButton("Cancel");
+ connect(item->cancelButton, &QPushButton::clicked, this,
+ &StatusBalloon::onCancelClicked);
+ buttonsLayout->addWidget(item->cancelButton);
+
+ layout->addLayout(buttonsLayout);
+
+ m_processesLayout->addWidget(item->processWidget);
+}
+
+void StatusBalloon::markProcessCompleted(const QUuid &processId)
+{
+ QMutexLocker locker(&m_processesMutex);
+
+ if (!m_processes.contains(processId)) {
+ return;
+ }
+
+ ProcessItem *item = m_processes[processId];
+ item->status = ProcessStatus::Completed;
+ item->endTime = QDateTime::currentDateTime();
+
+ updateUI();
+
+ // Check if all processes are done
+ bool allDone = true;
+ for (auto *proc : m_processes) {
+ if (proc->status == ProcessStatus::Running) {
+ allDone = false;
+ break;
+ }
+ }
+}
+
+void StatusBalloon::markProcessFailed(const QUuid &processId,
+ const QString &error)
+{
+ QMutexLocker locker(&m_processesMutex);
+
+ if (!m_processes.contains(processId)) {
+ return;
+ }
+
+ ProcessItem *item = m_processes[processId];
+ item->status = ProcessStatus::Failed;
+ item->endTime = QDateTime::currentDateTime();
+
+ updateUI();
+}
+
+void StatusBalloon::markProcessCancelled(const QUuid &processId)
+{
+ QMutexLocker locker(&m_processesMutex);
+
+ if (!m_processes.contains(processId)) {
+ return;
+ }
+
+ ProcessItem *item = m_processes[processId];
+ item->status = ProcessStatus::Cancelled;
+ item->endTime = QDateTime::currentDateTime();
+
+ updateUI();
+}
+
+void StatusBalloon::incrementFailedItems(const QUuid &processId)
+{
+ QMutexLocker locker(&m_processesMutex);
+
+ if (!m_processes.contains(processId)) {
+ return;
+ }
+
+ m_processes[processId]->failedItems++;
+ updateUI();
+}
+
+void StatusBalloon::updateUI()
+{
+ QMutexLocker locker(&m_processesMutex);
+
+ // Update header
+ int running = 0, completed = 0, failed = 0;
+ for (auto *item : m_processes) {
+ if (item->status == ProcessStatus::Running)
+ running++;
+ else if (item->status == ProcessStatus::Completed)
+ completed++;
+ else if (item->status == ProcessStatus::Failed)
+ failed++;
+ }
+
+ QString headerText = QString("Processes: %1 running").arg(running);
+ if (completed > 0 || failed > 0) {
+ headerText += QString(" • %1 completed").arg(completed);
+ if (failed > 0) {
+ headerText += QString(" • %1 failed").arg(failed);
+ }
+ }
+ m_headerLabel->setText(headerText);
+
+ // Update each process widget
+ for (auto *item : m_processes) {
+ if (!item->processWidget)
+ continue;
+
+ // Update status label
+ QString statusText;
+ if (item->status == ProcessStatus::Running) {
+ if (!item->currentFile.isEmpty()) {
+ statusText = item->currentFile;
+ } else {
+ statusText = "Processing...";
+ }
+ } else if (item->status == ProcessStatus::Completed) {
+ statusText = "Completed successfully";
+ } else if (item->status == ProcessStatus::Failed) {
+ statusText = "Failed";
+ } else if (item->status == ProcessStatus::Cancelled) {
+ statusText = "Cancelled";
+ }
+ item->statusLabel->setText(statusText);
+
+ // Update progress bar
+ if (item->totalItems > 0) {
+ int progress = (item->completedItems * 100) / item->totalItems;
+ item->progressBar->setValue(progress);
+ }
+
+ // Update stats
+ QString statsText = QString("%1 of %2 items")
+ .arg(item->completedItems)
+ .arg(item->totalItems);
+ if (item->failedItems > 0) {
+ statsText += QString(" • %1 failed").arg(item->failedItems);
+ }
+
+ if (item->status == ProcessStatus::Running &&
+ item->transferredBytes > 0) {
+ // Calculate transfer rate
+ QDateTime now = QDateTime::currentDateTime();
+ qint64 elapsed = m_lastUpdateTime[item->processId].msecsTo(now);
+ if (elapsed > 0) {
+ qint64 bytesDiff = item->transferredBytes -
+ m_lastBytesTransferred[item->processId];
+ qint64 bytesPerSecond = (bytesDiff * 1000) / elapsed;
+ if (bytesPerSecond > 0) {
+ statsText += " • " + formatTransferRate(bytesPerSecond);
+ }
+ m_lastBytesTransferred[item->processId] =
+ item->transferredBytes;
+ m_lastUpdateTime[item->processId] = now;
+ }
+ }
+
+ item->statsLabel->setText(statsText);
+
+ // Update buttons
+ if (item->status == ProcessStatus::Running) {
+ item->cancelButton->setVisible(true);
+ item->actionButton->setVisible(false);
+ } else {
+ item->cancelButton->setVisible(false);
+ if (item->type == ProcessType::Export &&
+ item->status == ProcessStatus::Completed) {
+ item->actionButton->setVisible(true);
+ }
+ }
+ }
+
+ showBalloon();
+}
+
+void StatusBalloon::showBalloon()
+{
+ qDebug() << "StatusBalloon::showBalloon" << sender();
+ QPoint pos = m_button->mapToGlobal(
+ QPoint(m_button->width() / 2, m_button->height()));
+
+ balloon(pos, -1, true);
+}
+
+bool StatusBalloon::isProcessRunning(const QUuid &processId) const
+{
+ QMutexLocker locker(&m_processesMutex);
+ if (!m_processes.contains(processId)) {
+ return false;
+ }
+ return m_processes[processId]->status == ProcessStatus::Running;
+}
+
+bool StatusBalloon::hasActiveProcesses() const
+{
+ QMutexLocker locker(&m_processesMutex);
+ for (auto *item : m_processes) {
+ if (item->status == ProcessStatus::Running) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool StatusBalloon::isCancelRequested(const QUuid &processId) const
+{
+ QMutexLocker locker(&m_processesMutex);
+ if (!m_processes.contains(processId)) {
+ return false;
+ }
+ return m_processes[processId]->cancelRequested.load();
+}
+
+void StatusBalloon::onCancelClicked()
+{
+ QPushButton *button = qobject_cast(sender());
+ if (!button)
+ return;
+
+ QMutexLocker locker(&m_processesMutex);
+
+ // Find which process this button belongs to
+ for (auto *item : m_processes) {
+ if (item->cancelButton == button) {
+ item->cancelRequested.store(true);
+ button->setEnabled(false);
+ button->setText("Cancelling...");
+ break;
+ }
+ }
+}
+
+void StatusBalloon::onOpenFolderClicked()
+{
+ QPushButton *button = qobject_cast(sender());
+ if (!button)
+ return;
+
+ QMutexLocker locker(&m_processesMutex);
+
+ for (auto *item : m_processes) {
+ if (item->actionButton == button && item->type == ProcessType::Export) {
+ QDesktopServices::openUrl(
+ QUrl::fromLocalFile(item->destinationPath));
+ break;
+ }
+ }
+}
+
+QString StatusBalloon::formatFileSize(qint64 bytes) const
+{
+ const qint64 KB = 1024;
+ const qint64 MB = KB * 1024;
+ const qint64 GB = MB * 1024;
+
+ if (bytes >= GB) {
+ return QString("%1 GB").arg(
+ QString::number(bytes / double(GB), 'f', 2));
+ } else if (bytes >= MB) {
+ return QString("%1 MB").arg(
+ QString::number(bytes / double(MB), 'f', 1));
+ } else if (bytes >= KB) {
+ return QString("%1 KB").arg(
+ QString::number(bytes / double(KB), 'f', 0));
+ } else {
+ return QString("%1 B").arg(bytes);
+ }
+}
+
+QString StatusBalloon::formatTransferRate(qint64 bytesPerSecond) const
+{
+ return formatFileSize(bytesPerSecond) + "/s";
+}
+
+void StatusBalloon::removeProcessWidget(const QUuid &processId)
+{
+ QMutexLocker locker(&m_processesMutex);
+
+ if (!m_processes.contains(processId)) {
+ return;
+ }
+
+ ProcessItem *item = m_processes[processId];
+ if (item->processWidget) {
+ m_processesLayout->removeWidget(item->processWidget);
+ item->processWidget->deleteLater();
+ }
+
+ // delete item;
+ m_processes.remove(processId);
+
+ if (m_processes.isEmpty()) {
+ hide();
+ }
+}
+
+ZIconWidget *StatusBalloon::getButton() { return m_button; }
diff --git a/src/statusballoon.h b/src/statusballoon.h
new file mode 100644
index 0000000..784b995
--- /dev/null
+++ b/src/statusballoon.h
@@ -0,0 +1,109 @@
+#ifndef STATUSBALLOON_H
+#define STATUSBALLOON_H
+
+#include "iDescriptor-ui.h"
+#include "iDescriptor.h"
+#include "qballoontip.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+enum class ProcessType { Export, Upload };
+
+enum class ProcessStatus { Queued, Running, Completed, Failed, Cancelled };
+
+struct ProcessItem {
+ QUuid processId;
+ ProcessType type;
+ ProcessStatus status;
+ QString title;
+ QString currentFile;
+ int totalItems;
+ int completedItems;
+ int failedItems;
+ qint64 totalBytes;
+ qint64 transferredBytes;
+ QDateTime startTime;
+ QDateTime endTime;
+ QString destinationPath; // For export
+ QWidget *processWidget;
+ QLabel *titleLabel;
+ QLabel *statusLabel;
+ QLabel *statsLabel;
+ QProgressBar *progressBar;
+ QPushButton *actionButton;
+ QPushButton *cancelButton;
+ std::atomic cancelRequested{false};
+};
+
+class Process : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit Process(QWidget *parent = nullptr);
+};
+
+class StatusBalloon : public QBalloonTip
+{
+ Q_OBJECT
+public:
+ explicit StatusBalloon(QWidget *parent = nullptr);
+ static StatusBalloon *sharedInstance();
+
+ // Process management
+ QUuid startExportProcess(const QString &title, int totalItems,
+ const QString &destinationPath);
+ QUuid startUploadProcess(const QString &title, int totalItems);
+
+ void onFileTransferProgress(const QUuid &processId, int currentItem,
+ const QString ¤tFile,
+ qint64 bytesTransferred, qint64 totalBytes);
+ void markProcessCompleted(const QUuid &processId);
+ void markProcessFailed(const QUuid &processId, const QString &error);
+ void markProcessCancelled(const QUuid &processId);
+ void incrementFailedItems(const QUuid &processId);
+
+ bool isProcessRunning(const QUuid &processId) const;
+ bool hasActiveProcesses() const;
+ bool isCancelRequested(const QUuid &processId) const;
+ ZIconWidget *getButton();
+private slots:
+ void onCancelClicked();
+ void onOpenFolderClicked();
+
+private:
+ void updateUI();
+ void showBalloon();
+ void createProcessWidget(ProcessItem *item);
+ QString formatFileSize(qint64 bytes) const;
+ QString formatTransferRate(qint64 bytesPerSecond) const;
+ void removeProcessWidget(const QUuid &processId);
+ void connectExportThreadSignals();
+ void onExportFinished(const QUuid &processId,
+ const ExportJobSummary &summary);
+ void onItemExported(const QUuid &processId, const ExportResult &result);
+
+ QVBoxLayout *m_mainLayout;
+ QLabel *m_headerLabel;
+ QWidget *m_processesContainer;
+ QVBoxLayout *m_processesLayout;
+
+ QMap m_processes;
+ QUuid m_currentProcessId;
+ mutable QMutex m_processesMutex;
+
+ QMap m_lastBytesTransferred;
+ QMap m_lastUpdateTime;
+ ZIconWidget *m_button =
+ new ZIconWidget(QIcon(":/resources/icons/UimProcess.png"), "Processes");
+};
+#endif // STATUSBALLOON_H
diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp
index a403d13..a8d56f1 100644
--- a/src/toolboxwidget.cpp
+++ b/src/toolboxwidget.cpp
@@ -298,7 +298,8 @@ void ToolboxWidget::updateDeviceList()
QString::fromStdString(device->udid).left(8) + "...";
m_deviceCombo->addItem(
QString::fromStdString(device->deviceInfo.productType) + " / " +
- shortUdid,
+ shortUdid +
+ (device->deviceInfo.isWireless ? " (Wi-Fi)" : ""),
QString::fromStdString(device->udid));
}
}