mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
implement statusballoon, refactor export logic and add new icons
This commit is contained in:
@@ -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 |
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 ¤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;
|
||||
}
|
||||
@@ -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 ¤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
|
||||
+68
-41
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¤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<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; }
|
||||
@@ -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 ¤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<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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user