Commit 548bdef9 authored by Carl Schwan's avatar Carl Schwan 🚴
Browse files

Add avatar to folder list



If available in the contact info. This heavily cache the avatars for
performance reasons.

Signed-off-by: Carl Schwan's avatarCarl Schwan <carl@carlschwan.eu>
parent 39df7a03
Pipeline #228248 passed with stage
in 2 minutes and 23 seconds
......@@ -15,6 +15,8 @@ set(kalendar_mail_SRCS
mailmodel.h
helper.h
helper.cpp
contactimageprovider.cpp
contactimageprovider.h
messagestatus.h
messagestatus.cpp
......
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "contactimageprovider.h"
#include <Akonadi/ContactSearchJob>
#include <KIO/TransferJob>
#include <QApplication>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QThread>
#include <KLocalizedString>
#include <kjob.h>
QQuickImageResponse *ContactImageProvider::requestImageResponse(const QString &email, const QSize &requestedSize)
{
return new ThumbnailResponse(email, requestedSize);
}
ThumbnailResponse::ThumbnailResponse(QString email, QSize size)
: m_email(std::move(email))
, requestedSize(size)
, localFile(QStringLiteral("%1/contact_picture_provider/%2.png").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), m_email))
, errorStr(QStringLiteral("Image request hasn't started"))
{
QImage cachedImage;
if (cachedImage.load(localFile)) {
m_image = cachedImage;
errorStr.clear();
Q_EMIT finished();
return;
}
// Execute a request on the main thread asynchronously
moveToThread(QApplication::instance()->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
}
void ThumbnailResponse::startRequest()
{
job = new Akonadi::ContactSearchJob();
job->setQuery(Akonadi::ContactSearchJob::Email, m_email.toLower(), Akonadi::ContactSearchJob::ExactMatch);
// Runs in the main thread, not QML thread
Q_ASSERT(QThread::currentThread() == QApplication::instance()->thread());
// Connect to any possible outcome including abandonment
// to make sure the QML thread is not left stuck forever.
connect(job, &Akonadi::ContactSearchJob::finished, this, &ThumbnailResponse::prepareResult);
}
bool ThumbnailResponse::searchPhoto(const KContacts::AddresseeList &list)
{
bool foundPhoto = false;
for (const KContacts::Addressee &addressee : list) {
const KContacts::Picture photo = addressee.photo();
if (!photo.isEmpty()) {
m_photo = photo;
foundPhoto = true;
break;
}
}
return foundPhoto;
}
void ThumbnailResponse::prepareResult()
{
Q_ASSERT(QThread::currentThread() == job->thread());
auto searchJob = static_cast<Akonadi::ContactSearchJob *>(job);
{
QWriteLocker _(&lock);
if (job->error() == KJob::NoError) {
bool ok = false;
const int contactSize(searchJob->contacts().size());
if (contactSize >= 1) {
if (contactSize > 1) {
qWarning() << " more than 1 contact was found we return first contact";
}
const KContacts::Addressee addressee = searchJob->contacts().at(0);
if (searchPhoto(searchJob->contacts())) {
// We have a data raw => we can update message
if (m_photo.isIntern()) {
m_image = m_photo.data();
ok = true;
} else {
const QUrl url = QUrl::fromUserInput(m_photo.url(), QString(), QUrl::AssumeLocalFile);
if (!url.isEmpty()) {
if (url.isLocalFile()) {
if (m_image.load(url.toLocalFile())) {
ok = true;
}
} else {
QByteArray imageData;
KIO::TransferJob *jobTransfert = KIO::get(url, KIO::NoReload);
QObject::connect(jobTransfert, &KIO::TransferJob::data, [&imageData](KIO::Job *, const QByteArray &data) {
imageData.append(data);
});
if (jobTransfert->exec()) {
if (m_image.loadFromData(imageData)) {
ok = true;
}
}
}
}
}
}
}
QString localPath = QFileInfo(localFile).absolutePath();
QDir dir;
if (!dir.exists(localPath)) {
dir.mkpath(localPath);
}
m_image.save(localFile);
if (ok) {
errorStr.clear();
} else {
errorStr = QStringLiteral("No image found");
}
} else if (job->error() == Akonadi::Job::UserCanceled) {
errorStr = i18n("Image request has been cancelled");
} else {
errorStr = job->errorString();
qWarning() << "ThumbnailResponse: no valid image for" << m_email << "-" << errorStr;
}
job = nullptr;
}
Q_EMIT finished();
}
void ThumbnailResponse::doCancel()
{
// Runs in the main thread, not QML thread
if (job) {
Q_ASSERT(QThread::currentThread() == job->thread());
job->kill();
}
}
QQuickTextureFactory *ThumbnailResponse::textureFactory() const
{
QReadLocker _(&lock);
return QQuickTextureFactory::textureFactoryForImage(m_image);
}
QString ThumbnailResponse::errorString() const
{
QReadLocker _(&lock);
return errorStr;
}
void ThumbnailResponse::cancel()
{
QMetaObject::invokeMethod(this, &ThumbnailResponse::doCancel, Qt::QueuedConnection);
}
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QQuickAsyncImageProvider>
#include <KContacts/Addressee>
#include <QAtomicPointer>
#include <QReadWriteLock>
namespace Akonadi
{
class ContactSearchJob;
}
class ThumbnailResponse : public QQuickImageResponse
{
Q_OBJECT
public:
ThumbnailResponse(QString mediaId, QSize requestedSize);
~ThumbnailResponse() override = default;
private Q_SLOTS:
void startRequest();
void prepareResult();
void doCancel();
private:
bool searchPhoto(const KContacts::AddresseeList &list);
const QString m_email;
QSize requestedSize;
const QString localFile;
QImage m_image;
KContacts::Picture m_photo;
QString errorStr;
Akonadi::ContactSearchJob *job = nullptr;
mutable QReadWriteLock lock; // Guards ONLY these two members above
QQuickTextureFactory *textureFactory() const override;
QString errorString() const override;
void cancel() override;
};
class ContactImageProvider : public QQuickAsyncImageProvider
{
public:
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
};
......@@ -7,6 +7,7 @@
#include <QQmlEngine>
#include <QtQml>
#include "contactimageprovider.h"
#include "helper.h"
#include "mailmanager.h"
#include "mailmodel.h"
......@@ -39,3 +40,9 @@ void CalendarPlugin::registerTypes(const char *uri)
qRegisterMetaType<MailModel *>("MailModel*");
}
void CalendarPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
{
Q_UNUSED(uri);
engine->addImageProvider(QLatin1String("contact"), new ContactImageProvider);
}
......@@ -12,4 +12,5 @@ class CalendarPlugin : public QQmlExtensionPlugin
public:
void registerTypes(const char *uri) override;
void initializeEngine(QQmlEngine *engine, const char *uri) override;
};
\ No newline at end of file
......@@ -5,7 +5,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.12 as Kirigami
import org.kde.kirigami 2.19 as Kirigami
Kirigami.AbstractListItem {
id: root
......@@ -76,6 +76,18 @@ Kirigami.AbstractListItem {
anchors.left: parent.left
anchors.right: parent.right
Kirigami.Avatar {
// Euristic to extract name from "Name <email>" pattern
name: author.replace(/<.*>/, '').replace(/\(.*\)/, '')
// Extract and use email address as unique identifier for image provider
source: 'image://contact/' + new RegExp("<(.*)>").exec(author)[1] ?? ''
Layout.rightMargin: Kirigami.Units.largeSpacing
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
Layout.preferredWidth: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
Layout.preferredHeight: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
ColumnLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
......@@ -119,3 +131,4 @@ Kirigami.AbstractListItem {
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment