diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index eb788c6c..a5de613d 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -20,23 +20,24 @@ ColumnLayout { required property string filesize 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() + 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 - } + if (hours < 1) { + return mm + ":" + ss + } + return hh + ":" + mm + ":" + ss + } id: content @@ -46,8 +47,11 @@ ColumnLayout { // 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: volumeSlider.desiredVolume * 100 } - + Rectangle { id: videoContainer visible: type == MtxEvent.VideoMessage @@ -60,6 +64,7 @@ ColumnLayout { property double divisor: isReply ? 4 : 2 property bool tooHigh: tempHeight > timelineRoot.height / divisor + color: Nheko.colors.window Layout.preferredHeight: tooHigh ? timelineRoot.height / divisor : tempHeight Layout.preferredWidth: tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth @@ -71,16 +76,16 @@ ColumnLayout { asynchronous: true fillMode: Image.PreserveAspectFit // Button and window colored overlay to cache media - Rectangle { + Item { // Display over video controls z: videoOutput.z + 1 visible: !mxcmedia.loaded anchors.fill: parent - color: Nheko.colors.window - opacity: 0.5 + //color: Nheko.colors.window + //opacity: 0.5 Image { property color buttonColor: (cacheVideoArea.containsMouse) ? Nheko.colors.highlight : - Nheko.colors.text + Nheko.colors.text anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter @@ -100,7 +105,7 @@ ColumnLayout { anchors.fill: parent fillMode: VideoOutput.PreserveAspectFit source: mxcmedia - flushMode: VideoOutput.FirstFrame + flushMode: VideoOutput.FirstFrame // TODO: once we can use Qt 5.12, use HoverHandler MouseArea { @@ -108,267 +113,262 @@ ColumnLayout { // Toggle play state on clicks onClicked: { if (controlRect.shouldShowControls && - !controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) { - (mxcmedia.state == MediaPlayer.PlayingState) ? - mxcmedia.pause() : - mxcmedia.play() + !controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) { + (mxcmedia.state == MediaPlayer.PlayingState) ? + mxcmedia.pause() : + mxcmedia.play() } } - Rectangle { - id: controlRect - property int controlHeight: 25 - property bool shouldShowControls: playerMouseArea.shouldShowControls || - volumeSliderRect.visible + 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 - } - - hoverEnabled: true - onPositionChanged: controlHideTimer.start() - - 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 + anchors.bottom: playerMouseArea.bottom + // Window color with 128/255 alpha + color: { + var wc = Nheko.colors.alternateBase + 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 + } } - (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 - } -} + 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)) + color: Nheko.colors.text + } + + 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 { + // TODO: the slider is slightly off-center on the left for some reason... + id: volumeSlider + + value: 1.0 + // Desired value to avoid loop onMoved -> media.volume -> value -> onMoved... + property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, + QtMultimedia.LogarithmicVolumeScale, + QtMultimedia.LinearVolumeScale) + + anchors.fill: parent + anchors.bottomMargin: parent.height * 0.1 + anchors.topMargin: parent.height * 0.1 + anchors.horizontalCenter: parent.horizontalCenter + orientation: Qt.Vertical + onDesiredVolumeChanged: { + mxcmedia.muted = !(desiredVolume > 0.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 + } + + hoverEnabled: true + onPositionChanged: controlHideTimer.start() + + 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 + } + }