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:
uncor3
2026-04-06 14:46:24 +00:00
parent 166fc8b2f5
commit e653bda458
13 changed files with 485 additions and 125 deletions
+80 -75
View File
@@ -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());
});
}
}
+4 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+40 -1
View File
@@ -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"
+1
View File
@@ -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]
+4
View File
@@ -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",
+44
View File
@@ -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
}
}
})
}
}
+111 -1
View File
@@ -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
}
}
})
}
}
+24 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 &currentFile,