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);