diff --git a/resources/icons/ui/volume-up.png b/resources/icons/ui/volume-up.png
new file mode 100644
index 00000000..4a42643f
Binary files /dev/null and b/resources/icons/ui/volume-up.png differ
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 6a2c642c..0a8587b3 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -56,15 +56,13 @@ Page {
property color bubbleBackground: Nheko.colors.highlight
property color bubbleText: Nheko.colors.highlightedText
- background: Rectangle {
- color: backgroundColor
- }
-
height: avatarSize + 2 * Nheko.paddingMedium
width: ListView.view.width
state: "normal"
ToolTip.visible: hovered && collapsed
ToolTip.text: model.tooltip
+ onClicked: Communities.setCurrentTagId(model.id)
+ onPressAndHold: communityContextMenu.show(model.id)
states: [
State {
name: "highlight"
@@ -108,9 +106,6 @@ Page {
}
- onClicked: Communities.setCurrentTagId(model.id)
- onPressAndHold: communityContextMenu.show(model.id)
-
RowLayout {
spacing: Nheko.paddingMedium
anchors.fill: parent
@@ -149,6 +144,10 @@ Page {
}
+ background: Rectangle {
+ color: backgroundColor
+ }
+
}
}
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index eb6db291..c738e5b4 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -3,14 +3,15 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
+import "../ui/media"
import QtMultimedia 5.15
import QtQuick 2.15
import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.2
+import QtQuick.Layouts 1.15
import im.nheko 1.0
-Rectangle {
- id: bg
+Item {
+ id: content
required property double proportionalHeight
required property int type
@@ -20,199 +21,86 @@ Rectangle {
required property string url
required property string body
required property string filesize
+ property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 400 : originalWidth)
+ property double tempHeight: tempWidth * proportionalHeight
+ property double divisor: isReply ? 4 : 2
+ property bool tooHigh: tempHeight > timelineRoot.height / divisor
+
+ height: (type == MtxEvent.VideoMessage ? tooHigh ? timelineRoot.height / divisor : tempHeight : 80) + fileInfoLabel.height
+ width: type == MtxEvent.VideoMessage ? tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth : 250
+
+ MxcMedia {
+ id: mxcmedia
+
+ // TODO: Show error in overlay or so?
+ onError: console.log(error)
+ roomm: room
+ // desiredVolume is a float from 0.0 -> 1.0, MediaPlayer volume is an int from 0 to 100
+ // this value automatically gets clamped for us between these two values.
+ volume: mediaControls.desiredVolume * 100
+ muted: mediaControls.muted
+ }
- radius: 10
- color: Nheko.colors.alternateBase
- height: Math.round(content.height + 24)
- width: parent ? parent.width : undefined
- ListView.onPooled: height = 4
- ListView.onReused: height = Math.round(content.height + 24)
-
- Column {
- id: content
-
- width: parent.width - 24
- anchors.centerIn: parent
-
- Rectangle {
- id: videoContainer
-
- property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 400 : originalWidth)
- property double tempHeight: tempWidth * proportionalHeight
- property double divisor: isReply ? 4 : 2
- property bool tooHigh: tempHeight > timelineView.height / divisor
-
- visible: type == MtxEvent.VideoMessage
- height: tooHigh ? timelineView.height / divisor : tempHeight
- width: tooHigh ? (timelineView.height / divisor) / proportionalHeight : tempWidth
-
- Image {
- anchors.fill: parent
- source: thumbnailUrl.replace("mxc://", "image://MxcImage/")
- asynchronous: true
- fillMode: Image.PreserveAspectFit
+ Rectangle {
+ id: videoContainer
- VideoOutput {
- anchors.fill: parent
- fillMode: VideoOutput.PreserveAspectFit
- flushMode: VideoOutput.FirstFrame
- source: mxcmedia
- }
-
- }
+ color: type == MtxEvent.VideoMessage ? Nheko.colors.window : "transparent"
+ width: parent.width
+ height: parent.height - fileInfoLabel.height
+ TapHandler {
+ onTapped: mediaControls.showControls()
}
- RowLayout {
- width: parent.width
-
- Text {
- id: positionText
+ Image {
+ anchors.fill: parent
+ source: thumbnailUrl.replace("mxc://", "image://MxcImage/")
+ asynchronous: true
+ fillMode: Image.PreserveAspectFit
- text: "--:--:--"
- color: Nheko.colors.text
- }
-
- Slider {
- id: progress
-
- //indeterminate: true
- function updatePositionTexts() {
- function formatTime(date) {
- var hh = date.getUTCHours();
- var mm = date.getUTCMinutes();
- var ss = date.getSeconds();
- if (hh < 10)
- hh = "0" + hh;
-
- if (mm < 10)
- mm = "0" + mm;
-
- if (ss < 10)
- ss = "0" + ss;
-
- return hh + ":" + mm + ":" + ss;
- }
-
- positionText.text = formatTime(new Date(mxcmedia.position));
- durationText.text = formatTime(new Date(mxcmedia.duration));
- }
-
- Layout.fillWidth: true
- value: mxcmedia.position
- from: 0
- to: mxcmedia.duration
- onMoved: mxcmedia.position = value
- onValueChanged: updatePositionTexts()
- palette: Nheko.colors
- }
+ VideoOutput {
+ id: videoOutput
- Text {
- id: durationText
-
- text: "--:--:--"
- color: Nheko.colors.text
+ visible: type == MtxEvent.VideoMessage
+ clip: true
+ anchors.fill: parent
+ fillMode: VideoOutput.PreserveAspectFit
+ source: mxcmedia
+ flushMode: VideoOutput.FirstFrame
}
}
- RowLayout {
- width: parent.width
- spacing: 15
-
- ImageButton {
- id: button
-
- Layout.alignment: Qt.AlignVCenter
- //color: Nheko.colors.window
- //radius: 22
- height: 32
- width: 32
- z: 3
- image: ":/icons/icons/ui/arrow-pointing-down.png"
- onClicked: {
- switch (button.state) {
- case "":
- mxcmedia.eventId = eventId;
- break;
- case "stopped":
- mxcmedia.play();
- console.log("play");
- button.state = "playing";
- break;
- case "playing":
- mxcmedia.pause();
- console.log("pause");
- button.state = "stopped";
- break;
- }
- }
- states: [
- State {
- name: "stopped"
-
- PropertyChanges {
- target: button
- image: ":/icons/icons/ui/play-sign.png"
- }
-
- },
- State {
- name: "playing"
-
- PropertyChanges {
- target: button
- image: ":/icons/icons/ui/pause-symbol.png"
- }
-
- }
- ]
-
- CursorShape {
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- }
-
- MxcMedia {
- id: mxcmedia
-
- roomm: room
- onError: console.log(errorString)
- onMediaStatusChanged: {
- if (status == MxcMedia.LoadedMedia) {
- progress.updatePositionTexts();
- button.state = "stopped";
- }
- }
- onStateChanged: {
- if (state == MxcMedia.StoppedState)
- button.state = "stopped";
-
- }
- }
-
- }
-
- ColumnLayout {
- id: col
+ }
- Text {
- Layout.fillWidth: true
- text: body
- elide: Text.ElideRight
- color: Nheko.colors.text
- }
+ MediaControls {
+ id: mediaControls
+
+ anchors.left: content.left
+ anchors.right: content.right
+ anchors.bottom: fileInfoLabel.top
+ playingVideo: type == MtxEvent.VideoMessage
+ positionValue: mxcmedia.position
+ duration: mxcmedia.duration
+ mediaLoaded: mxcmedia.loaded
+ mediaState: mxcmedia.state
+ onPositionChanged: mxcmedia.position = position
+ onPlayPauseActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play()
+ onLoadActivated: mxcmedia.eventId = eventId
+ }
- Text {
- Layout.fillWidth: true
- text: filesize
- textFormat: Text.PlainText
- elide: Text.ElideRight
- color: Nheko.colors.text
- }
+ // information about file name and file size
+ Label {
+ id: fileInfoLabel
- }
+ anchors.bottom: content.bottom
+ text: body + " [" + filesize + "]"
+ textFormat: Text.PlainText
+ elide: Text.ElideRight
+ color: Nheko.colors.text
+ background: Rectangle {
+ color: Nheko.colors.base
}
}
diff --git a/resources/qml/dialogs/ReadReceipts.qml b/resources/qml/dialogs/ReadReceipts.qml
index f551bae9..1bfdae84 100644
--- a/resources/qml/dialogs/ReadReceipts.qml
+++ b/resources/qml/dialogs/ReadReceipts.qml
@@ -66,9 +66,6 @@ ApplicationWindow {
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: model.mxid
- background: Rectangle {
- color: readReceiptsRoot.color
- }
RowLayout {
id: receiptLayout
@@ -113,6 +110,10 @@ ApplicationWindow {
cursorShape: Qt.PointingHandCursor
}
+ background: Rectangle {
+ color: readReceiptsRoot.color
+ }
+
}
}
diff --git a/resources/qml/ui/NhekoSlider.qml b/resources/qml/ui/NhekoSlider.qml
new file mode 100644
index 00000000..23e22f51
--- /dev/null
+++ b/resources/qml/ui/NhekoSlider.qml
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import im.nheko 1.0
+
+Slider {
+ id: control
+
+ property color progressColor: Nheko.colors.highlight
+ property bool alwaysShowSlider: true
+ property int sliderRadius: 16
+
+ value: 0
+ implicitHeight: sliderRadius
+ padding: 0
+
+ background: Rectangle {
+ x: control.leftPadding + handle.width / 2
+ y: control.topPadding + control.availableHeight / 2 - height / 2
+ implicitWidth: 200
+ implicitHeight: control.sliderRadius / 4
+ width: control.availableWidth - handle.width
+ height: implicitHeight
+ radius: height / 2
+ color: Nheko.colors.buttonText
+
+ Rectangle {
+ width: control.visualPosition * parent.width
+ height: parent.height
+ color: control.progressColor
+ radius: 2
+ }
+
+ }
+
+ handle: Rectangle {
+ x: control.leftPadding + control.visualPosition * background.width
+ y: control.topPadding + control.availableHeight / 2 - height / 2
+ implicitWidth: control.sliderRadius
+ implicitHeight: control.sliderRadius
+ radius: control.sliderRadius / 2
+ color: control.progressColor
+ visible: Settings.mobileMode || control.alwaysShowSlider || control.hovered || control.pressed
+ border.color: control.progressColor
+ }
+
+}
diff --git a/resources/qml/ui/media/MediaControls.qml b/resources/qml/ui/media/MediaControls.qml
new file mode 100644
index 00000000..7216e552
--- /dev/null
+++ b/resources/qml/ui/media/MediaControls.qml
@@ -0,0 +1,244 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "../"
+import "../../"
+import QtMultimedia 5.15
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import im.nheko 1.0
+
+Rectangle {
+ id: control
+
+ property alias desiredVolume: volumeSlider.desiredVolume
+ property bool muted: false
+ property bool playingVideo: false
+ property var mediaState
+ property bool mediaLoaded: false
+ property var duration
+ property var positionValue: 0
+ property var position
+ property bool shouldShowControls: !playingVideo || playerMouseArea.shouldShowControls || volumeSlider.state == "shown"
+
+ signal playPauseActivated()
+ signal loadActivated()
+
+ function showControls() {
+ controlHideTimer.restart();
+ }
+
+ function durationToString(duration) {
+ function maybeZeroPrepend(time) {
+ return (time < 10) ? "0" + time.toString() : time.toString();
+ }
+
+ var totalSeconds = Math.floor(duration / 1000);
+ var seconds = totalSeconds % 60;
+ var minutes = (Math.floor(totalSeconds / 60)) % 60;
+ var hours = (Math.floor(totalSeconds / (60 * 24))) % 24;
+ // Always show minutes and don't prepend zero into the leftmost element
+ var ss = maybeZeroPrepend(seconds);
+ var mm = (hours > 0) ? maybeZeroPrepend(minutes) : minutes.toString();
+ var hh = hours.toString();
+ if (hours < 1)
+ return mm + ":" + ss;
+
+ return hh + ":" + mm + ":" + ss;
+ }
+
+ color: {
+ var wc = Nheko.colors.alternateBase;
+ return Qt.rgba(wc.r, wc.g, wc.b, 0.5);
+ }
+ opacity: control.shouldShowControls ? 1 : 0
+ height: controlLayout.implicitHeight
+
+ HoverHandler {
+ id: playerMouseArea
+
+ property bool shouldShowControls: hovered || controlHideTimer.running || control.mediaState != MediaPlayer.PlayingState
+
+ onHoveredChanged: showControls()
+ }
+
+ ColumnLayout {
+ id: controlLayout
+
+ enabled: control.shouldShowControls
+ spacing: 0
+ anchors.bottom: control.bottom
+ anchors.left: control.left
+ anchors.right: control.right
+
+ NhekoSlider {
+ Layout.fillWidth: true
+ Layout.leftMargin: Nheko.paddingSmall
+ Layout.rightMargin: Nheko.paddingSmall
+ enabled: control.mediaLoaded
+ value: control.positionValue
+ onMoved: control.position = value
+ from: 0
+ to: control.duration
+ alwaysShowSlider: false
+ }
+
+ RowLayout {
+ Layout.margins: Nheko.paddingSmall
+ spacing: Nheko.paddingSmall
+ Layout.fillWidth: true
+
+ // Cache/Play/pause button
+ ImageButton {
+ id: playbackStateImage
+
+ Layout.alignment: Qt.AlignLeft
+ buttonTextColor: Nheko.colors.text
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: 24
+ image: {
+ if (control.mediaLoaded) {
+ if (control.mediaState == MediaPlayer.PlayingState)
+ return ":/icons/icons/ui/pause-symbol.png";
+ else
+ return ":/icons/icons/ui/play-sign.png";
+ } else {
+ return ":/icons/icons/ui/arrow-pointing-down.png";
+ }
+ }
+ onClicked: control.mediaLoaded ? control.playPauseActivated() : control.loadActivated()
+ }
+
+ ImageButton {
+ id: volumeButton
+
+ Layout.alignment: Qt.AlignLeft
+ buttonTextColor: Nheko.colors.text
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: 24
+ image: {
+ if (control.muted || control.desiredVolume <= 0)
+ return ":/icons/icons/ui/volume-off-indicator.png";
+ else
+ return ":/icons/icons/ui/volume-up.png";
+ }
+ onClicked: control.muted = !control.muted
+ }
+
+ NhekoSlider {
+ id: volumeSlider
+
+ property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, QtMultimedia.LogarithmicVolumeScale, QtMultimedia.LinearVolumeScale)
+
+ state: ""
+ Layout.alignment: Qt.AlignLeft
+ Layout.preferredWidth: 0
+ opacity: 0
+ orientation: Qt.Horizontal
+ value: 1
+ onDesiredVolumeChanged: {
+ control.muted = !(desiredVolume > 0);
+ }
+ transitions: [
+ Transition {
+ from: ""
+ to: "shown"
+
+ SequentialAnimation {
+ PauseAnimation {
+ duration: 50
+ }
+
+ NumberAnimation {
+ duration: 100
+ properties: "opacity"
+ easing.type: Easing.InQuad
+ }
+
+ }
+
+ NumberAnimation {
+ properties: "Layout.preferredWidth"
+ duration: 150
+ }
+
+ },
+ Transition {
+ from: "shown"
+ to: ""
+
+ SequentialAnimation {
+ PauseAnimation {
+ duration: 100
+ }
+
+ ParallelAnimation {
+ NumberAnimation {
+ duration: 100
+ properties: "opacity"
+ easing.type: Easing.InQuad
+ }
+
+ NumberAnimation {
+ properties: "Layout.preferredWidth"
+ duration: 150
+ }
+
+ }
+
+ }
+
+ }
+ ]
+
+ states: State {
+ name: "shown"
+ when: Settings.mobileMode || volumeButton.hovered || volumeSlider.hovered || volumeSlider.pressed
+
+ PropertyChanges {
+ target: volumeSlider
+ Layout.preferredWidth: 100
+ }
+
+ PropertyChanges {
+ target: volumeSlider
+ opacity: 1
+ }
+
+ }
+
+ }
+
+ Label {
+ Layout.alignment: Qt.AlignRight
+ text: (!control.mediaLoaded) ? "-- / --" : (durationToString(control.positionValue) + " / " + durationToString(control.duration))
+ color: Nheko.colors.text
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ }
+
+ }
+
+ // For hiding controls on stationary cursor
+ Timer {
+ id: controlHideTimer
+
+ interval: 1500 //ms
+ repeat: false
+ }
+
+ // Fade controls in/out
+ Behavior on opacity {
+ OpacityAnimator {
+ duration: 100
+ }
+
+ }
+
+}
diff --git a/resources/qml/ui/media/qmldir b/resources/qml/ui/media/qmldir
new file mode 100644
index 00000000..143b603d
--- /dev/null
+++ b/resources/qml/ui/media/qmldir
@@ -0,0 +1,3 @@
+module im.nheko.UI.Media
+VolumeSlider 1.0 VolumeSlider.qml
+MediaControls 1.0 MediaControls.qml
\ No newline at end of file
diff --git a/resources/qml/ui/qmldir b/resources/qml/ui/qmldir
index 831a723d..a2ce7514 100644
--- a/resources/qml/ui/qmldir
+++ b/resources/qml/ui/qmldir
@@ -1,3 +1,4 @@
module im.nheko.UI
+NhekoSlider 1.0 NhekoSlider.qml
Ripple 1.0 Ripple.qml
Spinner 1.0 Spinner.qml
\ No newline at end of file
diff --git a/resources/res.qrc b/resources/res.qrc
index 66b77205..a60f4ab0 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -3,6 +3,7 @@
icons/ui/at-solid.svg
icons/ui/volume-off-indicator.png
icons/ui/volume-off-indicator@2x.png
+ icons/ui/volume-up.png
icons/ui/black-bubble-speech.png
icons/ui/black-bubble-speech@2x.png
icons/ui/do-not-disturb-rounded-sign.png
@@ -179,9 +180,11 @@
qml/dialogs/UserProfile.qml
qml/emoji/EmojiPicker.qml
qml/emoji/StickerPicker.qml
+ qml/ui/NhekoSlider.qml
qml/ui/Ripple.qml
qml/ui/Spinner.qml
qml/ui/animations/BlinkAnimation.qml
+ qml/ui/media/MediaControls.qml
qml/voip/ActiveCallBar.qml
qml/voip/CallDevices.qml
qml/voip/CallInvite.qml
diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp
index db8c0f1f..df0298da 100644
--- a/src/ui/MxcMediaProxy.cpp
+++ b/src/ui/MxcMediaProxy.cpp
@@ -13,6 +13,11 @@
#include
#include
+#if defined(Q_OS_MACOS)
+// TODO (red_sky): Remove for Qt6. See other ifdef below
+#include
+#endif
+
#include "EventAccessors.h"
#include "Logging.h"
#include "MatrixClient.h"
@@ -75,7 +80,7 @@ MxcMediaProxy::startDownload()
QPointer self = this;
- auto processBuffer = [this, encryptionInfo, filename, self](QIODevice &device) {
+ auto processBuffer = [this, encryptionInfo, filename, self, suffix](QIODevice &device) {
if (!self)
return;
@@ -90,10 +95,34 @@ MxcMediaProxy::startDownload()
buffer.open(QIODevice::ReadOnly);
buffer.reset();
- QTimer::singleShot(0, this, [this, filename] {
+ QTimer::singleShot(0, this, [this, filename, suffix, encryptionInfo] {
+#if defined(Q_OS_MACOS)
+ if (encryptionInfo) {
+ // macOS has issues reading from a buffer in setMedia for whatever reason.
+ // Instead, write the buffer to a temporary file and read from that.
+ // This should be fixed in Qt6, so update this when we do that!
+ // TODO: REMOVE IN QT6
+ QTemporaryFile tempFile;
+ tempFile.setFileTemplate(tempFile.fileTemplate() + QLatin1Char('.') + suffix);
+ tempFile.open();
+ tempFile.write(buffer.data());
+ tempFile.close();
+ nhlog::ui()->debug("Playing media from temp buffer file: {}. Remove in QT6!",
+ filename.filePath().toStdString());
+ this->setMedia(QUrl::fromLocalFile(tempFile.fileName()));
+ } else {
+ nhlog::ui()->info(
+ "Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
+ this->setMedia(QUrl::fromLocalFile(filename.filePath()));
+ }
+#else
+ Q_UNUSED(suffix)
+ Q_UNUSED(encryptionInfo)
+
nhlog::ui()->info(
"Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
this->setMedia(QMediaContent(filename.fileName()), &buffer);
+#endif
emit loadedChanged();
});
};