mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
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:
+21
-1
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 << ":"
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user