mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
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:
+108
-211
@@ -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); });
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 ¤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<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 ¤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);
|
||||
});
|
||||
}
|
||||
@@ -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 ¤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);
|
||||
};
|
||||
|
||||
+2
-10
@@ -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
@@ -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;
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
+5
-4
@@ -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 ¤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;
|
||||
|
||||
Reference in New Issue
Block a user