mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
optimize gallerywidget
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user