optimize gallerywidget

This commit is contained in:
uncor3
2026-04-09 18:44:20 +00:00
parent 598d7b7721
commit 35d99a1c05
8 changed files with 202 additions and 259 deletions
+9 -9
View File
@@ -24,12 +24,12 @@
#include <QPixmap>
#include <libheif/heif.h>
QPixmap load_heic(const QByteArray &imageData)
QImage load_heic(const QByteArray &imageData)
{
heif_context *ctx = heif_context_alloc();
if (!ctx) {
qWarning() << "Failed to allocate heif_context";
return QPixmap();
return QImage();
}
heif_error err = heif_context_read_from_memory(ctx, imageData.constData(),
@@ -37,7 +37,7 @@ QPixmap load_heic(const QByteArray &imageData)
if (err.code != heif_error_Ok) {
qWarning() << "Failed to read HEIC from memory:" << err.message;
heif_context_free(ctx);
return QPixmap();
return QImage();
}
heif_image_handle *handle;
@@ -45,7 +45,7 @@ QPixmap load_heic(const QByteArray &imageData)
if (err.code != heif_error_Ok) {
qWarning() << "Failed to get primary image handle:" << err.message;
heif_context_free(ctx);
return QPixmap();
return QImage();
}
heif_image *img;
@@ -55,7 +55,7 @@ QPixmap load_heic(const QByteArray &imageData)
qWarning() << "Failed to decode HEIC image:" << err.message;
heif_image_handle_release(handle);
heif_context_free(ctx);
return QPixmap();
return QImage();
}
int width = heif_image_get_width(img, heif_channel_interleaved);
@@ -73,15 +73,15 @@ QPixmap load_heic(const QByteArray &imageData)
heif_image_release(img);
heif_image_handle_release(handle);
heif_context_free(ctx);
return QPixmap();
return QImage();
}
QImage qimg(data, width, height, stride, QImage::Format_RGB888);
QPixmap result = QPixmap::fromImage(qimg);
QImage copy =
qimg.copy(); // Deep copy since the original data will be freed
heif_image_release(img);
heif_image_handle_release(handle);
heif_context_free(ctx);
return result;
return copy;
}
+54 -58
View File
@@ -20,6 +20,7 @@
#include "gallerywidget.h"
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "imageloader.h"
#include "iomanagerclient.h"
#include "mediapreviewdialog.h"
#include "photomodel.h"
@@ -42,14 +43,12 @@
#include <QVBoxLayout>
#include <QtConcurrent/QtConcurrent>
// todo: dont load paths on main thread, handle
/*
FIXME: this needs to be refactored once we
figure out how to query Photos.sqlite
Check out:
https://github.com/ScottKjr3347/iOS_Local_PL_Photos.sqlite_Queries
*/
GalleryWidget::GalleryWidget(const std::shared_ptr<iDescriptorDevice> device,
QWidget *parent)
: QWidget{parent}, m_device(device)
@@ -295,9 +294,7 @@ void GalleryWidget::onExportSelected()
QList<QString> exportItems;
for (const QString &filePath : filePaths) {
QString fileName = filePath.split('/').last();
exportItems.append(filePath);
// exportItems.append(ExportItem(filePath, fileName, m_device->udid));
}
qDebug() << "Starting export of selected files:" << exportItems.size()
@@ -335,14 +332,14 @@ void GalleryWidget::onExportAll()
if (!m_model)
return;
QStringList filePaths = m_model->getFilteredFilePaths();
QList<QString> exportItems = m_model->getFilteredFilePaths();
if (filePaths.isEmpty()) {
if (exportItems.isEmpty()) {
QMessageBox::information(this, "No Items", "No items to export.");
return;
}
QString message =
QString("Export all %1 items currently shown?").arg(filePaths.size());
QString("Export all %1 items currently shown?").arg(exportItems.size());
int reply = QMessageBox::question(this, "Export All", message,
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
@@ -356,11 +353,6 @@ void GalleryWidget::onExportAll()
return;
}
QList<QString> exportItems;
for (const QString &filePath : filePaths) {
QString fileName = filePath.split('/').last();
}
qDebug() << "Starting export of:" << exportItems.size() << "items to"
<< exportDir;
@@ -590,8 +582,11 @@ void GalleryWidget::setControlsEnabled(bool enabled)
{
m_sortComboBox->setEnabled(enabled);
m_filterComboBox->setEnabled(enabled);
m_exportSelectedButton->setEnabled(
enabled && m_listView && m_listView->selectionModel()->hasSelection());
const bool hasSelection = m_listView && m_listView->selectionModel() &&
m_listView->selectionModel()->hasSelection();
m_exportSelectedButton->setEnabled(enabled && hasSelection);
}
/*
@@ -600,21 +595,24 @@ void GalleryWidget::setControlsEnabled(bool enabled)
Check out:
https://github.com/ScottKjr3347/iOS_Local_PL_Photos.sqlite_Queries
*/
QIcon GalleryWidget::loadAlbumThumbnail(const QString &albumPath)
QImage
GalleryWidget::loadAlbumThumbnail(const QString &albumPath,
std::shared_ptr<iDescriptorDevice> device)
{
// Get album directory contents
QList<QString> albumTree = m_device->afc_backend->list_dir(albumPath);
if (albumTree.isEmpty()) {
qDebug() << "Failed to read album directory:" << albumPath;
return QIcon();
if (QCoreApplication::closingDown() || !QGuiApplication::instance()) {
return {};
}
QList<QString> albumTree = device->afc_backend->list_dir(albumPath);
if (albumTree.isEmpty()) {
qDebug() << "Failed to read album directory:" << albumPath;
return {};
}
// Find the first image file
QString firstImagePath;
for (const QString &fileName : albumTree) {
bool isDir =
m_device->afc_backend->is_directory((albumPath + "/" + fileName));
device->afc_backend->is_directory((albumPath + "/" + fileName));
if (!isDir && (fileName.endsWith(".JPG", Qt::CaseInsensitive) ||
fileName.endsWith(".PNG", Qt::CaseInsensitive) ||
fileName.endsWith(".HEIC", Qt::CaseInsensitive))) {
@@ -624,57 +622,55 @@ QIcon GalleryWidget::loadAlbumThumbnail(const QString &albumPath)
}
if (firstImagePath.isEmpty()) {
qDebug() << "No images found in album:" << albumPath;
return QIcon();
return {};
}
QByteArray imageData =
m_device->afc_backend->file_to_buffer(firstImagePath);
QByteArray imageData = device->afc_backend->file_to_buffer(firstImagePath);
if (imageData.isEmpty()) {
qDebug() << "Could not read image data for thumbnail:" << albumPath;
return QIcon();
}
QPixmap thumbnail;
// Load HEIC
QImage thumbnail;
if (firstImagePath.endsWith(".HEIC", Qt::CaseInsensitive)) {
qDebug() << "Loading HEIC thumbnail from:" << firstImagePath;
thumbnail = load_heic(imageData);
} else {
// Load other formats
if (!thumbnail.loadFromData(imageData)) {
qDebug() << "Could not decode image data for thumbnail:"
<< firstImagePath;
return QIcon();
}
thumbnail.loadFromData(imageData);
}
if (thumbnail.isNull()) {
qDebug() << "Failed to load thumbnail from:" << firstImagePath;
return QIcon();
}
return QIcon(thumbnail);
return thumbnail;
}
void GalleryWidget::loadAlbumThumbnailAsync(const QString &albumPath,
QStandardItem *item)
{
auto *watcher = new QFutureWatcher<QIcon>(this);
Q_UNUSED(item);
connect(watcher, &QFutureWatcher<QIcon>::finished, this, [watcher, item]() {
QIcon result = watcher->result();
if (!result.isNull()) {
item->setIcon(result);
}
watcher->deleteLater();
auto *watcher = new QFutureWatcher<QImage>(this);
const auto device = m_device;
connect(watcher, &QFutureWatcher<QImage>::finished, this,
[this, watcher, albumPath]() {
const QImage result = watcher->result();
watcher->deleteLater();
if (result.isNull() || !m_albumModel ||
QCoreApplication::closingDown() ||
!QGuiApplication::instance()) {
return;
}
for (int row = 0; row < m_albumModel->rowCount(); ++row) {
QModelIndex idx = m_albumModel->index(row, 0);
if (idx.data(Qt::UserRole).toString() == albumPath) {
if (auto *it = m_albumModel->itemFromIndex(idx)) {
it->setIcon(QIcon(QPixmap::fromImage(result)));
}
break;
}
}
});
QFuture<QImage> future = QtConcurrent::run([albumPath, device]() {
return loadAlbumThumbnail(albumPath, device);
});
QFuture<QIcon> future = QtConcurrent::run(
[this, albumPath]() { return loadAlbumThumbnail(albumPath); });
watcher->setFuture(future);
}
+2 -1
View File
@@ -70,7 +70,8 @@ private:
void onAlbumListLoaded(const QList<QString> &dcimTree);
void setControlsEnabled(bool enabled);
QString selectExportDirectory();
QIcon loadAlbumThumbnail(const QString &albumPath);
static QImage loadAlbumThumbnail(const QString &albumPath,
std::shared_ptr<iDescriptorDevice> device);
void loadAlbumThumbnailAsync(const QString &albumPath, QStandardItem *item);
void onPhotoContextMenu(const QPoint &pos);
PhotoModel::FilterType getCurrentFilterType() const;
+54 -118
View File
@@ -16,9 +16,12 @@ extern "C" {
ImageLoader::ImageLoader(QObject *parent) : QObject(parent)
{
// TODO: maybe finetune to hardware ?
m_pool.setMaxThreadCount(10);
// 350 MB cache for thumbnails
m_cache.setMaxCost(350);
m_pool.setMaxThreadCount(15);
if (qApp) {
connect(qApp, &QCoreApplication::aboutToQuit, this,
[this]() { clear(); });
}
}
bool ImageLoader::isLoading(const QString &path)
@@ -31,11 +34,6 @@ void ImageLoader::requestThumbnail(
const std::shared_ptr<iDescriptorDevice> device, const QString &path,
unsigned int row)
{
if (auto *cached = m_cache.object(path)) {
emit thumbnailReady(path, *cached, row);
return;
}
{
QMutexLocker locker(&m_mutex);
if (m_pendingTasks.contains(path))
@@ -66,26 +64,19 @@ void ImageLoader::requestImageWithCallback(
int priority, std::function<void(const QPixmap &)> callback,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest, bool useAfc2)
{
/*
FIXME: row is passed as priority
nothing dangerous but a bit hacky, could be handled better
*/ //scale=false
auto *task =
new ImageTask(device, path, priority, false, hause_arrest, useAfc2);
/*
TODO: should we do this ?
this function is meant for the media preview dialog,
which only loads a image at a time
and not really related to the thumbnails in the photomodel
*/
// m_pendingTasks[path] = task;
connect(
task, &ImageTask::finished, this,
[this, path, callback](const QString &, const QPixmap &pixmap,
unsigned int row) { callback(pixmap); },
[callback](const QString &, const QImage &image, unsigned int) {
if (QCoreApplication::closingDown() ||
!QGuiApplication::instance()) {
callback(QPixmap());
return;
}
callback(image.isNull() ? QPixmap() : QPixmap::fromImage(image));
},
Qt::QueuedConnection);
m_pool.start(task, priority);
@@ -95,92 +86,59 @@ void ImageLoader::cancelThumbnail(const QString &path)
{
qDebug() << "Attempting to cancel thumbnail loading for" << path;
QMutexLocker locker(&m_mutex);
ImageTask *task = nullptr;
{
QMutexLocker locker(&m_mutex);
task = m_pendingTasks.take(path);
}
if (!m_pendingTasks.contains(path)) {
if (!task) {
return;
}
ImageTask *task = m_pendingTasks.value(path);
if (task && m_pool.tryTake(task)) {
if (m_pool.tryTake(task)) {
qDebug() << "Cancelled thumbnail loading for" << path;
m_pendingTasks.remove(path);
// should be safe to delete
delete task;
} else {
m_pendingTasks.remove(path);
}
}
void ImageLoader::clear()
{
qDebug() << "Clearing ImageLoader cache and pending tasks";
m_pool.clear();
m_pool.waitForDone();
{
QMutexLocker locker(&m_mutex);
for (auto it = m_pendingTasks.begin(); it != m_pendingTasks.end();) {
ImageTask *task = it.value();
if (task && m_pool.tryTake(task)) {
qDebug() << "Cancelled pending task";
// FIXME: should we do auto delete?
// delete task;
}
it = m_pendingTasks.erase(it);
}
}
/*
TODO: This could make the UI unresponsive but
maybe a good approch to handle
async cancellation properly(wireless)
Wait for any running tasks to complete
*/
// m_pool.waitForDone();
{
QMutexLocker locker(&m_mutex);
m_pendingTasks.clear();
}
m_cache.clear();
QMutexLocker locker(&m_mutex);
m_pendingTasks.clear();
}
void ImageLoader::onTaskFinished(const QString &path, const QPixmap &pixmap,
void ImageLoader::onTaskFinished(const QString &path, const QImage &image,
unsigned int row)
{
ImageTask *task = nullptr;
{
QMutexLocker locker(&m_mutex);
if (!m_pendingTasks.contains(path)) {
return;
}
task = m_pendingTasks.take(path);
m_pendingTasks.remove(path);
}
// FIXME
// if (task) {
// delete task;
// }
// Cache
m_cache.insert(path, new QPixmap(pixmap));
if (QCoreApplication::closingDown() || !QGuiApplication::instance()) {
return;
}
const QPixmap pixmap =
image.isNull() ? QPixmap() : QPixmap::fromImage(image);
emit thumbnailReady(path, pixmap, row);
}
// almost a copy of loadThumbnailFromDevice but without any scaling logic
QPixmap ImageLoader::loadImage(
QImage ImageLoader::loadImage(
const std::shared_ptr<iDescriptorDevice> device, const QString &filePath,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest, bool useAfc2)
{
if (QCoreApplication::closingDown()) {
qDebug() << "Application is closing, aborting loadImage for"
<< filePath;
if (QCoreApplication::closingDown() || !QGuiApplication::instance()) {
return {};
}
QByteArray imageData;
@@ -194,14 +152,9 @@ QPixmap ImageLoader::loadImage(
imageData = device->afc_backend->file_to_buffer(filePath);
}
if (imageData.isEmpty()) {
qDebug() << "Could not read from device:" << filePath;
return {};
}
if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) {
QPixmap img = load_heic(imageData);
return img.isNull() ? QPixmap() : img;
QImage img = load_heic(imageData);
return img.isNull() ? QImage() : img;
}
QBuffer buffer(&imageData);
@@ -211,31 +164,24 @@ QPixmap ImageLoader::loadImage(
if (reader.canRead()) {
QImage image = reader.read();
if (!image.isNull()) {
return QPixmap::fromImage(image);
return image;
}
qDebug() << "QImageReader failed to decode" << filePath
<< "Error:" << reader.errorString();
}
// Fallback for formats QImageReader might struggle with
QPixmap pixmap;
if (pixmap.loadFromData(imageData)) {
return pixmap;
QImage fallback;
if (fallback.loadFromData(imageData)) {
return fallback;
}
qDebug() << "Could not decode image data for:" << filePath;
return {};
}
QPixmap ImageLoader::loadThumbnailFromDevice(
QImage ImageLoader::loadThumbnailFromDevice(
const std::shared_ptr<iDescriptorDevice> device, const QString &filePath,
const QSize &size,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest, bool useAfc2)
{
if (QCoreApplication::closingDown()) {
qDebug()
<< "Application is closing, aborting loadThumbnailFromDevice for"
<< filePath;
if (QCoreApplication::closingDown() || !QGuiApplication::instance()) {
return {};
}
@@ -250,14 +196,9 @@ QPixmap ImageLoader::loadThumbnailFromDevice(
imageData = device->afc_backend->file_to_buffer(filePath);
}
if (imageData.isEmpty()) {
qDebug() << "Could not read from device:" << filePath;
return {};
}
if (filePath.endsWith(".HEIC", Qt::CaseInsensitive)) {
QPixmap img = load_heic(imageData);
return img.isNull() ? QPixmap()
QImage img = load_heic(imageData);
return img.isNull() ? QImage()
: img.scaled(size, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
@@ -269,31 +210,26 @@ QPixmap ImageLoader::loadThumbnailFromDevice(
if (reader.canRead()) {
QImage image = reader.read();
if (!image.isNull()) {
QImage scaled = image.scaled(size, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
return QPixmap::fromImage(scaled);
return image.scaled(size, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
qDebug() << "QImageReader failed to decode" << filePath
<< "Error:" << reader.errorString();
}
// Fallback for formats QImageReader might struggle with
QPixmap original;
if (original.loadFromData(imageData)) {
return original.scaled(size, Qt::KeepAspectRatio,
QImage fallback;
if (fallback.loadFromData(imageData)) {
return fallback.scaled(size, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
qDebug() << "Could not decode image data for:" << filePath;
return {};
}
QPixmap ImageLoader::generateVideoThumbnailFFmpeg(
QImage ImageLoader::generateVideoThumbnailFFmpeg(
const std::shared_ptr<iDescriptorDevice> device, const QString &filePath,
const QSize &requestedSize,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest, bool useAfc2)
{
QPixmap thumbnail;
QImage thumbnail;
if (QCoreApplication::closingDown()) {
qDebug() << "Application is closing, aborting "
"generateVideoThumbnailFFmpeg for"
@@ -551,9 +487,9 @@ QPixmap ImageLoader::generateVideoThumbnailFFmpeg(
might need to abstract the main logic to get the frame
and handle scaling separately
*/
thumbnail = QPixmap::fromImage(
thumbnail =
imgCopy.scaled(requestedSize, Qt::KeepAspectRatio,
Qt::SmoothTransformation));
Qt::SmoothTransformation);
}
av_frame_free(&rgbFrame);
@@ -570,4 +506,4 @@ QPixmap ImageLoader::generateVideoThumbnailFFmpeg(
avformat_close_input(&formatCtx);
return thumbnail;
}
}
+11 -11
View File
@@ -3,6 +3,7 @@
#include "iDescriptor.h"
#include <QCache>
#include <QGuiApplication>
#include <QHash>
#include <QImage>
#include <QMutex>
@@ -13,8 +14,6 @@
class ImageTask;
typedef struct AfcClient *AfcClientHandle;
class ImageLoader : public QObject
{
Q_OBJECT
@@ -36,30 +35,31 @@ public:
void cancelThumbnail(const QString &path);
bool isLoading(const QString &path);
void clear();
QCache<QString, QPixmap> m_cache;
static QPixmap loadThumbnailFromDevice(
static QImage loadThumbnailFromDevice(
const std::shared_ptr<iDescriptorDevice> device,
const QString &filePath, const QSize &size,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest =
std::nullopt,
bool useAfc2 = false);
static QPixmap generateVideoThumbnailFFmpeg(
static QImage generateVideoThumbnailFFmpeg(
const std::shared_ptr<iDescriptorDevice> device,
const QString &filePath, const QSize &size,
std::optional<std::shared_ptr<CXX::HauseArrest>> hause_arrest =
std::nullopt,
bool useAfc2 = false);
static QPixmap loadImage(const std::shared_ptr<iDescriptorDevice> device,
const QString &filePath,
std::optional<std::shared_ptr<CXX::HauseArrest>>
hause_arrest = std::nullopt,
bool useAfc2 = false);
static QImage loadImage(const std::shared_ptr<iDescriptorDevice> device,
const QString &filePath,
std::optional<std::shared_ptr<CXX::HauseArrest>>
hause_arrest = std::nullopt,
bool useAfc2 = false);
signals:
void thumbnailReady(const QString &path, const QPixmap &image,
unsigned int row);
private slots:
void onTaskFinished(const QString &path, const QPixmap &image,
void onTaskFinished(const QString &path, const QImage &image,
unsigned int row);
private:
+19 -17
View File
@@ -3,14 +3,14 @@
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include "imageloader.h"
#include <QGuiApplication>
#include <QImage>
#include <QObject>
#include <QPixmap>
#include <QRunnable>
#include <QString>
#include "imageloader.h"
class ImageTask : public QObject, public QRunnable
{
Q_OBJECT
@@ -27,30 +27,32 @@ public:
}
signals:
void finished(const QString &path, const QPixmap &image, unsigned int row);
void finished(const QString &path, const QImage &image, unsigned int row);
protected:
void run() override
{
bool isVideo = iDescriptor::Utils::isVideoFile(m_path);
if (QCoreApplication::closingDown()) {
return;
}
const bool isVideo = iDescriptor::Utils::isVideoFile(m_path);
if (isVideo) {
QPixmap thumbnail = ImageLoader::generateVideoThumbnailFFmpeg(
QImage image = ImageLoader::generateVideoThumbnailFFmpeg(
m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest, m_useAfc2);
emit finished(m_path, image, m_row);
return;
}
emit finished(m_path, thumbnail, m_row);
if (m_isThumbnail) {
QImage image = ImageLoader::loadThumbnailFromDevice(
m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest, m_useAfc2);
emit finished(m_path, image, m_row);
} else {
if (m_isThumbnail) {
QPixmap image = ImageLoader::loadThumbnailFromDevice(
m_device, m_path, THUMBNAIL_SIZE, m_hause_arrest,
m_useAfc2);
emit finished(m_path, image, m_row);
} else {
qDebug() << "Loading full image for:" << m_path;
QPixmap image = ImageLoader::loadImage(
m_device, m_path, m_hause_arrest, m_useAfc2);
emit finished(m_path, image, m_row);
}
QImage image = ImageLoader::loadImage(m_device, m_path,
m_hause_arrest, m_useAfc2);
emit finished(m_path, image, m_row);
}
}
+47 -41
View File
@@ -38,6 +38,8 @@ PhotoModel::PhotoModel(const std::shared_ptr<iDescriptorDevice> device,
: QAbstractListModel(parent), m_device(device), m_sortOrder(NewestFirst),
m_filterType(filterType)
{
// 350 MB cache for thumbnails
m_cache.setMaxCost(350 * 1024 * 1024);
}
void PhotoModel::clear()
@@ -50,13 +52,8 @@ void PhotoModel::clear()
m_photos.clear();
m_allPhotos.clear();
endResetModel();
m_cache.clear();
qDebug() << "Cleared PhotoModel data";
// FIXME : bug we use the same loader on every device
// FIXME: we shouldn't do this
// QHashPrivate::Span<QHashPrivate::Node<QString, ImageTask *>>::hasNode
// qhash.h 310 0x5555559d50e1
// ImageLoader::sharedInstance().clear();
}
PhotoModel::~PhotoModel()
@@ -90,8 +87,8 @@ QVariant PhotoModel::data(const QModelIndex &index, int role) const
case Qt::DecorationRole: {
ImageLoader &imgloader = ImageLoader::sharedInstance();
// Check memory cache first
if (QPixmap *cached = imgloader.m_cache.object(info.filePath)) {
// Check cache first
if (QPixmap *cached = m_cache.object(info.filePath)) {
return QIcon(*cached);
}
@@ -126,7 +123,8 @@ void PhotoModel::onThumbnailReady(const QString &path, const QPixmap &pixmap,
unsigned int rowHint)
{
Q_UNUSED(pixmap);
int cacheCost = pixmap.width() * pixmap.height() * pixmap.depth() / 8;
m_cache.insert(path, new QPixmap(pixmap), cacheCost);
QMutexLocker locker(&m_mutex);
int row = -1;
@@ -147,9 +145,7 @@ void PhotoModel::onThumbnailReady(const QString &path, const QPixmap &pixmap,
if (row == -1) {
// Thumbnail arrived for an item that is no longer in the model
qDebug() << "PhotoModel::onThumbnailReady: path not in current model:"
<< path << "(rowHint =" << rowHint
<< ", size =" << m_photos.size() << ")";
qDebug() << "PhotoModel::onThumbnailReady: path not in current model";
return;
}
@@ -158,42 +154,36 @@ void PhotoModel::onThumbnailReady(const QString &path, const QPixmap &pixmap,
emit dataChanged(idx, idx, {Qt::DecorationRole});
}
bool PhotoModel::populatePhotoPaths()
QPair<bool, QList<PhotoInfo>>
PhotoModel::populatePhotoPaths(QString albumPath,
std::shared_ptr<iDescriptorDevice> device)
{
// FIXME:DEADLOCK?
// QMutexLocker locker(&m_mutex);
connect(&ImageLoader::sharedInstance(), &ImageLoader::thumbnailReady, this,
&PhotoModel::onThumbnailReady);
if (m_albumPath.isEmpty()) {
if (albumPath.isEmpty()) {
qDebug() << "No album path set, skipping population";
return false;
return {false, {}};
}
m_allPhotos.clear();
QMap<QString, QVariant> photoPaths =
m_device->afc_backend->list_dir_with_creation_date(m_albumPath);
device->afc_backend->list_dir_with_creation_date(albumPath);
QList<PhotoInfo> photos;
for (auto it = photoPaths.constBegin(); it != photoPaths.constEnd(); ++it) {
const QString &fileName = it.key();
const QVariant &creationDateVariant = it.value();
if (iDescriptor::Utils::isGalleryFile(fileName)) {
PhotoInfo info;
info.filePath = m_albumPath + "/" + fileName;
info.filePath = albumPath + "/" + fileName;
info.fileName = fileName;
info.thumbnailRequested = false;
info.dateTime = creationDateVariant.toDateTime();
info.fileType = determineFileType(fileName);
m_allPhotos.append(info);
photos.append(info);
}
}
// // Apply initial filtering and sorting, which will also reset the model
applyFilterAndSort();
qDebug() << "Loaded" << m_allPhotos.size() << "media files from device";
qDebug() << "After filtering:" << m_photos.size() << "items shown";
return true;
return {true, photos};
}
// Sorting and filtering methods
@@ -291,40 +281,58 @@ QStringList PhotoModel::getAllFilePaths() const
return paths;
}
QStringList PhotoModel::getFilteredFilePaths() const
QList<QString> PhotoModel::getFilteredFilePaths() const
{
QStringList paths;
QList<QString> paths;
for (const PhotoInfo &info : m_photos) {
paths.append(info.filePath);
}
return paths;
}
PhotoInfo::FileType PhotoModel::determineFileType(const QString &fileName) const
PhotoInfo::FileType PhotoModel::determineFileType(const QString &fileName)
{
if (iDescriptor::Utils::isVideoFile(fileName))
return PhotoInfo::Video;
return PhotoInfo::Image;
}
int count = 0;
void PhotoModel::setAlbumPath(const QString &albumPath)
{
qDebug() << "Setting new album path:" << albumPath;
clear();
connect(&ImageLoader::sharedInstance(), &ImageLoader::thumbnailReady, this,
&PhotoModel::onThumbnailReady, Qt::UniqueConnection);
m_albumPath = albumPath;
QFutureWatcher<bool> *futureWatcher = new QFutureWatcher<bool>(this);
QFuture<bool> future =
QtConcurrent::run([this]() { return populatePhotoPaths(); });
const auto device = m_device;
auto *futureWatcher =
new QFutureWatcher<QPair<bool, QList<PhotoInfo>>>(this);
QFuture<QPair<bool, QList<PhotoInfo>>> future =
QtConcurrent::run([albumPath, device]() {
return populatePhotoPaths(albumPath, device);
});
futureWatcher->setFuture(future);
connect(futureWatcher, &QFutureWatcher<bool>::finished, this,
connect(futureWatcher,
&QFutureWatcher<QPair<bool, QList<PhotoInfo>>>::finished, this,
[this, futureWatcher]() {
const auto result = futureWatcher->result();
futureWatcher->deleteLater();
bool success = futureWatcher->result();
const bool success = result.first;
const QList<PhotoInfo> photos = result.second;
m_allPhotos = photos;
if (success) {
qDebug() << "Finished populating photo paths for album:"
<< m_albumPath;
applyFilterAndSort();
emit albumPathSet();
} else {
qDebug() << "Failed to populate photo paths for album:"
@@ -332,6 +340,4 @@ void PhotoModel::setAlbumPath(const QString &albumPath)
emit albumPathSetFailed();
}
});
}
// TODO:REMOVE
void PhotoModel::refreshPhotos() { populatePhotoPaths(); }
}
+6 -4
View File
@@ -63,7 +63,6 @@ public:
// Album management
void setAlbumPath(const QString &albumPath);
void refreshPhotos();
// Sorting and filtering
void setSortOrder(SortOrder order);
@@ -78,7 +77,7 @@ public:
// Get all items for export
QStringList getAllFilePaths() const;
QStringList getFilteredFilePaths() const;
QList<QString> getFilteredFilePaths() const;
void clear();
@@ -93,14 +92,17 @@ private:
FilterType m_filterType;
QMutex m_mutex;
QCache<QString, QPixmap> m_cache;
// Helper methods
bool populatePhotoPaths();
static QPair<bool, QList<PhotoInfo>>
populatePhotoPaths(QString albumPath,
std::shared_ptr<iDescriptorDevice> device);
void applyFilterAndSort();
void sortPhotos(QList<PhotoInfo> &photos) const;
bool matchesFilter(const PhotoInfo &info) const;
PhotoInfo::FileType determineFileType(const QString &fileName) const;
static PhotoInfo::FileType determineFileType(const QString &fileName);
private slots:
void onThumbnailReady(const QString &path, const QPixmap &pixmap,