summary refs log tree commit diff
path: root/src/ui
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2021-08-29 05:20:23 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2021-08-29 16:32:28 +0200
commitef068ac2b30bd5f9ed0f299dbc75eb3ace000042 (patch)
tree579d0ee8fac2001ba956ac8536f168f3d32472ae /src/ui
parentUse in memory media player instead of storing unencrypted files on disk (diff)
downloadnheko-ef068ac2b30bd5f9ed0f299dbc75eb3ace000042.tar.xz
Support animated images
fixes #461
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/MxcAnimatedImage.cpp164
-rw-r--r--src/ui/MxcAnimatedImage.h79
-rw-r--r--src/ui/MxcMediaProxy.cpp4
3 files changed, 245 insertions, 2 deletions
diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp
new file mode 100644
index 00000000..cfc03827
--- /dev/null
+++ b/src/ui/MxcAnimatedImage.cpp
@@ -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;
+}
diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h
new file mode 100644
index 00000000..7b9502e0
--- /dev/null
+++ b/src/ui/MxcAnimatedImage.h
@@ -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;
+};
diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp
index c1de2c31..dc65de7c 100644
--- a/src/ui/MxcMediaProxy.cpp
+++ b/src/ui/MxcMediaProxy.cpp
@@ -91,11 +91,11 @@ MxcMediaProxy::startDownload()
                 buffer.open(QIODevice::ReadOnly);
                 buffer.reset();
 
-                QTimer::singleShot(0, this, [this, self, filename] {
+                QTimer::singleShot(0, this, [this, filename] {
                         nhlog::ui()->info("Playing buffer with size: {}, {}",
                                           buffer.bytesAvailable(),
                                           buffer.isOpen());
-                        self->setMedia(QMediaContent(filename.fileName()), &buffer);
+                        this->setMedia(QMediaContent(filename.fileName()), &buffer);
                         emit loadedChanged();
                 });
         };