mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-25 20:48:52 +03:00
parent
09c041c8ac
commit
ef068ac2b3
8 changed files with 293 additions and 33 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,9 +185,9 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onStateChanged: {
|
onStateChanged: {
|
||||||
if (state == MxcMedia.StoppedState) {
|
if (state == MxcMedia.StoppedState)
|
||||||
button.state = "stopped";
|
button.state = "stopped";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
164
src/ui/MxcAnimatedImage.cpp
Normal 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
79
src/ui/MxcAnimatedImage.h
Normal 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;
|
||||||
|
};
|
|
@ -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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue