From b9899786680ee8c9d8d1c7a962aaf2f952e7222a Mon Sep 17 00:00:00 2001 From: uncor3 Date: Fri, 20 Mar 2026 03:05:57 +0000 Subject: [PATCH] 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. --- src/afcexplorerwidget.cpp | 319 ++++++------------ src/afcexplorerwidget.h | 12 +- src/appcontext.cpp | 97 +++--- src/appcontext.h | 1 + src/appswidget.cpp | 59 ++-- .../helpers/read_afc_file_to_byte_array.cpp | 25 +- src/diskusagewidget.h | 1 + src/exportalbum.cpp | 4 +- src/exportmanager.cpp | 68 +++- src/exportmanager.h | 9 +- src/exportmanagerthread.cpp | 240 ++++++++++--- src/exportmanagerthread.h | 28 +- src/gallerywidget.cpp | 12 +- src/iDescriptor-ui.h | 158 ++++++++- src/iDescriptor-utils.h | 5 + src/iDescriptor.h | 84 +++-- src/imageloader.cpp | 32 +- src/imageloader.h | 27 +- src/imagetask.h | 14 +- src/mediapreviewdialog.cpp | 4 +- src/mediastreamer.cpp | 8 +- src/qballoontip.h | 46 ++- src/servicemanager.cpp | 139 +++++++- src/servicemanager.h | 45 ++- src/statusballoon.cpp | 87 ++++- src/statusballoon.h | 9 +- 26 files changed, 1054 insertions(+), 479 deletions(-) diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp index dd59edf..f261dd9 100644 --- a/src/afcexplorerwidget.cpp +++ b/src/afcexplorerwidget.cpp @@ -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 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 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(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 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 selectedItems = m_fileList->selectedItems(); + if (selectedItems.isEmpty()) + return; + + QString currPath = "/"; + if (!m_history.isEmpty()) + currPath = m_history.top(); + if (!currPath.endsWith("/")) + currPath += "/"; + + QList 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); }); + } +} \ No newline at end of file diff --git a/src/afcexplorerwidget.h b/src/afcexplorerwidget.h index b448e18..4516d80 100644 --- a/src/afcexplorerwidget.h +++ b/src/afcexplorerwidget.h @@ -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 diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 0a98ad6..99b3846 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -21,8 +21,8 @@ #include "devicemonitor.h" #include "iDescriptor.h" #include "mainwindow.h" -// #include "settingsmanager.h" #include "networkdevicemanager.h" +#include "settingsmanager.h" #include #include #include @@ -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 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; +} \ No newline at end of file diff --git a/src/appcontext.h b/src/appcontext.h index 4817851..88dda01 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -59,6 +59,7 @@ private: QMap 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, diff --git a/src/appswidget.cpp b/src/appswidget.cpp index 222efc9..0501f1a 100644 --- a/src/appswidget.cpp +++ b/src/appswidget.cpp @@ -537,12 +537,8 @@ void AppsWidget::createAppCard( cardLayout->setSpacing(10); // App icon - QLabel *iconLabel = new QLabel(); - QPointer safeIconLabel = iconLabel; - QPixmap placeholderIcon = QApplication::style() - ->standardIcon(QStyle::SP_ComputerIcon) - .pixmap(64, 64); - iconLabel->setPixmap(placeholderIcon); + IDLoadingIconLabel *iconLabel = new IDLoadingIconLabel(); + QPointer 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(); } }); } diff --git a/src/core/helpers/read_afc_file_to_byte_array.cpp b/src/core/helpers/read_afc_file_to_byte_array.cpp index 197ce82..abe9668 100644 --- a/src/core/helpers/read_afc_file_to_byte_array.cpp +++ b/src/core/helpers/read_afc_file_to_byte_array.cpp @@ -22,35 +22,32 @@ #include #include -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(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 diff --git a/src/diskusagewidget.h b/src/diskusagewidget.h index b6a5c5e..8a3b1ba 100644 --- a/src/diskusagewidget.h +++ b/src/diskusagewidget.h @@ -20,6 +20,7 @@ #ifndef DISKUSAGEWIDGET_H #define DISKUSAGEWIDGET_H #include "diskusagebar.h" +#include "iDescriptor-ui.h" #include "iDescriptor.h" #include "qprocessindicator.h" diff --git a/src/exportalbum.cpp b/src/exportalbum.cpp index 6a79f0b..95d723f 100644 --- a/src/exportalbum.cpp +++ b/src/exportalbum.cpp @@ -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; diff --git a/src/exportmanager.cpp b/src/exportmanager.cpp index 1d3843a..d242549 100644 --- a/src/exportmanager.cpp +++ b/src/exportmanager.cpp @@ -52,6 +52,7 @@ ExportManager::~ExportManager() m_activeJobs.clear(); } +// FIXME: show error on ui QUuid ExportManager::startExport(const iDescriptorDevice *device, const QList &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 &items, + const QString &destinationPath, + std::optional 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::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"; +} \ No newline at end of file diff --git a/src/exportmanager.h b/src/exportmanager.h index fb37373..46e0475 100644 --- a/src/exportmanager.h +++ b/src/exportmanager.h @@ -53,8 +53,13 @@ public: const QString &destinationPath, std::optional altAfc = std::nullopt); - void cancelExport(const QUuid &jobId); + QUuid startImport(const iDescriptorDevice *device, + const QList &items, + const QString &destinationPath, + std::optional 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 m_activeJobs; + QMap m_activeJobs; // Manager owns the dialog ExportProgressDialog *m_exportProgressDialog; diff --git a/src/exportmanagerthread.cpp b/src/exportmanagerthread.cpp index 2c50b86..0c6d51c 100644 --- a/src/exportmanagerthread.cpp +++ b/src/exportmanagerthread.cpp @@ -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 altAfc, std::atomic &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 ¤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); - }; + 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 altAfc, std::atomic &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 ¤tFile = 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); + }); } \ No newline at end of file diff --git a/src/exportmanagerthread.h b/src/exportmanagerthread.h index b28f037..50903e9 100644 --- a/src/exportmanagerthread.h +++ b/src/exportmanagerthread.h @@ -2,8 +2,11 @@ #define EXPORTMANAGERTHREAD_H #include "iDescriptor.h" #include "servicemanager.h" +#include "statusballoon.h" #include #include +#include +#include #include class ExportManager; @@ -22,17 +25,36 @@ public: std::optional altAfc, std::atomic &cancelRequested, const QUuid &statusBalloonProcessId); + void executeImportJob(ImportJob *job); + ImportResult importSingleItem(const ImportItem &item, + const QString &destinationDir, + std::optional altAfc, + std::atomic &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> m_deviceQueues; + QSet m_deviceBusy; + + void startNextJobLocked(const QString &udid); signals: void exportProgress(const QUuid &jobId, int currentItem, int totalItems, const QString ¤tFileName); - void fileTransferProgress(const QUuid &jobId, int fileIndex, - const QString ¤tFile, + void fileTransferProgress(const QUuid &jobId, const QString ¤tFile, 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); }; diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp index 950c2a9..cdb9f85 100644 --- a/src/gallerywidget.cpp +++ b/src/gallerywidget.cpp @@ -276,13 +276,9 @@ void GalleryWidget::onExportSelected() } QList 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 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" diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h index 690ee82..82687d5 100644 --- a/src/iDescriptor-ui.h +++ b/src/iDescriptor-ui.h @@ -28,10 +28,14 @@ #include #include #include +#include +#include +#include #include #include #include #include +#include #include #include #include @@ -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(0.0, center - 0.3); + const qreal right = qMin(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; }; \ No newline at end of file diff --git a/src/iDescriptor-utils.h b/src/iDescriptor-utils.h index 7a34a1a..945b65a 100644 --- a/src/iDescriptor-utils.h +++ b/src/iDescriptor-utils.h @@ -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 \ No newline at end of file diff --git a/src/iDescriptor.h b/src/iDescriptor.h index 95fa529..0b2f796 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -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 class PItem +{ +public: + QString sourcePathOnDevice; + QString suggestedFileName; + std::string d_udid; + std::function callback; + + PItem() = default; + PItem(const QString &sourcePath, const QString &fileName, + std::string d_udid, + std::function callback = nullptr) + : sourcePathOnDevice(sourcePath), suggestedFileName(fileName), + d_udid(std::move(d_udid)), callback(std::move(callback)) + { + } +}; + +struct ExportItem : public PItem { + using PItem::PItem; +}; + +struct ImportItem : public PItem { + using PItem::PItem; +}; + +class JobBase +{ +public: QUuid jobId; - QList items; QString destinationPath; std::optional altAfc; std::atomic cancelRequested{false}; QUuid statusBalloonProcessId; // device udid std::string d_udid; + + virtual ~JobBase() = default; +}; + +template class Job : public JobBase +{ +public: + QList items; +}; + +// Concrete aliases +using ExportJob = Job; +using ImportJob = Job; + +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) diff --git a/src/imageloader.cpp b/src/imageloader.cpp index edf75cf..dc2378d 100644 --- a/src/imageloader.cpp +++ b/src/imageloader.cpp @@ -62,14 +62,15 @@ void ImageLoader::requestThumbnail(const iDescriptorDevice *device, */ void ImageLoader::requestImageWithCallback( const iDescriptorDevice *device, const QString &path, int priority, - std::function callback) + std::function callback, + std::optional 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 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 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 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; diff --git a/src/imageloader.h b/src/imageloader.h index 84cd25d..be7bab4 100644 --- a/src/imageloader.h +++ b/src/imageloader.h @@ -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 callback); + void requestImageWithCallback( + const iDescriptorDevice *device, const QString &path, int priority, + std::function callback, + std::optional altAfc = std::nullopt); void cancelThumbnail(const QString &path); bool isLoading(const QString &path); void clear(); QCache 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 altAfc = std::nullopt); + static QPixmap generateVideoThumbnailFFmpeg( + const iDescriptorDevice *device, const QString &filePath, + const QSize &size, + std::optional altAfc = std::nullopt); + static QPixmap + loadImage(const iDescriptorDevice *device, const QString &filePath, + std::optional altAfc = std::nullopt); signals: void thumbnailReady(const QString &path, const QPixmap &image, unsigned int row); diff --git a/src/imagetask.h b/src/imagetask.h index a5cf50a..958db99 100644 --- a/src/imagetask.h +++ b/src/imagetask.h @@ -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 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 m_altAfc; }; #endif // IMAGETASK_H diff --git a/src/mediapreviewdialog.cpp b/src/mediapreviewdialog.cpp index fbcdac4..3137c7c 100644 --- a/src/mediapreviewdialog.cpp +++ b/src/mediapreviewdialog.cpp @@ -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() diff --git a/src/mediastreamer.cpp b/src/mediastreamer.cpp index 22b1526..b89a926 100644 --- a/src/mediastreamer.cpp +++ b/src/mediastreamer.cpp @@ -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; diff --git a/src/qballoontip.h b/src/qballoontip.h index 5894092..a26f5fd 100644 --- a/src/qballoontip.h +++ b/src/qballoontip.h @@ -7,6 +7,46 @@ #include #include +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(); diff --git a/src/servicemanager.cpp b/src/servicemanager.cpp index 1d125b2..449ab4c 100644 --- a/src/servicemanager.cpp +++ b/src/servicemanager.cpp @@ -123,10 +123,10 @@ QByteArray ServiceManager::safeReadAfcFileToByteArray( const iDescriptorDevice *device, const char *path, std::optional altAfc) { - return executeOperation( + return executeAfcClientOperation( 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 progressCallback, - std::atomic *cancelRequested) + std::atomic *cancelRequested, std::optional altAfc) { qDebug() << "[ServiceManager::exportFileToPath] Exporting file from device path:" << device_path << "to local path:" << local_path; - // FIXME : use execute afc op - return executeOperation( + 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 progressCallback, + std::atomic *cancelRequested, std::optional 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 altAfc) +{ + return executeAfcClientOperation( + device, + [path, device](AfcClientHandle *client) { + return afc_remove_path(client, path); + }, + altAfc); +} \ No newline at end of file diff --git a/src/servicemanager.h b/src/servicemanager.h index f0fa37c..bc84d33 100644 --- a/src/servicemanager.h +++ b/src/servicemanager.h @@ -238,6 +238,37 @@ public: } } + template + static T executeAfcClientOperation( + const iDescriptorDevice *device, + std::function operation, + std::optional altAfc = std::nullopt) + { + try { + if (!device) { + return T{}; + } + + std::lock_guard 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 progressCallback = nullptr, - std::atomic *cancelRequested = nullptr); + std::atomic *cancelRequested = nullptr, + std::optional altAfc = std::nullopt); + + static IdeviceFfiError * + importFileToPath(const iDescriptorDevice *device, const char *local_path, + const char *device_path, + std::function progressCallback, + std::atomic *cancelRequested, + std::optional altAfc = std::nullopt); + + static IdeviceFfiError * + deletePath(const iDescriptorDevice *device, const char *path, + std::optional altAfc = std::nullopt); static IdeviceFfiError * takeScreenshot(const iDescriptorDevice *device, diff --git a/src/statusballoon.cpp b/src/statusballoon.cpp index 371f8d0..239b183 100644 --- a/src/statusballoon.cpp +++ b/src/statusballoon.cpp @@ -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 ¤tFile, 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); } diff --git a/src/statusballoon.h b/src/statusballoon.h index 7836b30..1416c87 100644 --- a/src/statusballoon.h +++ b/src/statusballoon.h @@ -18,7 +18,7 @@ #include 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 ¤tFile, 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;