Support animated images

fixes #461
This commit is contained in:
Nicolas Werner 2021-08-29 05:20:23 +02:00
parent 09c041c8ac
commit ef068ac2b3
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
8 changed files with 293 additions and 33 deletions

View file

@ -311,6 +311,7 @@ set(SRC_FILES
src/ui/InfoMessage.cpp src/ui/InfoMessage.cpp
src/ui/Label.cpp src/ui/Label.cpp
src/ui/LoadingIndicator.cpp src/ui/LoadingIndicator.cpp
src/ui/MxcAnimatedImage.cpp
src/ui/MxcMediaProxy.cpp src/ui/MxcMediaProxy.cpp
src/ui/NhekoCursorShape.cpp src/ui/NhekoCursorShape.cpp
src/ui/NhekoDropArea.cpp src/ui/NhekoDropArea.cpp
@ -522,6 +523,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/ui/InfoMessage.h src/ui/InfoMessage.h
src/ui/Label.h src/ui/Label.h
src/ui/LoadingIndicator.h src/ui/LoadingIndicator.h
src/ui/MxcAnimatedImage.h
src/ui/MxcMediaProxy.h src/ui/MxcMediaProxy.h
src/ui/Menu.h src/ui/Menu.h
src/ui/NhekoCursorShape.h src/ui/NhekoCursorShape.h

View file

@ -14,6 +14,7 @@ Item {
required property string body required property string body
required property string filename required property string filename
required property bool isReply required property bool isReply
required property string eventId
property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth) property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth)
property double tempHeight: tempWidth * proportionalHeight property double tempHeight: tempWidth * proportionalHeight
property double divisor: isReply ? 5 : 3 property double divisor: isReply ? 5 : 3
@ -37,6 +38,7 @@ Item {
Image { Image {
id: img id: img
visible: !mxcimage.loaded
anchors.fill: parent anchors.fill: parent
source: url.replace("mxc://", "image://MxcImage/") source: url.replace("mxc://", "image://MxcImage/")
asynchronous: true asynchronous: true
@ -53,38 +55,47 @@ Item {
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
} }
HoverHandler { }
id: mouseArea
MxcAnimatedImage {
id: mxcimage
visible: loaded
anchors.fill: parent
roomm: room
eventId: parent.eventId
}
HoverHandler {
id: mouseArea
}
Item {
id: overlay
anchors.fill: parent
visible: mouseArea.hovered
Rectangle {
id: container
width: parent.width
implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom
color: Nheko.colors.window
opacity: 0.75
} }
Item { Text {
id: overlay id: imgcaption
anchors.fill: parent
visible: mouseArea.hovered
Rectangle {
id: container
width: parent.width
implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom
color: Nheko.colors.window
opacity: 0.75
}
Text {
id: imgcaption
anchors.fill: container
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: filename ? filename : body
color: Nheko.colors.text
}
anchors.fill: container
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: filename ? filename : body
color: Nheko.colors.text
} }
} }

View file

@ -102,6 +102,7 @@ Item {
body: d.body body: d.body
filename: d.filename filename: d.filename
isReply: d.isReply isReply: d.isReply
eventId: d.eventId
} }
} }
@ -118,6 +119,7 @@ Item {
body: d.body body: d.body
filename: d.filename filename: d.filename
isReply: d.isReply isReply: d.isReply
eventId: d.eventId
} }
} }

View file

@ -185,9 +185,9 @@ Rectangle {
} }
} }
onStateChanged: { onStateChanged: {
if (state == MxcMedia.StoppedState) { if (state == MxcMedia.StoppedState)
button.state = "stopped"; button.state = "stopped";
}
} }
} }

View file

@ -35,6 +35,7 @@
#include "dialogs/ImageOverlay.h" #include "dialogs/ImageOverlay.h"
#include "emoji/EmojiModel.h" #include "emoji/EmojiModel.h"
#include "emoji/Provider.h" #include "emoji/Provider.h"
#include "ui/MxcAnimatedImage.h"
#include "ui/MxcMediaProxy.h" #include "ui/MxcMediaProxy.h"
#include "ui/NhekoCursorShape.h" #include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h" #include "ui/NhekoDropArea.h"
@ -177,6 +178,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser"); qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea"); qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea");
qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape"); qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape");
qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia"); qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
qmlRegisterUncreatableType<DeviceVerificationFlow>( qmlRegisterUncreatableType<DeviceVerificationFlow>(
"im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!"); "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");

164
src/ui/MxcAnimatedImage.cpp Normal file
View file

@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "MxcAnimatedImage.h"
#include <QDir>
#include <QFileInfo>
#include <QMimeDatabase>
#include <QQuickWindow>
#include <QSGImageNode>
#include <QStandardPaths>
#include "EventAccessors.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "timeline/TimelineModel.h"
void
MxcAnimatedImage::startDownload()
{
if (!room_)
return;
if (eventId_.isEmpty())
return;
auto event = room_->eventById(eventId_);
if (!event) {
nhlog::ui()->error("Failed to load media for event {}, event not found.",
eventId_.toStdString());
return;
}
QByteArray mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)).toUtf8();
animatable_ = QMovie::supportedFormats().contains(mimeType.split('/').back());
animatableChanged();
if (!animatable_)
return;
QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event));
QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
auto encryptionInfo = mtx::accessors::file(*event);
// If the message is a link to a non mxcUrl, don't download it
if (!mxcUrl.startsWith("mxc://")) {
return;
}
QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
const auto url = mxcUrl.toStdString();
const auto name = QString(mxcUrl).remove("mxc://");
QFileInfo filename(QString("%1/media_cache/media/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
.arg(name)
.arg(suffix));
if (QDir::cleanPath(name) != name) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}
QDir().mkpath(filename.path());
QPointer<MxcAnimatedImage> self = this;
auto processBuffer = [this, mimeType, encryptionInfo, self](QIODevice &device) {
if (!self)
return;
if (buffer.isOpen()) {
movie.stop();
movie.setDevice(nullptr);
buffer.close();
}
if (encryptionInfo) {
QByteArray ba = device.readAll();
std::string temp(ba.constData(), ba.size());
temp = mtx::crypto::to_string(
mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
buffer.setData(temp.data(), temp.size());
} else {
buffer.setData(device.readAll());
}
buffer.open(QIODevice::ReadOnly);
buffer.reset();
QTimer::singleShot(0, this, [this, mimeType] {
nhlog::ui()->info("Playing movie with size: {}, {}",
buffer.bytesAvailable(),
buffer.isOpen());
movie.setFormat(mimeType);
movie.setDevice(&buffer);
movie.start();
emit loadedChanged();
});
};
if (filename.isReadable()) {
QFile f(filename.filePath());
if (f.open(QIODevice::ReadOnly)) {
processBuffer(f);
return;
}
}
http::client()->download(
url,
[filename, url, processBuffer](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve media {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
QFile file(filename.filePath());
if (!file.open(QIODevice::WriteOnly))
return;
QByteArray ba(data.data(), (int)data.size());
file.write(ba);
file.close();
QBuffer buf(&ba);
buf.open(QBuffer::ReadOnly);
processBuffer(buf);
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
});
}
QSGNode *
MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
imageDirty = false;
QSGImageNode *n = static_cast<QSGImageNode *>(oldNode);
if (!n)
n = window()->createImageNode();
// n->setTexture(nullptr);
auto img = movie.currentImage();
if (!img.isNull())
n->setTexture(window()->createTextureFromImage(img));
else
return nullptr;
n->setSourceRect(img.rect());
n->setRect(QRect(0, 0, width(), height()));
n->setFiltering(QSGTexture::Linear);
n->setMipmapFiltering(QSGTexture::Linear);
return n;
}

79
src/ui/MxcAnimatedImage.h Normal file
View file

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QBuffer>
#include <QMovie>
#include <QObject>
#include <QQuickItem>
class TimelineModel;
// This is an AnimatedImage, that can draw encrypted images
class MxcAnimatedImage : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED)
Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged)
Q_PROPERTY(bool animatable READ animatable NOTIFY animatableChanged)
Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged)
public:
MxcAnimatedImage(QQuickItem *parent = nullptr)
: QQuickItem(parent)
{
connect(this, &MxcAnimatedImage::eventIdChanged, &MxcAnimatedImage::startDownload);
connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload);
connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame);
setFlag(QQuickItem::ItemHasContents);
// setAcceptHoverEvents(true);
}
bool animatable() const { return animatable_; }
bool loaded() const { return buffer.size() > 0; }
QString eventId() const { return eventId_; }
TimelineModel *room() const { return room_; }
void setEventId(QString newEventId)
{
if (eventId_ != newEventId) {
eventId_ = newEventId;
emit eventIdChanged();
}
}
void setRoom(TimelineModel *room)
{
if (room_ != room) {
room_ = room;
emit roomChanged();
}
}
QSGNode *updatePaintNode(QSGNode *oldNode,
QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;
signals:
void roomChanged();
void eventIdChanged();
void animatableChanged();
void loadedChanged();
private slots:
void startDownload();
void newFrame(int frame)
{
currentFrame = frame;
imageDirty = true;
update();
}
private:
TimelineModel *room_ = nullptr;
QString eventId_;
QString filename_;
bool animatable_ = false;
QBuffer buffer;
QMovie movie;
int currentFrame = 0;
bool imageDirty = true;
};

View file

@ -91,11 +91,11 @@ MxcMediaProxy::startDownload()
buffer.open(QIODevice::ReadOnly); buffer.open(QIODevice::ReadOnly);
buffer.reset(); buffer.reset();
QTimer::singleShot(0, this, [this, self, filename] { QTimer::singleShot(0, this, [this, filename] {
nhlog::ui()->info("Playing buffer with size: {}, {}", nhlog::ui()->info("Playing buffer with size: {}, {}",
buffer.bytesAvailable(), buffer.bytesAvailable(),
buffer.isOpen()); buffer.isOpen());
self->setMedia(QMediaContent(filename.fileName()), &buffer); this->setMedia(QMediaContent(filename.fileName()), &buffer);
emit loadedChanged(); emit loadedChanged();
}); });
}; };