diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp index 8ce1b2b..a275c9b 100644 --- a/src/afcexplorerwidget.cpp +++ b/src/afcexplorerwidget.cpp @@ -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 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 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 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 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 onExportFinished = [localPath]() { + QDesktopServices::openUrl(QUrl::fromLocalFile(localPath)); + }; + + if (m_useAfc2) { + IOManagerClient::sharedInstance()->startExport( + m_device, exportItems, directory, + "Exporting from File Explorer ", 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 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 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 ", 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()); + }); + } } \ No newline at end of file diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp index 295d410..2c107b4 100644 --- a/src/gallerywidget.cpp +++ b/src/gallerywidget.cpp @@ -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 &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); diff --git a/src/iomanagerclient.cpp b/src/iomanagerclient.cpp index f31d093..ae473e0 100644 --- a/src/iomanagerclient.cpp +++ b/src/iomanagerclient.cpp @@ -30,7 +30,7 @@ IOManagerClient::IOManagerClient(QObject *parent) : QObject(parent) {} void IOManagerClient::startExport( const std::shared_ptr device, const QList &items, const QString &destinationPath, - const QString &exportTitle) + const QString &exportTitle, std::optional> 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 device, const QList &items, const QString &destinationPath, - const QString &exportTitle, const QString &bundleId) + const QString &exportTitle, const QString &bundleId, + std::optional> 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 device, const QList &items, const QString &destinationPath, - const QString &exportTitle, bool useAfc2) + const QString &exportTitle, bool useAfc2, + std::optional> 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 device, const QList &items, const QString &destinationPath, - const QString &importTitle, std::optional altAfc) + const QString &importTitle, std::optional> 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 device, + const QList &items, const QString &destinationPath, + const QString &importTitle, const QString &bundleId, + std::optional> 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 device, + const QList &items, const QString &destinationPath, + const QString &importTitle, bool useAfc2, + std::optional> 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); diff --git a/src/iomanagerclient.h b/src/iomanagerclient.h index 2ce186f..cd7b632 100644 --- a/src/iomanagerclient.h +++ b/src/iomanagerclient.h @@ -41,24 +41,41 @@ public: IOManagerClient(const IOManagerClient &) = delete; IOManagerClient &operator=(const IOManagerClient &) = delete; - void startExport(const std::shared_ptr device, - const QList &items, - const QString &destinationPath, const QString &jobTitle); + void + startExport(const std::shared_ptr device, + const QList &items, const QString &destinationPath, + const QString &jobTitle, + std::optional> onComplete = std::nullopt); /* afc2 */ - void startExport(const std::shared_ptr device, - const QList &items, - const QString &destinationPath, const QString &jobTitle, - bool useAfc2); + void + startExport(const std::shared_ptr device, + const QList &items, const QString &destinationPath, + const QString &jobTitle, bool useAfc2, + std::optional> onComplete = std::nullopt); /* hause_arrest_afc*/ - void startExport(const std::shared_ptr device, - const QList &items, - const QString &destinationPath, const QString &exportTitle, - const QString &bundleId); + void + startExport(const std::shared_ptr device, + const QList &items, const QString &destinationPath, + const QString &exportTitle, const QString &bundleId, + std::optional> onComplete = std::nullopt); - void startImport(const std::shared_ptr device, - const QList &items, - const QString &destinationPath, const QString &jobTitle, - std::optional altAfc = std::nullopt); + void + startImport(const std::shared_ptr device, + const QList &items, const QString &destinationPath, + const QString &jobTitle, + std::optional> onComplete = std::nullopt); + /* hause_arrest_afc */ + void + startImport(const std::shared_ptr device, + const QList &items, const QString &destinationPath, + const QString &jobTitle, const QString &bundleId, + std::optional> onComplete = std::nullopt); + /* afc2 */ + void + startImport(const std::shared_ptr device, + const QList &items, const QString &destinationPath, + const QString &jobTitle, bool useAfc2, + std::optional> onComplete = std::nullopt); void cancel(const QUuid &jobId); void cancelAllJobs(); diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 0f862e5..d486cc9 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -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" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 9546ad5..e7ad3b6 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -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] diff --git a/src/rust/src/afc.rs b/src/rust/src/afc.rs index 6f3a698..a56c8ee 100644 --- a/src/rust/src/afc.rs +++ b/src/rust/src/afc.rs @@ -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", diff --git a/src/rust/src/afc2_services.rs b/src/rust/src/afc2_services.rs index 604a8c9..d730a54 100644 --- a/src/rust/src/afc2_services.rs +++ b/src/rust/src/afc2_services.rs @@ -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 = 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 + } + } + }) + } } diff --git a/src/rust/src/afc_services.rs b/src/rust/src/afc_services.rs index ad20df2..a83088b 100644 --- a/src/rust/src/afc_services.rs +++ b/src/rust/src/afc_services.rs @@ -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; @@ -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 = 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 { + 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 + let mut map: QMap = 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 + } + } + }) + } } diff --git a/src/rust/src/hause_arrest.rs b/src/rust/src/hause_arrest.rs index 85121a7..2724f5c 100644 --- a/src/rust/src/hause_arrest.rs +++ b/src/rust/src/hause_arrest.rs @@ -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 + } + } + }) + } } diff --git a/src/rust/src/io_manager.rs b/src/rust/src/io_manager.rs index beddc28..7c1435c 100644 --- a/src/rust/src/io_manager.rs +++ b/src/rust/src/io_manager.rs @@ -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(); diff --git a/src/statusballoon.cpp b/src/statusballoon.cpp index 72a567a..f9142c3 100644 --- a/src/statusballoon.cpp +++ b/src/statusballoon.cpp @@ -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> 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())); diff --git a/src/statusballoon.h b/src/statusballoon.h index 3742920..1ec16f9 100644 --- a/src/statusballoon.h +++ b/src/statusballoon.h @@ -40,7 +40,7 @@ struct ProcessItem { QDateTime endTime; QString destinationPath; // QUuid jobId; - + std::optional> 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> onComplete = std::nullopt); void onFileTransferProgress(const QUuid &processId, const QString ¤tFile,