feat: Add import functionality to ExportManager and related components

- Introduced ImportJob and ImportItem structures to handle import tasks.
- Enhanced ExportManagerThread to support import operations, including job queuing and progress tracking.
- Updated ServiceManager to facilitate file imports with optional AFC client handling.
- Modified ImageLoader and ImageTask to accommodate alternative AFC client for image loading.
- Implemented UI updates in StatusBalloon to reflect import process status alongside export.
- Refactored existing code to improve readability and maintainability, including the removal of unused variables and comments.
- Added a new loading icon label for better user feedback during import operations.
- Updated gallery widget to streamline export item creation by removing unnecessary index tracking.
- Enhanced error handling and logging for file operations during import and export processes.
This commit is contained in:
uncor3
2026-03-20 03:05:57 +00:00
parent ddcaab6f4d
commit b989978668
26 changed files with 1054 additions and 479 deletions
+108 -211
View File
@@ -112,27 +112,19 @@ void AfcExplorerWidget::onItemDoubleClicked(QListWidgetItem *item)
m_history.push(nextPath);
loadPath(nextPath);
} else {
const QString lowerFileName = name.toLower();
const bool isPreviewable =
lowerFileName.endsWith(".mp4") || lowerFileName.endsWith(".m4v") ||
lowerFileName.endsWith(".mov") || lowerFileName.endsWith(".avi") ||
lowerFileName.endsWith(".mkv") || lowerFileName.endsWith(".jpg") ||
lowerFileName.endsWith(".jpeg") || lowerFileName.endsWith(".png") ||
lowerFileName.endsWith(".gif") || lowerFileName.endsWith(".bmp");
const bool isPreviewable = iDescriptor::Utils::isPreviewableFile(name);
if (isPreviewable) {
auto *previewDialog =
new MediaPreviewDialog(m_device, m_afc, nextPath, this);
previewDialog->setAttribute(Qt::WA_DeleteOnClose);
previewDialog->show();
} else {
openWithDesktopService(nextPath, name);
openWithDesktopService(item);
}
}
}
void AfcExplorerWidget::openWithDesktopService(const QString &devicePath,
const QString &fileName)
void AfcExplorerWidget::openWithDesktopService(QListWidgetItem *item)
{
QTemporaryDir *tempDir = new QTemporaryDir();
if (!tempDir->isValid()) {
@@ -142,18 +134,7 @@ void AfcExplorerWidget::openWithDesktopService(const QString &devicePath,
return;
}
QString localPath = tempDir->path() + "/" + fileName;
int result = exportFileToPath(m_afc, devicePath.toUtf8().constData(),
localPath.toUtf8().constData());
if (result == 0) {
QDesktopServices::openUrl(QUrl::fromLocalFile(localPath));
// TODO: Clean up tempDir in destructor or keep a list of temp dirs
} else {
QMessageBox::warning(this, "Export Failed",
"Could not export the file from the device.");
delete tempDir;
}
exportAndOpenSelectedFile(item, tempDir->path());
}
void AfcExplorerWidget::onAddressBarReturnPressed()
@@ -292,15 +273,12 @@ 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, m_device->udid, index));
index++;
ExportItem(devicePath, fileName, m_device->udid));
}
// Start export with singleton - manager will show its own dialog
@@ -309,16 +287,7 @@ void AfcExplorerWidget::onFileListContextMenu(const QPoint &pos)
} else if (selectedAction == openAction) {
onItemDoubleClicked(item);
} else if (selectedAction == openNativeAction) {
QString fileName = item->text();
QString currPath = "/";
if (!m_history.isEmpty())
currPath = m_history.top();
if (!currPath.endsWith("/"))
currPath += "/";
QString devicePath =
currPath == "/" ? "/" + fileName : currPath + fileName;
openWithDesktopService(devicePath, fileName);
openWithDesktopService(item);
}
}
@@ -351,24 +320,27 @@ 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, m_device->udid, index));
index++;
exportItems.append(ExportItem(devicePath, fileName, m_device->udid));
}
// Start export with singleton - manager will show its own dialog
// Start export
ExportManager::sharedInstance()->startExport(m_device, exportItems, dir,
m_afc);
}
void AfcExplorerWidget::exportSelectedFile(QListWidgetItem *item,
const QString &directory)
void AfcExplorerWidget::exportAndOpenSelectedFile(QListWidgetItem *item,
const QString &directory)
{
if (!QDir(directory).exists()) {
QMessageBox::critical(this, "Error",
"Could not access the temporary directory.");
return;
}
QString fileName = item->text();
QString currPath = "/";
if (!m_history.isEmpty())
@@ -378,117 +350,24 @@ void AfcExplorerWidget::exportSelectedFile(QListWidgetItem *item,
QString devicePath = currPath == "/" ? "/" + fileName : currPath + fileName;
qDebug() << "Exporting file:" << devicePath;
// Save to selected directory
QString savePath = directory + "/" + fileName;
// FIXME: this should be async
int result = exportFileToPath(m_afc, devicePath.toStdString().c_str(),
savePath.toStdString().c_str());
qDebug() << "Export result:" << result;
if (result == 0) {
qDebug() << "Exported" << devicePath << "to" << savePath;
QMessageBox::StandardButton reply;
reply = QMessageBox::question(
this, "Export Successful",
"File exported successfully. Would you like to see the directory?",
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
QDesktopServices::openUrl(QUrl::fromLocalFile(directory));
}
} else {
qDebug() << "Failed to export" << devicePath;
QMessageBox::warning(this, "Export Failed",
"Failed to export the file from the device");
}
// Start export
QList<ExportItem> exportItems;
exportItems.append(ExportItem(
devicePath, fileName, m_device->udid,
[this, fileName, directory](const ExportResult &result) {
if (result.success) {
QString localPath = QDir(directory).filePath(fileName);
QDesktopServices::openUrl(QUrl::fromLocalFile(localPath));
} else {
QMessageBox::critical(this, "Error",
"Failed to export file for opening.");
}
}));
ExportManager::sharedInstance()->startExport(m_device, exportItems,
directory, m_afc);
}
/*
FIXME : abstract to services
even though we are using safe wrappers,
we better move this to services
*/
// FIXME: this should be async
// use connect to signals/slots to notify progress
// create a progress dialog to show progress
// dont do this on the main thread
int AfcExplorerWidget::exportFileToPath(AfcClientHandle *afc,
const char *device_path,
const char *local_path)
{
AfcFileHandle *afcHandle = nullptr;
IdeviceFfiError *err_open = ServiceManager::safeAfcFileOpen(
m_device, device_path, AfcRdOnly, &afcHandle);
if (err_open != nullptr) {
qDebug() << "Failed to open file on device:" << device_path
<< "Error Code:" << err_open->code
<< "Message:" << err_open->message;
idevice_error_free(err_open);
return -1;
}
FILE *out = fopen(local_path, "wb");
if (!out) {
qDebug() << "Failed to open local file:" << local_path;
IdeviceFfiError *err_close =
ServiceManager::safeAfcFileClose(m_device, afcHandle);
if (err_close != nullptr) {
idevice_error_free(err_close);
}
return -1;
}
const size_t CHUNK_SIZE = 256 * 1024; // 256KB chunks
uint8_t *chunkData = nullptr;
size_t bytesRead = 0;
// Read file in chunks
while (true) {
IdeviceFfiError *read_err = ServiceManager::safeAfcFileRead(
m_device, afcHandle, &chunkData, CHUNK_SIZE, &bytesRead);
if (read_err != nullptr) {
qDebug() << "Error reading file:" << read_err->message;
idevice_error_free(read_err);
break;
}
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);
ServiceManager::safeAfcFileClose(m_device, afcHandle);
return -1;
}
}
fclose(out);
IdeviceFfiError *err_close =
ServiceManager::safeAfcFileClose(m_device, afcHandle);
if (err_close != nullptr) {
qDebug() << "Failed to close AFC file:" << err_close->message;
idevice_error_free(err_close);
return -1;
}
return 0;
}
// should be disabled if there is an error loading afc
// FIXME: should be disabled if there is an error loading afc
void AfcExplorerWidget::onImportClicked()
{
QStringList fileNames = QFileDialog::getOpenFileNames(this, "Import Files");
@@ -501,61 +380,23 @@ void AfcExplorerWidget::onImportClicked()
if (!currPath.endsWith("/"))
currPath += "/";
// Import each file
QList<ImportItem> importItems;
for (const QString &localPath : fileNames) {
QFileInfo fi(localPath);
QString devicePath = currPath + fi.fileName();
int result = importFileToDevice(m_afc, devicePath.toStdString().c_str(),
localPath.toStdString().c_str());
if (result == 0)
qDebug() << "Imported" << localPath << "to" << devicePath;
else
qDebug() << "Failed to import" << localPath;
importItems.append(
ImportItem(localPath, currPath + QFileInfo(localPath).fileName(),
m_device->udid, [this](const ImportResult &result) {
if (result.success) {
// Refresh file list
QTimer::singleShot(100, this, [this]() {
if (!m_history.isEmpty())
loadPath(m_history.top());
});
}
}));
}
// Refresh file list
loadPath(currPath);
}
/*
FIXME : move to services
*/
int AfcExplorerWidget::importFileToDevice(AfcClientHandle *afc,
const char *device_path,
const char *local_path)
{
QFile in(local_path);
// if (!in.open(QIODevice::ReadOnly)) {
// qDebug() << "Failed to open local file for import:" << local_path;
// return -1;
// }
// uint64_t handle = 0;
// if (ServiceManager::safeAfcFileOpen(m_device, device_path,
// AFC_FOPEN_WRONLY,
// &handle, m_afc) != AFC_E_SUCCESS) {
// qDebug() << "Failed to open file on device for writing:" <<
// device_path; return -1;
// }
// char buffer[4096];
// qint64 bytesRead;
// while ((bytesRead = in.read(buffer, sizeof(buffer))) > 0) {
// uint32_t bytesWritten = 0;
// if (ServiceManager::safeAfcFileWrite(
// m_device, handle, buffer, static_cast<uint32_t>(bytesRead),
// &bytesWritten, m_afc) != AFC_E_SUCCESS ||
// bytesWritten != bytesRead) {
// qDebug() << "Failed to write to device file:" << device_path;
// ServiceManager::safeAfcFileClose(m_device, handle, m_afc);
// in.close();
// return -1;
// }
// }
// ServiceManager::safeAfcFileClose(m_device, handle, m_afc);
in.close();
return 0;
ExportManager::sharedInstance()->startImport(m_device, importItems,
currPath, m_afc);
}
void AfcExplorerWidget::setupFileExplorer()
@@ -619,6 +460,9 @@ void AfcExplorerWidget::setupFileExplorer()
QIcon(":/resources/icons/MaterialSymbolsLightKeyboardReturn.png"),
"Navigate to path");
m_deleteButton = new ZIconWidget(
QIcon(":/resources/icons/MaterialSymbolsDelete.png"), "Delete");
m_addressBar = new QLineEdit();
m_addressBar->setPlaceholderText("Enter path...");
m_addressBar->setText("/");
@@ -632,6 +476,7 @@ void AfcExplorerWidget::setupFileExplorer()
navLayout->addWidget(m_addressBar);
navLayout->addWidget(m_importBtn);
navLayout->addWidget(m_exportBtn);
navLayout->addWidget(m_deleteButton);
if (m_favEnabled)
navLayout->addWidget(m_addToFavoritesBtn);
@@ -719,6 +564,8 @@ void AfcExplorerWidget::setupFileExplorer()
&AfcExplorerWidget::onImportClicked);
connect(m_retryButton, &QPushButton::clicked, this,
&AfcExplorerWidget::onRetryClicked);
connect(m_deleteButton, &ZIconWidget::clicked, this,
&AfcExplorerWidget::onDeleteClicked);
connect(m_fileList->selectionModel(),
&QItemSelectionModel::selectionChanged, this,
&AfcExplorerWidget::updateButtonStates);
@@ -801,15 +648,16 @@ void AfcExplorerWidget::updateButtonStates()
{
QList<QListWidgetItem *> selectedItems = m_fileList->selectedItems();
// Export is only enabled if non-directory items are selected
bool hasExportableFiles = false;
bool enteriesDoNotContainDirectories = selectedItems.size() > 0;
for (QListWidgetItem *item : selectedItems) {
if (!item->data(Qt::UserRole).toBool()) { // Not a directory
hasExportableFiles = true;
if (item->data(Qt::UserRole).toBool()) { // a directory
enteriesDoNotContainDirectories = false;
break;
}
}
m_exportBtn->setEnabled(hasExportableFiles);
// TODO: implement directory export and remove
m_exportBtn->setEnabled(enteriesDoNotContainDirectories);
m_deleteButton->setEnabled(enteriesDoNotContainDirectories);
}
void AfcExplorerWidget::setErrorMessage(const QString &message)
@@ -892,3 +740,52 @@ void AfcExplorerWidget::goUp()
m_history.push(parentPath);
loadPath(parentPath);
}
void AfcExplorerWidget::onDeleteClicked()
{
QList<QListWidgetItem *> selectedItems = m_fileList->selectedItems();
if (selectedItems.isEmpty())
return;
QString currPath = "/";
if (!m_history.isEmpty())
currPath = m_history.top();
if (!currPath.endsWith("/"))
currPath += "/";
QList<QString> pathsToDelete;
for (QListWidgetItem *item : selectedItems) {
QString fileName = item->text();
QString devicePath =
currPath == "/" ? "/" + fileName : currPath + fileName;
pathsToDelete.append(devicePath);
}
QMessageBox::StandardButton reply = QMessageBox::question(
this, "Confirm Deletion",
QString("Are you sure you want to delete the selected %1 item(s)?")
.arg(pathsToDelete.size()),
QMessageBox::Yes | QMessageBox::No);
bool errorOccurred = false;
IdeviceFfiError *err = nullptr;
if (reply == QMessageBox::Yes) {
for (const QString &path : pathsToDelete) {
err = ServiceManager::deletePath(m_device,
path.toStdString().c_str(), m_afc);
if (err) {
errorOccurred = true;
qWarning() << "Failed to delete path:" << path
<< "Error:" << err->message;
idevice_error_free(err);
}
}
if (errorOccurred) {
QMessageBox::warning(
this, "Deletion Error",
"Some items could not be deleted. Check logs for details.");
}
QTimer::singleShot(100, this,
[this, currPath]() { loadPath(currPath); });
}
}
+5 -7
View File
@@ -87,6 +87,7 @@ private:
ZIconWidget *m_homeButton;
ZIconWidget *m_upButton;
ZIconWidget *m_enterButton;
ZIconWidget *m_deleteButton;
const iDescriptorDevice *m_device;
bool m_favEnabled;
AfcClientHandle *m_afc;
@@ -105,15 +106,12 @@ private:
void showErrorState();
void showFileListState();
void saveFavoritePlace(const QString &path, const QString &alias);
void openWithDesktopService(const QString &nextPath, const QString &name);
void openWithDesktopService(QListWidgetItem *item);
void onDeleteClicked();
void setupContextMenu();
void exportSelectedFile(QListWidgetItem *item);
void exportSelectedFile(QListWidgetItem *item, const QString &directory);
int exportFileToPath(AfcClientHandle *afc, const char *device_path,
const char *local_path);
int importFileToDevice(AfcClientHandle *afc, const char *device_path,
const char *local_path);
void exportAndOpenSelectedFile(QListWidgetItem *item,
const QString &directory);
void updateButtonStates();
void goUp();
#ifndef WIN32
+47 -50
View File
@@ -21,8 +21,8 @@
#include "devicemonitor.h"
#include "iDescriptor.h"
#include "mainwindow.h"
// #include "settingsmanager.h"
#include "networkdevicemanager.h"
#include "settingsmanager.h"
#include <QDebug>
#include <QMessageBox>
#include <QThreadPool>
@@ -151,13 +151,13 @@ void AppContext::cachePairedDevices()
QString::fromUtf8(mac_address), path);
m_pairingFileCache[QString::fromUtf8(
mac_address)] = path;
free(mac_address);
plist_mem_free(mac_address);
}
}
plist_free(root_node);
}
free(plist_data);
plist_mem_free(plist_data);
}
// Clean up
// idevice_pairing_file_free(pairing_file.unwrap().raw());
@@ -192,6 +192,10 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
QString ipAddress)
{
if (QCoreApplication::closingDown()) {
qDebug() << "Ignoring addDevice during shutdown for" << uniq.get();
return;
}
emit initStarted(uniq);
if (auto device = getDevice(uniq)) {
@@ -257,6 +261,7 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
// TODO:it could also be password protected, so check for
// that Initialization failed, cleaning up resources.
// PasswordProtected
// Invalidhostid
if (initResult->error && initResult->error->code ==
PairingDialogResponsePending) {
if (addType == AddType::Regular) {
@@ -461,14 +466,14 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
addType == AddType::UpgradeToWireless ||
addType == AddType::Regular) {
qDebug() << "Wireless device added: " << uniq;
// SettingsManager::sharedInstance()->doIfEnabled(
// SettingsManager::Setting::AutoRaiseWindow, []() {
// if (MainWindow *mainWindow =
// MainWindow::sharedInstance()) {
// mainWindow->raise();
// mainWindow->activateWindow();
// }
// });
SettingsManager::sharedInstance()->doIfEnabled(
SettingsManager::Setting::AutoRaiseWindow, []() {
if (MainWindow *mainWindow =
MainWindow::sharedInstance()) {
mainWindow->raise();
mainWindow->activateWindow();
}
});
emit deviceAdded(device);
emit deviceChange();
@@ -485,11 +490,11 @@ void AppContext::addDevice(iDescriptor::Uniq uniq,
int AppContext::getConnectedDeviceCount() const
{
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// return m_devices.size() + m_recoveryDevices.size();
// #else
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
return m_devices.size() + m_recoveryDevices.size();
#else
return m_devices.size();
// #endif
#endif
}
void AppContext::removeDevice(iDescriptor::Uniq uniq)
@@ -540,20 +545,7 @@ void AppContext::removeDevice(iDescriptor::Uniq uniq)
qDebug() << "Acquired lock, cleaning up device: "
<< QString::fromStdString(udid);
// FIXME: implement proper cleanup
if (device->afcClient)
afc_client_free(device->afcClient);
if (device->afc2Client)
afc_client_free(device->afc2Client);
// idevice_free(device->device);
if (device->heartbeatThread) {
device->heartbeatThread->requestInterruption();
// device->heartbeatThread->wait();
delete device->heartbeatThread;
}
delete device;
freeDevice(device);
}
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
@@ -599,12 +591,12 @@ QList<iDescriptorRecoveryDevice *> AppContext::getAllRecoveryDevices()
// Returns whether there are any devices connected (regular or recovery)
bool AppContext::noDevicesConnected() const
{
// #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
// return (m_devices.isEmpty() && m_recoveryDevices.isEmpty() &&
// m_pendingDevices.isEmpty());
// #else
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
return (m_devices.isEmpty() && m_recoveryDevices.isEmpty() &&
m_pendingDevices.isEmpty());
#else
return (m_devices.isEmpty() && m_pendingDevices.isEmpty());
// #endif
#endif
}
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
@@ -644,25 +636,12 @@ void AppContext::addRecoveryDevice(uint64_t ecid)
AppContext::~AppContext()
{
// FIXME: deviceRemoved can trigger, new devices being added while we are
// trying to clean up
for (auto device : m_devices) {
emit deviceRemoved(device->udid, device->deviceInfo.wifiMacAddress,
device->deviceInfo.ipAddress,
device->deviceInfo.isWireless);
if (device->afcClient)
afc_client_free(device->afcClient);
if (device->afc2Client)
afc_client_free(device->afc2Client);
// idevice_free(device->device);
if (device->heartbeatThread) {
device->heartbeatThread->requestInterruption();
device->heartbeatThread->wait();
delete device->heartbeatThread;
}
freeDevice(device);
}
m_devices.clear();
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
for (auto recoveryDevice : m_recoveryDevices) {
emit recoveryDeviceRemoved(recoveryDevice->ecid);
@@ -740,3 +719,21 @@ void AppContext::emitNoPairingFileForWirelessDevice(const QString &udid)
{
emit noPairingFileForWirelessDevice(udid);
}
void AppContext::freeDevice(iDescriptorDevice *device)
{
if (device->afcClient)
afc_client_free(device->afcClient);
if (device->afc2Client)
afc_client_free(device->afc2Client);
if (device->heartbeatThread) {
device->heartbeatThread->requestInterruption();
device->heartbeatThread->wait();
delete device->heartbeatThread;
}
lockdownd_client_free(device->lockdown);
idevice_provider_free(device->provider);
delete device;
}
+1
View File
@@ -59,6 +59,7 @@ private:
QMap<QString, QString> m_pairingFileCache;
void cachePairedDevices();
void emitNoPairingFileForWirelessDevice(const QString &udid);
void freeDevice(iDescriptorDevice *device);
signals:
void deviceAdded(const iDescriptorDevice *device);
void deviceRemoved(const std::string &udid, const std::string &macAddress,
+33 -26
View File
@@ -537,12 +537,8 @@ void AppsWidget::createAppCard(
cardLayout->setSpacing(10);
// App icon
QLabel *iconLabel = new QLabel();
QPointer<QLabel> safeIconLabel = iconLabel;
QPixmap placeholderIcon = QApplication::style()
->standardIcon(QStyle::SP_ComputerIcon)
.pixmap(64, 64);
iconLabel->setPixmap(placeholderIcon);
IDLoadingIconLabel *iconLabel = new IDLoadingIconLabel();
QPointer<IDLoadingIconLabel> safeIconLabel = iconLabel;
iconLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(iconLabel);
@@ -552,25 +548,31 @@ void AppsWidget::createAppCard(
QNetworkReply *reply = m_networkManager->get(request);
connect(
reply, &QNetworkReply::finished, this, [reply, safeIconLabel]() {
if (reply->error() == QNetworkReply::NoError && safeIconLabel) {
QByteArray data = reply->readAll();
QPixmap pixmap;
if (pixmap.loadFromData(data)) {
QPixmap scaled = pixmap.scaled(
64, 64, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
QPixmap rounded(64, 64);
rounded.fill(Qt::transparent);
if (safeIconLabel) {
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
QPixmap pixmap;
if (pixmap.loadFromData(data)) {
QPixmap scaled = pixmap.scaled(
64, 64, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
QPixmap rounded(64, 64);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, 64, 64), 16, 16);
painter.setClipPath(path);
painter.drawPixmap(0, 0, scaled);
painter.end();
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, 64, 64), 16, 16);
painter.setClipPath(path);
painter.drawPixmap(0, 0, scaled);
painter.end();
safeIconLabel->setPixmap(rounded);
safeIconLabel->setLoadedPixmap(rounded);
} else {
safeIconLabel->setLoadFailed();
}
} else {
safeIconLabel->setLoadFailed();
}
}
reply->deleteLater();
@@ -580,8 +582,11 @@ void AppsWidget::createAppCard(
fetchAppIconFromApple(
m_networkManager, bundleId,
[safeIconLabel](const QPixmap &pixmap, const QJsonObject &appInfo) {
// Check if iconLabel still exists
if (safeIconLabel && !pixmap.isNull()) {
Q_UNUSED(appInfo);
if (!safeIconLabel)
return;
if (!pixmap.isNull()) {
QPixmap scaled =
pixmap.scaled(64, 64, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
@@ -596,7 +601,9 @@ void AppsWidget::createAppCard(
painter.drawPixmap(0, 0, scaled);
painter.end();
safeIconLabel->setPixmap(rounded);
safeIconLabel->setLoadedPixmap(rounded);
} else {
safeIconLabel->setLoadFailed();
}
});
}
@@ -22,35 +22,32 @@
#include <QByteArray>
#include <QDebug>
QByteArray read_afc_file_to_byte_array(const iDescriptorDevice *device,
const char *path)
QByteArray read_afc_file_to_byte_array(AfcClientHandle *afc, const char *path)
{
AfcFileHandle *handle = nullptr;
IdeviceFfiError *err_open = // Use distinct variable name
ServiceManager::safeAfcFileOpen(device, path, AfcRdOnly, &handle);
IdeviceFfiError *err_open = afc_file_open(afc, path, AfcRdOnly, &handle);
if (err_open) {
qDebug() << "Could not open file" << path
<< "Error:" << err_open->message;
idevice_error_free(err_open); // Free the error object
idevice_error_free(err_open);
return QByteArray();
}
AfcFileInfo info = {};
IdeviceFfiError *err_info = // Use distinct variable name
ServiceManager::safeAfcGetFileInfo(device, path, &info);
IdeviceFfiError *err_info = afc_get_file_info(afc, path, &info);
if (err_info) {
qDebug() << "Could not get file info for file" << path
<< "Error:" << err_info->message;
idevice_error_free(err_info); // Free the error object
ServiceManager::safeAfcFileClose(device, handle); // Close handle
idevice_error_free(err_info);
afc_file_close(handle);
return QByteArray();
}
size_t fileSize = info.size;
if (fileSize == 0) {
ServiceManager::safeAfcFileClose(device, handle);
afc_file_close(handle);
afc_file_info_free(&info); // Free internal strings of info
return QByteArray();
}
@@ -60,14 +57,14 @@ QByteArray read_afc_file_to_byte_array(const iDescriptorDevice *device,
uint8_t *chunkData = nullptr;
size_t bytesRead = 0;
IdeviceFfiError *read_err = ServiceManager::safeAfcFileRead(
device, handle, &chunkData, fileSize, &bytesRead);
IdeviceFfiError *read_err =
afc_file_read(handle, &chunkData, fileSize, &bytesRead);
if (read_err) {
qDebug() << "AFC Error: Read failed for file" << path
<< "Error:" << read_err->message;
idevice_error_free(read_err);
ServiceManager::safeAfcFileClose(device, handle);
afc_file_close(handle);
afc_file_info_free(&info); // Free internal strings of info
return QByteArray();
}
@@ -75,7 +72,7 @@ QByteArray read_afc_file_to_byte_array(const iDescriptorDevice *device,
buffer.append(reinterpret_cast<const char *>(chunkData), bytesRead);
afc_file_read_data_free(chunkData, bytesRead);
ServiceManager::safeAfcFileClose(device, handle);
afc_file_close(handle);
if (bytesRead != fileSize) {
qDebug() << "AFC Error: Read mismatch for file" << path
+1
View File
@@ -20,6 +20,7 @@
#ifndef DISKUSAGEWIDGET_H
#define DISKUSAGEWIDGET_H
#include "diskusagebar.h"
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "qprocessindicator.h"
+1 -3
View File
@@ -115,7 +115,6 @@ void ExportAlbum::getTotalPhotoCount(const QStringList &paths)
errorOccurred = true;
idevice_error_free(err);
} else {
int index = 0;
for (size_t i = 0; i < innerCount; ++i) {
const char *item = items[i];
if (!item) {
@@ -130,8 +129,7 @@ void ExportAlbum::getTotalPhotoCount(const QStringList &paths)
QString filePath = path + "/" + QString::fromUtf8(item);
m_exportItems.append(
ExportItem(filePath, fileName, m_device->udid, index));
++index;
ExportItem(filePath, fileName, m_device->udid));
}
free_directory_listing(items, innerCount);
count += innerCount;
+63 -5
View File
@@ -52,6 +52,7 @@ ExportManager::~ExportManager()
m_activeJobs.clear();
}
// FIXME: show error on ui
QUuid ExportManager::startExport(const iDescriptorDevice *device,
const QList<ExportItem> &items,
const QString &destinationPath,
@@ -87,11 +88,9 @@ QUuid ExportManager::startExport(const iDescriptorDevice *device,
job->altAfc = altAfc;
job->d_udid = device->udid;
// fixme : pass ExportJob
job->statusBalloonProcessId =
StatusBalloon::sharedInstance()->startExportProcess(
QString("Exporting %1 item(s)").arg(items.size()), items.size(),
destinationPath);
job->statusBalloonProcessId = StatusBalloon::sharedInstance()->startProcess(
QString("Exporting %1 item(s)").arg(items.size()), items.size(),
destinationPath, ProcessType::Export);
// Use ExportManager's own jobId for its internal tracking and signals
const QUuid managerJobId = job->jobId;
@@ -112,6 +111,55 @@ QUuid ExportManager::startExport(const iDescriptorDevice *device,
return managerJobId;
}
// FIXME: show error on ui
QUuid ExportManager::startImport(const iDescriptorDevice *device,
const QList<ImportItem> &items,
const QString &destinationPath,
std::optional<AfcClientHandle *> altAfc)
{
qDebug() << "startExport() entry - items:" << items.size()
<< "dest:" << destinationPath;
if (!device) {
qWarning() << "Invalid device provided to ExportManager";
return QUuid();
}
if (items.isEmpty()) {
qWarning() << "No items provided for export";
return QUuid();
}
// Create new job
auto job = new ImportJob();
job->jobId = QUuid::createUuid();
job->items = items;
job->destinationPath = destinationPath;
job->altAfc = altAfc;
job->d_udid = device->udid;
job->statusBalloonProcessId = StatusBalloon::sharedInstance()->startProcess(
QString("Importing %1 item(s)").arg(items.size()), items.size(),
destinationPath, ProcessType::Import);
// 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[managerJobId] = job;
}
m_exportThread->executeImportJob(job);
qDebug() << "Started import job" << managerJobId << "for" << items.size()
<< "items";
return managerJobId;
}
void ExportManager::cancelExport(const QUuid &jobId)
{
QMutexLocker locker(&m_jobsMutex);
@@ -152,3 +200,13 @@ void ExportManager::cleanupJob(const QUuid &jobId)
// qDebug() << "Cleaned up export job" << jobId;
// }
}
void ExportManager::cancelAllJobs()
{
QMutexLocker locker(&m_jobsMutex);
for (auto jobPtr : m_activeJobs) {
if (jobPtr)
jobPtr->cancelRequested = true;
}
qDebug() << "Cancellation requested for all active jobs";
}
+7 -2
View File
@@ -53,8 +53,13 @@ public:
const QString &destinationPath,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
void cancelExport(const QUuid &jobId);
QUuid startImport(const iDescriptorDevice *device,
const QList<ImportItem> &items,
const QString &destinationPath,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
void cancelExport(const QUuid &jobId);
void cancelAllJobs();
bool isJobRunning(const QUuid &jobId) const;
static QString generateUniqueOutputPath(const QString &basePath);
@@ -90,7 +95,7 @@ private:
// Thread-safe storage for active jobs
mutable QMutex m_jobsMutex;
QMap<QUuid, ExportJob *> m_activeJobs;
QMap<QUuid, JobBase *> m_activeJobs;
// Manager owns the dialog
ExportProgressDialog *m_exportProgressDialog;
+187 -53
View File
@@ -1,4 +1,3 @@
#include "exportmanagerthread.h"
#include "appcontext.h"
#include "iDescriptor.h"
@@ -11,8 +10,20 @@
// TODO: unfinished
void ExportManagerThread::executeExportJob(ExportJob *job)
{
// FIXME: limit to 1 at a time per udid/device
QtConcurrent::run([this, job]() { executeExportJobInternal(job); });
const QString udid = QString::fromStdString(job->d_udid);
QMutexLocker locker(&m_queueMutex);
QueuedJob q;
q.type = QueuedJob::Type::Export;
q.exportJob = job;
auto &queue = m_deviceQueues[udid];
queue.enqueue(q);
if (!m_deviceBusy.contains(udid)) {
m_deviceBusy.insert(udid);
startNextJobLocked(udid);
}
}
void ExportManagerThread::executeExportJobInternal(ExportJob *job)
@@ -27,23 +38,18 @@ void ExportManagerThread::executeExportJobInternal(ExportJob *job)
<< 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";
if (job->cancelRequested.load() ||
StatusBalloon::sharedInstance()->isCancelRequested(
job->statusBalloonProcessId)) {
summary.wasCancelled = true;
qDebug() << "Export job" << job->jobId << "was cancelled";
// emit exportCancelled(job->jobId);
// return;
// }
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(item, job->destinationPath, job->altAfc,
job->cancelRequested, job->statusBalloonProcessId);
@@ -55,31 +61,6 @@ void ExportManagerThread::executeExportJobInternal(ExportJob *job)
}
emit itemExported(job->statusBalloonProcessId, 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
@@ -93,7 +74,7 @@ void ExportManagerThread::executeExportJobInternal(ExportJob *job)
ExportResult ExportManagerThread::exportSingleItem(
const ExportItem &item, const QString &destinationDir,
std::optional<AfcClientHandle *> altAfc, std::atomic<bool> &cancelRequested,
const QUuid &statusBalloonProcessId) // Change parameter name and type
const QUuid &statusBalloonProcessId)
{
ExportResult result;
result.sourceFilePath = item.sourcePathOnDevice;
@@ -106,16 +87,13 @@ ExportResult ExportManagerThread::exportSingleItem(
// Progress callback
const QString &currentFile = item.suggestedFileName;
int fileIndex = item.itemIndex;
auto progressCallback =
[this, statusBalloonProcessId, fileIndex,
currentFile](qint64 transferred, // Use statusBalloonProcessId
qint64 total) {
qDebug() << "Export progress callback for" << fileIndex
<< "- transferred:" << transferred << "total:" << total;
emit fileTransferProgress(statusBalloonProcessId, fileIndex,
currentFile, transferred, total);
};
auto progressCallback = [this, statusBalloonProcessId,
currentFile](qint64 transferred, qint64 total) {
qDebug() << "Export progress-transferred:" << transferred
<< "total:" << total;
emit fileTransferProgress(statusBalloonProcessId, currentFile,
transferred, total);
};
qDebug() << "About to export file from device:" << item.sourcePathOnDevice
<< "to" << outputPath;
@@ -134,7 +112,8 @@ ExportResult ExportManagerThread::exportSingleItem(
// Export file using ServiceManager
IdeviceFfiError *err = ServiceManager::exportFileToPath(
device, item.sourcePathOnDevice.toUtf8().constData(),
outputPath.toUtf8().constData(), progressCallback, &cancelRequested);
outputPath.toUtf8().constData(), progressCallback, &cancelRequested,
altAfc);
if (err != nullptr) {
result.errorMessage =
@@ -151,6 +130,128 @@ ExportResult ExportManagerThread::exportSingleItem(
return result;
}
void ExportManagerThread::executeImportJob(ImportJob *job)
{
const QString udid = QString::fromStdString(job->d_udid);
QMutexLocker locker(&m_queueMutex);
QueuedJob q;
q.type = QueuedJob::Type::Import;
q.importJob = job;
auto &queue = m_deviceQueues[udid];
queue.enqueue(q);
if (!m_deviceBusy.contains(udid)) {
m_deviceBusy.insert(udid);
startNextJobLocked(udid);
}
}
void ExportManagerThread::executeImportJobInternal(ImportJob *job)
{
qDebug() << "Worker thread started for import job" << job->jobId;
ExportJobSummary summary;
summary.jobId = job->jobId;
summary.totalItems = job->items.size();
summary.destinationPath = job->destinationPath;
qDebug() << "Executing import job" << job->jobId << "with"
<< job->items.size() << "items";
for (int i = 0; i < job->items.size(); ++i) {
if (job->cancelRequested.load() ||
StatusBalloon::sharedInstance()->isCancelRequested(
job->statusBalloonProcessId)) {
summary.wasCancelled = true;
qDebug() << "Import job" << job->jobId << "was cancelled";
emit exportCancelled(job->jobId);
return;
}
const ImportItem &item = job->items.at(i);
ImportResult result =
importSingleItem(item, job->destinationPath, job->altAfc,
job->cancelRequested, job->statusBalloonProcessId);
if (result.success) {
summary.successfulItems++;
summary.totalBytesTransferred += result.bytesTransferred;
} else {
summary.failedItems++;
}
emit itemImported(job->statusBalloonProcessId, result);
}
qDebug() << "Import job" << job->jobId
<< "completed - Success:" << summary.successfulItems
<< "Failed:" << summary.failedItems
<< "Bytes:" << summary.totalBytesTransferred;
emit exportFinished(job->jobId, summary);
}
ImportResult ExportManagerThread::importSingleItem(
const ImportItem &item, const QString &destinationDir,
std::optional<AfcClientHandle *> altAfc, std::atomic<bool> &cancelRequested,
const QUuid &statusBalloonProcessId)
{
ImportResult result;
result.sourceFilePath = item.sourcePathOnDevice;
// Generate output path
QString outputPath = QDir(destinationDir).filePath(item.suggestedFileName);
outputPath = generateUniqueOutputPath(outputPath);
result.outputFilePath = outputPath;
// Progress callback
const QString &currentFile = item.suggestedFileName;
auto progressCallback = [this, statusBalloonProcessId,
currentFile](qint64 transferred, qint64 total) {
qDebug() << "Import progress-transferred:" << transferred
<< "total:" << total;
emit fileTransferProgress(statusBalloonProcessId, currentFile,
transferred, total);
};
qDebug() << "About to import file from device:" << item.sourcePathOnDevice
<< "to" << outputPath;
iDescriptorDevice *device =
AppContext::sharedInstance()->getDevice(item.d_udid);
if (!device) {
result.errorMessage = QString("Device with UDID %1 not found")
.arg(QString::fromStdString(item.d_udid));
qDebug() << result.errorMessage;
return result;
}
// Import file using ServiceManager
IdeviceFfiError *err = ServiceManager::importFileToPath(
device, item.sourcePathOnDevice.toUtf8().constData(),
outputPath.toUtf8().constData(), progressCallback, &cancelRequested,
altAfc);
if (err != nullptr) {
result.errorMessage =
QString("Failed to import 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)) {
@@ -175,4 +276,37 @@ QString ExportManagerThread::generateUniqueOutputPath(const QString &basePath)
} while (QFile::exists(uniquePath) && counter < 10000);
return uniquePath;
}
void ExportManagerThread::startNextJobLocked(const QString &udid)
{
auto it = m_deviceQueues.find(udid);
if (it == m_deviceQueues.end() || it->isEmpty()) {
m_deviceQueues.remove(udid);
m_deviceBusy.remove(udid);
return;
}
QueuedJob job = it->head();
QtConcurrent::run([this, udid, job]() {
if (job.type == QueuedJob::Type::Export) {
executeExportJobInternal(job.exportJob);
} else {
executeImportJobInternal(job.importJob);
}
// schedule dequeue, start on this object's thread
QMetaObject::invokeMethod(
this,
[this, udid]() {
QMutexLocker locker(&m_queueMutex);
auto it = m_deviceQueues.find(udid);
if (it != m_deviceQueues.end() && !it->isEmpty()) {
it->dequeue();
}
startNextJobLocked(udid);
},
Qt::QueuedConnection);
});
}
+25 -3
View File
@@ -2,8 +2,11 @@
#define EXPORTMANAGERTHREAD_H
#include "iDescriptor.h"
#include "servicemanager.h"
#include "statusballoon.h"
#include <QDebug>
#include <QDir>
#include <QHash>
#include <QQueue>
#include <QThread>
class ExportManager;
@@ -22,17 +25,36 @@ public:
std::optional<AfcClientHandle *> altAfc,
std::atomic<bool> &cancelRequested,
const QUuid &statusBalloonProcessId);
void executeImportJob(ImportJob *job);
ImportResult importSingleItem(const ImportItem &item,
const QString &destinationDir,
std::optional<AfcClientHandle *> altAfc,
std::atomic<bool> &cancelRequested,
const QUuid &statusBalloonProcessId);
static QString generateUniqueOutputPath(const QString &basePath);
private:
void executeExportJobInternal(ExportJob *job);
QString generateUniqueOutputPath(const QString &basePath);
void executeImportJobInternal(ImportJob *job);
struct QueuedJob {
enum class Type { Export, Import } type;
ExportJob *exportJob = nullptr;
ImportJob *importJob = nullptr;
};
QMutex m_queueMutex;
QHash<QString, QQueue<QueuedJob>> m_deviceQueues;
QSet<QString> m_deviceBusy;
void startNextJobLocked(const QString &udid);
signals:
void exportProgress(const QUuid &jobId, int currentItem, int totalItems,
const QString &currentFileName);
void fileTransferProgress(const QUuid &jobId, int fileIndex,
const QString &currentFile,
void fileTransferProgress(const QUuid &jobId, const QString &currentFile,
qint64 bytesTransferred, qint64 totalFileSize);
void itemExported(const QUuid &jobId, const ExportResult &result);
void itemImported(const QUuid &jobId, const ImportResult &result);
void exportFinished(const QUuid &jobId, const ExportJobSummary &summary);
void exportCancelled(const QUuid &jobId);
};
+2 -10
View File
@@ -276,13 +276,9 @@ void GalleryWidget::onExportSelected()
}
QList<ExportItem> exportItems;
// FIXME: index
int index = 0;
for (const QString &filePath : filePaths) {
QString fileName = filePath.split('/').last();
exportItems.append(
ExportItem(filePath, fileName, m_device->udid, index));
++index;
exportItems.append(ExportItem(filePath, fileName, m_device->udid));
}
qDebug() << "Starting export of selected files:" << exportItems.size()
@@ -336,14 +332,10 @@ void GalleryWidget::onExportAll()
return;
}
// FIXME: index
int index = 0;
QList<ExportItem> exportItems;
for (const QString &filePath : filePaths) {
QString fileName = filePath.split('/').last();
exportItems.append(
ExportItem(filePath, fileName, m_device->udid, index));
++index;
exportItems.append(ExportItem(filePath, fileName, m_device->udid));
}
qDebug() << "Starting export of:" << exportItems.size() << "items to"
+156 -2
View File
@@ -28,10 +28,14 @@
#include <QMainWindow>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPalette>
#include <QPropertyAnimation>
#include <QScreen>
#include <QSlider>
#include <QSplitter>
#include <QSplitterHandle>
#include <QStyleHints>
#include <QStyleOption>
#include <QWheelEvent>
#include <QWidget>
@@ -62,6 +66,20 @@
#endif
#define THUMBNAIL_SIZE QSize(128, 128)
#define MIN_MAIN_WINDOW_SIZE QSize(900, 600)
inline bool isDarkMode()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
const auto scheme = QGuiApplication::styleHints()->colorScheme();
return scheme == Qt::ColorScheme::Dark;
#else
const QPalette defaultPalette;
const auto text = defaultPalette.color(QPalette::WindowText);
const auto window = defaultPalette.color(QPalette::Window);
return text.lightness() > window.lightness();
#endif // QT_VERSION
}
inline QString mergeStyles(QWidget *widget, const QString &newStyles)
{
@@ -75,8 +93,6 @@ inline QString mergeStyles(QWidget *widget, const QString &newStyles)
return existing + "\n" + newStyles;
}
#define MIN_MAIN_WINDOW_SIZE QSize(900, 600)
class ResponsiveGraphicsView : public QGraphicsView
{
public:
@@ -484,4 +500,142 @@ protected:
// Let the base class handle the rest of the event
QSlider::mousePressEvent(event);
}
};
class IDLoadingIconLabel : public QLabel
{
Q_OBJECT
Q_PROPERTY(qreal shimmerOffset READ shimmerOffset WRITE setShimmerOffset)
public:
explicit IDLoadingIconLabel(QWidget *parent = nullptr) : QLabel(parent)
{
setFixedSize(64, 64);
setAlignment(Qt::AlignCenter);
initAnimation();
}
~IDLoadingIconLabel() override { stopLoading(); }
qreal shimmerOffset() const { return m_shimmerOffset; }
void setShimmerOffset(qreal offset)
{
m_shimmerOffset = offset;
update();
}
void setLoadedPixmap(const QPixmap &pixmap)
{
stopLoading();
setPixmap(pixmap);
update();
}
void setLoadFailed()
{
stopLoading();
setPixmap(QPixmap());
m_failed = true;
update();
}
protected:
void paintEvent(QPaintEvent *event) override
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QRectF r = rect().adjusted(2, 2, -2, -2);
QPainterPath path;
path.addRoundedRect(r, 16, 16);
painter.setClipPath(path);
const bool dark = isDarkMode();
// shadcn-like neutral palette
const QColor base = dark ? QColor("#27272a") // zinc-900
: QColor("#e5e7eb"); // zinc-200
const QColor highlight = dark ? QColor("#3f3f46") // zinc-800
: QColor("#f4f4f5"); // zinc-100
if (m_animation &&
m_animation->state() == QAbstractAnimation::Running) {
// Skeleton shimmer background
QLinearGradient grad(r.topLeft(), r.topRight());
const qreal center = m_shimmerOffset;
const qreal left = qMax<qreal>(0.0, center - 0.3);
const qreal right = qMin<qreal>(1.0, center + 0.3);
grad.setColorAt(0.0, base);
grad.setColorAt(left, base);
grad.setColorAt(center, highlight);
grad.setColorAt(right, base);
grad.setColorAt(1.0, base);
painter.fillRect(r, grad);
} else {
painter.fillRect(r, base);
}
if (!pixmap().isNull() &&
(!m_animation ||
m_animation->state() != QAbstractAnimation::Running)) {
QPixmap pm = pixmap();
pm.setDevicePixelRatio(devicePixelRatioF());
QPixmap scaled =
pm.scaled(r.size().toSize(), Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
painter.drawPixmap(r.topLeft(), scaled);
return;
}
QColor textColor;
if (m_failed) {
// shadcn red-500
textColor = QColor("#ef4444");
} else {
// shadcn foreground-ish
textColor = dark ? QColor("#f9fafb") // zinc-50
: QColor("#18181b"); // zinc-900
}
painter.setPen(textColor);
QFont f = font();
f.setBold(true);
painter.setFont(f);
painter.drawText(r, Qt::AlignCenter, QStringLiteral("iD"));
}
private:
void initAnimation()
{
if (m_animation)
return;
m_animation = new QPropertyAnimation(this, "shimmerOffset", this);
m_animation->setDuration(1200);
m_animation->setStartValue(0.0);
m_animation->setEndValue(1.0);
m_animation->setLoopCount(-1);
m_animation->start();
}
void stopLoading()
{
if (!m_animation)
return;
m_animation->stop();
m_animation->deleteLater();
m_animation = nullptr;
}
private:
QPropertyAnimation *m_animation = nullptr;
qreal m_shimmerOffset = 0.0;
bool m_failed = false;
};
+5
View File
@@ -168,5 +168,10 @@ public:
fileName.endsWith(".MP4", Qt::CaseInsensitive) ||
fileName.endsWith(".M4V", Qt::CaseInsensitive);
}
static bool isPreviewableFile(const QString &fileName)
{
return isGalleryFile(fileName) || isVideoFile(fileName);
}
};
} // namespace iDescriptor
+63 -21
View File
@@ -30,6 +30,7 @@
#include <idevice++/bindings.hpp>
#include <idevice++/core_device_proxy.hpp>
#include <idevice++/diagnostics_relay.hpp>
#include <idevice++/dvt/location_simulation.hpp>
#include <idevice++/dvt/remote_server.hpp>
#include <idevice++/dvt/screenshot.hpp>
#include <idevice++/ffi.hpp>
@@ -504,10 +505,7 @@ struct NetworkDevice {
QPixmap load_heic(const QByteArray &data);
QByteArray read_afc_file_to_byte_array(const iDescriptorDevice *device,
const char *path);
bool isDarkMode();
QByteArray read_afc_file_to_byte_array(AfcClientHandle *afc, const char *path);
IdeviceFfiError *_install_IPA(const iDescriptorDevice *device,
const char *filePath, const char *ipaName);
@@ -635,21 +633,6 @@ inline int read_file(const char *filename, uint8_t **data, size_t *length)
return 1;
}
struct ExportItem {
QString sourcePathOnDevice;
QString suggestedFileName;
int itemIndex = -1;
std::string d_udid;
ExportItem() = default;
ExportItem(const QString &sourcePath, const QString &fileName,
std::string d_udid, int index)
: sourcePathOnDevice(sourcePath), suggestedFileName(fileName),
d_udid(d_udid), itemIndex(index)
{
}
};
struct ExportResult {
QString sourceFilePath;
QString outputFilePath;
@@ -668,15 +651,74 @@ struct ExportJobSummary {
bool wasCancelled = false;
};
struct ExportJob {
struct ImportResult;
template <typename ResultT> class PItem
{
public:
QString sourcePathOnDevice;
QString suggestedFileName;
std::string d_udid;
std::function<void(const ResultT &)> callback;
PItem() = default;
PItem(const QString &sourcePath, const QString &fileName,
std::string d_udid,
std::function<void(const ResultT &)> callback = nullptr)
: sourcePathOnDevice(sourcePath), suggestedFileName(fileName),
d_udid(std::move(d_udid)), callback(std::move(callback))
{
}
};
struct ExportItem : public PItem<ExportResult> {
using PItem<ExportResult>::PItem;
};
struct ImportItem : public PItem<ImportResult> {
using PItem<ImportResult>::PItem;
};
class JobBase
{
public:
QUuid jobId;
QList<ExportItem> items;
QString destinationPath;
std::optional<AfcClientHandle *> altAfc;
std::atomic<bool> cancelRequested{false};
QUuid statusBalloonProcessId;
// device udid
std::string d_udid;
virtual ~JobBase() = default;
};
template <typename ItemT> class Job : public JobBase
{
public:
QList<ItemT> items;
};
// Concrete aliases
using ExportJob = Job<ExportItem>;
using ImportJob = Job<ImportItem>;
struct ImportResult {
QString sourceFilePath;
QString outputFilePath;
bool success = false;
QString errorMessage;
qint64 bytesTransferred = 0;
};
struct ImportJobSummary {
QUuid jobId;
int totalItems = 0;
int successfulItems = 0;
int failedItems = 0;
qint64 totalBytesTransferred = 0;
QString destinationPath;
bool wasCancelled = false;
};
inline QString formatFileSize(qint64 bytes)
+17 -15
View File
@@ -62,14 +62,15 @@ void ImageLoader::requestThumbnail(const iDescriptorDevice *device,
*/
void ImageLoader::requestImageWithCallback(
const iDescriptorDevice *device, const QString &path, int priority,
std::function<void(const QPixmap &)> callback)
std::function<void(const QPixmap &)> callback,
std::optional<AfcClientHandle *> altAfc)
{
/*
FIXME: priority is passed as row
nothing dangerous but a bit hacky, should be handled better
*/ //scale=false
auto *task = new ImageTask(device, path, priority, false);
auto *task = new ImageTask(device, path, priority, false, altAfc);
/*
TODO: should we do this ?
@@ -170,10 +171,11 @@ void ImageLoader::onTaskFinished(const QString &path, const QPixmap &pixmap,
// almost a copy of loadThumbnailFromDevice but without any scaling logic
QPixmap ImageLoader::loadImage(const iDescriptorDevice *device,
const QString &filePath)
const QString &filePath,
std::optional<AfcClientHandle *> altAfc)
{
QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray(
device, filePath.toUtf8().constData());
device, filePath.toUtf8().constData(), altAfc);
if (imageData.isEmpty()) {
qDebug() << "Could not read from device:" << filePath;
@@ -208,12 +210,13 @@ QPixmap ImageLoader::loadImage(const iDescriptorDevice *device,
return {};
}
QPixmap ImageLoader::loadThumbnailFromDevice(const iDescriptorDevice *device,
const QString &filePath,
const QSize &size)
QPixmap
ImageLoader::loadThumbnailFromDevice(const iDescriptorDevice *device,
const QString &filePath, const QSize &size,
std::optional<AfcClientHandle *> altAfc)
{
QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray(
device, filePath.toUtf8().constData());
device, filePath.toUtf8().constData(), altAfc);
if (imageData.isEmpty()) {
qDebug() << "Could not read from device:" << filePath;
@@ -252,18 +255,17 @@ QPixmap ImageLoader::loadThumbnailFromDevice(const iDescriptorDevice *device,
return {};
}
QPixmap
ImageLoader::generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
const QString &filePath,
const QSize &requestedSize)
QPixmap ImageLoader::generateVideoThumbnailFFmpeg(
const iDescriptorDevice *device, const QString &filePath,
const QSize &requestedSize, std::optional<AfcClientHandle *> altAfc)
{
QPixmap thumbnail;
AfcFileHandle *fileHandle = nullptr;
IdeviceFfiError *err_open = // Use distinct variable name for clarity
ServiceManager::safeAfcFileOpen(device, filePath.toUtf8().constData(),
AfcFopenMode::AfcRdOnly, &fileHandle);
IdeviceFfiError *err_open = ServiceManager::safeAfcFileOpen(
device, filePath.toUtf8().constData(), AfcFopenMode::AfcRdOnly,
&fileHandle, altAfc);
if (err_open || fileHandle == nullptr) {
qWarning() << "Failed to open video file for thumbnail:" << filePath;
+15 -12
View File
@@ -25,22 +25,25 @@ public:
}
void requestThumbnail(const iDescriptorDevice *device, const QString &path,
unsigned int row = 0);
void
requestImageWithCallback(const iDescriptorDevice *device,
const QString &path, int priority,
std::function<void(const QPixmap &)> callback);
void requestImageWithCallback(
const iDescriptorDevice *device, const QString &path, int priority,
std::function<void(const QPixmap &)> callback,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
void cancelThumbnail(const QString &path);
bool isLoading(const QString &path);
void clear();
QCache<QString, QPixmap> m_cache;
static QPixmap loadThumbnailFromDevice(const iDescriptorDevice *device,
const QString &filePath,
const QSize &size);
static QPixmap generateVideoThumbnailFFmpeg(const iDescriptorDevice *device,
const QString &filePath,
const QSize &size);
static QPixmap loadImage(const iDescriptorDevice *device,
const QString &filePath);
static QPixmap loadThumbnailFromDevice(
const iDescriptorDevice *device, const QString &filePath,
const QSize &size,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
static QPixmap generateVideoThumbnailFFmpeg(
const iDescriptorDevice *device, const QString &filePath,
const QSize &size,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
static QPixmap
loadImage(const iDescriptorDevice *device, const QString &filePath,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
signals:
void thumbnailReady(const QString &path, const QPixmap &image,
unsigned int row);
+9 -5
View File
@@ -16,8 +16,10 @@ class ImageTask : public QObject, public QRunnable
Q_OBJECT
public:
ImageTask(const iDescriptorDevice *device, const QString &path,
unsigned int row, bool scale = true)
: m_device(device), m_path(path), m_isThumbnail(scale), m_row(row)
unsigned int row, bool scale = true,
std::optional<AfcClientHandle *> altAfc = std::nullopt)
: m_device(device), m_path(path), m_isThumbnail(scale), m_row(row),
m_altAfc(altAfc)
{
setAutoDelete(false);
}
@@ -32,17 +34,18 @@ protected:
if (isVideo) {
QPixmap thumbnail = ImageLoader::generateVideoThumbnailFFmpeg(
m_device, m_path, THUMBNAIL_SIZE);
m_device, m_path, THUMBNAIL_SIZE, m_altAfc);
emit finished(m_path, thumbnail, m_row);
} else {
if (m_isThumbnail) {
QPixmap image = ImageLoader::loadThumbnailFromDevice(
m_device, m_path, THUMBNAIL_SIZE);
m_device, m_path, THUMBNAIL_SIZE, m_altAfc);
emit finished(m_path, image, m_row);
} else {
qDebug() << "Loading full image for:" << m_path;
QPixmap image = ImageLoader::loadImage(m_device, m_path);
QPixmap image =
ImageLoader::loadImage(m_device, m_path, m_altAfc);
emit finished(m_path, image, m_row);
}
}
@@ -53,6 +56,7 @@ private:
QString m_path;
bool m_isThumbnail;
unsigned int m_row;
std::optional<AfcClientHandle *> m_altAfc;
};
#endif // IMAGETASK_H
+2 -2
View File
@@ -199,8 +199,8 @@ void MediaPreviewDialog::loadImage()
};
// 99999 is so that it gets the highest priority in the queue
unsigned int priority = 99999;
ImageLoader::sharedInstance().requestImageWithCallback(m_device, m_filePath,
priority, callback);
ImageLoader::sharedInstance().requestImageWithCallback(
m_device, m_filePath, priority, callback, m_afcClient);
}
void MediaPreviewDialog::loadVideo()
+4 -4
View File
@@ -276,7 +276,8 @@ void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte,
const QByteArray pathBytes = m_filePath.toUtf8();
IdeviceFfiError *err_open = ServiceManager::safeAfcFileOpen(
m_device, pathBytes.constData(), AfcRdOnly, &context->afcHandle);
m_device, pathBytes.constData(), AfcRdOnly, &context->afcHandle,
m_afcClient);
if (err_open || context->afcHandle == 0) {
qWarning() << "Failed to open file on device:" << m_filePath;
@@ -355,7 +356,7 @@ qint64 MediaStreamer::getFileSize()
AfcFileInfo info = {};
IdeviceFfiError *info_err = ServiceManager::safeAfcGetFileInfo(
m_device, pathBytes.constData(), &info);
m_device, pathBytes.constData(), &info, m_afcClient);
if (info_err || info.size == 0) {
qWarning() << "Failed to get file info for:" << m_filePath;
@@ -365,8 +366,7 @@ qint64 MediaStreamer::getFileSize()
size_t fileSize = info.size;
// FIXME : safe to free ?
// afc_file_info_free(&info);
afc_file_info_free(&info);
if (fileSize > 0) {
m_cachedFileSize = fileSize;
+43 -3
View File
@@ -7,6 +7,46 @@
#include <QSystemTrayIcon>
#include <QWidget>
class ZStatusIconWidget : public ZIconWidget
{
Q_OBJECT
public:
using ZIconWidget::ZIconWidget;
void setIndicatorVisible(bool visible)
{
if (m_indicatorVisible == visible)
return;
m_indicatorVisible = visible;
update();
}
bool isIndicatorVisible() const { return m_indicatorVisible; }
protected:
void paintEvent(QPaintEvent *event) override
{
ZIconWidget::paintEvent(event);
if (!m_indicatorVisible)
return;
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
const int radius = 5;
const int margin = 3;
QPoint center(width() - radius - margin, radius + margin);
p.setBrush(COLOR_ACCENT_BLUE);
p.setPen(Qt::NoPen);
p.drawEllipse(center, radius, radius);
}
private:
bool m_indicatorVisible = false;
};
class QBalloonTip : public QWidget
{
Q_OBJECT
@@ -17,9 +57,9 @@ public:
void updateBalloonPosition(const QPoint &pos);
void toggleBaloon(const QPoint &pos, int timeout, bool forceVisible);
void balloon(const QPoint &, int msecs);
ZIconWidget *getButton() { return m_button; }
ZIconWidget *m_button =
new ZIconWidget(QIcon(":/resources/icons/UimProcess.png"), "Processes");
ZStatusIconWidget *getButton() { return m_button; }
ZStatusIconWidget *m_button = new ZStatusIconWidget(
QIcon(":/resources/icons/UimProcess.png"), "Processes");
signals:
void messageClicked();
+128 -11
View File
@@ -123,10 +123,10 @@ QByteArray ServiceManager::safeReadAfcFileToByteArray(
const iDescriptorDevice *device, const char *path,
std::optional<AfcClientHandle *> altAfc)
{
return executeOperation<QByteArray>(
return executeAfcClientOperation<QByteArray>(
device,
[path, device]() -> QByteArray {
return read_afc_file_to_byte_array(device, path);
[path, device](AfcClientHandle *client) -> QByteArray {
return read_afc_file_to_byte_array(client, path);
},
altAfc);
}
@@ -216,25 +216,24 @@ bool ServiceManager::enableWirelessConnections(const iDescriptorDevice *device)
});
}
// fix altafc
IdeviceFfiError *ServiceManager::exportFileToPath(
const iDescriptorDevice *device, const char *device_path,
const char *local_path,
std::function<void(qint64, qint64)> progressCallback,
std::atomic<bool> *cancelRequested)
std::atomic<bool> *cancelRequested, std::optional<AfcClientHandle *> altAfc)
{
qDebug()
<< "[ServiceManager::exportFileToPath] Exporting file from device path:"
<< device_path << "to local path:" << local_path;
// FIXME : use execute afc op
return executeOperation<IdeviceFfiError *>(
return executeAfcClientOperation(
device,
[device, device_path, local_path, progressCallback,
cancelRequested]() -> IdeviceFfiError * {
cancelRequested](AfcClientHandle *afcClient) -> IdeviceFfiError * {
AfcFileHandle *afcHandle = nullptr;
IdeviceFfiError *err =
afc_file_open(device->afcClient, device_path,
AfcFopenMode::AfcRdOnly, &afcHandle);
IdeviceFfiError *err = afc_file_open(
afcClient, device_path, AfcFopenMode::AfcRdOnly, &afcHandle);
if (err != nullptr) {
qDebug() << "Failed to open file on device:" << device_path
@@ -333,7 +332,113 @@ IdeviceFfiError *ServiceManager::exportFileToPath(
}
return nullptr;
});
},
altAfc);
}
IdeviceFfiError *ServiceManager::importFileToPath(
const iDescriptorDevice *device, const char *local_path,
const char *device_path,
std::function<void(qint64, qint64)> progressCallback,
std::atomic<bool> *cancelRequested, std::optional<AfcClientHandle *> altAfc)
{
qDebug()
<< "[ServiceManager::importFileToPath] Importing file to device path:"
<< device_path << "from local path:" << local_path;
return executeAfcClientOperation(
device,
[device, local_path, device_path, progressCallback,
cancelRequested](AfcClientHandle *afc) -> IdeviceFfiError * {
AfcFileHandle *afcHandle = nullptr;
IdeviceFfiError *err = afc_file_open(
afc, device_path, AfcFopenMode::AfcWrOnly, &afcHandle);
if (err != nullptr) {
qDebug() << "Failed to open file on device for writing:"
<< device_path << "Error Code:" << err->code
<< "Message:" << err->message;
return err;
}
qDebug() << "File opened on device successfully for writing";
FILE *in = fopen(local_path, "rb");
if (!in) {
qDebug() << "Failed to open local file for reading:"
<< local_path;
IdeviceFfiError *err_close = afc_file_close(afcHandle);
if (err_close != nullptr) {
idevice_error_free(err_close);
}
return new IdeviceFfiError{1, "Failed to open local file"};
}
// 256KB chunks
const size_t CHUNK_SIZE = 256 * 1024;
uint8_t buffer[CHUNK_SIZE];
size_t bytesRead = 0;
qint64 totalBytesWritten = 0;
// Get total file size for progress
fseek(in, 0, SEEK_END);
qint64 totalFileSize = ftell(in);
fseek(in, 0, SEEK_SET);
while (true) {
// Check for cancellation
if (cancelRequested && cancelRequested->load()) {
fclose(in);
err = afc_file_close(afcHandle);
if (err != nullptr) {
idevice_error_free(err);
}
return new IdeviceFfiError{1, "Transfer cancelled"};
}
bytesRead = fread(buffer, 1, CHUNK_SIZE, in);
if (bytesRead == 0) {
if (feof(in)) {
// End of file
break;
} else {
qDebug() << "Error reading local file";
fclose(in);
IdeviceFfiError *err_close = afc_file_close(afcHandle);
if (err_close != nullptr) {
idevice_error_free(err_close);
}
return new IdeviceFfiError{1,
"Failed to read local file"};
}
}
err = afc_file_write(afcHandle, buffer, (uint32_t)bytesRead);
if (err != nullptr) {
qDebug() << "Error writing to device:" << err->message;
fclose(in);
IdeviceFfiError *err_close = afc_file_close(afcHandle);
if (err_close != nullptr) {
idevice_error_free(err_close);
}
return err;
}
totalBytesWritten += bytesRead;
if (progressCallback) {
progressCallback(totalBytesWritten, totalFileSize);
}
}
fclose(in);
IdeviceFfiError *err_close = afc_file_close(afcHandle);
if (err_close != nullptr) {
qDebug() << "Failed to close AFC file:" << err_close->message;
return err_close;
}
return nullptr;
},
altAfc);
}
IdeviceFfiError *
@@ -439,3 +544,15 @@ ServiceManager::safeParseDeviceBattery(const iDescriptorDevice *device,
return nullptr;
});
}
IdeviceFfiError *
ServiceManager::deletePath(const iDescriptorDevice *device, const char *path,
std::optional<AfcClientHandle *> altAfc)
{
return executeAfcClientOperation(
device,
[path, device](AfcClientHandle *client) {
return afc_remove_path(client, path);
},
altAfc);
}
+44 -1
View File
@@ -238,6 +238,37 @@ public:
}
}
template <typename T>
static T executeAfcClientOperation(
const iDescriptorDevice *device,
std::function<T(AfcClientHandle *client)> operation,
std::optional<AfcClientHandle *> altAfc = std::nullopt)
{
try {
if (!device) {
return T{};
}
std::lock_guard<std::recursive_mutex> lock(device->mutex);
// Double-check device is still valid after acquiring lock
if (!device->afcClient) {
return T{};
}
if (altAfc && !*altAfc) {
return T{};
}
// Determine which client to use
AfcClientHandle *client = altAfc ? *altAfc : device->afcClient;
return operation(client);
} catch (const std::exception &e) {
qDebug() << "Exception in executeAfcOperation:" << e.what();
return T{};
}
}
// Specific AFC operation wrappers
static IdeviceFfiError *safeAfcReadDirectory(
const iDescriptorDevice *device, const char *path, char ***dirs,
@@ -298,7 +329,19 @@ public:
const iDescriptorDevice *device, const char *device_path,
const char *local_path,
std::function<void(qint64, qint64)> progressCallback = nullptr,
std::atomic<bool> *cancelRequested = nullptr);
std::atomic<bool> *cancelRequested = nullptr,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
static IdeviceFfiError *
importFileToPath(const iDescriptorDevice *device, const char *local_path,
const char *device_path,
std::function<void(qint64, qint64)> progressCallback,
std::atomic<bool> *cancelRequested,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
static IdeviceFfiError *
deletePath(const iDescriptorDevice *device, const char *path,
std::optional<AfcClientHandle *> altAfc = std::nullopt);
static IdeviceFfiError *
takeScreenshot(const iDescriptorDevice *device,
+72 -15
View File
@@ -155,12 +155,18 @@ StatusBalloon::StatusBalloon(QWidget *parent) : QBalloonTip(parent)
#ifdef WIN32
setAttribute(Qt::WA_TranslucentBackground);
#endif
setObjectName("StatusBalloon");
setStyleSheet("QWidget#StatusBalloon { border-radius: 8px; border: "
"1px solid #ccc; }");
// Create main layout
m_mainLayout = new QVBoxLayout();
m_mainLayout->setSpacing(8);
m_mainLayout->setContentsMargins(5, 5, 5, 5);
m_noProcesesLabel =
new QLabel("Export & Import processes will appear here", this);
m_noProcesesLabel->setAlignment(Qt::AlignCenter);
m_noProcesesLabel->setWordWrap(true);
// Header label
m_headerLabel = new QLabel("Processes");
@@ -198,38 +204,40 @@ void StatusBalloon::connectExportThreadSignals()
ExportManager *exportManager = ExportManager::sharedInstance();
connect(exportManager->m_exportThread, &ExportManagerThread::exportFinished,
this, &StatusBalloon::onExportFinished);
this, &StatusBalloon::onExportFinished); //?
connect(exportManager->m_exportThread, &ExportManagerThread::itemExported,
this, &StatusBalloon::onItemExported);
connect(exportManager->m_exportThread, &ExportManagerThread::itemImported,
this, &StatusBalloon::onItemImported);
connect(exportManager->m_exportThread,
&ExportManagerThread::fileTransferProgress, this,
&StatusBalloon::onFileTransferProgress);
// QTimer::singleShot(3000, this, [this]() {
// // test
// startExportProcess("Test Export Process", 10,
// "/path/to/destination");
// startProcess("Test Export Process", 10, "/path/to/destination",
// ProcessType::Export);
// });
}
void StatusBalloon::onFileTransferProgress(const QUuid &processId,
int currentItem,
const QString &currentFile,
qint64 bytesTransferred,
qint64 totalBytes)
{
qDebug() << "StatusBalloon::updateProcessProgress";
// QMutexLocker locker(&m_processesMutex);
// FIXME
// QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
ProcessItem *item = m_processes[processId];
if (!item) {
qDebug() << "StatusBalloon::updateProcessProgress: unknown processId"
<< processId;
return;
}
ProcessItem *item = m_processes[processId];
item->completedItems = currentItem;
item->currentFile = currentFile;
item->transferredBytes = bytesTransferred;
item->totalBytes = totalBytes;
@@ -301,17 +309,51 @@ void StatusBalloon::onItemExported(const QUuid &processId,
updateHeader();
}
QUuid StatusBalloon::startExportProcess(const QString &title, int totalItems,
const QString &destinationPath)
void StatusBalloon::onItemImported(const QUuid &processId,
const ImportResult &result)
{
qDebug() << "StatusBalloon::onItemImported entry:" << processId
<< "Success:" << result.success;
QMutexLocker locker(&m_processesMutex);
if (!m_processes.contains(processId)) {
qDebug() << "StatusBalloon::onItemImported: unknown processId"
<< processId;
return;
}
ProcessItem *item = m_processes[processId];
if (result.success) {
item->completedItems += 1;
} else {
item->failedItems += 1;
}
if (item->completedItems + item->failedItems == item->totalItems) {
// meaning all items are processed, but we don't know if the overall
// status is
if (item->failedItems > 0) {
item->status = ProcessStatus::Failed;
} else {
item->status = ProcessStatus::Completed;
}
}
handleJobUpdate(item);
updateHeader();
}
QUuid StatusBalloon::startProcess(const QString &title, int totalItems,
const QString &destinationPath,
ProcessType type)
{
qDebug() << "StatusBalloon::startExportProcess entry:" << title
<< totalItems << destinationPath;
handleShow(); // ensure balloon is visible when process starts
handleShow(true); // ensure balloon is visible when process starts
auto *item = new ProcessItem();
item->processId = QUuid::createUuid();
item->type = ProcessType::Export;
item->type = type;
item->status = ProcessStatus::Running;
item->title = title;
item->totalItems = totalItems;
@@ -333,6 +375,10 @@ QUuid StatusBalloon::startExportProcess(const QString &title, int totalItems,
createProcessWidget(item);
updateHeader();
// show blue dot when there is at least one running process
if (m_button)
m_button->setIndicatorVisible(true);
return item->processId;
}
@@ -380,10 +426,10 @@ void StatusBalloon::updateHeader()
void StatusBalloon::handleShow(bool forceVisible)
{
QPoint buttonBottomCenter = m_button->mapToGlobal(
QPoint pos = m_button->mapToGlobal(
QPoint(m_button->width() / 2, m_button->height()));
toggleBaloon(buttonBottomCenter, -1, forceVisible);
toggleBaloon(pos, -1, forceVisible);
}
bool StatusBalloon::isProcessRunning(const QUuid &processId) const
@@ -465,9 +511,12 @@ void StatusBalloon::removeProcessWidget(const QUuid &processId)
item->processWidget->deleteLater();
}
// delete item;
m_processes.remove(processId);
// hide dot if no active processes left
if (m_button && !hasActiveProcesses())
m_button->setIndicatorVisible(false);
if (m_processes.isEmpty()) {
hide();
}
@@ -481,6 +530,7 @@ void StatusBalloon::handleJobUpdate(ProcessItem *item)
QString statusText;
if (item->status == ProcessStatus::Running) {
if (!item->currentFile.isEmpty()) {
// FIXME :Exporting... filename.ext or / Importing ... filename.ext
statusText = item->currentFile;
} else {
statusText = "Processing...";
@@ -523,8 +573,15 @@ void StatusBalloon::resizeEvent(QResizeEvent *event)
if (!m_noProcesesLabel)
return;
const int margin = 10;
int maxWidth = qMax(0, width() - 2 * margin);
m_noProcesesLabel->setMaximumWidth(maxWidth);
m_noProcesesLabel->adjustSize();
int x = (width() - m_noProcesesLabel->width()) / 2;
int y = (height() - m_noProcesesLabel->height()) / 2;
x = qMax(margin, x);
y = qMax(margin, y);
m_noProcesesLabel->move(x, y);
}
+5 -4
View File
@@ -18,7 +18,7 @@
#include <atomic>
class BalloonProcess;
enum class ProcessType { Export, Upload };
enum class ProcessType { Export, Import };
enum class ProcessStatus { Queued, Running, Completed, Failed, Cancelled };
@@ -71,10 +71,10 @@ public:
static StatusBalloon *sharedInstance();
// Process management
QUuid startExportProcess(const QString &title, int totalItems,
const QString &destinationPath);
QUuid startProcess(const QString &title, int totalItems,
const QString &destinationPath, ProcessType type);
void onFileTransferProgress(const QUuid &processId, int currentItem,
void onFileTransferProgress(const QUuid &processId,
const QString &currentFile,
qint64 bytesTransferred, qint64 totalBytes);
@@ -100,6 +100,7 @@ private:
void onExportFinished(const QUuid &processId,
const ExportJobSummary &summary);
void onItemExported(const QUuid &processId, const ExportResult &result);
void onItemImported(const QUuid &processId, const ImportResult &result);
void handleJobUpdate(ProcessItem *item);
QVBoxLayout *m_mainLayout;