implement statusballoon, refactor export logic and add new icons

This commit is contained in:
uncor3
2026-01-08 21:09:47 +00:00
parent 868efa4525
commit 2a71f011d9
29 changed files with 1532 additions and 546 deletions
+2
View File
@@ -35,6 +35,8 @@
<file>resources/icons/ClarityEyeLine.png</file>
<file>resources/icons/MaterialSymbolsLightImageOutlineSharp.png</file>
<file>resources/icons/MaterialSymbolsFolder.png</file>
<file>resources/icons/QlementineIconsWireless116.png</file>
<file>resources/icons/UimProcess.png</file>
<file>qml/MapView.qml</file>
<file>resources/iphone.png</file>
<file>resources/ios-wallpapers/iphone-ios4.png</file>
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+8 -3
View File
@@ -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);
}
}
+46 -33
View File
@@ -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<NetworkDevice> 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<NetworkDevice> 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);
}
+7 -4
View File
@@ -34,8 +34,9 @@ public:
QList<iDescriptorDevice *> getAllDevices();
explicit AppContext(QObject *parent = nullptr);
bool noDevicesConnected() const;
void cachePairingFile(const QString &udid, IdevicePairingFile *pairingFile);
const IdevicePairingFile *getCachedPairingFile(const QString &udid) const;
// QMap<WiFiMACAddress, PairingFilePath>
void cachePairingFile(const QString &udid, const QString &pairingFilePath);
const QString getCachedPairingFile(const QString &udid) const;
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// QList<iDescriptorRecoveryDevice *> 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<QString, IdevicePairingFile *> m_pairingFileCache;
QMap<QString, QString> 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);
+26 -34
View File
@@ -24,16 +24,15 @@
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
#include "libirecovery.h"
#endif
#include <QDebug>
#include <string.h>
#include "../../heartbeat.h"
#include <QDebug>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sstream>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
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<IdevicePairingFile *>(wirelessArgs.pairing_file),
APP_LABEL, &provider);
const_cast<IdevicePairingFile *>(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>(
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
+3 -2
View File
@@ -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
+18 -5
View File
@@ -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,
+3 -2
View File
@@ -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);
+39 -235
View File
@@ -18,8 +18,9 @@
*/
#include "exportmanager.h"
#include "exportprogressdialog.h"
#include "servicemanager.h"
#include "statusballoon.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
@@ -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<AfcClientHandle *> 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<void>(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<void>::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<void>::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<AfcClientHandle *> altAfc,
std::atomic<bool> &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<int>(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;
// }
}
+4 -49
View File
@@ -20,6 +20,7 @@
#ifndef EXPORTMANAGER_H
#define EXPORTMANAGER_H
#include "exportmanagerthread.h"
#include "iDescriptor.h"
#include <QFuture>
#include <QFutureWatcher>
@@ -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<ExportItem> items;
QString destinationPath;
std::optional<AfcClientHandle *> altAfc;
std::atomic<bool> cancelRequested{false};
QFuture<void> future;
QFutureWatcher<void> *watcher = nullptr;
};
void executeExportJob(ExportJob *job);
ExportResult exportSingleItem(iDescriptorDevice *device,
const ExportItem &item,
const QString &destinationDir,
std::optional<AfcClientHandle *> altAfc,
std::atomic<bool> &cancelRequested,
const QUuid &jobId);
QString generateUniqueOutputPath(const QString &basePath) const;
QString extractFileName(const QString &devicePath) const;
void cleanupJob(const QUuid &jobId);
+166
View File
@@ -0,0 +1,166 @@
#include "exportmanagerthread.h"
#include "iDescriptor.h"
#include "servicemanager.h"
#include <QDebug>
#include <QDir>
#include <QThread>
#include <QtConcurrent>
// 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<AfcClientHandle *> altAfc,
std::atomic<bool> &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 &currentFile = 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;
}
+40
View File
@@ -0,0 +1,40 @@
#ifndef EXPORTMANAGERTHREAD_H
#define EXPORTMANAGERTHREAD_H
#include "iDescriptor.h"
#include "servicemanager.h"
#include <QDebug>
#include <QDir>
#include <QThread>
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<AfcClientHandle *> altAfc,
std::atomic<bool> &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 &currentFileName);
void fileTransferProgress(const QUuid &jobId, int fileIndex,
const QString &currentFile,
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
+68 -41
View File
@@ -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 <QVBoxLayout>
#include <QtConcurrent/QtConcurrent>
// 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<AFCFileTree>(this);
auto future = ServiceManager::getFileTreeAsync(m_device, "/DCIM", true);
watcher->setFuture(future);
connect(watcher, &QFutureWatcher<AFCFileTree>::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<int>(PhotoModel::ImagesOnly));
m_filterComboBox->addItem("All Media", static_cast<int>(PhotoModel::All));
m_filterComboBox->addItem("Images Only",
static_cast<int>(PhotoModel::ImagesOnly));
m_filterComboBox->addItem("Videos Only",
static_cast<int>(PhotoModel::VideosOnly));
m_filterComboBox->setCurrentIndex(2); // Default to All
m_filterComboBox->setCurrentIndex(
static_cast<int>(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<ExportItem>
// QList<ExportItem> exportItems;
// for (const QString &filePath : filePaths) {
// QString fileName = filePath.split('/').last();
// exportItems.append(ExportItem(filePath, fileName));
// }
// Convert QStringList to QList<ExportItem>
QList<ExportItem> 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
+3 -2
View File
@@ -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;
+45 -19
View File
@@ -1,26 +1,19 @@
#ifndef HEARTBEATTHREAD_H
#define HEARTBEATTHREAD_H
#include "iDescriptor.h"
#include <QDebug>
#include <QThread>
#include <idevice++/bindings.hpp>
#include <idevice++/core_device_proxy.hpp>
#include <idevice++/dvt/remote_server.hpp>
#include <idevice++/dvt/screenshot.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/heartbeat.hpp>
#include <idevice++/provider.hpp>
#include <idevice++/readwrite.hpp>
#include <idevice++/rsd.hpp>
#include <idevice++/usbmuxd.hpp>
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
+56 -4
View File
@@ -32,6 +32,7 @@
#include <idevice++/dvt/remote_server.hpp>
#include <idevice++/dvt/screenshot.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/heartbeat.hpp>
#include <idevice++/installation_proxy.hpp>
#include <idevice++/lockdown.hpp>
#include <idevice++/provider.hpp>
@@ -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;
}
}
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<ExportItem> items;
QString destinationPath;
std::optional<AfcClientHandle *> altAfc;
std::atomic<bool> cancelRequested{false};
QUuid statusBalloonProcessId;
};
+1
View File
@@ -228,6 +228,7 @@ void LiveScreenWidget::updateScreenshot()
// return;
// }
qDebug() << "Updating screenshot...";
// FIXME: move to services
try {
// TakeScreenshotResult result = take_screenshot(m_shotrClient);
ScreenshotData screenshot;
+33 -39
View File
@@ -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 <QApplication>
#include <QDesktopServices>
#include <QMenu>
@@ -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);
+16 -19
View File
@@ -40,9 +40,8 @@ extern "C" {
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
// 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<PhotoModel *>(this)->thumbnailNeedsToBeLoaded(
index.row());
}
@@ -516,7 +521,6 @@ void PhotoModel::requestThumbnail(int index)
connect(watcher, &QFutureWatcher<QPixmap>::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(); }
+1 -1
View File
@@ -90,7 +90,7 @@ signals:
void thumbnailNeedsToBeLoaded(int index);
void exportRequested(const QStringList &filePaths);
private slots:
public slots:
void requestThumbnail(int index);
private:
+46 -40
View File
@@ -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; }
+9 -12
View File
@@ -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;
+126 -1
View File
@@ -19,6 +19,7 @@
#include "servicemanager.h"
#include "iDescriptor.h"
#include <QtConcurrent>
IdeviceFfiError *
ServiceManager::safeAfcReadDirectory(const iDescriptorDevice *device,
@@ -139,6 +140,15 @@ AFCFileTree ServiceManager::safeGetFileTree(const iDescriptorDevice *device,
});
}
QFuture<AFCFileTree>
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;
});
}
}
IdeviceFfiError *ServiceManager::exportFileToPath(
const iDescriptorDevice *device, const char *device_path,
const char *local_path,
std::function<void(qint64, qint64)> progressCallback,
std::atomic<bool> *cancelRequested)
{
qDebug()
<< "[serviceManager::exportFileToPath] Exporting file from device path:"
<< device_path << "to local path:" << local_path;
return executeOperation<IdeviceFfiError *>(
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
});
}
+12
View File
@@ -22,6 +22,7 @@
#include "iDescriptor.h"
#include <QDebug>
#include <QFuture>
#include <functional>
#include <mutex>
#include <optional>
@@ -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<AFCFileTree>
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<void(qint64, qint64)> progressCallback = nullptr,
std::atomic<bool> *cancelRequested = nullptr);
};
#endif // SERVICEMANAGER_H
+643
View File
@@ -0,0 +1,643 @@
#include "statusballoon.h"
#include "exportmanager.h"
#include "exportmanagerthread.h"
#include "iDescriptor.h"
#include "qballoontip.h"
#include <QApplication>
#include <QBasicTimer>
#include <QDateTime>
#include <QDebug>
#include <QDesktopServices>
#include <QGraphicsDropShadowEffect>
#include <QGridLayout>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QMutexLocker>
#include <QPainter>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QScreen>
#include <QStyle>
#include <QTimer>
#include <QTimerEvent>
#include <QUrl>
#include <QUuid>
#include <qpainterpath.h>
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 &currentFile,
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<QPushButton *>(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<QPushButton *>(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; }
+109
View File
@@ -0,0 +1,109 @@
#ifndef STATUSBALLOON_H
#define STATUSBALLOON_H
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "qballoontip.h"
#include <QBasicTimer>
#include <QDateTime>
#include <QIcon>
#include <QLabel>
#include <QMutex>
#include <QProgressBar>
#include <QPushButton>
#include <QSystemTrayIcon>
#include <QUuid>
#include <QVBoxLayout>
#include <QWidget>
#include <atomic>
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<bool> 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 &currentFile,
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<QUuid, ProcessItem *> m_processes;
QUuid m_currentProcessId;
mutable QMutex m_processesMutex;
QMap<QUuid, qint64> m_lastBytesTransferred;
QMap<QUuid, QDateTime> m_lastUpdateTime;
ZIconWidget *m_button =
new ZIconWidget(QIcon(":/resources/icons/UimProcess.png"), "Processes");
};
#endif // STATUSBALLOON_H
+2 -1
View File
@@ -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));
}
}