diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro
index 55086b8..0a04807 100644
--- a/harbour-fernschreiber.pro
+++ b/harbour-fernschreiber.pro
@@ -14,6 +14,8 @@ TARGET = harbour-fernschreiber
CONFIG += sailfishapp sailfishapp_i18n
+QT += core dbus
+
SOURCES += src/harbour-fernschreiber.cpp \
src/chatlistmodel.cpp \
src/chatmodel.cpp \
diff --git a/images/background-black.png b/images/background-black.png
index 2f1216e..f5a008b 100644
Binary files a/images/background-black.png and b/images/background-black.png differ
diff --git a/images/background-white.png b/images/background-white.png
index 3347b9d..427d19e 100644
Binary files a/images/background-white.png and b/images/background-white.png differ
diff --git a/images/icon-l-fullscreen.png b/images/icon-l-fullscreen.png
new file mode 100644
index 0000000..841af69
Binary files /dev/null and b/images/icon-l-fullscreen.png differ
diff --git a/qml/components/VideoPreview.qml b/qml/components/VideoPreview.qml
new file mode 100644
index 0000000..d0e442c
--- /dev/null
+++ b/qml/components/VideoPreview.qml
@@ -0,0 +1,467 @@
+/*
+ Copyright (C) 2020 Sebastian J. Wolf
+
+ This file is part of Fernschreiber.
+
+ Fernschreiber 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.
+
+ Fernschreiber 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 Fernschreiber. If not, see .
+*/
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import QtMultimedia 5.0
+import "../js/functions.js" as Functions
+
+Item {
+ id: videoMessageComponent
+
+ property variant videoData;
+ property string videoUrl;
+ property int previewFileId;
+ property int videoFileId;
+ property bool fullscreen : false;
+
+ width: parent.width
+ height: parent.height
+
+ Timer {
+ id: screensaverTimer
+ interval: 30000
+ running: false
+ repeat: true
+ triggeredOnStart: true
+ onTriggered: {
+ tdLibWrapper.controlScreenSaver(false);
+ }
+ }
+
+ function getTimeString(rawSeconds) {
+ var minutes = Math.floor( rawSeconds / 60 );
+ var seconds = rawSeconds - ( minutes * 60 );
+
+ if ( minutes < 10 ) {
+ minutes = "0" + minutes;
+ }
+ if ( seconds < 10 ) {
+ seconds = "0" + seconds;
+ }
+ return minutes + ":" + seconds;
+ }
+
+ function disableScreensaver() {
+ screensaverTimer.start();
+ }
+
+ function enableScreensaver() {
+ screensaverTimer.stop();
+ tdLibWrapper.controlScreenSaver(true);
+ }
+
+ Component.onCompleted: {
+ updateVideoThumbnail();
+ }
+
+ function updateVideoThumbnail() {
+ if (typeof videoData === "object") {
+ previewFileId = videoData.thumbnail.photo.id;
+ videoFileId = videoData.video.id;
+ if (videoData.thumbnail.photo.local.is_downloading_completed) {
+ placeholderImage.source = videoData.thumbnail.photo.local.path;
+ } else {
+ tdLibWrapper.downloadFile(previewFileId);
+ }
+ }
+ }
+
+ function handlePlay() {
+ if (videoData.video.local.is_downloading_completed) {
+ videoUrl = videoData.video.local.path;
+ videoComponentLoader.active = true;
+ } else {
+ videoDownloadBusyIndicator.running = true;
+ tdLibWrapper.downloadFile(videoFileId);
+ }
+ }
+
+ Connections {
+ target: tdLibWrapper
+ onFileUpdated: {
+ if (typeof videoData === "object") {
+ if (fileInformation.local.is_downloading_completed) {
+ if (fileId === previewFileId) {
+ videoData.thumbnail.photo = fileInformation;
+ placeholderImage.source = fileInformation.local.path;
+ }
+ if (fileId === videoFileId) {
+ videoDownloadBusyIndicator.running = false;
+ videoData.video = fileInformation;
+ videoUrl = fileInformation.local.path;
+ videoComponentLoader.active = true;
+ }
+ }
+ }
+ }
+ }
+
+ Image {
+ id: placeholderImage
+ width: parent.width
+ height: parent.height
+ fillMode: Image.PreserveAspectCrop
+ visible: status === Image.Ready ? true : false
+ }
+
+ Image {
+ id: imageLoadingBackgroundImage
+ source: "../../images/background" + ( Theme.colorScheme ? "-black" : "-white" ) + ".png"
+ anchors {
+ centerIn: parent
+ }
+ width: parent.width - Theme.paddingSmall
+ height: parent.height - Theme.paddingSmall
+ visible: placeholderImage.status !== Image.Ready
+
+ fillMode: Image.PreserveAspectFit
+ opacity: 0.15
+ }
+
+ Rectangle {
+ id: placeholderBackground
+ color: "black"
+ opacity: 0.3
+ height: parent.height
+ width: parent.width
+ visible: playButton.visible
+ }
+
+ Row {
+ width: parent.width
+ height: parent.height
+ Item {
+ height: parent.height
+ width: videoMessageComponent.fullscreen ? parent.width : ( parent.width / 2 )
+ Image {
+ id: playButton
+ anchors.centerIn: parent
+ width: Theme.iconSizeLarge
+ height: Theme.iconSizeLarge
+ source: "image://theme/icon-l-play?white"
+ visible: placeholderImage.status === Image.Ready ? true : false
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ fullscreenItem.visible = false;
+ handlePlay();
+ }
+ }
+ }
+ BusyIndicator {
+ id: videoDownloadBusyIndicator
+ running: false
+ visible: running
+ anchors.centerIn: parent
+ size: BusyIndicatorSize.Large
+ }
+ }
+ Item {
+ id: fullscreenItem
+ height: parent.height
+ width: parent.width / 2
+ visible: !videoMessageComponent.fullscreen
+ Image {
+ id: fullscreenButton
+ anchors.centerIn: parent
+ width: Theme.iconSizeLarge
+ height: Theme.iconSizeLarge
+ source: "../../images/icon-l-fullscreen.png"
+ visible: ( placeholderImage.status === Image.Ready && !videoMessageComponent.fullscreen ) ? true : false
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ // TODO show video page once it's there...
+ //pageStack.push(Qt.resolvedUrl("../pages/VideoPage.qml"), {"tweetModel": tweet.retweeted_status ? tweet.retweeted_status : tweet});
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: videoErrorShade
+ width: parent.width
+ height: parent.height
+ color: "lightgrey"
+ visible: placeholderImage.status === Image.Error ? true : false
+ opacity: 0.3
+ }
+
+ Rectangle {
+ id: errorTextOverlay
+ color: "black"
+ opacity: 0.8
+ width: parent.width
+ height: parent.height
+ visible: false
+ }
+
+ Text {
+ id: errorText
+ visible: false
+ width: parent.width
+ color: Theme.primaryColor
+ font.pixelSize: Theme.fontSizeExtraSmall
+ horizontalAlignment: Text.AlignHCenter
+ anchors {
+ verticalCenter: parent.verticalCenter
+ }
+ wrapMode: Text.Wrap
+ text: ""
+ }
+
+ Loader {
+ id: videoComponentLoader
+ active: false
+ width: parent.width
+ height: Functions.getVideoHeight(parent.width, videoData)
+ sourceComponent: videoComponent
+ }
+
+ Component {
+ id: videoComponent
+
+ Item {
+ width: parent ? parent.width : 0
+ height: parent ? parent.height : 0
+
+ Connections {
+ target: messageVideo
+ onPlaying: {
+ playButton.visible = false;
+ placeholderImage.visible = false;
+ messageVideo.visible = true;
+ }
+ }
+
+ Video {
+ id: messageVideo
+
+ Component.onCompleted: {
+ if (messageVideo.error === MediaPlayer.NoError) {
+ messageVideo.play();
+ timeLeftTimer.start();
+ } else {
+ errorText.text = qsTr("Error loading video! " + messageVideo.errorString)
+ errorTextOverlay.visible = true;
+ errorText.visible = true;
+ }
+ }
+
+ onStatusChanged: {
+ if (status == MediaPlayer.NoMedia) {
+ console.log("No Media");
+ videoBusyIndicator.visible = false;
+ }
+ if (status == MediaPlayer.Loading) {
+ console.log("Loading");
+ videoBusyIndicator.visible = true;
+ }
+ if (status == MediaPlayer.Loaded) {
+ console.log("Loaded");
+ videoBusyIndicator.visible = false;
+ }
+ if (status == MediaPlayer.Buffering) {
+ console.log("Buffering");
+ videoBusyIndicator.visible = true;
+ }
+ if (status == MediaPlayer.Stalled) {
+ console.log("Stalled");
+ videoBusyIndicator.visible = true;
+ }
+ if (status == MediaPlayer.Buffered) {
+ console.log("Buffered");
+ videoBusyIndicator.visible = false;
+ }
+ if (status == MediaPlayer.EndOfMedia) {
+ console.log("End of Media");
+ videoBusyIndicator.visible = false;
+ }
+ if (status == MediaPlayer.InvalidMedia) {
+ console.log("Invalid Media");
+ videoBusyIndicator.visible = false;
+ }
+ if (status == MediaPlayer.UnknownStatus) {
+ console.log("Unknown Status");
+ videoBusyIndicator.visible = false;
+ }
+ }
+
+ visible: false
+ width: parent.width
+ height: parent.height
+ source: videoUrl
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (messageVideo.playbackState === MediaPlayer.PlayingState) {
+ enableScreensaver();
+ messageVideo.pause();
+ timeLeftItem.visible = true;
+ } else {
+ disableScreensaver();
+ messageVideo.play();
+ timeLeftTimer.start();
+ }
+ }
+ }
+ onStopped: {
+ enableScreensaver();
+ messageVideo.visible = false;
+ placeholderImage.visible = true;
+ playButton.visible = true;
+ videoComponentLoader.active = false;
+ fullscreenItem.visible = !videoMessageComponent.fullscreen;
+ }
+ }
+
+ BusyIndicator {
+ id: videoBusyIndicator
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ visible: false
+ running: visible
+ size: BusyIndicatorSize.Medium
+ onVisibleChanged: {
+ if (visible) {
+ enableScreensaver();
+ } else {
+ disableScreensaver();
+ }
+ }
+ }
+
+ Timer {
+ id: timeLeftTimer
+ repeat: false
+ interval: 2000
+ onTriggered: {
+ timeLeftItem.visible = false;
+ }
+ }
+
+ Item {
+ id: timeLeftItem
+ width: parent.width
+ height: parent.height
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: messageVideo.visible
+ opacity: visible ? 1 : 0
+ Behavior on opacity { NumberAnimation {} }
+
+ Rectangle {
+ id: positionTextOverlay
+ color: "black"
+ opacity: 0.3
+ width: parent.width
+ height: parent.height
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: pausedRow.visible
+ }
+
+ Row {
+ id: pausedRow
+ width: parent.width
+ height: parent.height - ( messageVideoSlider.visible ? messageVideoSlider.height : 0 ) - ( positionText.visible ? positionText.height : 0 )
+ visible: videoComponentLoader.active && messageVideo.playbackState === MediaPlayer.PausedState
+ Item {
+ height: parent.height
+ width: videoMessageComponent.fullscreen ? parent.width : ( parent.width / 2 )
+ Image {
+ id: pausedPlayButton
+ anchors.centerIn: parent
+ width: Theme.iconSizeLarge
+ height: Theme.iconSizeLarge
+ source: "image://theme/icon-l-play?white"
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ disableScreensaver();
+ messageVideo.play();
+ timeLeftTimer.start();
+ }
+ }
+ }
+ }
+ Item {
+ id: pausedFullscreenItem
+ height: parent.height
+ width: parent.width / 2
+ visible: !videoMessageComponent.fullscreen
+ Image {
+ id: pausedFullscreenButton
+ anchors.centerIn: parent
+ width: Theme.iconSizeLarge
+ height: Theme.iconSizeLarge
+ source: "../../images/icon-l-fullscreen.png"
+ visible: ( videoComponentLoader.active && messageVideo.playbackState === MediaPlayer.PausedState ) ? true : false
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ // TODO go to video page once it's done
+ // pageStack.push(Qt.resolvedUrl("../pages/VideoPage.qml"), {"tweetModel": videoMessageComponent.tweet});
+ }
+ }
+ }
+ }
+ }
+
+ Slider {
+ id: messageVideoSlider
+ width: parent.width
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: positionText.top
+ minimumValue: 0
+ maximumValue: messageVideo.duration ? messageVideo.duration : 0
+ stepSize: 1
+ value: messageVideo.position
+ enabled: messageVideo.seekable
+ visible: (messageVideo.duration > 0)
+ onReleased: {
+ messageVideo.seek(Math.floor(value));
+ messageVideo.play();
+ timeLeftTimer.start();
+ }
+ valueText: getTimeString(Math.round((messageVideo.duration - messageVideoSlider.value) / 1000))
+ }
+
+ Text {
+ id: positionText
+ visible: messageVideo.visible && messageVideo.duration === 0
+ color: Theme.primaryColor
+ font.pixelSize: videoMessageComponent.fullscreen ? Theme.fontSizeSmall : Theme.fontSizeTiny
+ anchors {
+ bottom: parent.bottom
+ bottomMargin: Theme.paddingSmall
+ horizontalCenter: positionTextOverlay.horizontalCenter
+ }
+ wrapMode: Text.Wrap
+ text: ( messageVideo.duration - messageVideo.position ) > 0 ? getTimeString(Math.round((messageVideo.duration - messageVideo.position) / 1000)) : "-:-"
+ }
+ }
+
+ }
+
+
+ }
+
+}
diff --git a/qml/js/functions.js b/qml/js/functions.js
index bee5f27..21ffe4c 100644
--- a/qml/js/functions.js
+++ b/qml/js/functions.js
@@ -187,3 +187,12 @@ function handleLink(link) {
Qt.openUrlExternally(link);
}
}
+
+function getVideoHeight(videoWidth, videoData) {
+ if (typeof videoData !== "undefined") {
+ var aspectRatio = videoData.height / videoData.width;
+ return Math.round(videoWidth * aspectRatio);
+ } else {
+ return 1;
+ }
+}
diff --git a/qml/pages/ChatPage.qml b/qml/pages/ChatPage.qml
index ade2a98..6bd548b 100644
--- a/qml/pages/ChatPage.qml
+++ b/qml/pages/ChatPage.qml
@@ -440,6 +440,33 @@ Page {
anchors.horizontalCenter: parent.horizontalCenter
}
+ VideoPreview {
+ id: messageVideoPreview
+ videoData: ( display.content['@type'] === "messageVideo" ) ? display.content.video : ""
+ width: parent.width
+ height: Functions.getVideoHeight(width, display.content.video)
+ visible: display.content['@type'] === "messageVideo"
+ }
+
+// Row {
+// id: audioRow
+// visible: display.content['@type'] === "messageVoiceNote"
+// width: parent.width
+// Image {
+// id: audioPlayButton
+// width: Theme.iconSizeLarge
+// height: Theme.iconSizeLarge
+// source: "image://theme/icon-l-play?white"
+// visible: placeholderImage.status === Image.Ready ? true : false
+// MouseArea {
+// anchors.fill: parent
+// onClicked: {
+// // Play the stuff...
+// }
+// }
+// }
+// }
+
Timer {
id: messageDateUpdater
interval: 60000
diff --git a/rpm/harbour-fernschreiber.spec b/rpm/harbour-fernschreiber.spec
index 1b6997f..1cc5925 100644
--- a/rpm/harbour-fernschreiber.spec
+++ b/rpm/harbour-fernschreiber.spec
@@ -12,7 +12,7 @@ Name: harbour-fernschreiber
Summary: Fernschreiber is a Telegram client for Sailfish OS
Version: 0.1
-Release: 1
+Release: 2
Group: Qt/Qt
License: LICENSE
URL: http://werkwolf.eu/
diff --git a/rpm/harbour-fernschreiber.yaml b/rpm/harbour-fernschreiber.yaml
index 78944c6..0b965f8 100644
--- a/rpm/harbour-fernschreiber.yaml
+++ b/rpm/harbour-fernschreiber.yaml
@@ -1,7 +1,7 @@
Name: harbour-fernschreiber
Summary: Fernschreiber is a Telegram client for Sailfish OS
Version: 0.1
-Release: 1
+Release: 2
# The contents of the Group field should be one of the groups listed here:
# https://github.com/mer-tools/spectacle/blob/master/data/GROUPS
Group: Qt/Qt
diff --git a/src/chatmodel.cpp b/src/chatmodel.cpp
index e050f29..c2bc5b1 100644
--- a/src/chatmodel.cpp
+++ b/src/chatmodel.cpp
@@ -1,6 +1,8 @@
#include "chatmodel.h"
#include
+#include
+#include
ChatModel::ChatModel(TDLibWrapper *tdLibWrapper)
{
@@ -87,6 +89,7 @@ void ChatModel::handleMessagesReceived(const QVariantList &messages)
while (messagesIterator.hasNext()) {
QVariantMap currentMessage = messagesIterator.next().toMap();
if (currentMessage.value("chat_id").toString() == this->chatId) {
+
this->messagesToBeAdded.append(currentMessage);
}
}
@@ -144,3 +147,34 @@ void ChatModel::insertMessages()
}
}
}
+
+QVariantMap ChatModel::enhanceMessage(const QVariantMap &message)
+{
+ QVariantMap enhancedMessage = message;
+ if (enhancedMessage.value("content").toMap().value("@type").toString() == "messageVoiceNote" ) {
+ QVariantMap contentMap = enhancedMessage.value("content").toMap();
+ QVariantMap voiceNoteMap = contentMap.value("voice_note").toMap();
+ QByteArray waveBytes = QByteArray::fromBase64(voiceNoteMap.value("waveform").toByteArray());
+ QBitArray waveBits(waveBytes.count() * 8);
+
+ for (int i = 0; i < waveBytes.count(); i++) {
+ for (int b = 0; b < 8; b++) {
+ waveBits.setBit( i * 8 + b, waveBytes.at(i) & (1 << (7 - b)) );
+ }
+ }
+ int waveSize = 10;
+ int waveformSets = waveBits.size() / waveSize;
+ QVariantList decodedWaveform;
+ for (int i = 0; i < waveformSets; i++) {
+ int waveformHeight = 0;
+ for (int j = 0; j < waveSize; j++) {
+ waveformHeight = waveformHeight + ( waveBits.at(i * waveSize + j) * (2 ^ (j)) );
+ }
+ decodedWaveform.append(waveformHeight);
+ }
+ voiceNoteMap.insert("decoded_voice_note", decodedWaveform);
+ contentMap.insert("voice_note", voiceNoteMap);
+ enhancedMessage.insert("content", contentMap);
+ }
+ return enhancedMessage;
+}
diff --git a/src/chatmodel.h b/src/chatmodel.h
index f3b44d6..606425a 100644
--- a/src/chatmodel.h
+++ b/src/chatmodel.h
@@ -42,6 +42,7 @@ private:
bool inIncrementalUpdate;
void insertMessages();
+ QVariantMap enhanceMessage(const QVariantMap &message);
};
#endif // CHATMODEL_H
diff --git a/src/tdlibwrapper.cpp b/src/tdlibwrapper.cpp
index 3fca934..d172603 100644
--- a/src/tdlibwrapper.cpp
+++ b/src/tdlibwrapper.cpp
@@ -27,6 +27,8 @@
#include
#include
#include
+#include
+#include
TDLibWrapper::TDLibWrapper(QObject *parent) : QObject(parent)
{
@@ -274,6 +276,21 @@ void TDLibWrapper::handleAdditionalInformation(const QString &additionalInformat
}
}
+void TDLibWrapper::controlScreenSaver(const bool &enabled)
+{
+ qDebug() << "[TDLibWrapper] Controlling device screen saver" << enabled;
+ QDBusConnection dbusConnection = QDBusConnection::connectToBus(QDBusConnection::SystemBus, "system");
+ QDBusInterface dbusInterface("com.nokia.mce", "/com/nokia/mce/request", "com.nokia.mce.request", dbusConnection);
+
+ if (enabled) {
+ qDebug() << "Enabling screensaver";
+ dbusInterface.call("req_display_cancel_blanking_pause");
+ } else {
+ qDebug() << "Disabling screensaver";
+ dbusInterface.call("req_display_blanking_pause");
+ }
+}
+
void TDLibWrapper::handleVersionDetected(const QString &version)
{
this->version = version;
diff --git a/src/tdlibwrapper.h b/src/tdlibwrapper.h
index 9a40127..b7fc8c4 100644
--- a/src/tdlibwrapper.h
+++ b/src/tdlibwrapper.h
@@ -69,6 +69,7 @@ public:
Q_INVOKABLE QVariantMap getSuperGroup(const QString &groupId);
Q_INVOKABLE void copyPictureToDownloads(const QString &filePath);
Q_INVOKABLE void handleAdditionalInformation(const QString &additionalInformation);
+ Q_INVOKABLE void controlScreenSaver(const bool &enabled);
// Direct TDLib functions
Q_INVOKABLE void sendRequest(const QVariantMap &requestObject);