mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-22 03:45:51 +08:00
implement callbacks for export/import jobs and fix ui bugs
- Updated IOManagerClient to include optional completion callbacks for export and import methods. - Fix a bug that happens on wayland in statusbaloon
This commit is contained in:
+80
-75
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
|
||||
#include "afcexplorerwidget.h"
|
||||
// #include "exportmanager.h"
|
||||
#include "iDescriptor-ui.h"
|
||||
#include "iDescriptor.h"
|
||||
#include "mediapreviewdialog.h"
|
||||
@@ -152,8 +151,8 @@ void AfcExplorerWidget::onItemDoubleClicked(QListWidgetItem *item)
|
||||
} else {
|
||||
const bool isPreviewable = iDescriptor::Utils::isPreviewableFile(name);
|
||||
if (isPreviewable) {
|
||||
auto *previewDialog =
|
||||
new MediaPreviewDialog(m_device, nextPath, m_hauseArrest);
|
||||
auto *previewDialog = new MediaPreviewDialog(
|
||||
m_device, nextPath, m_hauseArrest, m_useAfc2, this);
|
||||
previewDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
previewDialog->show();
|
||||
} else {
|
||||
@@ -349,8 +348,6 @@ void AfcExplorerWidget::onExportClicked()
|
||||
|
||||
void AfcExplorerWidget::handleExport(QList<QListWidgetItem *> filesToExport)
|
||||
{
|
||||
|
||||
// Ask user for a directory to save all files
|
||||
QString dir =
|
||||
QFileDialog::getExistingDirectory(this, "Select Export Directory");
|
||||
if (dir.isEmpty())
|
||||
@@ -381,7 +378,7 @@ void AfcExplorerWidget::handleExport(QList<QListWidgetItem *> filesToExport)
|
||||
m_hauseArrest.value()->get_bundle_id());
|
||||
} else {
|
||||
IOManagerClient::sharedInstance()->startExport(
|
||||
m_device, exportItems, dir, "Exporting from File Explorer", true);
|
||||
m_device, exportItems, dir, "Exporting from File Explorer");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,64 +391,72 @@ void AfcExplorerWidget::exportAndOpenSelectedFile(QListWidgetItem *item,
|
||||
return;
|
||||
}
|
||||
|
||||
QString fileName = item->text();
|
||||
QList<QString> exportItems;
|
||||
QString currPath = "/";
|
||||
if (!m_history.isEmpty())
|
||||
currPath = m_history.top();
|
||||
if (!currPath.endsWith("/"))
|
||||
currPath += "/";
|
||||
QString devicePath = currPath == "/" ? "/" + fileName : currPath + fileName;
|
||||
qDebug() << "Exporting file:" << devicePath;
|
||||
|
||||
// FIXME
|
||||
// // 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, "Exporting to open file", m_afc);
|
||||
QString fileName = item->text();
|
||||
QString devicePath = currPath == "/" ? "/" + fileName : currPath + fileName;
|
||||
exportItems.append(devicePath);
|
||||
|
||||
QString localPath = QDir(directory).filePath(fileName);
|
||||
|
||||
std::function<void()> onExportFinished = [localPath]() {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(localPath));
|
||||
};
|
||||
|
||||
if (m_useAfc2) {
|
||||
IOManagerClient::sharedInstance()->startExport(
|
||||
m_device, exportItems, directory,
|
||||
"Exporting from File Explorer <AFC2>", true, onExportFinished);
|
||||
} else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) {
|
||||
IOManagerClient::sharedInstance()->startExport(
|
||||
m_device, exportItems, directory,
|
||||
"Exporting from File Explorer (App Container)",
|
||||
m_hauseArrest.value()->get_bundle_id(), onExportFinished);
|
||||
} else {
|
||||
IOManagerClient::sharedInstance()->startExport(
|
||||
m_device, exportItems, directory, "Exporting from File Explorer",
|
||||
onExportFinished);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: should be disabled if there is an error loading afc
|
||||
void AfcExplorerWidget::onImportClicked()
|
||||
{
|
||||
// FIXME
|
||||
// QStringList fileNames = QFileDialog::getOpenFileNames(this, "Import
|
||||
// Files"); if (fileNames.isEmpty())
|
||||
// return;
|
||||
QStringList fileNames = QFileDialog::getOpenFileNames(this, "Import Files");
|
||||
if (fileNames.isEmpty())
|
||||
return;
|
||||
|
||||
// QString currPath = "/";
|
||||
// if (!m_history.isEmpty())
|
||||
// currPath = m_history.top();
|
||||
// if (!currPath.endsWith("/"))
|
||||
// currPath += "/";
|
||||
QString currPath = "/";
|
||||
if (!m_history.isEmpty())
|
||||
currPath = m_history.top();
|
||||
if (!currPath.endsWith("/"))
|
||||
currPath += "/";
|
||||
|
||||
// QList<ImportItem> importItems;
|
||||
|
||||
// for (const QString &localPath : fileNames) {
|
||||
// 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());
|
||||
// });
|
||||
// }
|
||||
// }));
|
||||
// }
|
||||
// ExportManager::sharedInstance()->startImport(
|
||||
// m_device, importItems, currPath, "Importing Files", m_afc);
|
||||
QPointer safeThis(this);
|
||||
std::function<void()> onImportFinished = [this, currPath, safeThis]() {
|
||||
if (!safeThis || safeThis.isNull())
|
||||
return;
|
||||
QTimer::singleShot(100, this, [this]() {
|
||||
if (!m_history.isEmpty())
|
||||
loadPath(m_history.top());
|
||||
});
|
||||
};
|
||||
if (m_useAfc2) {
|
||||
IOManagerClient::sharedInstance()->startImport(
|
||||
m_device, fileNames, currPath, "Importing <AFC2>", true,
|
||||
onImportFinished);
|
||||
} else if (m_hauseArrest.has_value() && m_hauseArrest.value() != nullptr) {
|
||||
IOManagerClient::sharedInstance()->startImport(
|
||||
m_device, fileNames, currPath, "Importing to App Container",
|
||||
m_hauseArrest.value()->get_bundle_id(), onImportFinished);
|
||||
} else {
|
||||
IOManagerClient::sharedInstance()->startImport(
|
||||
m_device, fileNames, currPath, "Importing", onImportFinished);
|
||||
}
|
||||
}
|
||||
|
||||
void AfcExplorerWidget::setupFileExplorer()
|
||||
@@ -822,27 +827,27 @@ void AfcExplorerWidget::onDeleteClicked()
|
||||
.arg(pathsToDelete.size()),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
bool errorOccurred = false;
|
||||
// FIXME
|
||||
// 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); });
|
||||
// }
|
||||
bool success = false;
|
||||
|
||||
for (const QString &path : pathsToDelete) {
|
||||
if (m_useAfc2) {
|
||||
success = m_device->afc2_backend->delete_path(path);
|
||||
} else if (m_hauseArrest.has_value() &&
|
||||
m_hauseArrest.value() != nullptr) {
|
||||
success = m_hauseArrest.value()->delete_path(path);
|
||||
} else {
|
||||
success = m_device->afc_backend->delete_path(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
QMessageBox::critical(this, "Error",
|
||||
"Failed to delete one or more items.");
|
||||
} else {
|
||||
// Refresh the current directory after deletion
|
||||
QTimer::singleShot(100, this, [this]() {
|
||||
if (!m_history.isEmpty())
|
||||
loadPath(m_history.top());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -456,8 +456,8 @@ void GalleryWidget::setupPhotoGalleryView()
|
||||
return;
|
||||
|
||||
qDebug() << "Opening preview for" << filePath;
|
||||
auto *previewDialog =
|
||||
new MediaPreviewDialog(m_device, filePath);
|
||||
auto *previewDialog = new MediaPreviewDialog(
|
||||
m_device, filePath, std::nullopt, false, this);
|
||||
previewDialog->show();
|
||||
});
|
||||
|
||||
@@ -487,7 +487,8 @@ void GalleryWidget::onAlbumListLoaded(const QList<QString> &dcimTree)
|
||||
QString fullPath = QString("/DCIM/%1").arg(albumName);
|
||||
item->setData(fullPath, Qt::UserRole);
|
||||
|
||||
item->setIcon(QIcon::fromTheme("folder"));
|
||||
item->setIcon(QIcon(":/resources/icons/"
|
||||
"MaterialSymbolsLightImageOutlineSharp.png"));
|
||||
m_albumModel->appendRow(item);
|
||||
|
||||
loadAlbumThumbnailAsync(fullPath, item);
|
||||
|
||||
+88
-10
@@ -30,7 +30,7 @@ IOManagerClient::IOManagerClient(QObject *parent) : QObject(parent) {}
|
||||
void IOManagerClient::startExport(
|
||||
const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &exportTitle)
|
||||
const QString &exportTitle, std::optional<std::function<void()>> onComplete)
|
||||
{
|
||||
qDebug() << "startExport() entry - items:" << items.size()
|
||||
<< "dest:" << destinationPath;
|
||||
@@ -63,7 +63,8 @@ void IOManagerClient::startExport(
|
||||
QUuid jobId = QUuid::createUuid();
|
||||
|
||||
StatusBalloon::sharedInstance()->startProcess(
|
||||
exportTitle, items.size(), destinationPath, ProcessType::Export, jobId);
|
||||
exportTitle, items.size(), destinationPath, ProcessType::Export, jobId,
|
||||
onComplete);
|
||||
|
||||
AppContext::sharedInstance()->ioManager->start_export(
|
||||
device->udid, jobId, items, destinationPath);
|
||||
@@ -76,7 +77,8 @@ void IOManagerClient::startExport(
|
||||
void IOManagerClient::startExport(
|
||||
const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &exportTitle, const QString &bundleId)
|
||||
const QString &exportTitle, const QString &bundleId,
|
||||
std::optional<std::function<void()>> onComplete)
|
||||
{
|
||||
qDebug() << "startExport() hause_arrest entry - items:" << items.size()
|
||||
<< "dest:" << destinationPath;
|
||||
@@ -109,7 +111,8 @@ void IOManagerClient::startExport(
|
||||
QUuid jobId = QUuid::createUuid();
|
||||
|
||||
StatusBalloon::sharedInstance()->startProcess(
|
||||
exportTitle, items.size(), destinationPath, ProcessType::Export, jobId);
|
||||
exportTitle, items.size(), destinationPath, ProcessType::Export, jobId,
|
||||
onComplete);
|
||||
|
||||
AppContext::sharedInstance()->ioManager->start_export_with_hause_arrest_afc(
|
||||
device->udid, jobId, items, destinationPath, bundleId);
|
||||
@@ -122,9 +125,10 @@ void IOManagerClient::startExport(
|
||||
void IOManagerClient::startExport(
|
||||
const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &exportTitle, bool useAfc2)
|
||||
const QString &exportTitle, bool useAfc2,
|
||||
std::optional<std::function<void()>> onComplete)
|
||||
{
|
||||
qDebug() << "startExport() hause_arrest entry - items:" << items.size()
|
||||
qDebug() << "startExport() afc2 - items:" << items.size()
|
||||
<< "dest:" << destinationPath;
|
||||
if (!device) {
|
||||
qWarning() << "Invalid device provided to ExportManager";
|
||||
@@ -155,7 +159,8 @@ void IOManagerClient::startExport(
|
||||
QUuid jobId = QUuid::createUuid();
|
||||
|
||||
StatusBalloon::sharedInstance()->startProcess(
|
||||
exportTitle, items.size(), destinationPath, ProcessType::Export, jobId);
|
||||
exportTitle, items.size(), destinationPath, ProcessType::Export, jobId,
|
||||
onComplete);
|
||||
|
||||
AppContext::sharedInstance()->ioManager->start_export_with_afc2(
|
||||
device->udid, jobId, items, destinationPath);
|
||||
@@ -167,9 +172,9 @@ void IOManagerClient::startExport(
|
||||
void IOManagerClient::startImport(
|
||||
const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &importTitle, std::optional<bool> altAfc)
|
||||
const QString &importTitle, std::optional<std::function<void()>> onComplete)
|
||||
{
|
||||
qDebug() << "startExport() entry - items:" << items.size()
|
||||
qDebug() << "startImport() entry - items:" << items.size()
|
||||
<< "dest:" << destinationPath;
|
||||
if (!device) {
|
||||
qWarning() << "Invalid device provided to ExportManager";
|
||||
@@ -188,7 +193,8 @@ void IOManagerClient::startImport(
|
||||
QUuid jobId = QUuid::createUuid();
|
||||
|
||||
StatusBalloon::sharedInstance()->startProcess(
|
||||
importTitle, items.size(), destinationPath, ProcessType::Import, jobId);
|
||||
importTitle, items.size(), destinationPath, ProcessType::Import, jobId,
|
||||
onComplete);
|
||||
|
||||
AppContext::sharedInstance()->ioManager->start_import(
|
||||
device->udid, jobId, items, destinationPath);
|
||||
@@ -197,6 +203,78 @@ void IOManagerClient::startImport(
|
||||
<< "items";
|
||||
}
|
||||
|
||||
/* hause_arrest */
|
||||
void IOManagerClient::startImport(
|
||||
const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &importTitle, const QString &bundleId,
|
||||
std::optional<std::function<void()>> onComplete)
|
||||
{
|
||||
qDebug() << "startImport() entry - items:" << items.size()
|
||||
<< "dest:" << destinationPath;
|
||||
if (!device) {
|
||||
qWarning() << "Invalid device provided to ExportManager";
|
||||
QMessageBox::critical(nullptr, "Import Error",
|
||||
"Invalid device specified for import.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.isEmpty()) {
|
||||
qWarning() << "No items provided for export";
|
||||
QMessageBox::information(nullptr, "Import Error",
|
||||
"No items selected for import.");
|
||||
return;
|
||||
}
|
||||
|
||||
QUuid jobId = QUuid::createUuid();
|
||||
|
||||
StatusBalloon::sharedInstance()->startProcess(
|
||||
importTitle, items.size(), destinationPath, ProcessType::Import, jobId,
|
||||
onComplete);
|
||||
|
||||
AppContext::sharedInstance()->ioManager->start_import_with_hause_arrest_afc(
|
||||
device->udid, jobId, items, destinationPath, bundleId);
|
||||
|
||||
qDebug() << "Started import job" << jobId << "for" << items.size()
|
||||
<< "items";
|
||||
}
|
||||
|
||||
/* afc2 */
|
||||
void IOManagerClient::startImport(
|
||||
const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &importTitle, bool useAfc2,
|
||||
std::optional<std::function<void()>> onComplete)
|
||||
{
|
||||
qDebug() << "startImport() entry - items:" << items.size()
|
||||
<< "dest:" << destinationPath;
|
||||
if (!device) {
|
||||
qWarning() << "Invalid device provided to ExportManager";
|
||||
QMessageBox::critical(nullptr, "Import Error",
|
||||
"Invalid device specified for import.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.isEmpty()) {
|
||||
qWarning() << "No items provided for export";
|
||||
QMessageBox::information(nullptr, "Import Error",
|
||||
"No items selected for import.");
|
||||
return;
|
||||
}
|
||||
|
||||
QUuid jobId = QUuid::createUuid();
|
||||
|
||||
StatusBalloon::sharedInstance()->startProcess(
|
||||
importTitle, items.size(), destinationPath, ProcessType::Import, jobId,
|
||||
onComplete);
|
||||
|
||||
AppContext::sharedInstance()->ioManager->start_import_with_afc2(
|
||||
device->udid, jobId, items, destinationPath);
|
||||
|
||||
qDebug() << "Started import job" << jobId << "for" << items.size()
|
||||
<< "items";
|
||||
}
|
||||
|
||||
void IOManagerClient::cancel(const QUuid &jobId)
|
||||
{
|
||||
AppContext::sharedInstance()->ioManager->cancel_job(jobId);
|
||||
|
||||
+32
-15
@@ -41,24 +41,41 @@ public:
|
||||
IOManagerClient(const IOManagerClient &) = delete;
|
||||
IOManagerClient &operator=(const IOManagerClient &) = delete;
|
||||
|
||||
void startExport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items,
|
||||
const QString &destinationPath, const QString &jobTitle);
|
||||
void
|
||||
startExport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &jobTitle,
|
||||
std::optional<std::function<void()>> onComplete = std::nullopt);
|
||||
/* afc2 */
|
||||
void startExport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items,
|
||||
const QString &destinationPath, const QString &jobTitle,
|
||||
bool useAfc2);
|
||||
void
|
||||
startExport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &jobTitle, bool useAfc2,
|
||||
std::optional<std::function<void()>> onComplete = std::nullopt);
|
||||
/* hause_arrest_afc*/
|
||||
void startExport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items,
|
||||
const QString &destinationPath, const QString &exportTitle,
|
||||
const QString &bundleId);
|
||||
void
|
||||
startExport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &exportTitle, const QString &bundleId,
|
||||
std::optional<std::function<void()>> onComplete = std::nullopt);
|
||||
|
||||
void startImport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items,
|
||||
const QString &destinationPath, const QString &jobTitle,
|
||||
std::optional<bool> altAfc = std::nullopt);
|
||||
void
|
||||
startImport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &jobTitle,
|
||||
std::optional<std::function<void()>> onComplete = std::nullopt);
|
||||
/* hause_arrest_afc */
|
||||
void
|
||||
startImport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &jobTitle, const QString &bundleId,
|
||||
std::optional<std::function<void()>> onComplete = std::nullopt);
|
||||
/* afc2 */
|
||||
void
|
||||
startImport(const std::shared_ptr<iDescriptorDevice> device,
|
||||
const QList<QString> &items, const QString &destinationPath,
|
||||
const QString &jobTitle, bool useAfc2,
|
||||
std::optional<std::function<void()>> onComplete = std::nullopt);
|
||||
|
||||
void cancel(const QUuid &jobId);
|
||||
void cancelAllJobs();
|
||||
|
||||
Generated
+40
-1
@@ -768,6 +768,17 @@ version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -1218,6 +1229,7 @@ dependencies = [
|
||||
"cxx-qt",
|
||||
"cxx-qt-build",
|
||||
"cxx-qt-lib",
|
||||
"filetime",
|
||||
"futures",
|
||||
"idevice",
|
||||
"once_cell",
|
||||
@@ -1427,6 +1439,18 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.37.0"
|
||||
@@ -1623,7 +1647,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -1702,6 +1726,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.8.0"
|
||||
@@ -1905,6 +1935,15 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
|
||||
@@ -23,6 +23,7 @@ regex = "1.12.3"
|
||||
urlencoding = "2.1.3"
|
||||
serde_json = "1.0.149"
|
||||
rusqlite = { version = "0.39.0", features = ["bundled"] }
|
||||
filetime = "0.2.27"
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -13,6 +13,10 @@ pub async fn check_is_dir_and_list(
|
||||
match afc.list_dir(&path_str).await {
|
||||
Ok(list) => {
|
||||
for name in list {
|
||||
// ui already has up/down buttons maybe unnecessary
|
||||
if name == "." || name == ".." {
|
||||
continue;
|
||||
}
|
||||
let full_path = format!("{}/{}", path_str, name);
|
||||
let is_dir = match afc.get_file_info(&full_path).await {
|
||||
Ok(info) => info.st_ifmt == "S_IFDIR",
|
||||
|
||||
@@ -75,6 +75,9 @@ mod qobject {
|
||||
|
||||
#[qinvokable]
|
||||
fn is_available(self: &Afc2Backend) -> bool;
|
||||
|
||||
#[qinvokable]
|
||||
fn delete_path(self: &Afc2Backend, path: &QString) -> bool;
|
||||
}
|
||||
|
||||
impl cxx_qt::Threading for Afc2Backend {}
|
||||
@@ -210,6 +213,10 @@ impl qobject::Afc2Backend {
|
||||
|
||||
let mut qlist: QList<QString> = QList::default();
|
||||
for name in list {
|
||||
// ui already has up/down buttons maybe unnecessary
|
||||
if name == "." || name == ".." {
|
||||
continue;
|
||||
}
|
||||
qlist.append(QString::from(name));
|
||||
}
|
||||
qlist
|
||||
@@ -775,4 +782,41 @@ impl qobject::Afc2Backend {
|
||||
|
||||
QString::from(url_clone_for_log)
|
||||
}
|
||||
|
||||
fn delete_path(self: &Self, path: &QString) -> bool {
|
||||
let udid = self.get_udid().to_string();
|
||||
let path_str = path.to_string();
|
||||
|
||||
run_sync(async move {
|
||||
let afc_arc = {
|
||||
let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned();
|
||||
|
||||
let device = match maybe_device {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("delete_path: device {udid} not found");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match device.afc2 {
|
||||
Some(afc) => afc.clone(),
|
||||
None => {
|
||||
eprintln!("AFC2 service not available for device {}", udid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut afc = afc_arc.lock().await;
|
||||
|
||||
match afc.remove(path_str).await {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
eprintln!("delete_path: remove_path failed: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use cxx_qt::Threading;
|
||||
use cxx_qt_lib::{QByteArray, QList, QMap, QMapPair_QString_QVariant, QString};
|
||||
use cxx_qt_lib::{
|
||||
QByteArray, QDateTime, QList, QMap, QMapPair_QString_QVariant, QString, QTimeZone, QVariant,
|
||||
};
|
||||
|
||||
use crate::{APP_DEVICE_STATE, RUNTIME, VIDEO_STREAMS, afc, run_sync};
|
||||
use idevice::{
|
||||
@@ -22,6 +24,7 @@ mod qobject {
|
||||
include!("cxx-qt-lib/qbytearray.h");
|
||||
include!("cxx-qt-lib/qmap.h");
|
||||
include!("cxx-qt-lib/qvariant.h");
|
||||
include!("cxx-qt-lib/qdatetime.h");
|
||||
|
||||
type QString = cxx_qt_lib::QString;
|
||||
type QList_QString = cxx_qt_lib::QList<QString>;
|
||||
@@ -56,6 +59,7 @@ mod qobject {
|
||||
|
||||
#[qinvokable]
|
||||
fn check_is_dir_and_list(self: &AfcBackend, path: &QString);
|
||||
|
||||
#[qsignal]
|
||||
fn check_is_dir_and_list_finished(
|
||||
self: Pin<&mut AfcBackend>,
|
||||
@@ -74,6 +78,12 @@ mod qobject {
|
||||
|
||||
#[qinvokable]
|
||||
fn start_video_stream(self: &AfcBackend, file_path: &QString) -> QString;
|
||||
|
||||
#[qinvokable]
|
||||
fn list_dir_with_creation_date(self: &AfcBackend, path: &QString) -> QMap_QString_QVariant;
|
||||
|
||||
#[qinvokable]
|
||||
fn delete_path(self: &AfcBackend, path: &QString) -> bool;
|
||||
}
|
||||
|
||||
impl cxx_qt::Threading for AfcBackend {}
|
||||
@@ -179,6 +189,11 @@ impl qobject::AfcBackend {
|
||||
|
||||
let mut qlist: QList<QString> = QList::default();
|
||||
for name in list {
|
||||
// ui already has up/down buttons maybe unnecessary
|
||||
if name == "." || name == ".." {
|
||||
continue;
|
||||
}
|
||||
|
||||
qlist.append(QString::from(name));
|
||||
}
|
||||
qlist
|
||||
@@ -694,4 +709,99 @@ impl qobject::AfcBackend {
|
||||
|
||||
QString::from(url_clone_for_log)
|
||||
}
|
||||
|
||||
fn list_dir_with_creation_date(self: &Self, path: &QString) -> QMap<QMapPair_QString_QVariant> {
|
||||
let udid = self.get_udid().to_string();
|
||||
let dir_str = path.to_string();
|
||||
|
||||
let entries: Vec<(String, i64)> = run_sync(async move {
|
||||
let afc_arc = {
|
||||
let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned();
|
||||
|
||||
let device = match maybe_device {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("list_dir_with_creation_date: device {udid} not found");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
device.afc.clone()
|
||||
};
|
||||
|
||||
let mut afc = afc_arc.lock().await;
|
||||
|
||||
let names = match afc.list_dir(&dir_str).await {
|
||||
Ok(list) => list,
|
||||
Err(e) => {
|
||||
eprintln!("list_dir_with_creation_date: list_dir({dir_str}) failed: {e}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let mut result = Vec::new();
|
||||
for name in names {
|
||||
if name == "." || name == ".." {
|
||||
continue;
|
||||
}
|
||||
|
||||
let full_path = format!("{}/{}", dir_str, name);
|
||||
match afc.get_file_info(full_path.clone()).await {
|
||||
Ok(info) => {
|
||||
// use creation time; could also choose info.modified
|
||||
let creation_utc = info.creation.and_utc();
|
||||
let msecs = creation_utc.timestamp_millis();
|
||||
result.push((name, msecs));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"list_dir_with_creation_date: get_file_info({full_path}) failed: {e}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
});
|
||||
|
||||
// Build QMap<QString, QVariant(QDateTime)>
|
||||
let mut map: QMap<QMapPair_QString_QVariant> = QMap::default();
|
||||
for (full_path, msecs) in entries {
|
||||
let dt = QDateTime::from_msecs_since_epoch(msecs, &QTimeZone::utc());
|
||||
let var = QVariant::from(&dt);
|
||||
map.insert(QString::from(full_path), var);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
fn delete_path(self: &Self, path: &QString) -> bool {
|
||||
let udid = self.get_udid().to_string();
|
||||
let path_str = path.to_string();
|
||||
|
||||
run_sync(async move {
|
||||
let afc_arc = {
|
||||
let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned();
|
||||
|
||||
let device = match maybe_device {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("delete_path: device {udid} not found");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
device.afc.clone()
|
||||
};
|
||||
|
||||
let mut afc = afc_arc.lock().await;
|
||||
|
||||
match afc.remove(&path_str).await {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
eprintln!("delete_path: delete({path_str}) failed: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ mod qobject {
|
||||
|
||||
#[qinvokable]
|
||||
fn start_video_stream(self: &HauseArrest, file_path: &QString) -> QString;
|
||||
|
||||
#[qinvokable]
|
||||
fn delete_path(self: &HauseArrest, path: &QString) -> bool;
|
||||
}
|
||||
|
||||
impl cxx_qt::Threading for HauseArrest {}
|
||||
@@ -152,7 +155,6 @@ impl qobject::HauseArrest {
|
||||
});
|
||||
}
|
||||
|
||||
// change signature to need &mut self
|
||||
fn check_is_dir_and_list(self: &Self, path: &QString) {
|
||||
let qt_t = self.qt_thread();
|
||||
let path_str = path.to_string();
|
||||
@@ -332,4 +334,25 @@ impl qobject::HauseArrest {
|
||||
|
||||
QString::from(url_clone_for_log)
|
||||
}
|
||||
fn delete_path(&self, path: &QString) -> bool {
|
||||
let path_str = path.to_string();
|
||||
let afc_opt = self.rust().afc_handle.clone();
|
||||
|
||||
run_sync(async move {
|
||||
let Some(afc_handle) = afc_opt else {
|
||||
eprintln!("HouseArrest: AfcClient not initialized");
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut afc_client = afc_handle.lock().await;
|
||||
|
||||
match afc_client.remove(&path_str).await {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
eprintln!("delete_path: failed to delete {}: {}", path_str, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+23
-11
@@ -1,4 +1,4 @@
|
||||
use crate::{APP_DEVICE_STATE, RUNTIME, VIDEO_STREAMS, afc, utils};
|
||||
use crate::{APP_DEVICE_STATE, RUNTIME, VIDEO_STREAMS, utils};
|
||||
use cxx_qt::{CxxQtType, Threading};
|
||||
use cxx_qt_lib::QUuid;
|
||||
use idevice::{IdeviceService, afc::AfcClient, services::afc::opcode::AfcFopenMode};
|
||||
@@ -869,13 +869,13 @@ async fn export_single_item(
|
||||
.unwrap_or_else(|| base_path.to_str().unwrap_or(""))
|
||||
.to_string();
|
||||
|
||||
let file_size = match afc::get_file_size(afc_client, device_path.to_string()).await {
|
||||
Some(size) => size,
|
||||
None => {
|
||||
//return on error
|
||||
return Err(format!("Failed to get file size for {device_path}"));
|
||||
}
|
||||
};
|
||||
// Use AFC get_file_info for size and timestamps
|
||||
let info = afc_client
|
||||
.get_file_info(device_path.to_string())
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get file info for {device_path}: {e}"))?;
|
||||
let file_size = info.size as i64;
|
||||
let modified = info.modified;
|
||||
|
||||
let mut remote = afc_client
|
||||
.open(device_path, AfcFopenMode::RdOnly)
|
||||
@@ -917,12 +917,26 @@ async fn export_single_item(
|
||||
&job_id_signal,
|
||||
&qobject::QString::from(file_name_owned),
|
||||
transferred_now,
|
||||
file_size as i64,
|
||||
file_size,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
/* preserve original modification time on exported file */
|
||||
if transferred > 0 {
|
||||
use filetime::FileTime;
|
||||
|
||||
let modified_utc = modified.and_utc();
|
||||
let mtime = FileTime::from_unix_time(
|
||||
modified_utc.timestamp(),
|
||||
modified_utc.timestamp_subsec_nanos(),
|
||||
);
|
||||
|
||||
// ignore errors
|
||||
let _ = filetime::set_file_times(&output_path, mtime, mtime);
|
||||
}
|
||||
|
||||
Ok(ExportItemResult {
|
||||
success: !cancel_flag.load(Ordering::Relaxed),
|
||||
bytes_transferred: transferred,
|
||||
@@ -1138,8 +1152,6 @@ async fn import_single_item(
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a unique local output path by appending a numeric suffix.
|
||||
/// Mirrors ExportManagerThread::generateUniqueOutputPath logic.
|
||||
async fn generate_unique_output_path(base: &Path) -> PathBuf {
|
||||
if fs::metadata(base).await.is_err() {
|
||||
return base.to_path_buf();
|
||||
|
||||
+29
-4
@@ -165,6 +165,13 @@ void BalloonProcess::updateUI()
|
||||
statusText = m_item->currentFile.isEmpty() ? "Starting..." : "Running";
|
||||
} else if (m_item->status == ProcessStatus::Completed) {
|
||||
statusText = "Completed successfully";
|
||||
|
||||
QTimer::singleShot(1000, this, [this]() {
|
||||
if (m_item->onComplete.has_value() && m_item->onComplete.value()) {
|
||||
m_item->onComplete.value()();
|
||||
}
|
||||
});
|
||||
|
||||
} else if (m_item->status == ProcessStatus::Failed) {
|
||||
statusText = "Failed";
|
||||
} else if (m_item->status == ProcessStatus::Cancelled) {
|
||||
@@ -300,6 +307,7 @@ StatusBalloon::StatusBalloon(QWidget *parent) : QBalloonTip(parent)
|
||||
// Header label
|
||||
m_headerLabel = new QLabel("Processes");
|
||||
m_headerLabel->hide();
|
||||
m_headerLabel->setWordWrap(true);
|
||||
QFont headerFont = m_headerLabel->font();
|
||||
headerFont.setPointSize(headerFont.pointSize() + 2);
|
||||
headerFont.setBold(true);
|
||||
@@ -467,9 +475,10 @@ void StatusBalloon::onItemImported(const QUuid &job_id,
|
||||
updateHeader();
|
||||
}
|
||||
|
||||
QUuid StatusBalloon::startProcess(const QString &title, int totalItems,
|
||||
const QString &destinationPath,
|
||||
ProcessType type, const QUuid &jobId)
|
||||
QUuid StatusBalloon::startProcess(
|
||||
const QString &title, int totalItems, const QString &destinationPath,
|
||||
ProcessType type, const QUuid &jobId,
|
||||
std::optional<std::function<void()>> onComplete)
|
||||
{
|
||||
handleShow(true);
|
||||
|
||||
@@ -481,6 +490,7 @@ QUuid StatusBalloon::startProcess(const QString &title, int totalItems,
|
||||
item->totalItems = totalItems;
|
||||
item->startTime = QDateTime::currentDateTime();
|
||||
item->destinationPath = destinationPath;
|
||||
item->onComplete = std::move(onComplete);
|
||||
|
||||
{
|
||||
QMutexLocker locker(&m_processesMutex);
|
||||
@@ -521,7 +531,7 @@ void StatusBalloon::updateHeader()
|
||||
}
|
||||
int total = running + completed + failed + canceled;
|
||||
|
||||
QString headerText = QString("Processes: %1 running").arg(running);
|
||||
QString headerText = QString("Processes:\n %1 running").arg(running);
|
||||
if (completed > 0 || failed > 0 || canceled > 0) {
|
||||
headerText += QString(" • %1 completed").arg(completed);
|
||||
if (failed > 0) {
|
||||
@@ -545,6 +555,21 @@ void StatusBalloon::updateHeader()
|
||||
|
||||
void StatusBalloon::handleShow(bool forceVisible)
|
||||
{
|
||||
/* required on Wayland */
|
||||
QWidget *anchorWindow =
|
||||
m_button ? m_button->window() : QApplication::activeWindow();
|
||||
if (!anchorWindow) {
|
||||
if (m_button)
|
||||
m_button->setIndicatorVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure popup has a real QWidget parent.
|
||||
if (parentWidget() != anchorWindow) {
|
||||
setParent(anchorWindow, Qt::ToolTip);
|
||||
}
|
||||
/**/
|
||||
|
||||
QPoint pos = m_button->mapToGlobal(
|
||||
QPoint(m_button->width() / 2, m_button->height()));
|
||||
|
||||
|
||||
+5
-4
@@ -40,7 +40,7 @@ struct ProcessItem {
|
||||
QDateTime endTime;
|
||||
QString destinationPath;
|
||||
// QUuid jobId;
|
||||
|
||||
std::optional<std::function<void()>> onComplete;
|
||||
BalloonProcess *processWidget = nullptr;
|
||||
};
|
||||
|
||||
@@ -92,9 +92,10 @@ public:
|
||||
static StatusBalloon *sharedInstance();
|
||||
|
||||
// Process management
|
||||
QUuid startProcess(const QString &title, int totalItems,
|
||||
const QString &destinationPath, ProcessType type,
|
||||
const QUuid &jobId);
|
||||
QUuid startProcess(
|
||||
const QString &title, int totalItems, const QString &destinationPath,
|
||||
ProcessType type, const QUuid &jobId,
|
||||
std::optional<std::function<void()>> onComplete = std::nullopt);
|
||||
|
||||
void onFileTransferProgress(const QUuid &processId,
|
||||
const QString ¤tFile,
|
||||
|
||||
Reference in New Issue
Block a user