diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..9085c34
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "rlottie"]
+ path = rlottie
+ url = https://github.com/Samsung/rlottie.git
diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro
index 1a6994a..2961f7c 100644
--- a/harbour-fernschreiber.pro
+++ b/harbour-fernschreiber.pro
@@ -14,10 +14,12 @@ TARGET = harbour-fernschreiber
CONFIG += sailfishapp sailfishapp_i18n
-PKGCONFIG += nemonotifications-qt5 ngf-qt5
+PKGCONFIG += nemonotifications-qt5 ngf-qt5 zlib
QT += core dbus sql
+DEFINES += QT_STATICPLUGIN
+
SOURCES += src/harbour-fernschreiber.cpp \
src/appsettings.cpp \
src/chatlistmodel.cpp \
@@ -31,7 +33,8 @@ SOURCES += src/harbour-fernschreiber.cpp \
src/stickermanager.cpp \
src/tdlibfile.cpp \
src/tdlibreceiver.cpp \
- src/tdlibwrapper.cpp
+ src/tdlibwrapper.cpp \
+ src/tgsplugin.cpp
DISTFILES += qml/harbour-fernschreiber.qml \
qml/components/AudioPreview.qml \
@@ -145,4 +148,69 @@ HEADERS += \
src/tdlibfile.h \
src/tdlibreceiver.h \
src/tdlibsecrets.h \
- src/tdlibwrapper.h
+ src/tdlibwrapper.h \
+ src/tgsplugin.h
+
+# https://github.com/Samsung/rlottie.git
+
+RLOTTIE_CONFIG = $${PWD}/rlottie/src/vector/config.h
+PRE_TARGETDEPS += $${RLOTTIE_CONFIG}
+QMAKE_EXTRA_TARGETS += rlottie_config
+
+rlottie_config.target = $${RLOTTIE_CONFIG}
+rlottie_config.commands = touch $${RLOTTIE_CONFIG} # Empty config is fine
+
+DEFINES += LOTTIE_THREAD_SUPPORT
+
+INCLUDEPATH += \
+ rlottie/inc \
+ rlottie/src/vector \
+ rlottie/src/vector/freetype
+
+SOURCES += \
+ rlottie/src/lottie/lottieanimation.cpp \
+ rlottie/src/lottie/lottieitem.cpp \
+ rlottie/src/lottie/lottieitem_capi.cpp \
+ rlottie/src/lottie/lottiekeypath.cpp \
+ rlottie/src/lottie/lottieloader.cpp \
+ rlottie/src/lottie/lottiemodel.cpp \
+ rlottie/src/lottie/lottieparser.cpp
+
+SOURCES += \
+ rlottie/src/vector/freetype/v_ft_math.cpp \
+ rlottie/src/vector/freetype/v_ft_raster.cpp \
+ rlottie/src/vector/freetype/v_ft_stroker.cpp \
+ rlottie/src/vector/stb/stb_image.cpp \
+ rlottie/src/vector/varenaalloc.cpp \
+ rlottie/src/vector/vbezier.cpp \
+ rlottie/src/vector/vbitmap.cpp \
+ rlottie/src/vector/vbrush.cpp \
+ rlottie/src/vector/vdasher.cpp \
+ rlottie/src/vector/vdrawable.cpp \
+ rlottie/src/vector/vdrawhelper.cpp \
+ rlottie/src/vector/vdrawhelper_common.cpp \
+ rlottie/src/vector/vdrawhelper_neon.cpp \
+ rlottie/src/vector/vdrawhelper_sse2.cpp \
+ rlottie/src/vector/vmatrix.cpp \
+ rlottie/src/vector/vimageloader.cpp \
+ rlottie/src/vector/vinterpolator.cpp \
+ rlottie/src/vector/vpainter.cpp \
+ rlottie/src/vector/vpath.cpp \
+ rlottie/src/vector/vpathmesure.cpp \
+ rlottie/src/vector/vraster.cpp \
+ rlottie/src/vector/vrle.cpp
+
+NEON = $$system(g++ -dM -E -x c++ - < /dev/null | grep __ARM_NEON__)
+SSE2 = $$system(g++ -dM -E -x c++ - < /dev/null | grep __SSE2__)
+
+!isEmpty(NEON) {
+ message(Using NEON render functions)
+ SOURCES += rlottie/src/vector/pixman/pixman-arm-neon-asm.S
+} else {
+ !isEmpty(SSE2) {
+ message(Using SSE2 render functions)
+ SOURCES += rlottie/src/vector/vdrawhelper_sse2.cpp
+ } else {
+ message(Using default render functions)
+ }
+}
diff --git a/qml/components/StickerPreview.qml b/qml/components/StickerPreview.qml
index 89d6ba0..68e28b0 100644
--- a/qml/components/StickerPreview.qml
+++ b/qml/components/StickerPreview.qml
@@ -18,76 +18,77 @@
*/
import QtQuick 2.6
import Sailfish.Silica 1.0
+import WerkWolf.Fernschreiber 1.0
Item {
-
property ListItem messageListItem
- property var rawMessage: messageListItem.myMessage
- property var stickerData: rawMessage.content.sticker;
- property int usedFileId;
+ readonly property var stickerData: messageListItem.myMessage.content.sticker;
+ readonly property bool animated: stickerData.is_animated && appSettings.animateStickers
+ readonly property bool stickerVisible: staticStickerLoader.item ? staticStickerLoader.item.visible :
+ animatedStickerLoader.item ? animatedStickerLoader.item.visible : false
- width: stickerData.width
- height: stickerData.height
+ implicitWidth: stickerData.width
+ implicitHeight: stickerData.height
- Component.onCompleted: {
- if (stickerData) {
- if (stickerData.is_animated) {
- // Use thumbnail until we can decode TGS files
- usedFileId = stickerData.thumbnail.photo.id;
- if (stickerData.thumbnail.photo.local.is_downloading_completed) {
- stickerImage.source = stickerData.thumbnail.photo.local.path;
- } else {
- tdLibWrapper.downloadFile(usedFileId);
- }
- } else {
- usedFileId = stickerData.sticker.id;
- if (stickerData.sticker.local.is_downloading_completed) {
- stickerImage.source = stickerData.sticker.local.path;
- } else {
- tdLibWrapper.downloadFile(usedFileId);
+ TDLibFile {
+ id: file
+ tdlib: tdLibWrapper
+ fileInformation: stickerData.sticker
+ autoLoad: true
+ }
+
+ Item {
+ width: stickerData.width
+ height: stickerData.height
+ // (centered in image mode, text-like in sticker mode)
+ x: appSettings.showStickersAsImages ? (parent.width - width)/2 :
+ messageListItem.isOwnMessage ? (parent.width - width) : 0
+ anchors.verticalCenter: parent.verticalCenter
+
+ Loader {
+ id: animatedStickerLoader
+ anchors.fill: parent
+ active: animated
+ sourceComponent: Component {
+ AnimatedImage {
+ anchors.fill: parent
+ source: file.path
+ asynchronous: true
+ paused: !Qt.application.active
+ cache: false
}
}
}
- }
- Connections {
- target: tdLibWrapper
- onFileUpdated: {
- if (stickerData) {
- if (fileId === usedFileId && fileInformation.local.is_downloading_completed) {
- if (stickerData.is_animated) {
- stickerData.thumbnail.photo = fileInformation;
- } else {
- stickerData.sticker = fileInformation;
- }
- stickerImage.source = fileInformation.local.path;
+ Loader {
+ id: staticStickerLoader
+ anchors.fill: parent
+ active: !animated
+ sourceComponent: Component {
+ Image {
+ anchors.fill: parent
+ source: file.path
+ fillMode: Image.PreserveAspectFit
+ autoTransform: true
+ asynchronous: true
+ visible: opacity > 0
+ opacity: status === Image.Ready ? 1 : 0
+ Behavior on opacity { FadeAnimation {} }
}
}
}
- }
- Image {
- id: stickerImage
- anchors.fill: parent
+ Loader {
+ anchors.fill: parent
+ sourceComponent: Component {
+ BackgroundImage {}
+ }
- fillMode: Image.PreserveAspectFit
- autoTransform: true
- asynchronous: true
- visible: opacity > 0
- opacity: status === Image.Ready ? 1 : 0
- Behavior on opacity { FadeAnimation {} }
- }
-
- Loader {
- anchors.fill: parent
- sourceComponent: Component {
- BackgroundImage {}
+ active: opacity > 0
+ opacity: !stickerVisible && !placeHolderDelayTimer.running ? 0.15 : 0
+ Behavior on opacity { FadeAnimation {} }
}
-
- active: opacity > 0
- opacity: !stickerImage.visible && !placeHolderDelayTimer.running ? 0.15 : 0
- Behavior on opacity { FadeAnimation {} }
}
Timer {
diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml
index af4b6b1..8a8962a 100644
--- a/qml/pages/SettingsPage.qml
+++ b/qml/pages/SettingsPage.qml
@@ -117,6 +117,15 @@ Page {
text: qsTr("Appearance")
}
+ TextSwitch {
+ checked: appSettings.animateStickers
+ text: qsTr("Animate stickers")
+ automaticCheck: false
+ onClicked: {
+ appSettings.animateStickers = !checked
+ }
+ }
+
TextSwitch {
checked: appSettings.showStickersAsImages
text: qsTr("Show stickers as images")
diff --git a/rlottie b/rlottie
new file mode 160000
index 0000000..bf3d272
--- /dev/null
+++ b/rlottie
@@ -0,0 +1 @@
+Subproject commit bf3d272df3916a0c34575ac8286cb0fe672fd0d4
diff --git a/src/appsettings.cpp b/src/appsettings.cpp
index b1ccc7b..02904e6 100644
--- a/src/appsettings.cpp
+++ b/src/appsettings.cpp
@@ -24,6 +24,7 @@ namespace {
const QString KEY_SEND_BY_ENTER("sendByEnter");
const QString KEY_USE_OPEN_WITH("useOpenWith");
const QString KEY_SHOW_STICKERS_AS_IMAGES("showStickersAsImages");
+ const QString KEY_ANIMATE_STICKERS("animateStickers");
const QString KEY_NOTIFICATION_FEEDBACK("notificationFeedback");
}
@@ -73,6 +74,20 @@ void AppSettings::setShowStickersAsImages(bool showAsImages)
}
}
+bool AppSettings::animateStickers() const
+{
+ return settings.value(KEY_ANIMATE_STICKERS, true).toBool();
+}
+
+void AppSettings::setAnimateStickers(bool animate)
+{
+ if (animateStickers() != animate) {
+ LOG(KEY_ANIMATE_STICKERS << animate);
+ settings.setValue(KEY_ANIMATE_STICKERS, animate);
+ emit animateStickersChanged();
+ }
+}
+
AppSettings::NotificationFeedback AppSettings::notificationFeedback() const
{
return (NotificationFeedback) settings.value(KEY_NOTIFICATION_FEEDBACK, (int) NotificationFeedbackAll).toInt();
diff --git a/src/appsettings.h b/src/appsettings.h
index 58b1b81..5df3d6c 100644
--- a/src/appsettings.h
+++ b/src/appsettings.h
@@ -26,6 +26,7 @@ class AppSettings : public QObject {
Q_PROPERTY(bool sendByEnter READ getSendByEnter WRITE setSendByEnter NOTIFY sendByEnterChanged)
Q_PROPERTY(bool useOpenWith READ getUseOpenWith WRITE setUseOpenWith NOTIFY useOpenWithChanged)
Q_PROPERTY(bool showStickersAsImages READ showStickersAsImages WRITE setShowStickersAsImages NOTIFY showStickersAsImagesChanged)
+ Q_PROPERTY(bool animateStickers READ animateStickers WRITE setAnimateStickers NOTIFY animateStickersChanged)
Q_PROPERTY(NotificationFeedback notificationFeedback READ notificationFeedback WRITE setNotificationFeedback NOTIFY notificationFeedbackChanged)
public:
@@ -48,6 +49,9 @@ public:
bool showStickersAsImages() const;
void setShowStickersAsImages(bool showAsImages);
+ bool animateStickers() const;
+ void setAnimateStickers(bool animate);
+
NotificationFeedback notificationFeedback() const;
void setNotificationFeedback(NotificationFeedback feedback);
@@ -55,6 +59,7 @@ signals:
void sendByEnterChanged();
void useOpenWithChanged();
void showStickersAsImagesChanged();
+ void animateStickersChanged();
void notificationFeedbackChanged();
private:
diff --git a/src/harbour-fernschreiber.cpp b/src/harbour-fernschreiber.cpp
index 614ec47..5491f65 100644
--- a/src/harbour-fernschreiber.cpp
+++ b/src/harbour-fernschreiber.cpp
@@ -38,6 +38,9 @@
#include "dbusadaptor.h"
#include "processlauncher.h"
#include "stickermanager.h"
+#include "tgsplugin.h"
+
+Q_IMPORT_PLUGIN(TgsIOPlugin)
int main(int argc, char *argv[])
{
diff --git a/src/tgsplugin.cpp b/src/tgsplugin.cpp
new file mode 100644
index 0000000..3bd67ed
--- /dev/null
+++ b/src/tgsplugin.cpp
@@ -0,0 +1,310 @@
+/*
+ Copyright (C) 2020 Slava Monich at al.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+#include "tgsplugin.h"
+#include "rlottie.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#define LOG(x) qDebug() << "[TgsIOHandler]" << qPrintable(fileName) << x
+#define WARN(x) qWarning() << "[TgsIOHandler]" << qPrintable(fileName) << x
+
+class TgsIOHandler : public QImageIOHandler
+{
+public:
+ static const QByteArray NAME;
+ static const QByteArray GZ_MAGIC;
+ typedef std::string ByteArray;
+
+ TgsIOHandler(QIODevice* device, const QByteArray& format);
+ ~TgsIOHandler();
+
+ ByteArray uncompress();
+ bool load();
+ void render(int frameIndex);
+ void finishRendering();
+
+ // QImageIOHandler
+ bool canRead() const Q_DECL_OVERRIDE;
+ QByteArray name() const Q_DECL_OVERRIDE;
+ bool read(QImage* image) Q_DECL_OVERRIDE;
+ QVariant option(ImageOption option) const Q_DECL_OVERRIDE;
+ bool supportsOption(ImageOption option) const Q_DECL_OVERRIDE;
+ bool jumpToNextImage() Q_DECL_OVERRIDE;
+ bool jumpToImage(int imageNumber) Q_DECL_OVERRIDE;
+ int loopCount() const Q_DECL_OVERRIDE;
+ int imageCount() const Q_DECL_OVERRIDE;
+ int nextImageDelay() const Q_DECL_OVERRIDE;
+ int currentImageNumber() const Q_DECL_OVERRIDE;
+ QRect currentImageRect() const Q_DECL_OVERRIDE;
+
+public:
+ QString fileName;
+ QSize size;
+ qreal frameRate;
+ int frameCount;
+ int currentFrame;
+ QImage firstImage;
+ QImage prevImage;
+ QImage currentImage;
+ std::future currentRender;
+ std::unique_ptr animation;
+};
+
+const QByteArray TgsIOHandler::NAME("tgs");
+const QByteArray TgsIOHandler::GZ_MAGIC("\x1f\x8b");
+
+TgsIOHandler::TgsIOHandler(QIODevice* device, const QByteArray& format) :
+ frameRate(0.),
+ frameCount(0),
+ currentFrame(0)
+{
+ QFileDevice* file = qobject_cast(device);
+ if (file) {
+ fileName = QFileInfo(file->fileName()).fileName();
+ }
+ setDevice(device);
+ setFormat(format);
+}
+
+TgsIOHandler::~TgsIOHandler()
+{
+ if (currentRender.valid()) {
+ currentRender.get();
+ }
+ LOG("Done");
+}
+
+TgsIOHandler::ByteArray TgsIOHandler::uncompress()
+{
+ ByteArray unzipped;
+ const QByteArray zipped(device()->readAll());
+ if (!zipped.isEmpty()) {
+ z_stream unzip;
+ memset(&unzip, 0, sizeof(unzip));
+ unzip.next_in = (Bytef*)zipped.constData();
+ // Add 16 for decoding gzip header
+ int zerr = inflateInit2(&unzip, MAX_WBITS + 16);
+ if (zerr == Z_OK) {
+ const uint chunk = 0x1000;
+ unzipped.resize(chunk);
+ unzip.next_out = (Bytef*)unzipped.data();
+ unzip.avail_in = zipped.size();
+ unzip.avail_out = chunk;
+ LOG("Compressed size" << zipped.size());
+ while (unzip.avail_out > 0 && zerr == Z_OK) {
+ zerr = inflate(&unzip, Z_NO_FLUSH);
+ if (zerr == Z_OK && unzip.avail_out < chunk) {
+ // Data may get relocated, update next_out too
+ unzipped.resize(unzipped.size() + chunk);
+ unzip.next_out = (Bytef*)unzipped.data() + unzip.total_out;
+ unzip.avail_out += chunk;
+ }
+ }
+ if (zerr == Z_STREAM_END) {
+ unzipped.resize(unzip.next_out - (Bytef*)unzipped.data());
+ LOG("Uncompressed size" << unzipped.size());
+ } else {
+ unzipped.clear();
+ }
+ inflateEnd(&unzip);
+ }
+ }
+ return unzipped;
+}
+
+bool TgsIOHandler::load()
+{
+ if (!animation && device()) {
+ ByteArray json(uncompress());
+ if (json.size() > 0) {
+ animation = rlottie::Animation::loadFromData(json, std::string(), std::string(), false);
+ if (animation) {
+ size_t width, height;
+ animation->size(width, height);
+ frameRate = animation->frameRate();
+ frameCount = (int) animation->totalFrame();
+ size = QSize(width, height);
+ LOG(size << frameCount << "frames," << frameRate << "fps");
+ render(0); // Pre-render first frame
+ }
+ }
+ }
+ return animation != Q_NULLPTR;
+}
+
+void TgsIOHandler::finishRendering()
+{
+ if (currentRender.valid()) {
+ currentRender.get();
+ prevImage = currentImage;
+ if (!currentFrame && !firstImage.isNull()) {
+ LOG("Rendered first frame");
+ firstImage = currentImage;
+ }
+ } else {
+ // Must be the first frame
+ prevImage = currentImage;
+ }
+}
+
+void TgsIOHandler::render(int frameIndex)
+{
+ currentFrame = frameIndex % frameCount;
+ if (!currentFrame && !firstImage.isNull()) {
+ // The first frame only gets rendered once
+ currentImage = firstImage;
+ } else {
+ const int width = (int)size.width();
+ const int height = (int)size.height();
+ currentImage = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
+ currentRender = animation->render(currentFrame,
+ rlottie::Surface((uint32_t*)currentImage.bits(),
+ width, height, currentImage.bytesPerLine()));
+ }
+}
+
+bool TgsIOHandler::read(QImage* out)
+{
+ if (load() && frameCount > 0) {
+ // We must have the first frame, will wait if necessary
+ if (currentFrame && currentRender.valid()) {
+ std::future_status status = currentRender.wait_for(std::chrono::milliseconds(0));
+ if (status != std::future_status::ready) {
+ LOG("Skipping frame" << currentFrame);
+ currentFrame = (currentFrame + 1) % frameCount;
+ *out = prevImage;
+ return true;
+ }
+ }
+ finishRendering();
+ *out = currentImage;
+ render(currentFrame + 1);
+ return true;
+ }
+ return false;
+}
+
+bool TgsIOHandler::canRead() const
+{
+ if (!device()) {
+ return false;
+ } else if (animation) {
+ return true;
+ } else {
+ // Need to support uncompressed data?
+ return device()->peek(2) == GZ_MAGIC;
+ }
+}
+
+QByteArray TgsIOHandler::name() const
+{
+ return NAME;
+}
+
+QVariant TgsIOHandler::option(ImageOption option) const
+{
+ switch (option) {
+ case Size:
+ ((TgsIOHandler*)this)->load(); // Cast off const
+ return size;
+ case Animation:
+ return true;
+ case ImageFormat:
+ return QImage::Format_ARGB32_Premultiplied;
+ default:
+ break;
+ }
+ return QVariant();
+}
+
+bool TgsIOHandler::supportsOption(ImageOption option) const
+{
+ switch(option) {
+ case Size:
+ case Animation:
+ case ImageFormat:
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+bool TgsIOHandler::jumpToNextImage()
+{
+ if (frameCount) {
+ finishRendering();
+ render(currentFrame + 1);
+ return true;
+ }
+ return false;
+}
+
+bool TgsIOHandler::jumpToImage(int imageNumber)
+{
+ if (frameCount) {
+ if (imageNumber != currentFrame) {
+ finishRendering();
+ render(imageNumber);
+ }
+ return true;
+ }
+ return false;
+}
+
+int TgsIOHandler::loopCount() const
+{
+ return -1;
+}
+
+int TgsIOHandler::imageCount() const
+{
+ return frameCount;
+}
+
+int TgsIOHandler::currentImageNumber() const
+{
+ return currentFrame;
+}
+
+QRect TgsIOHandler::currentImageRect() const
+{
+ return QRect(QPoint(), size);
+}
+
+int TgsIOHandler::nextImageDelay() const
+{
+ return frameRate > 0 ? (int)(1000/frameRate) : 33;
+}
+
+QImageIOPlugin::Capabilities TgsIOPlugin::capabilities(QIODevice*, const QByteArray& format) const
+{
+ return Capabilities((format == TgsIOHandler::NAME) ? CanRead : 0);
+}
+
+QImageIOHandler* TgsIOPlugin::create(QIODevice* device, const QByteArray& format) const
+{
+ return new TgsIOHandler(device, format);
+}
diff --git a/src/tgsplugin.h b/src/tgsplugin.h
new file mode 100644
index 0000000..dfadcf4
--- /dev/null
+++ b/src/tgsplugin.h
@@ -0,0 +1,33 @@
+/*
+ Copyright (C) 2020 Slava Monich at al.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+#ifndef TGS_IMAGE_IO_PLUGIN
+#define TGS_IMAGE_IO_PLUGIN
+
+#include
+#include
+
+class TgsIOPlugin : public QImageIOPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "tgsplugin.json")
+public:
+ Capabilities capabilities(QIODevice* device, const QByteArray& format) const Q_DECL_OVERRIDE;
+ QImageIOHandler* create(QIODevice* device, const QByteArray& format) const Q_DECL_OVERRIDE;
+};
+
+#endif // TGS_IMAGE_IO_PLUGIN
diff --git a/src/tgsplugin.json b/src/tgsplugin.json
new file mode 100644
index 0000000..ac65e14
--- /dev/null
+++ b/src/tgsplugin.json
@@ -0,0 +1,3 @@
+{
+ "Keys": [ "tgs" ]
+}