New WIP image provider.
This commit is contained in:
parent
ec43131a10
commit
c46d38e38e
|
@ -51,7 +51,7 @@ ColumnLayout {
|
||||||
|
|
||||||
visible: avatarVisible
|
visible: avatarVisible
|
||||||
hint: author.displayName
|
hint: author.displayName
|
||||||
source: author.avatarUrl
|
source: author.avatarMediaId
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
|
|
@ -51,7 +51,7 @@ ColumnLayout {
|
||||||
|
|
||||||
visible: avatarVisible
|
visible: avatarVisible
|
||||||
hint: author.displayName
|
hint: author.displayName
|
||||||
source: author.avatarUrl
|
source: author.avatarMediaId
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
@ -74,15 +74,19 @@ ColumnLayout {
|
||||||
|
|
||||||
id: img
|
id: img
|
||||||
|
|
||||||
source: "image://mxc/" + (content.thumbnail_url ? content.thumbnail_url : content.url)
|
source: downloaded ? progressInfo.localPath : "image://mxc/" +
|
||||||
|
(content.info && content.info.thumbnail_info ?
|
||||||
|
content.thumbnailMediaId : content.mediaId)
|
||||||
|
sourceSize.width: 200
|
||||||
|
sourceSize.height: 200
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: OpacityMask {
|
layer.effect: OpacityMask {
|
||||||
maskSource: Rectangle {
|
maskSource: Rectangle {
|
||||||
width: img.width
|
width: img.width
|
||||||
height: img.height
|
height: img.height
|
||||||
radius: 24
|
radius: 24
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoMouseArea {
|
AutoMouseArea {
|
||||||
|
|
|
@ -47,7 +47,7 @@ ColumnLayout {
|
||||||
|
|
||||||
visible: avatarVisible
|
visible: avatarVisible
|
||||||
hint: author.displayName
|
hint: author.displayName
|
||||||
source: author.avatarUrl
|
source: author.avatarMediaId
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
@ -146,7 +146,7 @@ ColumnLayout {
|
||||||
Layout.preferredHeight: 36
|
Layout.preferredHeight: 36
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
|
|
||||||
source: replyAuthor ? replyAuthor.avatarUrl : ""
|
source: replyAuthor ? replyAuthor.avatarMediaId : ""
|
||||||
hint: replyAuthor ? replyAuthor.displayName : "H"
|
hint: replyAuthor ? replyAuthor.displayName : "H"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ Drawer {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
hint: room ? room.displayName : "No name"
|
hint: room ? room.displayName : "No name"
|
||||||
source: room ? room.avatarUrl : null
|
source: room ? room.avatarMediaId : null
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
|
|
@ -39,7 +39,7 @@ Control {
|
||||||
|
|
||||||
id: headerImage
|
id: headerImage
|
||||||
|
|
||||||
source: currentRoom.avatarUrl
|
source: currentRoom.avatarMediaId
|
||||||
hint: currentRoom ? currentRoom.displayName : "No name"
|
hint: currentRoom ? currentRoom.displayName : "No name"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@ Item {
|
||||||
Layout.margins: 12
|
Layout.margins: 12
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
source: root.user ? root.user.avatarUrl : null
|
source: root.user ? root.user.avatarMediaId : null
|
||||||
hint: root.user ? root.user.displayName : "?"
|
hint: root.user ? root.user.displayName : "?"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -648,7 +648,7 @@ Item {
|
||||||
|
|
||||||
visible: !searchField.active
|
visible: !searchField.active
|
||||||
|
|
||||||
source: root.user ? root.user.avatarUrl : null
|
source: root.user ? root.user.avatarMediaId : null
|
||||||
hint: root.user ? root.user.displayName : "?"
|
hint: root.user ? root.user.displayName : "?"
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|
|
@ -62,7 +62,7 @@ Item {
|
||||||
|
|
||||||
id: roomHeader
|
id: roomHeader
|
||||||
|
|
||||||
avatar: currentRoom ? currentRoom.avatarUrl : ""
|
avatar: currentRoom ? currentRoom.avatarMediaId : ""
|
||||||
topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
|
topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
|
||||||
atTop: messageListView.atYBeginning
|
atTop: messageListView.atYBeginning
|
||||||
|
|
||||||
|
@ -386,7 +386,7 @@ Item {
|
||||||
Layout.preferredWidth: 24
|
Layout.preferredWidth: 24
|
||||||
Layout.preferredHeight: 24
|
Layout.preferredHeight: 24
|
||||||
|
|
||||||
source: modelData.avatarUrl
|
source: modelData.avatarMediaId
|
||||||
hint: modelData.displayName
|
hint: modelData.displayName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ Control {
|
||||||
Layout.preferredWidth: 32
|
Layout.preferredWidth: 32
|
||||||
Layout.preferredHeight: 32
|
Layout.preferredHeight: 32
|
||||||
|
|
||||||
source: replyUser ? replyUser.avatarUrl : ""
|
source: replyUser ? replyUser.avatarMediaId : ""
|
||||||
hint: replyUser ? replyUser.displayName : "No name"
|
hint: replyUser ? replyUser.displayName : "No name"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ Control {
|
||||||
width: 20
|
width: 20
|
||||||
height: 20
|
height: 20
|
||||||
visible: !isEmoji
|
visible: !isEmoji
|
||||||
source: modelData.avatarUrl || null
|
source: modelData.avatarMediaId || null
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
height: parent.height
|
height: parent.height
|
||||||
|
|
|
@ -1,76 +1,87 @@
|
||||||
#include "imageprovider.h"
|
#include "imageprovider.h"
|
||||||
|
|
||||||
#include <QFile>
|
#include <connection.h>
|
||||||
#include <QMetaObject>
|
#include <jobs/mediathumbnailjob.h>
|
||||||
#include <QStandardPaths>
|
|
||||||
#include <QtCore/QDebug>
|
#include <QtCore/QDebug>
|
||||||
#include <QtCore/QWaitCondition>
|
#include <QtCore/QReadWriteLock>
|
||||||
|
|
||||||
#include "jobs/mediathumbnailjob.h"
|
using QMatrixClient::BaseJob;
|
||||||
|
using QMatrixClient::Connection;
|
||||||
|
|
||||||
#include "connection.h"
|
ThumbnailResponse::ThumbnailResponse(Connection* c, QString mediaId,
|
||||||
|
const QSize& requestedSize)
|
||||||
using QMatrixClient::MediaThumbnailJob;
|
: c(c),
|
||||||
|
mediaId(std::move(mediaId)),
|
||||||
ImageProvider::ImageProvider(QObject* parent)
|
requestedSize(requestedSize),
|
||||||
: QObject(parent),
|
errorStr("Image request hasn't started") {
|
||||||
QQuickImageProvider(
|
moveToThread(c->thread());
|
||||||
QQmlImageProviderBase::Image,
|
if (requestedSize.isEmpty()) {
|
||||||
QQmlImageProviderBase::ForceAsynchronousImageLoading) {}
|
errorStr.clear();
|
||||||
|
emit finished();
|
||||||
QImage ImageProvider::requestImage(const QString& id, QSize* pSize,
|
return;
|
||||||
const QSize& requestedSize) {
|
|
||||||
if (!id.startsWith("mxc://")) {
|
|
||||||
qWarning() << "ImageProvider: won't fetch an invalid id:" << id
|
|
||||||
<< "doesn't follow server/mediaId pattern";
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
// Execute a request on the main thread asynchronously
|
||||||
QUrl mxcUri{id};
|
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest,
|
||||||
|
Qt::QueuedConnection);
|
||||||
QImage result = image(mxcUri, requestedSize);
|
|
||||||
if (result.isNull()) return {};
|
|
||||||
if (!requestedSize.isEmpty() && result.size() != requestedSize) {
|
|
||||||
QImage scaled = result.scaled(requestedSize, Qt::KeepAspectRatio,
|
|
||||||
Qt::SmoothTransformation);
|
|
||||||
if (pSize != nullptr) *pSize = scaled.size();
|
|
||||||
return scaled;
|
|
||||||
}
|
|
||||||
if (pSize != nullptr) *pSize = result.size();
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage ImageProvider::image(const QUrl& mxc, const QSize& size) {
|
void ThumbnailResponse::startRequest() {
|
||||||
QUrl tempfilePath = QUrl::fromLocalFile(
|
// Runs in the main thread, not QML thread
|
||||||
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" +
|
if (mediaId.count('/') != 1) {
|
||||||
mxc.fileName() + ".png");
|
errorStr =
|
||||||
QImage cachedImage;
|
QStringLiteral("Media id '%1' doesn't follow server/mediaId pattern")
|
||||||
if (cachedImage.load(tempfilePath.toLocalFile())) {
|
.arg(mediaId);
|
||||||
return cachedImage;
|
emit finished();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaThumbnailJob* job = nullptr;
|
QWriteLocker _(&lock);
|
||||||
QReadLocker locker(&m_lock);
|
job = c->getThumbnail(mediaId, requestedSize);
|
||||||
|
// Connect to any possible outcome including abandonment
|
||||||
|
// to make sure the QML thread is not left stuck forever.
|
||||||
|
connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult);
|
||||||
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
void ThumbnailResponse::prepareResult() {
|
||||||
m_connection, [=] { return m_connection->getThumbnail(mxc, size); },
|
|
||||||
Qt::BlockingQueuedConnection, &job);
|
|
||||||
|
|
||||||
if (!job) {
|
|
||||||
qDebug() << "ImageProvider: failed to send a request";
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
QImage result;
|
|
||||||
{
|
{
|
||||||
QWaitCondition condition; // The most compact way to block on a signal
|
QWriteLocker _(&lock);
|
||||||
job->connect(job, &MediaThumbnailJob::finished, job, [&] {
|
Q_ASSERT(job->error() != BaseJob::Pending);
|
||||||
result = job->thumbnail();
|
|
||||||
condition.wakeAll();
|
if (job->error() == BaseJob::Success) {
|
||||||
});
|
image = job->thumbnail();
|
||||||
condition.wait(&m_lock);
|
errorStr.clear();
|
||||||
|
} else {
|
||||||
|
errorStr = job->errorString();
|
||||||
|
qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-"
|
||||||
|
<< errorStr;
|
||||||
|
}
|
||||||
|
job = nullptr;
|
||||||
}
|
}
|
||||||
|
emit finished();
|
||||||
result.save(tempfilePath.toLocalFile());
|
}
|
||||||
|
|
||||||
return result;
|
QQuickTextureFactory* ThumbnailResponse::textureFactory() const {
|
||||||
|
QReadLocker _(&lock);
|
||||||
|
return QQuickTextureFactory::textureFactoryForImage(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ThumbnailResponse::errorString() const {
|
||||||
|
QReadLocker _(&lock);
|
||||||
|
return errorStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThumbnailResponse::cancel() {
|
||||||
|
QWriteLocker _(&lock);
|
||||||
|
if (job) {
|
||||||
|
job->abandon();
|
||||||
|
job = nullptr;
|
||||||
|
}
|
||||||
|
errorStr = "Image request has been cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
QQuickImageResponse* ImageProvider::requestImageResponse(
|
||||||
|
const QString& id, const QSize& requestedSize) {
|
||||||
|
qDebug() << "ImageProvider: requesting " << id;
|
||||||
|
return new ThumbnailResponse(m_connection.load(), id, requestedSize);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,61 @@
|
||||||
#ifndef IMAGEPROVIDER_H
|
#ifndef IMAGEPROVIDER_H
|
||||||
#define IMAGEPROVIDER_H
|
#define IMAGEPROVIDER_H
|
||||||
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <jobs/mediathumbnailjob.h>
|
||||||
|
#include <QThreadPool>
|
||||||
|
#include <QtCore/QAtomicPointer>
|
||||||
#include <QtCore/QReadWriteLock>
|
#include <QtCore/QReadWriteLock>
|
||||||
#include <QtQuick/QQuickImageProvider>
|
#include <QtQuick/QQuickAsyncImageProvider>
|
||||||
|
|
||||||
#include "connection.h"
|
namespace QMatrixClient {
|
||||||
|
class Connection;
|
||||||
|
}
|
||||||
|
|
||||||
class ImageProvider : public QObject, public QQuickImageProvider {
|
class ThumbnailResponse : public QQuickImageResponse {
|
||||||
|
public:
|
||||||
|
ThumbnailResponse(QMatrixClient::Connection* c, QString mediaId,
|
||||||
|
const QSize& requestedSize);
|
||||||
|
~ThumbnailResponse() override = default;
|
||||||
|
|
||||||
|
void startRequest();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QMatrixClient::Connection* c;
|
||||||
|
const QString mediaId;
|
||||||
|
const QSize requestedSize;
|
||||||
|
QMatrixClient::MediaThumbnailJob* job = nullptr;
|
||||||
|
|
||||||
|
QImage image;
|
||||||
|
QString errorStr;
|
||||||
|
mutable QReadWriteLock lock;
|
||||||
|
|
||||||
|
void prepareResult();
|
||||||
|
QQuickTextureFactory* textureFactory() const override;
|
||||||
|
QString errorString() const override;
|
||||||
|
void cancel() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImageProvider : public QObject, public QQuickAsyncImageProvider {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE
|
Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE
|
||||||
setConnection NOTIFY connectionChanged)
|
setConnection NOTIFY connectionChanged)
|
||||||
public:
|
public:
|
||||||
explicit ImageProvider(QObject* parent = nullptr);
|
explicit ImageProvider() : QObject(), QQuickAsyncImageProvider() {}
|
||||||
|
|
||||||
QImage requestImage(const QString& id, QSize* pSize,
|
QQuickImageResponse* requestImageResponse(
|
||||||
const QSize& requestedSize) override;
|
const QString& id, const QSize& requestedSize) override;
|
||||||
|
|
||||||
void initializeEngine(QQmlEngine* engine, const char* uri);
|
|
||||||
|
|
||||||
QMatrixClient::Connection* connection() { return m_connection; }
|
QMatrixClient::Connection* connection() { return m_connection; }
|
||||||
void setConnection(QMatrixClient::Connection* newConnection) {
|
void setConnection(QMatrixClient::Connection* connection) {
|
||||||
if (m_connection != newConnection) {
|
m_connection.store(connection);
|
||||||
m_connection = newConnection;
|
|
||||||
emit connectionChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void connectionChanged();
|
void connectionChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QReadWriteLock m_lock;
|
QAtomicPointer<QMatrixClient::Connection> m_connection;
|
||||||
QMatrixClient::Connection* m_connection = nullptr;
|
|
||||||
|
|
||||||
QImage image(const QUrl& mxc, const QSize& size);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // IMAGEPROVIDER_H
|
#endif // IMAGEPROVIDER_H
|
||||||
|
|
|
@ -14,10 +14,6 @@
|
||||||
|
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
static QString parseAvatarUrl(QUrl url) {
|
|
||||||
return url.host() + "/" + url.path();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> MessageEventModel::roleNames() const {
|
QHash<int, QByteArray> MessageEventModel::roleNames() const {
|
||||||
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
||||||
roles[EventTypeRole] = "eventType";
|
roles[EventTypeRole] = "eventType";
|
||||||
|
|
|
@ -82,7 +82,6 @@ void RoomListModel::connectRoomSignals(SpectralRoom* room) {
|
||||||
if (event->isStateEvent()) return;
|
if (event->isStateEvent()) return;
|
||||||
User* sender = room->user(event->senderId());
|
User* sender = room->user(event->senderId());
|
||||||
if (sender == room->localUser()) return;
|
if (sender == room->localUser()) return;
|
||||||
QUrl _url = room->avatarUrl();
|
|
||||||
emit newMessage(
|
emit newMessage(
|
||||||
room->id(), event->id(), room->displayName(),
|
room->id(), event->id(), room->displayName(),
|
||||||
sender->displayname(), utils::eventToString(*event),
|
sender->displayname(), utils::eventToString(*event),
|
||||||
|
@ -151,7 +150,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const {
|
||||||
}
|
}
|
||||||
SpectralRoom* room = m_rooms.at(index.row());
|
SpectralRoom* room = m_rooms.at(index.row());
|
||||||
if (role == NameRole) return room->displayName();
|
if (role == NameRole) return room->displayName();
|
||||||
if (role == AvatarRole) return room->avatarUrl();
|
if (role == AvatarRole) return room->avatarMediaId();
|
||||||
if (role == TopicRole) return room->topic();
|
if (role == TopicRole) return room->topic();
|
||||||
if (role == CategoryRole) {
|
if (role == CategoryRole) {
|
||||||
if (room->joinState() == JoinState::Invite) return RoomType::Invited;
|
if (room->joinState() == JoinState::Invite) return RoomType::Invited;
|
||||||
|
|
|
@ -69,7 +69,7 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const {
|
||||||
return user->id();
|
return user->id();
|
||||||
}
|
}
|
||||||
if (role == AvatarRole) {
|
if (role == AvatarRole) {
|
||||||
return user->avatarUrl();
|
return user->avatarMediaId();
|
||||||
}
|
}
|
||||||
|
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
|
Loading…
Reference in New Issue