use low level apis to generate video thumbnail & remove unnecessary code

- Adjusted status bar layout in MainWindow to include app version label.
- Enhanced video thumbnail generation in PhotoModel using FFmpeg for better performance and resource management.
- Streamlined MediaStreamerManager to ensure proper cleanup and thread safety.
- Updated ServiceManager to include safe methods for retrieving file info and handling AFC operations.
- Removed get_device_version
- Cleaned up code and improved readability across multiple files.
- Passed correct args to ZUpdater
This commit is contained in:
uncor3
2025-11-09 20:27:22 +00:00
parent 7b99a26962
commit f0ab7efc6e
23 changed files with 553 additions and 366 deletions
+21 -1
View File
@@ -1,8 +1,9 @@
cmake_minimum_required(VERSION 3.16)
project(iDescriptor VERSION 0.1 LANGUAGES CXX)
project(iDescriptor VERSION 0.1.0 LANGUAGES CXX)
# Feature options
option(ENABLE_RECOVERY_DEVICE_SUPPORT "Enable recovery device support (requires libirecovery)" ON)
option(PACKAGE_MANAGER_MANAGED "Build as package manager managed version (auto updates will be handled by the package manager)" OFF)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
@@ -117,6 +118,12 @@ pkg_check_modules(QRENCODE REQUIRED IMPORTED_TARGET libqrencode)
pkg_check_modules(HEIF REQUIRED IMPORTED_TARGET libheif)
pkg_check_modules(ZIP REQUIRED IMPORTED_TARGET libzip)
# Add FFmpeg libraries for video thumbnail generation
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
pkg_check_modules(SWSCALE REQUIRED IMPORTED_TARGET libswscale)
if(ENABLE_RECOVERY_DEVICE_SUPPORT)
find_library(IRECOVERY_LIBRARY
NAMES irecovery-1.0
@@ -264,6 +271,10 @@ target_link_libraries(iDescriptor PRIVATE
qtermwidget6
PkgConfig::HEIF
PkgConfig::ZIP
PkgConfig::AVFORMAT
PkgConfig::AVCODEC
PkgConfig::AVUTIL
PkgConfig::SWSCALE
airplay
ipatool-go
ZUpdater
@@ -316,6 +327,15 @@ if(ENABLE_RECOVERY_DEVICE_SUPPORT)
target_compile_definitions(iDescriptor PRIVATE ENABLE_RECOVERY_DEVICE_SUPPORT)
endif()
if (PACKAGE_MANAGER_MANAGED)
target_compile_definitions(iDescriptor PRIVATE PACKAGE_MANAGER_MANAGED)
message(STATUS "Building as package manager managed version, updates will be handled by the package manager")
endif()
target_compile_definitions(iDescriptor PRIVATE
APP_VERSION="${PROJECT_VERSION}"
)
set_target_properties(iDescriptor PROPERTIES
${BUNDLE_ID_OPTION}
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
@@ -54,18 +54,39 @@ QByteArray read_afc_file_to_byte_array(afc_client_t afcClient, const char *path)
QByteArray buffer;
buffer.resize(fileSize);
uint32_t bytesRead = 0;
afc_error_t read_err = afc_file_read(afcClient, fd_handle, buffer.data(),
buffer.size(), &bytesRead);
uint64_t totalBytesRead = 0;
const uint32_t CHUNK_SIZE = 1024 * 1024; // Read in 1MB chunks
char *p = buffer.data();
while (totalBytesRead < fileSize) {
uint32_t bytesToRead =
std::min((uint64_t)CHUNK_SIZE, fileSize - totalBytesRead);
uint32_t bytesReadThisChunk = 0;
afc_error_t read_err =
afc_file_read(afcClient, fd_handle, p + totalBytesRead, bytesToRead,
&bytesReadThisChunk);
if (read_err != AFC_E_SUCCESS) {
qDebug() << "AFC Error: Read failed for file" << path
<< "Error:" << read_err;
afc_file_close(afcClient, fd_handle);
return QByteArray();
}
if (bytesReadThisChunk == 0) {
// Premature end of file
break;
}
totalBytesRead += bytesReadThisChunk;
}
afc_file_close(afcClient, fd_handle);
if (read_err != AFC_E_SUCCESS || bytesRead != fileSize) {
if (totalBytesRead != fileSize) {
qDebug() << "AFC Error: Read mismatch for file" << path
<< "Error:" << read_err << "Read:" << bytesRead
<< "Expected:" << fileSize;
<< "Read:" << totalBytesRead << "Expected:" << fileSize;
return QByteArray(); // Read failed
}
return buffer;
};
}
+1 -1
View File
@@ -202,7 +202,7 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc,
int minor = (parts.length() > 1) ? parts[1].toInt() : 0;
int patch = (parts.length() > 2) ? parts[2].toInt() : 0;
d.parsedDeviceVersion = IDESCRIPTOR_DEVICE_VERSION(major, minor, patch);
d.parsedDeviceVersion = IDEVICE_DEVICE_VERSION(major, minor, patch);
/*DiskInfo*/
try {
+4 -4
View File
@@ -97,7 +97,7 @@ mobile_image_mounter_error_t mount_dev_image(idevice_t device,
goto leave;
}
if (device_version >= IDESCRIPTOR_DEVICE_VERSION(7, 0, 0)) {
if (device_version >= IDEVICE_DEVICE_VERSION(7, 0, 0)) {
disk_image_upload_type = DISK_IMAGE_UPLOAD_TYPE_UPLOAD_IMAGE;
}
@@ -139,7 +139,7 @@ mobile_image_mounter_error_t mount_dev_image(idevice_t device,
qDebug() << "Using image:" << image_path;
qDebug() << "Using signature:" << image_sig_path;
if (device_version >= IDESCRIPTOR_DEVICE_VERSION(16, 0, 0)) {
if (device_version >= IDEVICE_DEVICE_VERSION(16, 0, 0)) {
uint8_t dev_mode_status = 0;
plist_t val = NULL;
ldret = lockdownd_get_value(lckd, "com.apple.security.mac.amfi",
@@ -182,14 +182,14 @@ mobile_image_mounter_error_t mount_dev_image(idevice_t device,
goto leave;
}
image_size = fst.st_size;
if (device_version < IDESCRIPTOR_DEVICE_VERSION(17, 0, 0) &&
if (device_version < IDEVICE_DEVICE_VERSION(17, 0, 0) &&
stat(image_sig_path, &fst) != 0) {
qDebug() << "ERROR: stat:" << image_sig_path << ":" << strerror(errno);
res = -1;
goto leave;
}
if (device_version < IDESCRIPTOR_DEVICE_VERSION(17, 0, 0)) {
if (device_version < IDEVICE_DEVICE_VERSION(17, 0, 0)) {
f = fopen(image_sig_path, "rb");
if (!f) {
qDebug() << "Error opening signature file" << image_sig_path << ":"
+1 -1
View File
@@ -71,7 +71,7 @@ bool set_location(idevice_t device, char *lat, char *lon)
lerr = lockdownd_start_service(lockdown, DT_SIMULATELOCATION_SERVICE,
&svc);
if (lerr != LOCKDOWN_E_SUCCESS) {
unsigned int device_version = get_device_version(device);
unsigned int device_version = idevice_get_device_version(device);
lockdownd_client_free(lockdown);
idevice_free(device);
+2 -2
View File
@@ -92,7 +92,7 @@ void DevDiskImageHelper::start()
m_loadingIndicator->start();
showStatus("Please wait...");
unsigned int device_version = get_device_version(m_device->device);
unsigned int device_version = idevice_get_device_version(m_device->device);
unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF;
@@ -137,7 +137,7 @@ void DevDiskImageHelper::onMountButtonClicked()
m_isMounting = true;
// Check if we need to download first
unsigned int device_version = get_device_version(m_device->device);
unsigned int device_version = idevice_get_device_version(m_device->device);
unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF;
+1 -1
View File
@@ -186,7 +186,7 @@ void DevDiskImagesWidget::displayImages()
// todo wtf is this
if (m_currentDevice && m_currentDevice->device) {
unsigned int device_version =
get_device_version(m_currentDevice->device);
idevice_get_device_version(m_currentDevice->device);
deviceMajorVersion = (device_version >> 16) & 0xFF;
deviceMinorVersion = (device_version >> 8) & 0xFF;
hasConnectedDevice = true;
+2 -2
View File
@@ -338,7 +338,7 @@ bool DevDiskManager::isImageDownloaded(const QString &version,
bool DevDiskManager::downloadCompatibleImage(iDescriptorDevice *device)
{
unsigned int device_version = get_device_version(device->device);
unsigned int device_version = idevice_get_device_version(device->device);
unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF;
qDebug() << "Device version:" << deviceMajorVersion << "."
@@ -400,7 +400,7 @@ bool DevDiskManager::downloadCompatibleImage(iDescriptorDevice *device)
// FIXME:DOES NOT CHECK IF THERE IS ALREADY AN IMAGE MOUNTED
bool DevDiskManager::mountCompatibleImage(iDescriptorDevice *device)
{
unsigned int device_version = get_device_version(device->device);
unsigned int device_version = idevice_get_device_version(device->device);
unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
unsigned int deviceMinorVersion = (device_version >> 8) & 0xFF;
+1 -1
View File
@@ -161,7 +161,7 @@ QString DeviceImageWidget::getMockupNameFromDisplayName(
int DeviceImageWidget::getIosVersionFromDevice() const
{
unsigned int version = get_device_version(m_device->device);
unsigned int version = idevice_get_device_version(m_device->device);
if (version > 0) {
int majorVersion = (version >> 16) & 0xFF;
+8 -4
View File
@@ -182,14 +182,18 @@ void GalleryWidget::onSortOrderChanged()
: "Oldest First");
}
PhotoModel::FilterType GalleryWidget::getCurrentFilterType() const
{
int filterValue = m_filterComboBox->currentData().toInt();
return static_cast<PhotoModel::FilterType>(filterValue);
}
void GalleryWidget::onFilterChanged()
{
if (!m_model)
return;
int filterValue = m_filterComboBox->currentData().toInt();
PhotoModel::FilterType filter =
static_cast<PhotoModel::FilterType>(filterValue);
PhotoModel::FilterType filter = getCurrentFilterType();
m_model->setFilterType(filter);
QString filterName = m_filterComboBox->currentText();
@@ -447,7 +451,7 @@ void GalleryWidget::onAlbumSelected(const QString &albumPath)
// Create model if not exists
if (!m_model) {
m_model = new PhotoModel(m_device, this);
m_model = new PhotoModel(m_device, getCurrentFilterType(), this);
m_listView->setModel(m_model);
// Update export button states based on selection
+2 -1
View File
@@ -21,6 +21,7 @@
#define GALLERYWIDGET_H
#include "iDescriptor.h"
#include "photomodel.h"
#include <QWidget>
QT_BEGIN_NAMESPACE
@@ -34,7 +35,6 @@ class QLabel;
class QStandardItem;
QT_END_NAMESPACE
class PhotoModel;
class ExportManager;
class ExportProgressDialog;
@@ -67,6 +67,7 @@ private:
QIcon loadAlbumThumbnail(const QString &albumPath);
void loadAlbumThumbnailAsync(const QString &albumPath, QStandardItem *item);
void onPhotoContextMenu(const QPoint &pos);
PhotoModel::FilterType getCurrentFilterType() const;
iDescriptorDevice *m_device;
bool m_loaded = false;
+2 -56
View File
@@ -37,7 +37,6 @@
#define TOOL_NAME "iDescriptor"
#define APP_LABEL "iDescriptor"
#define APP_VERSION "0.1.0"
#define APP_COPYRIGHT "© 2025 Uncore. All rights reserved."
#define AFC2_SERVICE_NAME "com.apple.afc2"
#define RECOVERY_CLIENT_CONNECTION_TRIES 3
@@ -84,6 +83,7 @@ struct DiskInfo {
uint64_t totalDataAvailable;
};
// Carefull not all the vars are initialized in init_device.cpp
struct DeviceInfo {
enum class ActivationState {
Activated,
@@ -147,7 +147,6 @@ struct DeviceInfo {
std::string mobileSubscriberCountryCode;
std::string mobileSubscriberNetworkCode;
std::string modelNumber;
// NonVolatileRAM omitted (unknown type)
std::string ioNVRAMSyncNowProperty;
bool systemAudioVolumeSaved;
bool autoBoot;
@@ -425,57 +424,4 @@ QByteArray read_afc_file_to_byte_array(afc_client_t afcClient,
bool isDarkMode();
instproxy_error_t install_IPA(idevice_t device, afc_client_t afc,
const char *filePath);
#define IDESCRIPTOR_DEVICE_VERSION(maj, min, patch) \
((((maj) & 0xFF) << 16) | (((min) & 0xFF) << 8) | ((patch) & 0xFF))
/*
we need this because idevice_get_device_version
is not always available in libimobiledevice
which could cause issues when installed from package managers
*/
inline unsigned int get_device_version(idevice_t _device)
{
if (!_device) {
return 0;
}
lockdownd_client_t lockdown = NULL;
if (lockdownd_client_new_with_handshake(
_device, &lockdown, "iDescriptor") != LOCKDOWN_E_SUCCESS) {
return 0;
}
plist_t node = NULL;
if (lockdownd_get_value(lockdown, NULL, "ProductVersion", &node) !=
LOCKDOWN_E_SUCCESS) {
lockdownd_client_free(lockdown);
return 0;
}
unsigned int version_number = 0;
if (node && plist_get_node_type(node) == PLIST_STRING) {
char *version_string = NULL;
plist_get_string_val(node, &version_string);
if (version_string) {
QString q_version = QString(version_string);
QStringList parts = q_version.split('.');
int major = (parts.length() > 0) ? parts[0].toInt() : 0;
int minor = (parts.length() > 1) ? parts[1].toInt() : 0;
int patch = (parts.length() > 2) ? parts[2].toInt() : 0;
version_number = IDESCRIPTOR_DEVICE_VERSION(major, minor, patch);
free(version_string);
}
}
if (node) {
plist_free(node);
}
lockdownd_client_free(lockdown);
return version_number;
}
const char *filePath);
+1 -1
View File
@@ -352,7 +352,6 @@ void iFuseWidget::onProcessFinished(int exitCode,
"Device mounted successfully at: " + m_currentMountPath, false);
auto *b = new iFuseDiskUnmountButton(m_currentMountPath);
MainWindow::sharedInstance()->statusBar()->addPermanentWidget(b);
QProcess *processToKill = m_ifuseProcess;
QString currentMountPath = m_currentMountPath;
connect(b, &iFuseDiskUnmountButton::clicked, this,
@@ -369,6 +368,7 @@ void iFuseWidget::onProcessFinished(int exitCode,
MainWindow::sharedInstance()->statusBar()->removeWidget(b);
b->deleteLater();
});
MainWindow::sharedInstance()->statusBar()->addPermanentWidget(b);
QDesktopServices::openUrl(QUrl::fromLocalFile(currentMountPath));
} else {
QString errorOutput = m_ifuseProcess->readAllStandardError();
+16 -18
View File
@@ -31,14 +31,14 @@
#include <QVBoxLayout>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/screenshotr.h>
// todo add a retry button when failed
LiveScreenWidget::LiveScreenWidget(iDescriptorDevice *device, QWidget *parent)
: QWidget{parent}, m_device(device), m_timer(nullptr),
m_shotrClient(nullptr), m_fps(20)
{
setWindowTitle("Live Screen - iDescriptor");
unsigned int device_version = get_device_version(m_device->device);
unsigned int device_version = idevice_get_device_version(m_device->device);
unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
if (deviceMajorVersion > 16) {
@@ -91,23 +91,21 @@ LiveScreenWidget::LiveScreenWidget(iDescriptorDevice *device, QWidget *parent)
// Start the initialization process - auto-mount mode
auto *helper = new DevDiskImageHelper(m_device, this);
connect(
helper, &DevDiskImageHelper::mountingCompleted, this,
[this, helper](bool success) {
helper->deleteLater();
connect(helper, &DevDiskImageHelper::mountingCompleted, this,
[this, helper](bool success) {
helper->deleteLater();
if (success) {
// for some reason it does not work immediately, so delay a bit
QTimer::singleShot(1000, this, [this]() {
initializeScreenshotService(true);
});
} else {
m_statusLabel->setText("Failed to mount developer disk image");
QMessageBox::critical(this, "Mount Failed",
"Could not mount developer disk image.\n"
"Screenshot feature is not available.");
}
});
if (success) {
// for some reason it does not work immediately, so delay a
// bit
QTimer::singleShot(1000, this, [this]() {
initializeScreenshotService(true);
});
} else {
m_statusLabel->setText(
"Failed to mount developer disk image");
}
});
helper->start();
}
+24 -9
View File
@@ -193,9 +193,14 @@ MainWindow::MainWindow(QWidget *parent)
ui->statusbar->addWidget(m_connectedDeviceCountLabel);
ui->statusbar->setContentsMargins(0, 0, 0, 0);
ui->statusbar->addPermanentWidget(settingsButton);
ui->statusbar->addPermanentWidget(githubButton);
ui->statusbar->addPermanentWidget(settingsButton);
QLabel *appVersionLabel = new QLabel(QString("v%1").arg(APP_VERSION));
appVersionLabel->setContentsMargins(5, 0, 5, 0);
appVersionLabel->setStyleSheet(
"QLabel:hover { background-color : #13131319; }");
ui->statusbar->addPermanentWidget(appVersionLabel);
#ifdef __linux__
QList<QString> mounted_iFusePaths = iFuseManager::getMountPoints();
@@ -239,6 +244,17 @@ MainWindow::MainWindow(QWidget *parent)
// Example usage with customization
UpdateProcedure updateProcedure;
bool packageManagerManaged = false;
bool isPortable = false;
#ifdef WIN32
// dynamic portable detection read .portable file in app dir on Windows
QString appDir = QApplication::applicationDirPath();
QFile portableFile(appDir + "/.portable");
if (portableFile.exists()) {
isPortable = true;
}
#endif
switch (ZUpdater::detectPlatform()) {
// todo: adjust for portable
@@ -261,8 +277,11 @@ MainWindow::MainWindow(QWidget *parent)
"Do you want to install the downloaded update now?",
};
break;
// todo: adjust for pkg managers
case Platform::Linux:
// currently only on linux (arch aur) is enabled
#ifdef PACKAGE_MANAGER_MANAGED
packageManagerManaged = true;
#endif
updateProcedure = UpdateProcedure{
false,
true,
@@ -277,13 +296,9 @@ MainWindow::MainWindow(QWidget *parent)
};
}
m_updater = new ZUpdater("uncor3/libtest", APP_VERSION, "iDescriptor",
updateProcedure,
false, // isPortable - set to true if running
// portable version on Windows
false, // isPackageManaged - set to true if
// installed via package manager on Linux
this);
m_updater =
new ZUpdater("uncor3/libtest", APP_VERSION, "iDescriptor",
updateProcedure, isPortable, packageManagerManaged, this);
qDebug() << "Checking for updates...";
SettingsManager::sharedInstance()->doIfEnabled(
SettingsManager::Setting::AutoCheckUpdates,
+1 -1
View File
@@ -203,7 +203,7 @@ void MediaPreviewDialog::loadMedia()
void MediaPreviewDialog::loadImage()
{
auto future = QtConcurrent::run(
[this]() { return PhotoModel::loadImage(m_device, m_filePath, ""); });
[this]() { return PhotoModel::loadImage(m_device, m_filePath); });
auto *watcher = new QFutureWatcher<QPixmap>(this);
connect(watcher, &QFutureWatcher<QPixmap>::finished, this,
+17 -38
View File
@@ -34,6 +34,7 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device,
afc_client_t afcClient,
const QString &filePath)
{
QMutexLocker locker(&m_streamersMutex);
// Check if we already have a streamer for this file
auto it = m_streamers.find(filePath);
@@ -55,12 +56,12 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device,
}
}
// Create new streamer
auto *streamer = new MediaStreamer(device, afcClient, filePath, this);
// Create new streamer without a QObject parent
auto *streamer = new MediaStreamer(device, afcClient, filePath, nullptr);
if (!streamer->isListening()) {
qWarning() << "MediaStreamerManager: Failed to create streamer for"
<< filePath;
streamer->deleteLater();
delete streamer;
return QUrl();
}
@@ -71,10 +72,6 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device,
info.refCount = 1;
m_streamers[filePath] = info;
// Connect to destruction signal for cleanup
connect(streamer, &QObject::destroyed, this,
&MediaStreamerManager::onStreamerDestroyed);
qDebug() << "MediaStreamerManager: Created new streamer for" << filePath
<< "at" << streamer->getUrl().toString();
@@ -83,52 +80,34 @@ QUrl MediaStreamerManager::getStreamUrl(iDescriptorDevice *device,
void MediaStreamerManager::releaseStreamer(const QString &filePath)
{
QMutexLocker locker(&m_streamersMutex);
auto it = m_streamers.find(filePath);
if (it != m_streamers.end()) {
it->refCount--;
qDebug() << "MediaStreamerManager: Released streamer for" << filePath
<< "refCount:" << it->refCount;
// If no more references, mark for cleanup but don't delete immediately
// This allows for quick reuse if the same file is opened again soon
// If no more references, delete it immediately.
// deleteLater() will not work in a thread without an event loop.
if (it->refCount <= 0) {
qDebug() << "MediaStreamerManager: Streamer for" << filePath
<< "ready for cleanup";
qDebug() << "MediaStreamerManager: Deleting streamer for"
<< filePath;
delete it->streamer;
m_streamers.erase(it);
}
}
}
void MediaStreamerManager::cleanup()
{
QMutexLocker locker(&m_streamersMutex);
auto it = m_streamers.begin();
while (it != m_streamers.end()) {
if (it->refCount <= 0) {
qDebug() << "MediaStreamerManager: Cleaning up streamer for"
<< it.key();
if (it->streamer) {
it->streamer->deleteLater();
}
it = m_streamers.erase(it);
} else {
++it;
}
}
}
void MediaStreamerManager::onStreamerDestroyed()
{
// Find and remove the destroyed streamer
auto it = m_streamers.begin();
while (it != m_streamers.end()) {
if (it->streamer == sender()) {
qDebug() << "MediaStreamerManager: Streamer destroyed for"
<< it.key();
it = m_streamers.erase(it);
break;
} else {
++it;
qDebug() << "MediaStreamerManager: Cleaning up streamer for"
<< it.key();
if (it->streamer) {
delete it->streamer;
}
it = m_streamers.erase(it);
}
}
+1 -6
View File
@@ -35,10 +35,8 @@
* streamers for the same file. It automatically cleans up unused streamers
* and provides thread-safe access.
*/
class MediaStreamerManager : public QObject
class MediaStreamerManager
{
Q_OBJECT
public:
/**
* @brief Get the singleton instance
@@ -69,9 +67,6 @@ public:
private:
~MediaStreamerManager();
private slots:
void onStreamerDestroyed();
private:
struct StreamerInfo {
MediaStreamer *streamer;
+374 -199
View File
@@ -25,32 +25,36 @@
#include <QEventLoop>
#include <QIcon>
#include <QImage>
#include <QImageReader>
#include <QMediaPlayer>
#include <QPixmap>
#include <QRegularExpression>
#include <QSemaphore>
#include <QTimer>
#include <QVideoFrame>
#include <QVideoSink>
#include <QtConcurrent/QtConcurrent>
#include <algorithm>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
PhotoModel::PhotoModel(iDescriptorDevice *device, QObject *parent)
: QAbstractListModel(parent), m_device(device), m_thumbnailSize(256, 256),
m_sortOrder(NewestFirst), m_filterType(All)
// Limit concurrent video thumbnail generation to 2 to prevent resource
// exhaustion
QSemaphore PhotoModel::m_videoThumbnailSemaphore(4);
PhotoModel::PhotoModel(iDescriptorDevice *device, FilterType filterType,
QObject *parent)
: QAbstractListModel(parent), m_device(device), m_thumbnailSize(64, 64),
m_sortOrder(NewestFirst), m_filterType(filterType)
{
// Set up cache directory for persistent storage
m_cacheDir =
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/photo_thumbs";
QDir().mkpath(m_cacheDir);
// Configure memory cache (150MB limit)
m_thumbnailCache.setMaxCost(150 * 1024 * 1024);
// 350 MB cache for thumbnails
m_thumbnailCache.setMaxCost(350 * 1024 * 1024);
connect(this, &PhotoModel::thumbnailNeedsToBeLoaded, this,
&PhotoModel::requestThumbnail, Qt::QueuedConnection);
// Don't populate paths in constructor - wait for setAlbumPath
}
PhotoModel::~PhotoModel()
@@ -67,59 +71,306 @@ PhotoModel::~PhotoModel()
m_activeLoaders.clear();
m_loadingPaths.clear();
m_thumbnailCache.clear();
QDir(m_cacheDir).removeRecursively();
}
QPixmap PhotoModel::generateVideoThumbnail(iDescriptorDevice *device,
const QString &filePath,
const QSize &requestedSize)
QPixmap PhotoModel::generateVideoThumbnailFFmpeg(iDescriptorDevice *device,
const QString &filePath,
const QSize &requestedSize)
{
QPixmap thumbnail;
QEventLoop loop;
// Use a timer to handle potential timeouts
QTimer::singleShot(5000, &loop, &QEventLoop::quit);
uint64_t fileHandle = 0;
auto player = std::make_unique<QMediaPlayer>();
auto sink = std::make_unique<QVideoSink>();
player->setVideoSink(sink.get());
afc_error_t openResult = ServiceManager::safeAfcFileOpen(
device, filePath.toUtf8().constData(), AFC_FOPEN_RDONLY, &fileHandle);
// This lambda will be called when a frame is ready
QObject::connect(sink.get(), &QVideoSink::videoFrameChanged,
[&](const QVideoFrame &frame) {
if (frame.isValid()) {
QImage img = frame.toImage();
if (!img.isNull()) {
thumbnail = QPixmap::fromImage(img.scaled(
requestedSize, Qt::KeepAspectRatio,
Qt::SmoothTransformation));
}
}
// We got our frame, so we can stop the loop
if (loop.isRunning()) {
loop.quit();
}
});
// Get the streaming URL and start playback
QUrl streamUrl = MediaStreamerManager::sharedInstance()->getStreamUrl(
device, device->afcClient, filePath);
if (streamUrl.isEmpty()) {
qWarning() << "Could not get stream URL for video thumbnail:"
<< filePath;
if (openResult != AFC_E_SUCCESS || fileHandle == 0) {
qWarning() << "Failed to open video file for thumbnail:" << filePath;
return {};
}
player->setSource(streamUrl);
player->setPosition(1000); // Seek 1 second in to get a good frame
player->play(); // Start playback to trigger frame capture
// Get file size
char **fileInfo = nullptr;
afc_error_t infoResult = ServiceManager::safeAfcGetFileInfo(
device, filePath.toUtf8().constData(), &fileInfo);
// Wait for the videoFrameChanged signal or timeout
loop.exec();
uint64_t fileSize = 0;
if (infoResult == AFC_E_SUCCESS && fileInfo) {
for (int i = 0; fileInfo[i]; i += 2) {
if (strcmp(fileInfo[i], "st_size") == 0) {
fileSize = strtoull(fileInfo[i + 1], nullptr, 10);
break;
}
}
afc_dictionary_free(fileInfo);
}
if (fileSize == 0) {
ServiceManager::safeAfcFileClose(device, fileHandle);
qWarning() << "Invalid video file size for thumbnail:" << filePath;
return {};
}
// Create custom AVIOContext for reading from device on-demand
AVFormatContext *formatCtx = avformat_alloc_context();
if (!formatCtx) {
ServiceManager::safeAfcFileClose(device, fileHandle);
qWarning() << "Failed to allocate format context";
return {};
}
// Context for streaming read from device
struct StreamContext {
iDescriptorDevice *device;
uint64_t fileHandle;
uint64_t fileSize;
uint64_t currentPos;
};
StreamContext *streamCtx =
new StreamContext{device, fileHandle, fileSize, 0};
// Custom read function that reads from device on-demand
auto readPacket = [](void *opaque, uint8_t *buf, int bufSize) -> int {
StreamContext *ctx = static_cast<StreamContext *>(opaque);
if (ctx->currentPos >= ctx->fileSize) {
return AVERROR_EOF;
}
uint32_t toRead =
std::min(static_cast<uint32_t>(bufSize),
static_cast<uint32_t>(ctx->fileSize - ctx->currentPos));
uint32_t bytesRead = 0;
afc_error_t result = ServiceManager::safeAfcFileRead(
ctx->device, ctx->fileHandle, reinterpret_cast<char *>(buf), toRead,
&bytesRead);
if (result != AFC_E_SUCCESS || bytesRead == 0) {
return AVERROR(EIO);
}
ctx->currentPos += bytesRead;
return static_cast<int>(bytesRead);
};
// Custom seek function using AFC seek
auto seekPacket = [](void *opaque, int64_t offset, int whence) -> int64_t {
StreamContext *ctx = static_cast<StreamContext *>(opaque);
if (whence == AVSEEK_SIZE) {
return static_cast<int64_t>(ctx->fileSize);
}
int64_t newPos = 0;
int seekWhence = SEEK_SET;
if (whence == SEEK_SET) {
newPos = offset;
seekWhence = SEEK_SET;
} else if (whence == SEEK_CUR) {
newPos = static_cast<int64_t>(ctx->currentPos) + offset;
seekWhence = SEEK_SET;
} else if (whence == SEEK_END) {
newPos = static_cast<int64_t>(ctx->fileSize) + offset;
seekWhence = SEEK_SET;
} else {
return -1;
}
if (newPos < 0 || newPos > static_cast<int64_t>(ctx->fileSize)) {
return -1;
}
// Use AFC seek
afc_error_t result = ServiceManager::safeAfcFileSeek(
ctx->device, ctx->fileHandle, newPos, seekWhence);
if (result != AFC_E_SUCCESS) {
return -1;
}
ctx->currentPos = static_cast<uint64_t>(newPos);
return newPos;
};
const int avioBufferSize = 32768; // 32KB buffer for streaming
unsigned char *avioBuffer =
static_cast<unsigned char *>(av_malloc(avioBufferSize));
if (!avioBuffer) {
delete streamCtx;
ServiceManager::safeAfcFileClose(device, fileHandle);
avformat_free_context(formatCtx);
return {};
}
AVIOContext *avioCtx =
avio_alloc_context(avioBuffer, avioBufferSize, 0, streamCtx, readPacket,
nullptr, seekPacket);
if (!avioCtx) {
av_free(avioBuffer);
delete streamCtx;
ServiceManager::safeAfcFileClose(device, fileHandle);
avformat_free_context(formatCtx);
return {};
}
formatCtx->pb = avioCtx;
formatCtx->flags |= AVFMT_FLAG_CUSTOM_IO;
// Open input
if (avformat_open_input(&formatCtx, nullptr, nullptr, nullptr) < 0) {
qWarning() << "Failed to open video format";
av_free(avioCtx->buffer);
avio_context_free(&avioCtx);
avformat_free_context(formatCtx);
return {};
}
// Find stream info
if (avformat_find_stream_info(formatCtx, nullptr) < 0) {
qWarning() << "Failed to find stream info";
avformat_close_input(&formatCtx);
av_free(avioCtx->buffer);
avio_context_free(&avioCtx);
return {};
}
// Find video stream
int videoStreamIndex = -1;
const AVCodec *codec = nullptr;
AVCodecParameters *codecParams = nullptr;
for (unsigned int i = 0; i < formatCtx->nb_streams; i++) {
if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
codecParams = formatCtx->streams[i]->codecpar;
codec = avcodec_find_decoder(codecParams->codec_id);
break;
}
}
if (videoStreamIndex == -1 || !codec) {
qWarning() << "No video stream found";
avformat_close_input(&formatCtx);
av_free(avioCtx->buffer);
avio_context_free(&avioCtx);
return {};
}
// Allocate codec context
AVCodecContext *codecCtx = avcodec_alloc_context3(codec);
if (!codecCtx) {
avformat_close_input(&formatCtx);
av_free(avioCtx->buffer);
avio_context_free(&avioCtx);
return {};
}
if (avcodec_parameters_to_context(codecCtx, codecParams) < 0) {
avcodec_free_context(&codecCtx);
avformat_close_input(&formatCtx);
av_free(avioCtx->buffer);
avio_context_free(&avioCtx);
return {};
}
// Open codec
if (avcodec_open2(codecCtx, codec, nullptr) < 0) {
avcodec_free_context(&codecCtx);
avformat_close_input(&formatCtx);
av_free(avioCtx->buffer);
avio_context_free(&avioCtx);
return {};
}
// Allocate frame
AVFrame *frame = av_frame_alloc();
AVPacket *packet = av_packet_alloc();
if (!frame || !packet) {
if (frame)
av_frame_free(&frame);
if (packet)
av_packet_free(&packet);
avcodec_free_context(&codecCtx);
avformat_close_input(&formatCtx);
av_free(avioCtx->buffer);
avio_context_free(&avioCtx);
return {};
}
// Read frames until we get a valid one
bool frameDecoded = false;
while (av_read_frame(formatCtx, packet) >= 0) {
if (packet->stream_index == videoStreamIndex) {
if (avcodec_send_packet(codecCtx, packet) >= 0) {
if (avcodec_receive_frame(codecCtx, frame) >= 0) {
frameDecoded = true;
av_packet_unref(packet);
break;
}
}
}
av_packet_unref(packet);
}
if (frameDecoded) {
// Convert frame to RGB24
SwsContext *swsCtx =
sws_getContext(frame->width, frame->height,
static_cast<AVPixelFormat>(frame->format),
frame->width, frame->height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (swsCtx) {
AVFrame *rgbFrame = av_frame_alloc();
if (rgbFrame) {
rgbFrame->format = AV_PIX_FMT_RGB24;
rgbFrame->width = frame->width;
rgbFrame->height = frame->height;
if (av_frame_get_buffer(rgbFrame, 0) >= 0) {
sws_scale(swsCtx, frame->data, frame->linesize, 0,
frame->height, rgbFrame->data,
rgbFrame->linesize);
// Convert to QImage
QImage img(rgbFrame->data[0], rgbFrame->width,
rgbFrame->height, rgbFrame->linesize[0],
QImage::Format_RGB888);
// Create a deep copy since AVFrame will be freed
QImage imgCopy = img.copy();
// Scale to requested size
thumbnail = QPixmap::fromImage(
imgCopy.scaled(requestedSize, Qt::KeepAspectRatio,
Qt::SmoothTransformation));
}
av_frame_free(&rgbFrame);
}
sws_freeContext(swsCtx);
}
}
// Cleanup
player->stop();
MediaStreamerManager::sharedInstance()->releaseStreamer(filePath);
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&codecCtx);
avformat_close_input(&formatCtx);
// Close the AFC file handle
ServiceManager::safeAfcFileClose(device, fileHandle);
// Free AVIO context and stream context
av_free(avioCtx->buffer);
avio_context_free(&avioCtx);
delete streamCtx;
return thumbnail;
}
@@ -147,27 +398,24 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const
case Qt::DecorationRole: {
qDebug() << "DecorationRole requested for index:" << index.row();
QString cacheKey = getThumbnailCacheKey(info.filePath);
// Check memory cache first (works for both images AND videos)
if (QPixmap *cached = m_thumbnailCache.object(cacheKey)) {
// Check memory cache first
if (QPixmap *cached = m_thumbnailCache.object(info.filePath)) {
qDebug() << "Cache HIT for:" << info.fileName;
return QIcon(*cached);
}
// Prevent duplicate requests - this is CRITICAL for both images and
// videos
if (m_activeLoaders.contains(cacheKey) ||
m_loadingPaths.contains(info.filePath)) {
// Prevent duplicate requests
if (m_loadingPaths.contains(info.filePath) ||
m_activeLoaders.contains(info.filePath)) {
qDebug() << "Already loading:" << info.fileName;
// Return appropriate placeholder based on file type
if (info.fileName.endsWith(".MOV", Qt::CaseInsensitive) ||
info.fileName.endsWith(".MP4", Qt::CaseInsensitive) ||
info.fileName.endsWith(".M4V", Qt::CaseInsensitive)) {
// return QIcon::fromTheme("video-x-generic");
return QIcon(":/resources/icons/video-x-generic.png");
} else {
return QIcon::fromTheme("image-x-generic");
return QIcon(":/resources/icons/"
"MaterialSymbolsLightImageOutlineSharp.png");
}
}
@@ -185,7 +433,8 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const
// return QIcon::fromTheme("video-x-generic");
return QIcon(":/resources/icons/video-x-generic.png");
} else {
return QIcon::fromTheme("image-x-generic");
return QIcon(
":/resources/icons/MaterialSymbolsLightImageOutlineSharp.png");
}
}
@@ -222,23 +471,6 @@ void PhotoModel::clearCache()
}
}
QString PhotoModel::getThumbnailCacheKey(const QString &filePath) const
{
// Create unique key based on file path and thumbnail size
QString key = QString("%1_%2x%3")
.arg(filePath)
.arg(m_thumbnailSize.width())
.arg(m_thumbnailSize.height());
return QString(
QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Md5)
.toHex());
}
QString PhotoModel::getThumbnailCachePath(const QString &filePath) const
{
return m_cacheDir + "/" + getThumbnailCacheKey(filePath) + ".jpg";
}
void PhotoModel::requestThumbnail(int index)
{
if (index < 0 || index >= m_photos.size())
@@ -247,37 +479,26 @@ void PhotoModel::requestThumbnail(int index)
PhotoInfo &info = m_photos[index];
info.thumbnailRequested = true;
QString cacheKey = getThumbnailCacheKey(info.filePath);
if (m_activeLoaders.contains(cacheKey) ||
m_loadingPaths.contains(info.filePath))
if (m_loadingPaths.contains(info.filePath))
return;
m_loadingPaths.insert(info.filePath);
auto *watcher = new QFutureWatcher<QPixmap>();
m_activeLoaders[cacheKey] = watcher;
m_activeLoaders[info.filePath] = watcher;
// Connect the finished signal to handle both images and videos
connect(watcher, &QFutureWatcher<QPixmap>::finished, this,
[this, watcher, cacheKey, filePath = info.filePath]() {
[this, watcher, filePath = info.filePath]() {
qDebug() << "Thumbnail load finished for:" << filePath;
QPixmap thumbnail = watcher->result();
// Remove from loading sets
m_loadingPaths.remove(filePath);
m_activeLoaders.remove(cacheKey);
// scale down and store in cache
m_activeLoaders.remove(filePath);
if (!thumbnail.isNull()) {
// Cache the thumbnail (both memory and disk)
int cost = thumbnail.width() * thumbnail.height() * 4;
m_thumbnailCache.insert(
cacheKey,
new QPixmap(thumbnail.scaled(m_thumbnailSize,
Qt::KeepAspectRatio,
Qt::SmoothTransformation)),
cost);
m_thumbnailCache.insert(filePath, new QPixmap(thumbnail),
cost);
// Find the model index and emit dataChanged
for (int i = 0; i < m_photos.size(); ++i) {
if (m_photos[i].filePath == filePath) {
QModelIndex idx = createIndex(i, 0);
@@ -290,53 +511,34 @@ void PhotoModel::requestThumbnail(int index)
<< QFileInfo(filePath).fileName();
}
// Clean up the watcher
watcher->deleteLater();
});
// Determine if this is a video or image and load accordingly
bool isVideo = info.fileName.endsWith(".MOV", Qt::CaseInsensitive) ||
info.fileName.endsWith(".MP4", Qt::CaseInsensitive) ||
info.fileName.endsWith(".M4V", Qt::CaseInsensitive);
QString cachePath = getThumbnailCachePath(info.filePath);
QFuture<QPixmap> future;
if (isVideo) {
// Load video thumbnail asynchronously
// todo: implement
future = QtConcurrent::run([this]() {
// Check disk cache first
// if (QFile::exists(cachePath)) {
// QPixmap cached(cachePath);
// if (!cached.isNull() && cached.size() == m_thumbnailSize) {
// qDebug() << "Video disk cache HIT for:"
// << QFileInfo(info.filePath).fileName();
// return cached;
// }
// }
future = QtConcurrent::run([this, info]() {
// Acquire semaphore FIRST to limit concurrent video processing
qDebug() << "Waiting for semaphore for:" << info.fileName;
m_videoThumbnailSemaphore.acquire();
qDebug() << "Acquired semaphore for:" << info.fileName;
// // Generate video thumbnail
// QPixmap thumbnail = generateVideoThumbnail(m_device,
// info.filePath,
// m_thumbnailSize);
// Generate video thumbnail using FFmpeg directly (no QMediaPlayer)
QPixmap thumbnail = generateVideoThumbnailFFmpeg(
m_device, info.filePath, m_thumbnailSize);
// // Save to disk cache if successful
// if (!thumbnail.isNull()) {
// QDir().mkpath(QFileInfo(cachePath).absolutePath());
// if (thumbnail.save(cachePath, "JPG", 85)) {
// qDebug() << "Saved video thumbnail to disk cache:"
// << QFileInfo(info.filePath).fileName();
// }
// }
return QPixmap(); // Placeholder until implemented
// return thumbnail;
// Release semaphore
qDebug() << "Releasing semaphore for:" << info.fileName;
m_videoThumbnailSemaphore.release();
return thumbnail;
});
} else {
// Load image thumbnail asynchronously (existing logic)
future = QtConcurrent::run([info, cachePath, this]() {
future = QtConcurrent::run([info, this]() {
return loadThumbnailFromDevice(m_device, info.filePath,
m_thumbnailSize, cachePath);
m_thumbnailSize);
});
}
@@ -346,66 +548,56 @@ void PhotoModel::requestThumbnail(int index)
// Static function that runs in worker thread
QPixmap PhotoModel::loadThumbnailFromDevice(iDescriptorDevice *device,
const QString &filePath,
const QSize &size,
const QString &cachePath)
const QSize &size)
{
// Check disk cache first
if (QFile::exists(cachePath)) {
QPixmap cached(cachePath);
if (!cached.isNull() && cached.size() == size) {
qDebug() << "Disk cache HIT for:" << QFileInfo(filePath).fileName();
return cached;
}
}
// Load from device using ServiceManager
QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray(
device, filePath.toUtf8().constData());
if (imageData.isEmpty()) {
qDebug() << "Could not read from device:" << filePath;
return QPixmap(); // Return empty pixmap on error
return {}; // Return empty pixmap on error
}
if (filePath.endsWith(".HEIC")) {
if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) {
qDebug() << "Loading HEIC image from data for:" << filePath;
QPixmap img = load_heic(imageData);
return img.isNull() ? QPixmap() : img;
return img.isNull() ? QPixmap()
: img.scaled(size, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
// Load pixmap from data
// Use QImageReader for efficient, low-memory scaled loading
QBuffer buffer(&imageData);
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer);
if (reader.canRead()) {
// This is the key optimization: it decodes a smaller image directly,
// saving a massive amount of memory.
reader.setScaledSize(size);
QImage image = reader.read();
if (!image.isNull()) {
return QPixmap::fromImage(image);
}
qDebug() << "QImageReader failed to decode" << filePath
<< "Error:" << reader.errorString();
}
// Fallback for formats QImageReader might struggle with
QPixmap original;
if (!original.loadFromData(imageData)) {
qDebug() << "Could not decode image data for:" << filePath;
return QPixmap();
if (original.loadFromData(imageData)) {
return original.scaled(size, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
// Scale to thumbnail size
QPixmap thumbnail =
original.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
// Save to disk cache
QDir().mkpath(QFileInfo(cachePath).absolutePath());
if (thumbnail.save(cachePath, "JPG", 85)) {
qDebug() << "Saved to disk cache:" << QFileInfo(filePath).fileName();
}
return thumbnail;
qDebug() << "Could not decode image data for:" << filePath;
return {};
}
QPixmap PhotoModel::loadImage(iDescriptorDevice *device,
const QString &filePath, const QString &cachePath)
const QString &filePath)
{
// Check disk cache first
if (QFile::exists(cachePath)) {
QPixmap cached(cachePath);
if (!cached.isNull()) {
qDebug() << "Disk cache HIT for:" << QFileInfo(filePath).fileName();
return cached;
}
}
// Load from device using ServiceManager
QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray(
device, filePath.toUtf8().constData());
@@ -420,20 +612,12 @@ QPixmap PhotoModel::loadImage(iDescriptorDevice *device,
return img.isNull() ? QPixmap() : img;
}
// TODO
// Load pixmap from data
QPixmap original;
if (!original.loadFromData(imageData)) {
qDebug() << "Could not decode image data for:" << filePath;
return QPixmap();
}
// Save to disk cache
QDir().mkpath(QFileInfo(cachePath).absolutePath());
if (original.save(cachePath, "JPG", 85)) {
qDebug() << "Saved to disk cache:" << QFileInfo(filePath).fileName();
}
return original;
}
@@ -446,25 +630,17 @@ void PhotoModel::populatePhotoPaths()
return;
}
beginResetModel();
m_allPhotos.clear();
m_photos.clear();
// // Your existing logic to populate photo paths
// char **files = nullptr;
// qDebug() << "Populating photos from album path:" << m_albumPath;
// First verify the album path exists
QByteArray albumPathBytes = m_albumPath.toUtf8();
const char *albumPathCStr = albumPathBytes.constData();
char **albumInfo = nullptr;
afc_error_t infoResult =
afc_get_file_info(m_device->afcClient, albumPathCStr, &albumInfo);
ServiceManager::safeAfcGetFileInfo(m_device, albumPathCStr, &albumInfo);
if (infoResult != AFC_E_SUCCESS) {
qDebug() << "Album path does not exist or cannot be accessed:"
<< m_albumPath << "Error:" << infoResult;
endResetModel();
return;
}
if (albumInfo) {
@@ -484,7 +660,6 @@ void PhotoModel::populatePhotoPaths()
if (readResult != AFC_E_SUCCESS) {
qDebug() << "Failed to read photo directory:" << photoDir
<< "Error:" << readResult;
endResetModel();
return;
}
@@ -511,11 +686,9 @@ void PhotoModel::populatePhotoPaths()
afc_dictionary_free(files);
}
// Apply initial filtering and sorting
// Apply initial filtering and sorting, which will also reset the model
applyFilterAndSort();
endResetModel();
qDebug() << "Loaded" << m_allPhotos.size() << "media files from device";
qDebug() << "After filtering:" << m_photos.size() << "items shown";
}
@@ -541,11 +714,16 @@ void PhotoModel::applyFilterAndSort()
{
beginResetModel();
// int i = 0;
// Filter photos
m_photos.clear();
for (const PhotoInfo &info : m_allPhotos) {
if (matchesFilter(info)) {
m_photos.append(info);
// if (i == 3) {
// break;
// }
// i++;
}
}
@@ -634,14 +812,11 @@ QStringList PhotoModel::getFilteredFilePaths() const
// Helper methods
QDateTime PhotoModel::extractDateTimeFromFile(const QString &filePath) const
{
// Use AFC to get actual file creation time from device
plist_t info = nullptr;
afc_error_t afc_err = afc_get_file_info_plist(
// TODO:AFC CLIENT IS NOT LONG LIVED
m_device->afcClient, filePath.toUtf8().constData(), &info);
afc_error_t afc_err = ServiceManager::safeAfcGetFileInfoPlist(
m_device, filePath.toUtf8().constData(), &info);
if (afc_err == AFC_E_SUCCESS && info) {
// Try to get st_birthtime (creation time) first
plist_t birthtime_node = plist_dict_get_item(info, "st_birthtime");
if (birthtime_node &&
plist_get_node_type(birthtime_node) == PLIST_UINT) {
+10 -12
View File
@@ -27,6 +27,7 @@
#include <QDateTime>
#include <QFutureWatcher>
#include <QPixmap>
#include <QSemaphore>
#include <QSize>
#include <QStandardPaths>
@@ -49,7 +50,8 @@ public:
enum FilterType { All, ImagesOnly, VideosOnly };
explicit PhotoModel(iDescriptorDevice *device, QObject *parent = nullptr);
explicit PhotoModel(iDescriptorDevice *device, FilterType filterType,
QObject *parent = nullptr);
~PhotoModel();
// QAbstractItemModel interface
@@ -81,13 +83,12 @@ public:
QStringList getAllFilePaths() const;
QStringList getFilteredFilePaths() const;
static QPixmap loadImage(iDescriptorDevice *device, const QString &filePath,
const QString &cachePath);
static QPixmap loadImage(iDescriptorDevice *device,
const QString &filePath);
// Static helper methods
static QPixmap loadThumbnailFromDevice(iDescriptorDevice *device,
const QString &filePath,
const QSize &size,
const QString &cachePath);
const QSize &size);
signals:
void thumbnailNeedsToBeLoaded(int index);
void exportRequested(const QStringList &filePaths);
@@ -105,7 +106,6 @@ private:
// Thumbnail management
QSize m_thumbnailSize;
mutable QCache<QString, QPixmap> m_thumbnailCache;
QString m_cacheDir;
mutable QHash<QString, QFutureWatcher<QPixmap> *> m_activeLoaders;
mutable QSet<QString> m_loadingPaths;
@@ -119,15 +119,13 @@ private:
void sortPhotos(QList<PhotoInfo> &photos) const;
bool matchesFilter(const PhotoInfo &info) const;
QString getThumbnailCacheKey(const QString &filePath) const;
QString getThumbnailCachePath(const QString &filePath) const;
QDateTime extractDateTimeFromFile(const QString &filePath) const;
PhotoInfo::FileType determineFileType(const QString &fileName) const;
static QPixmap generateVideoThumbnail(iDescriptorDevice *device,
const QString &filePath,
const QSize &requestedSize);
static QPixmap generateVideoThumbnailFFmpeg(iDescriptorDevice *device,
const QString &filePath,
const QSize &requestedSize);
static QSemaphore m_videoThumbnailSemaphore;
};
#endif // PHOTOMODEL_H
+25
View File
@@ -45,6 +45,19 @@ ServiceManager::safeAfcGetFileInfo(iDescriptorDevice *device, const char *path,
altAfc);
}
afc_error_t
ServiceManager::safeAfcGetFileInfoPlist(iDescriptorDevice *device,
const char *path, plist_t *info,
std::optional<afc_client_t> altAfc)
{
return executeAfcOperation(
device,
[path, info](afc_client_t client) {
return afc_get_file_info_plist(client, path, info);
},
altAfc);
}
afc_error_t ServiceManager::safeAfcFileOpen(iDescriptorDevice *device,
const char *path,
afc_file_mode_t mode,
@@ -112,6 +125,18 @@ afc_error_t ServiceManager::safeAfcFileSeek(iDescriptorDevice *device,
altAfc);
}
afc_error_t ServiceManager::safeAfcFileTell(iDescriptorDevice *device,
uint64_t handle, uint64_t *position,
std::optional<afc_client_t> altAfc)
{
return executeAfcOperation(
device,
[handle, position](afc_client_t client) {
return afc_file_tell(client, handle, position);
},
altAfc);
}
QByteArray
ServiceManager::safeReadAfcFileToByteArray(iDescriptorDevice *device,
const char *path,
+10
View File
@@ -179,6 +179,12 @@ public:
safeAfcGetFileInfo(iDescriptorDevice *device, const char *path,
char ***info,
std::optional<afc_client_t> altAfc = std::nullopt);
static afc_error_t
safeAfcGetFileInfoPlist(iDescriptorDevice *device, const char *path,
plist_t *info,
std::optional<afc_client_t> altAfc = std::nullopt);
static afc_error_t
safeAfcFileOpen(iDescriptorDevice *device, const char *path,
afc_file_mode_t mode, uint64_t *handle,
@@ -198,6 +204,10 @@ public:
safeAfcFileSeek(iDescriptorDevice *device, uint64_t handle, int64_t offset,
int whence,
std::optional<afc_client_t> altAfc = std::nullopt);
static afc_error_t
safeAfcFileTell(iDescriptorDevice *device, uint64_t handle,
uint64_t *position,
std::optional<afc_client_t> altAfc = std::nullopt);
// Utility functions
static QByteArray safeReadAfcFileToByteArray(
+1 -1
View File
@@ -143,7 +143,7 @@ VirtualLocation::VirtualLocation(iDescriptorDevice *device, QWidget *parent)
});
DevDiskManager::sharedInstance()->downloadCompatibleImage(m_device);
unsigned int device_version = get_device_version(m_device->device);
unsigned int device_version = idevice_get_device_version(m_device->device);
unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF;
if (deviceMajorVersion > 16) {