summary refs log tree commit diff
path: root/resources
diff options
context:
space:
mode:
Diffstat (limited to 'resources')
-rw-r--r--resources/icons/ui/volume-up.pngbin0 -> 617 bytes
-rw-r--r--resources/qml/delegates/PlayableMediaMessage.qml529
-rw-r--r--resources/res.qrc1
3 files changed, 344 insertions, 186 deletions
diff --git a/resources/icons/ui/volume-up.png b/resources/icons/ui/volume-up.png
new file mode 100644
index 00000000..4a42643f
--- /dev/null
+++ b/resources/icons/ui/volume-up.png
Binary files differdiff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index eb6db291..fbc4a637 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -9,9 +9,7 @@ import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
-Rectangle {
-    id: bg
-
+ColumnLayout {
     required property double proportionalHeight
     required property int type
     required property int originalWidth
@@ -21,200 +19,359 @@ Rectangle {
     required property string body
     required property string filesize
 
-    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 {
+    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
+	}
+
+    id: content
+    Layout.maximumWidth: parent? parent.width: undefined
+    MxcMedia {
+        id: mxcmedia
+        // TODO: Show error in overlay or so?
+        onError: console.log(error)
+        roomm: room
+		onMediaStatusChanged: {
+			if (status == MxcMedia.LoadedMedia) {
+				progress.updatePositionTexts();
+			}
+		}
+    }
+      
+    Rectangle {
+        id: videoContainer
+        visible: type == MtxEvent.VideoMessage
+        //property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : /////model.data.width)
+        // property double tempWidth: (model.data.width < 1) ? 400 : model.data.width
+        // property double tempHeight: tempWidth * model.data.proportionalHeight
+        //property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 400 : originalWidth)
+        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
+
+        Layout.maximumWidth: Layout.preferredWidth
+        Layout.preferredHeight: tooHigh ? timelineRoot.height / divisor : tempHeight
+        Layout.preferredWidth: tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth
+        Image {
+            anchors.fill: parent
+            source: thumbnailUrl.replace("mxc://", "image://MxcImage/")
+            asynchronous: true
+            fillMode: Image.PreserveAspectFit
+            // Button and window colored overlay to cache media
+            Rectangle {
+                // Display over video controls
+                z: videoOutput.z + 1
+                visible: !mxcmedia.loaded
                 anchors.fill: parent
-                source: thumbnailUrl.replace("mxc://", "image://MxcImage/")
-                asynchronous: true
-                fillMode: Image.PreserveAspectFit
-
-                VideoOutput {
-                    anchors.fill: parent
-                    fillMode: VideoOutput.PreserveAspectFit
-                    flushMode: VideoOutput.FirstFrame
-                    source: mxcmedia
+                color: Nheko.colors.window
+                opacity: 0.5
+                Image {
+                    property color buttonColor: (cacheVideoArea.containsMouse) ? Nheko.colors.highlight :
+                        Nheko.colors.text
+
+                    anchors.verticalCenter: parent.verticalCenter
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+buttonColor						
                 }
-
-            }
-
-        }
-
-        RowLayout {
-            width: parent.width
-
-            Text {
-                id: positionText
-
-                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
-            }
-
-            Text {
-                id: durationText
-
-                text: "--:--:--"
-                color: Nheko.colors.text
-            }
-
-        }
-
-        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 {
+                MouseArea {
+                    id: cacheVideoArea
                     anchors.fill: parent
-                    cursorShape: Qt.PointingHandCursor
+                    hoverEnabled: true
+                    enabled: !mxcmedia.loaded
+                    onClicked: mxcmedia.eventId = eventId
                 }
-
-                MxcMedia {
-                    id: mxcmedia
-
-                    roomm: room
-                    onError: console.log(errorString)
-                    onMediaStatusChanged: {
-                        if (status == MxcMedia.LoadedMedia) {
-                            progress.updatePositionTexts();
-                            button.state = "stopped";
+            }
+            VideoOutput {
+                id: videoOutput
+                clip: true
+                anchors.fill: parent
+                fillMode: VideoOutput.PreserveAspectFit
+                source: mxcmedia
+				flushMode: VideoOutput.FirstFrame
+
+                // TODO: once we can use Qt 5.12, use HoverHandler
+                MouseArea {
+                    id: playerMouseArea
+                    // Toggle play state on clicks
+                    onClicked: {
+                        if (controlRect.shouldShowControls &&
+                            !controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) {
+                                (mxcmedia.state == MediaPlayer.PlayingState) ?
+                                    mxcmedia.pause() :
+                                    mxcmedia.play()
                         }
                     }
-                    onStateChanged: {
-                        if (state == MxcMedia.StoppedState)
-                            button.state = "stopped";
-
+					Rectangle {
+						id: controlRect
+						property int controlHeight: 25
+						property bool shouldShowControls: playerMouseArea.shouldShowControls ||
+							volumeSliderRect.visible
+
+						anchors.bottom: playerMouseArea.bottom
+						// Window color with 128/255 alpha
+						color: {
+							var wc = Nheko.colors.window
+							return Qt.rgba(wc.r, wc.g, wc.b, 0.5)
+						}
+						height: 40
+						width: playerMouseArea.width
+						opacity: shouldShowControls ? 1 : 0
+						// Fade controls in/out
+						Behavior on opacity {
+							OpacityAnimator {
+								duration: 100
+							}
+						}
+
+						RowLayout {
+							anchors.fill: parent
+							width: parent.width
+							// Play/pause button
+							Image {
+								id: playbackStateImage
+								fillMode: Image.PreserveAspectFit
+								Layout.preferredHeight: controlRect.controlHeight
+								Layout.alignment: Qt.AlignVCenter
+								property color controlColor: (playbackStateArea.containsMouse) ?
+									Nheko.colors.highlight : Nheko.colors.text
+
+								source: (mxcmedia.state == MediaPlayer.PlayingState) ?
+									"image://colorimage/:/icons/icons/ui/pause-symbol.png?"+controlColor :
+									"image://colorimage/:/icons/icons/ui/play-sign.png?"+controlColor
+								MouseArea {
+									id: playbackStateArea
+									
+									anchors.fill: parent
+									hoverEnabled: true
+									onClicked: {
+										(mxcmedia.state == MediaPlayer.PlayingState) ?
+											mxcmedia.pause() :
+											mxcmedia.play()
+									}
+								}
+							}
+							Label {
+								text: (!mxcmedia.loaded) ? "-/-" :
+									durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration)
+							}
+
+							Slider {
+								Layout.fillWidth: true
+								Layout.minimumWidth: 50
+								height: controlRect.controlHeight
+								value: mxcmedia.position
+								onMoved: mxcmedia.position = value
+								from: 0
+								to: mxcmedia.duration
+							}
+							// Volume slider activator
+							Image {
+								property color controlColor: (volumeImageArea.containsMouse) ?
+									Nheko.colors.highlight : Nheko.colors.text
+
+								// TODO: add icons for different volume levels
+								id: volumeImage
+								source: (mxcmedia.volume > 0 && !mxcmedia.muted) ?
+									"image://colorimage/:/icons/icons/ui/volume-up.png?"+ controlColor :
+									"image://colorimage/:/icons/icons/ui/volume-off-indicator.png?"+ controlColor
+								Layout.rightMargin: 5
+								Layout.preferredHeight: controlRect.controlHeight
+								fillMode: Image.PreserveAspectFit
+								MouseArea {
+									id: volumeImageArea	
+									anchors.fill: parent
+									hoverEnabled: true
+									onClicked: mxcmedia.muted = !mxcmedia.muted
+									onExited: volumeSliderHideTimer.start()
+									onPositionChanged: volumeSliderHideTimer.start()
+									// For hiding volume slider after a while
+									Timer {
+										id: volumeSliderHideTimer
+										interval: 1500
+										repeat: false
+										running: false
+									}
+								}
+								Rectangle {
+									id: volumeSliderRect
+									opacity: (visible) ? 1 : 0
+									Behavior on opacity {
+										OpacityAnimator {
+											duration: 100
+										}
+									}
+									// TODO: figure out a better way to put the slider popup above controlRect
+									anchors.bottom: volumeImage.top
+									anchors.bottomMargin: 10
+									anchors.horizontalCenter: volumeImage.horizontalCenter
+									color: {
+										var wc = Nheko.colors.window
+										return Qt.rgba(wc.r, wc.g, wc.b, 0.5)
+									}
+									/* TODO: base width on the slider width (some issue with it not having a geometry
+										when using the width here?) */
+									width: volumeImage.width * 0.7
+									radius: volumeSlider.width / 2
+									height: controlRect.height * 2 //100
+									visible: volumeImageArea.containsMouse ||
+										volumeSliderHideTimer.running ||
+										volumeSliderRectMouseArea.containsMouse
+									Slider {
+										// Desired value to avoid loop onMoved -> media.volume -> value -> onMoved...
+										property real desiredVolume: 1
+										
+										// TODO: the slider is slightly off-center on the left for some reason...
+										id: volumeSlider
+										from: 0
+										to: 1
+										value: (mxcmedia.muted) ? 0 :
+											QtMultimedia.convertVolume(desiredVolume,
+												QtMultimedia.LinearVolumeScale,
+												QtMultimedia.LogarithmicVolumeScale)
+										anchors.fill: parent
+										anchors.bottomMargin: parent.height * 0.1
+										anchors.topMargin: parent.height * 0.1
+										anchors.horizontalCenter: parent.horizontalCenter
+										orientation: Qt.Vertical
+										onMoved: desiredVolume = QtMultimedia.convertVolume(value,
+											QtMultimedia.LogarithmicVolumeScale,
+											QtMultimedia.LinearVolumeScale)
+										/* This would be better handled in 'media', but it has some issue with listening
+											to this signal */
+										onDesiredVolumeChanged: mxcmedia.muted = !(desiredVolume > 0)
+									}
+									// Used for resetting the timer on mouse moves on volumeSliderRect
+									MouseArea {
+										id: volumeSliderRectMouseArea
+										anchors.fill: parent
+										hoverEnabled: true
+										propagateComposedEvents: true
+										onExited: volumeSliderHideTimer.start()
+
+										onClicked: mouse.accepted = false
+										onPressed: mouse.accepted = false
+										onReleased: mouse.accepted = false
+										onPressAndHold: mouse.accepted = false
+										onPositionChanged: {
+											mouse.accepted = false
+											volumeSliderHideTimer.start()
+										}
+									}
+								}
+							}
+
+						}
+					}
+                    // This breaks separation of concerns but this same thing doesn't work when called from controlRect...
+                    property bool shouldShowControls: (containsMouse && controlHideTimer.running) ||
+                        (mxcmedia.state != MediaPlayer.PlayingState) ||
+                        controlRect.contains(mapToItem(controlRect, mouseX, mouseY))
+
+                    // For hiding controls on stationary cursor
+                    Timer {
+                        id: controlHideTimer
+                        interval: 1500 //ms
+                        repeat: false
                     }
-                }
 
-            }
-
-            ColumnLayout {
-                id: col
-
-                Text {
-                    Layout.fillWidth: true
-                    text: body
-                    elide: Text.ElideRight
-                    color: Nheko.colors.text
-                }
+                    hoverEnabled: true
+                    onPositionChanged: controlHideTimer.start()
 
-                Text {
-                    Layout.fillWidth: true
-                    text: filesize
-                    textFormat: Text.PlainText
-                    elide: Text.ElideRight
-                    color: Nheko.colors.text
+                    x: videoOutput.contentRect.x
+                    y: videoOutput.contentRect.y
+                    width: videoOutput.contentRect.width
+                    height: videoOutput.contentRect.height
+                    propagateComposedEvents: true
                 }
-
             }
-
         }
-
     }
-
+    // Audio player
+    // TODO: share code with the video player
+    Rectangle {
+		id: audioControlRect
+		
+		visible: type != MtxEvent.VideoMessage
+		property int controlHeight: 25
+		Layout.preferredHeight: 40
+		RowLayout {
+			anchors.fill: parent
+			width: parent.width
+			// Play/pause button
+			Image {
+				id: audioPlaybackStateImage
+				fillMode: Image.PreserveAspectFit
+				Layout.preferredHeight: controlRect.controlHeight
+				Layout.alignment: Qt.AlignVCenter
+				property color controlColor: (audioPlaybackStateArea.containsMouse) ?
+					Nheko.colors.highlight : Nheko.colors.text
+
+				source: {
+                    if (!mxcmedia.loaded)
+                        return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+controlColor
+                    return (mxcmedia.state == MediaPlayer.PlayingState) ?
+                        "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+controlColor :
+                        "image://colorimage/:/icons/icons/ui/play-sign.png?"+controlColor
+                }
+				MouseArea {
+					id: audioPlaybackStateArea
+					
+					anchors.fill: parent
+					hoverEnabled: true
+					onClicked: {
+                        if (!mxcmedia.loaded) {
+                            mxcmedia.eventId = eventId
+                            return
+                        }
+						(mxcmedia.state == MediaPlayer.PlayingState) ?
+							mxcmedia.pause() :
+							mxcmedia.play()
+					}
+				}
+			}
+			Label {
+				text: (!mxcmedia.loaded) ? "-/-" :
+					durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration)
+			}
+
+			Slider {
+				Layout.fillWidth: true
+				Layout.minimumWidth: 50
+				height: controlRect.controlHeight
+				value: mxcmedia.position
+				onMoved: mxcmedia.seek(value)
+				from: 0
+				to: mxcmedia.duration
+			}
+		}
+	}
+    
+    Label {
+        id: fileInfoLabel
+        
+        background: Rectangle {
+            color: Nheko.colors.base
+        }
+		Layout.fillWidth: true
+		text: body + " [" + filesize + "]"
+		textFormat: Text.PlainText
+		elide: Text.ElideRight
+		color: Nheko.colors.text
+	}
 }
diff --git a/resources/res.qrc b/resources/res.qrc
index 66b77205..ccb5a637 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -3,6 +3,7 @@
         <file>icons/ui/at-solid.svg</file>
         <file>icons/ui/volume-off-indicator.png</file>
         <file>icons/ui/volume-off-indicator@2x.png</file>
+        <file>icons/ui/volume-up.png</file>
         <file>icons/ui/black-bubble-speech.png</file>
         <file>icons/ui/black-bubble-speech@2x.png</file>
         <file>icons/ui/do-not-disturb-rounded-sign.png</file>