From 42bef68accdd76ac784b331adbeb982014e8c3ed Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Mon, 19 Jul 2021 14:11:03 -0400 Subject: [PATCH 01/13] Import and update lurkki's branch --- resources/icons/ui/volume-up.png | Bin 0 -> 617 bytes .../qml/delegates/PlayableMediaMessage.qml | 560 +++++++++++------- resources/res.qrc | 1 + 3 files changed, 361 insertions(+), 200 deletions(-) create mode 100644 resources/icons/ui/volume-up.png diff --git a/resources/icons/ui/volume-up.png b/resources/icons/ui/volume-up.png new file mode 100644 index 0000000000000000000000000000000000000000..4a42643f23ae3a55142a0ba0973ccfc5e33ba514 GIT binary patch literal 617 zcmV-v0+#)WP)__f4}@;6v1 zB9V~fM=4gaQiB><%EFGw+HcVm8A=SM7>oCLyJv3R>)t!><<#3feb0HG@ArA%^E}VF zwAQ*Xuf-gN)`O>*z<#v{EH=OE%Px|=2M#I-chFAUT< zZW&$%5m%cbpMl{p$t2blRHAreb zQNYTomBOWvdm-Q_1HMI*@H1hxhXTGM zVI8spoy5H|1i%u!PWU&K3SS-aPlWsj2^%fze13-0$?wLh`VT4JWl@NkTV+m{1biZEP=ca_hrQh7gb*IwvhKQ{dQnQ@}E=gHXK&jhO<<* z;aAxq1Z%<^XLDt(IEXHX-17lH74Xf_tF=e*J5!J1{LJ_e-d3TNaT#q0@na!&_;2L7 zP3Jn^8e(gcpnUapUB|gK(vTdGH6+UeHh{Nb?4Cx*b8EIG8^o(Xex(`m-14Thy5fs$ zm#IzZ9n)w^8@1}Td_)Otc_Y??C;sCf6SBJLt(EW_*N+nGL1vFW00000NkvXXu0mjf Dn#U?~ literal 0 HcmV?d00001 diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index fd764d52..3face74d 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -9,9 +9,7 @@ import QtQuick.Controls 2.1 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,205 +19,367 @@ 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) + 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() - 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 - - VideoOutput { - anchors.fill: parent - fillMode: VideoOutput.PreserveAspectFit - source: media - } - - } - - } - - 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(media.position)); - durationText.text = formatTime(new Date(media.duration)); - } - - Layout.fillWidth: true - value: media.position - from: 0 - to: media.duration - onMoved: media.seek(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 "": - room.cacheMedia(eventId); - break; - case "stopped": - media.play(); - console.log("play"); - button.state = "playing"; - break; - case "playing": - media.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 - } - - MediaPlayer { - id: media - - onError: console.log(errorString) - onStatusChanged: { - if (status == MediaPlayer.Loaded) - progress.updatePositionTexts(); - - } - onStopped: button.state = "stopped" - } - - Connections { - target: room - onMediaCached: { - if (mxcUrl == url) { - media.source = cacheUrl; - button.state = "stopped"; - console.log("media loaded: " + mxcUrl + " at " + cacheUrl); - } - console.log("media cached: " + mxcUrl + " at " + cacheUrl); - } - } - - } - - ColumnLayout { - id: col - - Text { - Layout.fillWidth: true - text: body - elide: Text.ElideRight - color: Nheko.colors.text - } - - Text { - Layout.fillWidth: true - text: filesize - textFormat: Text.PlainText - elide: Text.ElideRight - color: Nheko.colors.text - } - - } - - } + if (hours < 1) + return mm + ":" + ss + return hh + ":" + mm + ":" + ss + } + id: content + Layout.maximumWidth: parent? parent.width: undefined + MediaPlayer { + id: media + // TODO: Show error in overlay or so? + onError: console.log(errorString) + volume: volumeSlider.desiredVolume } + Connections { + property bool mediaCached: false + + id: mediaCachedObserver + target: room + onMediaCached: { + if (mxcUrl == url) { + mediaCached = true + media.source = "file://" + cacheUrl + console.log("media loaded: " + mxcUrl + " at " + cacheUrl) + } + console.log("media cached: " + mxcUrl + " at " + cacheUrl) + } + } + + 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: !mediaCachedObserver.mediaCached + anchors.fill: parent + 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 + } + MouseArea { + id: cacheVideoArea + anchors.fill: parent + hoverEnabled: true + enabled: !mediaCachedObserver.mediaCached + onClicked: room.cacheMedia(eventId) + } + } + VideoOutput { + id: videoOutput + clip: true + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectFit + source: media + // 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))) { + (media.playbackState == MediaPlayer.PlayingState) ? + media.pause() : + media.play() + } + } + 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: (media.playbackState == 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: { + (media.playbackState == MediaPlayer.PlayingState) ? + media.pause() : + media.play() + } + } + } + Label { + text: (!mediaCachedObserver.mediaCached) ? "-/-" : + durationToString(media.position) + "/" + durationToString(media.duration) + } + + Slider { + Layout.fillWidth: true + Layout.minimumWidth: 50 + height: controlRect.controlHeight + value: media.position + onMoved: media.seek(value) + from: 0 + to: media.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: (media.volume > 0 && !media.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: media.muted = !media.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: (media.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: media.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) || + (media.playbackState != 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 (!mediaCachedObserver.mediaCached) + return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+controlColor + return (media.playbackState == 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 (!mediaCachedObserver.mediaCached) { + room.cacheMedia(eventId) + return + } + (media.playbackState == MediaPlayer.PlayingState) ? + media.pause() : + media.play() + } + } + } + Label { + text: (!mediaCachedObserver.mediaCached) ? "-/-" : + durationToString(media.position) + "/" + durationToString(media.duration) + } + + Slider { + Layout.fillWidth: true + Layout.minimumWidth: 50 + height: controlRect.controlHeight + value: media.position + onMoved: media.seek(value) + from: 0 + to: media.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 f41835f9..1e7d0a25 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 From 8e433a7ed2f06a4ad30a6f74323ad373e8eb9cd7 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Wed, 3 Nov 2021 23:06:32 -0400 Subject: [PATCH 02/13] PlayableMediaMessage fixes on macOS --- src/ui/MxcMediaProxy.cpp | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp index db8c0f1f..2015b420 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_UNSUED(suffix) + Q_UNUSED(encryptionInfo) + nhlog::ui()->info( "Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen()); this->setMedia(QMediaContent(filename.fileName()), &buffer); +#endif emit loadedChanged(); }); }; From 4bd0ec89398068cade5dabddd1bb57731d425550 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Wed, 3 Nov 2021 23:54:51 -0400 Subject: [PATCH 03/13] Fix syntax issue --- src/ui/MxcMediaProxy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp index 2015b420..df0298da 100644 --- a/src/ui/MxcMediaProxy.cpp +++ b/src/ui/MxcMediaProxy.cpp @@ -116,7 +116,7 @@ MxcMediaProxy::startDownload() this->setMedia(QUrl::fromLocalFile(filename.filePath())); } #else - Q_UNSUED(suffix) + Q_UNUSED(suffix) Q_UNUSED(encryptionInfo) nhlog::ui()->info( From 13a5194c08090d7cac24f6873dee2cb2b92c3842 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Mon, 8 Nov 2021 19:18:11 -0500 Subject: [PATCH 04/13] Minor fixes for undefined qml behavior --- resources/qml/delegates/PlayableMediaMessage.qml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index fbc4a637..eb788c6c 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -39,17 +39,13 @@ ColumnLayout { } id: content - Layout.maximumWidth: parent? parent.width: undefined + + Layout.fillWidth: true MxcMedia { id: mxcmedia // TODO: Show error in overlay or so? onError: console.log(error) roomm: room - onMediaStatusChanged: { - if (status == MxcMedia.LoadedMedia) { - progress.updatePositionTexts(); - } - } } Rectangle { @@ -65,9 +61,10 @@ ColumnLayout { 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 + Layout.maximumWidth: Layout.preferredWidth + Image { anchors.fill: parent source: thumbnailUrl.replace("mxc://", "image://MxcImage/") From 42b74509ea6e0daef6b9c45207560810f714add1 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Mon, 8 Nov 2021 22:55:16 -0500 Subject: [PATCH 05/13] Incorporate nico's suggestions, fix volume slider --- .../qml/delegates/PlayableMediaMessage.qml | 562 +++++++++--------- 1 file changed, 281 insertions(+), 281 deletions(-) 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 + } + } From f6fcae124f1f98cf4ac060cfc7f3e83c9e8b2979 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Tue, 9 Nov 2021 19:28:53 -0500 Subject: [PATCH 06/13] Initial Refactoring into separate controls --- .../qml/delegates/PlayableMediaMessage.qml | 95 ++-------------- resources/qml/ui/media/VolumeControl.qml | 105 ++++++++++++++++++ resources/qml/ui/media/qmldir | 2 + resources/res.qrc | 1 + 4 files changed, 115 insertions(+), 88 deletions(-) create mode 100644 resources/qml/ui/media/VolumeControl.qml create mode 100644 resources/qml/ui/media/qmldir diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index a5de613d..ceeeeb1a 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -3,6 +3,7 @@ // 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 @@ -50,6 +51,7 @@ ColumnLayout { // 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 + muted: volumeSlider.muted } Rectangle { @@ -123,7 +125,7 @@ ColumnLayout { id: controlRect property int controlHeight: 25 property bool shouldShowControls: playerMouseArea.shouldShowControls || - volumeSliderRect.visible + volumeSlider.controlsVisible anchors.bottom: playerMouseArea.bottom // Window color with 128/255 alpha @@ -182,96 +184,13 @@ ColumnLayout { 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 + VolumeControl { + id: volumeSlider + orientation: Qt.Vertical 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() - } - } - } - } + } } } diff --git a/resources/qml/ui/media/VolumeControl.qml b/resources/qml/ui/media/VolumeControl.qml new file mode 100644 index 00000000..b826dfc6 --- /dev/null +++ b/resources/qml/ui/media/VolumeControl.qml @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtMultimedia 5.15 +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import im.nheko 1.0 + +// Volume slider activator +Image { + property alias desiredVolume: volumeSlider.desiredVolume + property alias orientation: volumeSlider.orientation + property alias controlsVisible: volumeSliderRect.visible + property bool muted: false + property color controlColor: (volumeImageArea.containsMouse) ? + Nheko.colors.highlight : Nheko.colors.text + + // TODO: add icons for different volume levels + id: volumeImage + source: (desiredVolume > 0 && !muted) ? + "image://colorimage/:/icons/icons/ui/volume-up.png?"+ controlColor : + "image://colorimage/:/icons/icons/ui/volume-off-indicator.png?"+ controlColor + + fillMode: Image.PreserveAspectFit + + MouseArea { + id: volumeImageArea + anchors.fill: parent + hoverEnabled: true + onExited: volumeSliderHideTimer.start() + onPositionChanged: volumeSliderHideTimer.start() + onClicked: volumeImage.muted = !volumeImage.muted + // 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: volumeSliderRect.height * 0.1 + anchors.topMargin: volumeSliderRect.height * 0.1 + anchors.horizontalCenter: volumeSliderRect.horizontalCenter + orientation: Qt.Vertical + onDesiredVolumeChanged: { + volumeImage.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() + } + } + } +} \ No newline at end of file diff --git a/resources/qml/ui/media/qmldir b/resources/qml/ui/media/qmldir new file mode 100644 index 00000000..14df35df --- /dev/null +++ b/resources/qml/ui/media/qmldir @@ -0,0 +1,2 @@ +module im.nheko.UI.Media +VolumeSlider 1.0 VolumeSlider.qml \ No newline at end of file diff --git a/resources/res.qrc b/resources/res.qrc index ccb5a637..1e6a22fc 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -183,6 +183,7 @@ qml/ui/Ripple.qml qml/ui/Spinner.qml qml/ui/animations/BlinkAnimation.qml + qml/ui/media/VolumeControl.qml qml/voip/ActiveCallBar.qml qml/voip/CallDevices.qml qml/voip/CallInvite.qml From c1c9c71b08915a27538e34e422255d53b7bf1fdf Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Tue, 9 Nov 2021 22:17:00 -0500 Subject: [PATCH 07/13] Move rest of controls to separate file --- .../qml/delegates/PlayableMediaMessage.qml | 318 +++++++----------- resources/qml/ui/media/MediaControls.qml | 141 ++++++++ resources/qml/ui/media/VolumeControl.qml | 67 ++-- resources/qml/ui/media/qmldir | 3 +- resources/res.qrc | 1 + 5 files changed, 302 insertions(+), 228 deletions(-) create mode 100644 resources/qml/ui/media/MediaControls.qml diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index ceeeeb1a..3af3a993 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -11,6 +11,8 @@ import QtQuick.Layouts 1.2 import im.nheko 1.0 ColumnLayout { + id: content + required property double proportionalHeight required property int type required property int originalWidth @@ -22,52 +24,51 @@ ColumnLayout { function durationToString(duration) { function maybeZeroPrepend(time) { - return (time < 10) ? "0" + time.toString() : - time.toString() + 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 + 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.fillWidth: true + 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: volumeSlider.desiredVolume * 100 - muted: volumeSlider.muted + // 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 } 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 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 - color: Nheko.colors.window + visible: type == MtxEvent.VideoMessage + color: Nheko.colors.window Layout.preferredHeight: tooHigh ? timelineRoot.height / divisor : tempHeight Layout.preferredWidth: tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth Layout.maximumWidth: Layout.preferredWidth @@ -77,217 +78,144 @@ ColumnLayout { source: thumbnailUrl.replace("mxc://", "image://MxcImage/") asynchronous: true fillMode: Image.PreserveAspectFit + // Button and window colored overlay to cache media Item { // Display over video controls z: videoOutput.z + 1 visible: !mxcmedia.loaded anchors.fill: parent + //color: Nheko.colors.window //opacity: 0.5 Image { - property color buttonColor: (cacheVideoArea.containsMouse) ? Nheko.colors.highlight : - Nheko.colors.text + 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 + source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + buttonColor } + MouseArea { id: cacheVideoArea + anchors.fill: parent hoverEnabled: true enabled: !mxcmedia.loaded onClicked: mxcmedia.eventId = eventId } + } + 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 + MediaControls { + id: mediaControls + + anchors.fill: parent + x: videoOutput.contentRect.x + y: videoOutput.contentRect.y + width: videoOutput.contentRect.width + height: videoOutput.contentRect.height + positionValue: mxcmedia.position + duration: mxcmedia.duration + mediaLoaded: mxcmedia.loaded + mediaState: mxcmedia.state + volumeOrientation: Qt.Vertical + onPositionChanged: mxcmedia.position = position + onActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play() + } + + } + + } + + } + // Audio player + + // TODO: share code with the video player + Rectangle { + id: audioControlRect + + property int controlHeight: 25 + + visible: type != MtxEvent.VideoMessage + Layout.preferredHeight: 40 + + RowLayout { + anchors.fill: parent + width: parent.width + + // Play/pause button + Image { + id: audioPlaybackStateImage + + property color controlColor: (audioPlaybackStateArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text + + fillMode: Image.PreserveAspectFit + Layout.preferredHeight: controlRect.controlHeight + Layout.alignment: Qt.AlignVCenter + 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: playerMouseArea - // Toggle play state on clicks + id: audioPlaybackStateArea + + anchors.fill: parent + hoverEnabled: true onClicked: { - if (controlRect.shouldShowControls && - !controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) { - (mxcmedia.state == MediaPlayer.PlayingState) ? - mxcmedia.pause() : - mxcmedia.play() + if (!mxcmedia.loaded) { + mxcmedia.eventId = eventId; + return ; } + (mxcmedia.state == MediaPlayer.PlayingState) ? mxcmedia.pause() : mxcmedia.play(); } - Rectangle { - id: controlRect - property int controlHeight: 25 - property bool shouldShowControls: playerMouseArea.shouldShowControls || - volumeSlider.controlsVisible + } - 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 - } - } + } - 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 + Label { + text: (!mxcmedia.loaded) ? "-/-" : durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration) + } - 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 + Slider { + Layout.fillWidth: true + Layout.minimumWidth: 50 + height: controlRect.controlHeight + value: mxcmedia.position + onMoved: mxcmedia.position = value + from: 0 + to: mxcmedia.duration + } - 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 - } + } - VolumeControl { - id: volumeSlider - orientation: Qt.Vertical - Layout.rightMargin: 5 - Layout.preferredHeight: controlRect.controlHeight - } + Label { + id: fileInfoLabel - } - } - // 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)) + Layout.fillWidth: true + text: body + " [" + filesize + "]" + textFormat: Text.PlainText + elide: Text.ElideRight + color: Nheko.colors.text - // For hiding controls on stationary cursor - Timer { - id: controlHideTimer - interval: 1500 //ms - repeat: false - } + background: Rectangle { + color: Nheko.colors.base + } - 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 - } - } +} diff --git a/resources/qml/ui/media/MediaControls.qml b/resources/qml/ui/media/MediaControls.qml new file mode 100644 index 00000000..de3e98a7 --- /dev/null +++ b/resources/qml/ui/media/MediaControls.qml @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtMultimedia 5.15 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.2 +import im.nheko 1.0 + +Item { + id: control + + property alias desiredVolume: volumeSlider.desiredVolume + property alias muted: volumeSlider.muted + property alias volumeOrientation: volumeSlider.orientation + property var mediaState + property bool mediaLoaded: false + property var duration + property var positionValue: 0 + property var position + property int controlHeight: 25 + property bool shouldShowControls: playerMouseArea.shouldShowControls || volumeSlider.controlsVisible + + signal activated(real mouseX, real mouseY) + + 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; + } + + MouseArea { + id: playerMouseArea + + property bool shouldShowControls: (containsMouse && controlHideTimer.running) || (control.mediaState != MediaPlayer.PlayingState) || controlRect.contains(mapToItem(controlRect, mouseX, mouseY)) + + onClicked: control.activated(mouseX, mouseY) + hoverEnabled: true + onPositionChanged: controlHideTimer.start() + onExited: controlHideTimer.start() + onEntered: controlHideTimer.start() + anchors.fill: control + propagateComposedEvents: true + } + + Rectangle { + id: controlRect + + // Window color with 128/255 alpha + color: { + var wc = Nheko.colors.alternateBase; + return Qt.rgba(wc.r, wc.g, wc.b, 0.5); + } + anchors.bottom: control.bottom + anchors.left: control.left + anchors.right: control.right + height: 40 + opacity: control.shouldShowControls ? 1 : 0 + + RowLayout { + anchors.fill: parent + width: parent.width + + // Play/pause button + Image { + id: playbackStateImage + + property color controlColor: (playbackStateArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text + + fillMode: Image.PreserveAspectFit + Layout.preferredHeight: control.controlHeight + Layout.alignment: Qt.AlignVCenter + source: (control.mediaState == 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: control.activated(mouseX, mouseY) + } + + } + + Label { + text: (!control.mediaLoaded) ? "-/-" : (durationToString(control.positionValue) + "/" + durationToString(control.duration)) + color: Nheko.colors.text + } + + Slider { + Layout.fillWidth: true + Layout.minimumWidth: 50 + height: control.controlHeight + value: control.positionValue + onMoved: control.position = value + from: 0 + to: control.duration + } + + VolumeControl { + id: volumeSlider + + Layout.rightMargin: 5 + Layout.preferredHeight: control.controlHeight + } + + } + + // Fade controls in/out + Behavior on opacity { + OpacityAnimator { + duration: 100 + } + + } + + } + + // For hiding controls on stationary cursor + Timer { + id: controlHideTimer + + interval: 1500 //ms + repeat: false + } + +} diff --git a/resources/qml/ui/media/VolumeControl.qml b/resources/qml/ui/media/VolumeControl.qml index b826dfc6..cd844ed5 100644 --- a/resources/qml/ui/media/VolumeControl.qml +++ b/resources/qml/ui/media/VolumeControl.qml @@ -5,101 +5,104 @@ import QtMultimedia 5.15 import QtQuick 2.15 import QtQuick.Controls 2.15 - import im.nheko 1.0 // Volume slider activator Image { + // TODO: add icons for different volume levels + id: volumeImage + property alias desiredVolume: volumeSlider.desiredVolume property alias orientation: volumeSlider.orientation property alias controlsVisible: volumeSliderRect.visible property bool muted: false - property color controlColor: (volumeImageArea.containsMouse) ? - Nheko.colors.highlight : Nheko.colors.text - - // TODO: add icons for different volume levels - id: volumeImage - source: (desiredVolume > 0 && !muted) ? - "image://colorimage/:/icons/icons/ui/volume-up.png?"+ controlColor : - "image://colorimage/:/icons/icons/ui/volume-off-indicator.png?"+ controlColor + property color controlColor: (volumeImageArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text + source: (desiredVolume > 0 && !muted) ? "image://colorimage/:/icons/icons/ui/volume-up.png?" + controlColor : "image://colorimage/:/icons/icons/ui/volume-off-indicator.png?" + controlColor fillMode: Image.PreserveAspectFit MouseArea { - id: volumeImageArea + id: volumeImageArea + anchors.fill: parent hoverEnabled: true onExited: volumeSliderHideTimer.start() onPositionChanged: volumeSliderHideTimer.start() onClicked: volumeImage.muted = !volumeImage.muted + // 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) + 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 + 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) + property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, QtMultimedia.LogarithmicVolumeScale, QtMultimedia.LinearVolumeScale) - anchors.fill: parent + value: 1 + anchors.fill: volumeSliderRect anchors.bottomMargin: volumeSliderRect.height * 0.1 anchors.topMargin: volumeSliderRect.height * 0.1 anchors.horizontalCenter: volumeSliderRect.horizontalCenter orientation: Qt.Vertical onDesiredVolumeChanged: { - volumeImage.muted = !(desiredVolume > 0.0) + volumeImage.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() + mouse.accepted = false; + volumeSliderHideTimer.start(); } } + + Behavior on opacity { + OpacityAnimator { + duration: 100 + } + + } + // TODO: figure out a better way to put the slider popup above controlRect + } -} \ No newline at end of file + +} diff --git a/resources/qml/ui/media/qmldir b/resources/qml/ui/media/qmldir index 14df35df..143b603d 100644 --- a/resources/qml/ui/media/qmldir +++ b/resources/qml/ui/media/qmldir @@ -1,2 +1,3 @@ module im.nheko.UI.Media -VolumeSlider 1.0 VolumeSlider.qml \ No newline at end of file +VolumeSlider 1.0 VolumeSlider.qml +MediaControls 1.0 MediaControls.qml \ No newline at end of file diff --git a/resources/res.qrc b/resources/res.qrc index 1e6a22fc..538095ab 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -183,6 +183,7 @@ qml/ui/Ripple.qml qml/ui/Spinner.qml qml/ui/animations/BlinkAnimation.qml + qml/ui/media/MediaControls.qml qml/ui/media/VolumeControl.qml qml/voip/ActiveCallBar.qml qml/voip/CallDevices.qml From df17e4e28b92dcef5154043a0eadaa5c39aeba92 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Tue, 9 Nov 2021 22:33:16 -0500 Subject: [PATCH 08/13] Fix audio player and use same controls for video and audio --- .../qml/delegates/PlayableMediaMessage.qml | 129 +++++++++--------- 1 file changed, 63 insertions(+), 66 deletions(-) diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 3af3a993..18345245 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -67,7 +67,6 @@ ColumnLayout { property double divisor: isReply ? 4 : 2 property bool tooHigh: tempHeight > timelineRoot.height / divisor - visible: type == MtxEvent.VideoMessage color: Nheko.colors.window Layout.preferredHeight: tooHigh ? timelineRoot.height / divisor : tempHeight Layout.preferredWidth: tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth @@ -109,99 +108,97 @@ ColumnLayout { VideoOutput { id: videoOutput - + visible: type == MtxEvent.VideoMessage clip: true anchors.fill: parent fillMode: VideoOutput.PreserveAspectFit source: mxcmedia flushMode: VideoOutput.FirstFrame + } - MediaControls { - id: mediaControls - - anchors.fill: parent - x: videoOutput.contentRect.x - y: videoOutput.contentRect.y - width: videoOutput.contentRect.width - height: videoOutput.contentRect.height - positionValue: mxcmedia.position - duration: mxcmedia.duration - mediaLoaded: mxcmedia.loaded - mediaState: mxcmedia.state - volumeOrientation: Qt.Vertical - onPositionChanged: mxcmedia.position = position - onActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play() - } + MediaControls { + id: mediaControls + anchors.fill: parent + x: type == MtxEvent.VideoMessage ? videoOutput.contentRect.x : videoContainer.x + y: type == MtxEvent.VideoMessage ? videoOutput.contentRect.y : videoContainer.y + width: type == MtxEvent.VideoMessage ? videoOutput.contentRect.width : videoContainer.width + height: type == MtxEvent.VideoMessage ? videoOutput.contentRect.height : videoContainer.height + positionValue: mxcmedia.position + duration: mxcmedia.duration + mediaLoaded: mxcmedia.loaded + mediaState: mxcmedia.state + volumeOrientation: Qt.Vertical + onPositionChanged: mxcmedia.position = position + onActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play() } } } // Audio player - // TODO: share code with the video player - Rectangle { - id: audioControlRect + // Rectangle { + // id: audioControlRect - property int controlHeight: 25 + // property int controlHeight: 25 - visible: type != MtxEvent.VideoMessage - Layout.preferredHeight: 40 + // visible: type != MtxEvent.VideoMessage + // Layout.preferredHeight: 40 - RowLayout { - anchors.fill: parent - width: parent.width + // RowLayout { + // anchors.fill: parent + // width: parent.width - // Play/pause button - Image { - id: audioPlaybackStateImage + // // Play/pause button + // Image { + // id: audioPlaybackStateImage - property color controlColor: (audioPlaybackStateArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text + // property color controlColor: (audioPlaybackStateArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text - fillMode: Image.PreserveAspectFit - Layout.preferredHeight: controlRect.controlHeight - Layout.alignment: Qt.AlignVCenter - source: { - if (!mxcmedia.loaded) - return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + controlColor; + // fillMode: Image.PreserveAspectFit + // Layout.preferredHeight: controlRect.controlHeight + // Layout.alignment: Qt.AlignVCenter + // 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; - } + // 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 + // MouseArea { + // id: audioPlaybackStateArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - if (!mxcmedia.loaded) { - mxcmedia.eventId = eventId; - return ; - } - (mxcmedia.state == MediaPlayer.PlayingState) ? mxcmedia.pause() : mxcmedia.play(); - } - } + // 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) - } + // 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 - } + // Slider { + // Layout.fillWidth: true + // Layout.minimumWidth: 50 + // height: controlRect.controlHeight + // value: mxcmedia.position + // onMoved: mxcmedia.position = value + // from: 0 + // to: mxcmedia.duration + // } - } + // } - } + // } Label { id: fileInfoLabel From e3eb87cc214337dbf3e629706857a78c6bfda284 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Tue, 9 Nov 2021 23:52:59 -0500 Subject: [PATCH 09/13] More player fixes --- .../qml/delegates/PlayableMediaMessage.qml | 124 +----------------- resources/qml/ui/media/MediaControls.qml | 24 +++- 2 files changed, 25 insertions(+), 123 deletions(-) diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 18345245..e6bcdcac 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -22,25 +22,6 @@ ColumnLayout { required property string body 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(); - if (hours < 1) - return mm + ":" + ss; - - return hh + ":" + mm + ":" + ss; - } - Layout.fillWidth: true MxcMedia { @@ -58,19 +39,14 @@ ColumnLayout { Rectangle { id: videoContainer - //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 - color: Nheko.colors.window - Layout.preferredHeight: tooHigh ? timelineRoot.height / divisor : tempHeight + color: type == MtxEvent.VideoMessage ? Nheko.colors.window : "transparent" + Layout.preferredHeight: type == MtxEvent.VideoMessage ? tooHigh ? timelineRoot.height / divisor : tempHeight : 40 Layout.preferredWidth: tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth - Layout.maximumWidth: Layout.preferredWidth Image { anchors.fill: parent @@ -78,36 +54,9 @@ ColumnLayout { asynchronous: true fillMode: Image.PreserveAspectFit - // Button and window colored overlay to cache media - Item { - // Display over video controls - z: videoOutput.z + 1 - visible: !mxcmedia.loaded - anchors.fill: parent - - //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 - } - - MouseArea { - id: cacheVideoArea - - anchors.fill: parent - hoverEnabled: true - enabled: !mxcmedia.loaded - onClicked: mxcmedia.eventId = eventId - } - - } - VideoOutput { id: videoOutput + visible: type == MtxEvent.VideoMessage clip: true anchors.fill: parent @@ -130,76 +79,15 @@ ColumnLayout { mediaState: mxcmedia.state volumeOrientation: Qt.Vertical onPositionChanged: mxcmedia.position = position - onActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play() + onPlayPauseActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play() + onLoadActivated: mxcmedia.eventId = eventId } } } - // Audio player - // TODO: share code with the video player - // Rectangle { - // id: audioControlRect - - // property int controlHeight: 25 - - // visible: type != MtxEvent.VideoMessage - // Layout.preferredHeight: 40 - - // RowLayout { - // anchors.fill: parent - // width: parent.width - - // // Play/pause button - // Image { - // id: audioPlaybackStateImage - - // property color controlColor: (audioPlaybackStateArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text - - // fillMode: Image.PreserveAspectFit - // Layout.preferredHeight: controlRect.controlHeight - // Layout.alignment: Qt.AlignVCenter - // 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.position = value - // from: 0 - // to: mxcmedia.duration - // } - - // } - - // } + // information about file name and file size Label { id: fileInfoLabel diff --git a/resources/qml/ui/media/MediaControls.qml b/resources/qml/ui/media/MediaControls.qml index de3e98a7..ec522391 100644 --- a/resources/qml/ui/media/MediaControls.qml +++ b/resources/qml/ui/media/MediaControls.qml @@ -22,7 +22,8 @@ Item { property int controlHeight: 25 property bool shouldShowControls: playerMouseArea.shouldShowControls || volumeSlider.controlsVisible - signal activated(real mouseX, real mouseY) + signal playPauseActivated(real mouseX, real mouseY) + signal loadActivated(real mouseX, real mouseY) function durationToString(duration) { function maybeZeroPrepend(time) { @@ -48,7 +49,9 @@ Item { property bool shouldShowControls: (containsMouse && controlHideTimer.running) || (control.mediaState != MediaPlayer.PlayingState) || controlRect.contains(mapToItem(controlRect, mouseX, mouseY)) - onClicked: control.activated(mouseX, mouseY) + onClicked: { + control.mediaLoaded ? control.playPauseActivated(mouseX, mouseY) : control.loadActivated(mouseX, mouseY); + } hoverEnabled: true onPositionChanged: controlHideTimer.start() onExited: controlHideTimer.start() @@ -75,7 +78,7 @@ Item { anchors.fill: parent width: parent.width - // Play/pause button + // Cache/Play/pause button Image { id: playbackStateImage @@ -84,14 +87,25 @@ Item { fillMode: Image.PreserveAspectFit Layout.preferredHeight: control.controlHeight Layout.alignment: Qt.AlignVCenter - source: (control.mediaState == MediaPlayer.PlayingState) ? "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + controlColor : "image://colorimage/:/icons/icons/ui/play-sign.png?" + controlColor + source: { + if (control.mediaLoaded) { + if (control.mediaState == MediaPlayer.PlayingState) + return "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + controlColor; + else + return "image://colorimage/:/icons/icons/ui/play-sign.png?" + controlColor; + } else { + return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + controlColor; + } + } MouseArea { id: playbackStateArea anchors.fill: parent hoverEnabled: true - onClicked: control.activated(mouseX, mouseY) + onClicked: { + control.mediaLoaded ? control.playPauseActivated(mouseX, mouseY) : control.loadActivated(mouseX, mouseY); + } } } From c5e8b2da15a65622d1783c19afeeb0ad4aebc4b8 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Thu, 11 Nov 2021 00:16:25 -0500 Subject: [PATCH 10/13] More refactoring and layout updates --- .../qml/delegates/PlayableMediaMessage.qml | 8 +- resources/qml/ui/NhekoSlider.qml | 77 ++++++++++ resources/qml/ui/media/MediaControls.qml | 144 +++++++++++------- resources/qml/ui/media/VolumeControl.qml | 33 ++-- resources/qml/ui/qmldir | 1 + resources/res.qrc | 1 + 6 files changed, 190 insertions(+), 74 deletions(-) create mode 100644 resources/qml/ui/NhekoSlider.qml diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index e6bcdcac..2b4c4d49 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -7,7 +7,7 @@ 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 ColumnLayout { @@ -45,8 +45,8 @@ ColumnLayout { property bool tooHigh: tempHeight > timelineRoot.height / divisor color: type == MtxEvent.VideoMessage ? Nheko.colors.window : "transparent" - Layout.preferredHeight: type == MtxEvent.VideoMessage ? tooHigh ? timelineRoot.height / divisor : tempHeight : 40 - Layout.preferredWidth: tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth + Layout.preferredHeight: type == MtxEvent.VideoMessage ? tooHigh ? timelineRoot.height / divisor : tempHeight : 80 + Layout.preferredWidth: type == MtxEvent.VideoMessage ? tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth : 250 Image { anchors.fill: parent @@ -73,11 +73,11 @@ ColumnLayout { y: type == MtxEvent.VideoMessage ? videoOutput.contentRect.y : videoContainer.y width: type == MtxEvent.VideoMessage ? videoOutput.contentRect.width : videoContainer.width height: type == MtxEvent.VideoMessage ? videoOutput.contentRect.height : videoContainer.height + playingVideo: type == MtxEvent.VideoMessage positionValue: mxcmedia.position duration: mxcmedia.duration mediaLoaded: mxcmedia.loaded mediaState: mxcmedia.state - volumeOrientation: Qt.Vertical onPositionChanged: mxcmedia.position = position onPlayPauseActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play() onLoadActivated: mxcmedia.eventId = eventId diff --git a/resources/qml/ui/NhekoSlider.qml b/resources/qml/ui/NhekoSlider.qml new file mode 100644 index 00000000..887cb80c --- /dev/null +++ b/resources/qml/ui/NhekoSlider.qml @@ -0,0 +1,77 @@ +// 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: slider + + property real sliderWidth + property real sliderHeight + property bool alwaysShowSlider: true + + anchors.bottomMargin: orientation == Qt.Vertical ? Nheko.paddingMedium : undefined + anchors.topMargin: orientation == Qt.Vertical ? Nheko.paddingMedium : undefined + anchors.leftMargin: orientation == Qt.Vertical ? undefined : Nheko.paddingMedium + anchors.rightMargin: orientation == Qt.Vertical ? undefined : Nheko.paddingMedium + + background: Rectangle { + x: slider.leftPadding + (slider.orientation == Qt.Vertical ? slider.availableWidth / 2 - width / 2 : 0) + y: slider.topPadding + (slider.orientation == Qt.Vertical ? 0 : slider.availableHeight / 2 - height / 2) + // implicitWidth: slider.orientation == Qt.Vertical ? 8 : 100 + // implicitHeight: slider.orientation == Qt.Vertical ? 100 : 8 + width: slider.orientation == Qt.Vertical ? sliderWidth : slider.availableWidth + height: slider.orientation == Qt.Vertical ? slider.availableHeight : sliderHeight + radius: 2 + color: { + if (slider.orientation == Qt.Vertical) { + return Nheko.colors.highlight; + } else { + var col = Nheko.colors.buttonText; + return Qt.rgba(col.r, col.g, col.b, 0.5); + } + } + border.color: { + var col = Nheko.colors.base; + return Qt.rgba(col.r, col.g, col.b, 0.5); + } + + Rectangle { + width: slider.orientation == Qt.Vertical ? parent.width : slider.visualPosition * parent.width + height: slider.orientation == Qt.Vertical ? slider.visualPosition * parent.height : parent.height + color: { + if (slider.orientation == Qt.Vertical) { + return Nheko.colors.buttonText; + } else { + return Nheko.colors.highlight; + } + } + radius: 2 + } + + } + + handle: Rectangle { + x: { + if (slider.orientation == Qt.Vertical) + return slider.leftPadding + slider.availableWidth / 2 - width / 2; + else + return slider.leftPadding + slider.visualPosition * (slider.availableWidth - width); + } + y: { + if (slider.orientation == Qt.Vertical) + return slider.topPadding + slider.visualPosition * (slider.availableHeight - height); + else + return slider.topPadding + slider.availableHeight / 2 - height / 2; + } + implicitWidth: 16 + implicitHeight: 16 + radius: slider.width / 2 + color: Nheko.colors.highlight + visible: alwaysShowSlider || slider.hovered || slider.pressed || Settings.mobileMode + } + +} diff --git a/resources/qml/ui/media/MediaControls.qml b/resources/qml/ui/media/MediaControls.qml index ec522391..b529462d 100644 --- a/resources/qml/ui/media/MediaControls.qml +++ b/resources/qml/ui/media/MediaControls.qml @@ -2,10 +2,11 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import "../" 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 Item { @@ -13,14 +14,14 @@ Item { property alias desiredVolume: volumeSlider.desiredVolume property alias muted: volumeSlider.muted - property alias volumeOrientation: volumeSlider.orientation + property bool playingVideo: false property var mediaState property bool mediaLoaded: false property var duration property var positionValue: 0 property var position property int controlHeight: 25 - property bool shouldShowControls: playerMouseArea.shouldShowControls || volumeSlider.controlsVisible + property bool shouldShowControls: !playingVideo || playerMouseArea.shouldShowControls || volumeSlider.controlsVisible signal playPauseActivated(real mouseX, real mouseY) signal loadActivated(real mouseX, real mouseY) @@ -47,7 +48,7 @@ Item { MouseArea { id: playerMouseArea - property bool shouldShowControls: (containsMouse && controlHideTimer.running) || (control.mediaState != MediaPlayer.PlayingState) || controlRect.contains(mapToItem(controlRect, mouseX, mouseY)) + property bool shouldShowControls: (containsMouse && controlHideTimer.running) || (control.mediaState != MediaPlayer.PlayingState) || controlLayout.contains(mapToItem(controlLayout, mouseX, mouseY)) onClicked: { control.mediaLoaded ? control.playPauseActivated(mouseX, mouseY) : control.loadActivated(mouseX, mouseY); @@ -60,76 +61,103 @@ Item { propagateComposedEvents: true } - Rectangle { - id: controlRect + ColumnLayout { - // Window color with 128/255 alpha - color: { - var wc = Nheko.colors.alternateBase; - return Qt.rgba(wc.r, wc.g, wc.b, 0.5); - } + id: controlLayout + opacity: control.shouldShowControls ? 1 : 0 + + // spacing: Nheko.paddingSmall anchors.bottom: control.bottom anchors.left: control.left anchors.right: control.right - height: 40 - opacity: control.shouldShowControls ? 1 : 0 - RowLayout { - anchors.fill: parent - width: parent.width + NhekoSlider { + Layout.fillWidth: true + Layout.minimumWidth: 50 + Layout.leftMargin: Nheko.paddingMedium + Layout.rightMargin: Nheko.paddingMedium + height: control.controlHeight + value: control.positionValue + onMoved: control.position = value + from: 0 + to: control.duration + sliderHeight: 8 + alwaysShowSlider: false + } - // Cache/Play/pause button - Image { - id: playbackStateImage + Rectangle { + id: controlRect - property color controlColor: (playbackStateArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text + // Window color with 128/255 alpha + color: { + var wc = Nheko.colors.alternateBase; + return Qt.rgba(wc.r, wc.g, wc.b, 0.5); + } - fillMode: Image.PreserveAspectFit - Layout.preferredHeight: control.controlHeight - Layout.alignment: Qt.AlignVCenter - source: { - if (control.mediaLoaded) { - if (control.mediaState == MediaPlayer.PlayingState) - return "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + controlColor; - else - return "image://colorimage/:/icons/icons/ui/play-sign.png?" + controlColor; - } else { - return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + controlColor; + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + + height: 35 + Layout.fillWidth: true + + RowLayout { + anchors.left: controlRect.left + anchors.bottom: controlRect.bottom + anchors.right: controlRect.right + anchors.margins: Nheko.paddingSmall + anchors.verticalCenter: controlRect.verticalCenter + spacing: Nheko.paddingSmall + + // Cache/Play/pause button + Image { + Layout.alignment: Qt.AlignLeft + id: playbackStateImage + + property color controlColor: (playbackStateArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text + + fillMode: Image.PreserveAspectFit + Layout.preferredHeight: control.controlHeight + source: { + if (control.mediaLoaded) { + if (control.mediaState == MediaPlayer.PlayingState) + return "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + controlColor; + else + return "image://colorimage/:/icons/icons/ui/play-sign.png?" + controlColor; + } else { + return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + controlColor; + } } + + MouseArea { + id: playbackStateArea + + anchors.fill: parent + hoverEnabled: true + onClicked: { + control.mediaLoaded ? control.playPauseActivated(mouseX, mouseY) : control.loadActivated(mouseX, mouseY); + } + } + } - MouseArea { - id: playbackStateArea - - anchors.fill: parent - hoverEnabled: true - onClicked: { - control.mediaLoaded ? control.playPauseActivated(mouseX, mouseY) : control.loadActivated(mouseX, mouseY); - } + VolumeControl { + Layout.alignment: Qt.AlignLeft + id: volumeSlider + orientation: Qt.Horizontal + Layout.rightMargin: 5 + Layout.preferredHeight: control.controlHeight } - } + Label { + Layout.alignment: Qt.AlignRight - Label { - text: (!control.mediaLoaded) ? "-/-" : (durationToString(control.positionValue) + "/" + durationToString(control.duration)) - color: Nheko.colors.text - } + text: (!control.mediaLoaded) ? "-/-" : (durationToString(control.positionValue) + "/" + durationToString(control.duration)) + color: Nheko.colors.text + } - Slider { - Layout.fillWidth: true - Layout.minimumWidth: 50 - height: control.controlHeight - value: control.positionValue - onMoved: control.position = value - from: 0 - to: control.duration - } + Item { + Layout.fillWidth: true + } - VolumeControl { - id: volumeSlider - - Layout.rightMargin: 5 - Layout.preferredHeight: control.controlHeight } } diff --git a/resources/qml/ui/media/VolumeControl.qml b/resources/qml/ui/media/VolumeControl.qml index cd844ed5..e87550ac 100644 --- a/resources/qml/ui/media/VolumeControl.qml +++ b/resources/qml/ui/media/VolumeControl.qml @@ -2,9 +2,12 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import "../" + import QtMultimedia 5.15 import QtQuick 2.15 import QtQuick.Controls 2.15 + import im.nheko 1.0 // Volume slider activator @@ -17,6 +20,7 @@ Image { property alias controlsVisible: volumeSliderRect.visible property bool muted: false property color controlColor: (volumeImageArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text + width: sourceSize.width + volumeSliderRect.implicitWidth source: (desiredVolume > 0 && !muted) ? "image://colorimage/:/icons/icons/ui/volume-up.png?" + controlColor : "image://colorimage/:/icons/icons/ui/volume-off-indicator.png?" + controlColor fillMode: Image.PreserveAspectFit @@ -45,32 +49,38 @@ Image { id: volumeSliderRect opacity: (visible) ? 1 : 0 - anchors.bottom: volumeImage.top - anchors.bottomMargin: 10 - anchors.horizontalCenter: volumeImage.horizontalCenter + anchors.bottom: volumeSlider.orientation == Qt.Vertical ? volumeImage.top : undefined + anchors.left: volumeSlider.orientation == Qt.Vertical ? undefined : volumeImage.right + anchors.horizontalCenter: volumeSlider.orientation == Qt.Vertical ? volumeImage.horizontalCenter : undefined + anchors.verticalCenter: volumeSlider.orientation == Qt.Vertical ? undefined : volumeImage.verticalCenter color: { - var wc = Nheko.colors.window; - return Qt.rgba(wc.r, wc.g, wc.b, 0.5); + if (volumeSlider.orientation == Qt.Vertical) { + var wc = Nheko.colors.window; + return Qt.rgba(wc.r, wc.g, wc.b, 0.5); + } else { + return "transparent"; + } } /* 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 + width: volumeSlider.orientation == Qt.Vertical ? volumeImage.width * 0.7 : 100 radius: volumeSlider.width / 2 - height: controlRect.height * 2 //100 + height: volumeSlider.orientation == Qt.Vertical ? 100 : volumeImage.height * 0.7 visible: volumeImageArea.containsMouse || volumeSliderHideTimer.running || volumeSliderRectMouseArea.containsMouse - Slider { + NhekoSlider { // TODO: the slider is slightly off-center on the left for some reason... id: volumeSlider + sliderWidth: 8 + sliderHeight: 8 // Desired value to avoid loop onMoved -> media.volume -> value -> onMoved... property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, QtMultimedia.LogarithmicVolumeScale, QtMultimedia.LinearVolumeScale) value: 1 anchors.fill: volumeSliderRect - anchors.bottomMargin: volumeSliderRect.height * 0.1 - anchors.topMargin: volumeSliderRect.height * 0.1 - anchors.horizontalCenter: volumeSliderRect.horizontalCenter + anchors.horizontalCenter: orientation == Qt.Vertical ? volumeSliderRect.horizontalCenter : undefined + anchors.verticalCenter: orientation == Qt.Vertical ? undefined : volumeSliderRect.verticalCenter orientation: Qt.Vertical onDesiredVolumeChanged: { volumeImage.muted = !(desiredVolume > 0); @@ -101,7 +111,6 @@ Image { } } - // TODO: figure out a better way to put the slider popup above controlRect } 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 538095ab..4e243251 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -180,6 +180,7 @@ 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 From ffc60180ded0c84387e3a3dbd81ab99ac8079edc Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 11 Nov 2021 19:18:45 +0100 Subject: [PATCH 11/13] Cleanup positioning of player elements --- .../qml/delegates/PlayableMediaMessage.qml | 42 ++-- resources/qml/ui/NhekoSlider.qml | 79 +++---- resources/qml/ui/media/MediaControls.qml | 194 ++++++++++++------ resources/qml/ui/media/VolumeControl.qml | 117 ----------- resources/res.qrc | 1 - 5 files changed, 173 insertions(+), 260 deletions(-) delete mode 100644 resources/qml/ui/media/VolumeControl.qml diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 2b4c4d49..67214dd7 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -10,7 +10,7 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import im.nheko 1.0 -ColumnLayout { +Item { id: content required property double proportionalHeight @@ -22,7 +22,13 @@ ColumnLayout { required property string body required property string filesize - Layout.fillWidth: true + 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 @@ -38,15 +44,10 @@ ColumnLayout { 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 > timelineRoot.height / divisor - color: type == MtxEvent.VideoMessage ? Nheko.colors.window : "transparent" - Layout.preferredHeight: type == MtxEvent.VideoMessage ? tooHigh ? timelineRoot.height / divisor : tempHeight : 80 - Layout.preferredWidth: type == MtxEvent.VideoMessage ? tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth : 250 + width: parent.width + height: parent.height - fileInfoLabel.height + Image { anchors.fill: parent @@ -65,14 +66,18 @@ ColumnLayout { flushMode: VideoOutput.FirstFrame } + + } + + } + MediaControls { id: mediaControls - anchors.fill: parent - x: type == MtxEvent.VideoMessage ? videoOutput.contentRect.x : videoContainer.x - y: type == MtxEvent.VideoMessage ? videoOutput.contentRect.y : videoContainer.y - width: type == MtxEvent.VideoMessage ? videoOutput.contentRect.width : videoContainer.width - height: type == MtxEvent.VideoMessage ? videoOutput.contentRect.height : videoContainer.height + anchors.left: content.left + anchors.right: content.right + anchors.bottom: fileInfoLabel.top + playingVideo: type == MtxEvent.VideoMessage positionValue: mxcmedia.position duration: mxcmedia.duration @@ -83,15 +88,12 @@ ColumnLayout { onLoadActivated: mxcmedia.eventId = eventId } - } - - } - // information about file name and file size Label { id: fileInfoLabel - Layout.fillWidth: true + anchors.bottom: content.bottom + text: body + " [" + filesize + "]" textFormat: Text.PlainText elide: Text.ElideRight diff --git a/resources/qml/ui/NhekoSlider.qml b/resources/qml/ui/NhekoSlider.qml index 887cb80c..6cf1fd2d 100644 --- a/resources/qml/ui/NhekoSlider.qml +++ b/resources/qml/ui/NhekoSlider.qml @@ -7,71 +7,42 @@ import QtQuick.Controls 2.15 import im.nheko 1.0 Slider { - id: slider + id: control + value: 0 - property real sliderWidth - property real sliderHeight + property color progressColor: Nheko.colors.highlight property bool alwaysShowSlider: true + property int sliderRadius: 16 + implicitHeight: sliderRadius - anchors.bottomMargin: orientation == Qt.Vertical ? Nheko.paddingMedium : undefined - anchors.topMargin: orientation == Qt.Vertical ? Nheko.paddingMedium : undefined - anchors.leftMargin: orientation == Qt.Vertical ? undefined : Nheko.paddingMedium - anchors.rightMargin: orientation == Qt.Vertical ? undefined : Nheko.paddingMedium + padding: 0 background: Rectangle { - x: slider.leftPadding + (slider.orientation == Qt.Vertical ? slider.availableWidth / 2 - width / 2 : 0) - y: slider.topPadding + (slider.orientation == Qt.Vertical ? 0 : slider.availableHeight / 2 - height / 2) - // implicitWidth: slider.orientation == Qt.Vertical ? 8 : 100 - // implicitHeight: slider.orientation == Qt.Vertical ? 100 : 8 - width: slider.orientation == Qt.Vertical ? sliderWidth : slider.availableWidth - height: slider.orientation == Qt.Vertical ? slider.availableHeight : sliderHeight - radius: 2 - color: { - if (slider.orientation == Qt.Vertical) { - return Nheko.colors.highlight; - } else { - var col = Nheko.colors.buttonText; - return Qt.rgba(col.r, col.g, col.b, 0.5); - } - } - border.color: { - var col = Nheko.colors.base; - return Qt.rgba(col.r, col.g, col.b, 0.5); - } + 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: slider.orientation == Qt.Vertical ? parent.width : slider.visualPosition * parent.width - height: slider.orientation == Qt.Vertical ? slider.visualPosition * parent.height : parent.height - color: { - if (slider.orientation == Qt.Vertical) { - return Nheko.colors.buttonText; - } else { - return Nheko.colors.highlight; - } - } + width: control.visualPosition * parent.width + height: parent.height + color: control.progressColor radius: 2 } - } handle: Rectangle { - x: { - if (slider.orientation == Qt.Vertical) - return slider.leftPadding + slider.availableWidth / 2 - width / 2; - else - return slider.leftPadding + slider.visualPosition * (slider.availableWidth - width); - } - y: { - if (slider.orientation == Qt.Vertical) - return slider.topPadding + slider.visualPosition * (slider.availableHeight - height); - else - return slider.topPadding + slider.availableHeight / 2 - height / 2; - } - implicitWidth: 16 - implicitHeight: 16 - radius: slider.width / 2 - color: Nheko.colors.highlight - visible: alwaysShowSlider || slider.hovered || slider.pressed || Settings.mobileMode + 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 index b529462d..f5321dca 100644 --- a/resources/qml/ui/media/MediaControls.qml +++ b/resources/qml/ui/media/MediaControls.qml @@ -3,28 +3,33 @@ // 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 -Item { +Rectangle { id: control property alias desiredVolume: volumeSlider.desiredVolume - property alias muted: volumeSlider.muted + 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 int controlHeight: 25 property bool shouldShowControls: !playingVideo || playerMouseArea.shouldShowControls || volumeSlider.controlsVisible + color: { + var wc = Nheko.colors.alternateBase; + return Qt.rgba(wc.r, wc.g, wc.b, 0.5); + } + height: controlLayout.implicitHeight - signal playPauseActivated(real mouseX, real mouseY) - signal loadActivated(real mouseX, real mouseY) + signal playPauseActivated() + signal loadActivated() function durationToString(duration) { function maybeZeroPrepend(time) { @@ -51,7 +56,7 @@ Item { property bool shouldShowControls: (containsMouse && controlHideTimer.running) || (control.mediaState != MediaPlayer.PlayingState) || controlLayout.contains(mapToItem(controlLayout, mouseX, mouseY)) onClicked: { - control.mediaLoaded ? control.playPauseActivated(mouseX, mouseY) : control.loadActivated(mouseX, mouseY); + control.mediaLoaded ? control.playPauseActivated() : control.loadActivated(); } hoverEnabled: true onPositionChanged: controlHideTimer.start() @@ -66,102 +71,155 @@ Item { id: controlLayout opacity: control.shouldShowControls ? 1 : 0 - // spacing: Nheko.paddingSmall + spacing: 0 anchors.bottom: control.bottom anchors.left: control.left anchors.right: control.right NhekoSlider { Layout.fillWidth: true - Layout.minimumWidth: 50 - Layout.leftMargin: Nheko.paddingMedium - Layout.rightMargin: Nheko.paddingMedium - height: control.controlHeight + Layout.leftMargin: Nheko.paddingSmall + Layout.rightMargin: Nheko.paddingSmall + + enabled: control.mediaLoaded + value: control.positionValue onMoved: control.position = value from: 0 to: control.duration - sliderHeight: 8 alwaysShowSlider: false } - Rectangle { - id: controlRect - // Window color with 128/255 alpha - color: { - var wc = Nheko.colors.alternateBase; - return Qt.rgba(wc.r, wc.g, wc.b, 0.5); - } - - Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - - height: 35 + RowLayout { + Layout.margins: Nheko.paddingSmall + spacing: Nheko.paddingSmall Layout.fillWidth: true - RowLayout { - anchors.left: controlRect.left - anchors.bottom: controlRect.bottom - anchors.right: controlRect.right - anchors.margins: Nheko.paddingSmall - anchors.verticalCenter: controlRect.verticalCenter - spacing: Nheko.paddingSmall + // Cache/Play/pause button + ImageButton { + Layout.alignment: Qt.AlignLeft + id: playbackStateImage - // Cache/Play/pause button - Image { - Layout.alignment: Qt.AlignLeft - id: playbackStateImage + buttonTextColor: Nheko.colors.text + Layout.preferredHeight: 24 + Layout.preferredWidth: 24 - property color controlColor: (playbackStateArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text - - fillMode: Image.PreserveAspectFit - Layout.preferredHeight: control.controlHeight - source: { - if (control.mediaLoaded) { - if (control.mediaState == MediaPlayer.PlayingState) - return "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + controlColor; - else - return "image://colorimage/:/icons/icons/ui/play-sign.png?" + controlColor; - } else { - return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + controlColor; - } + 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"; } + } - MouseArea { - id: playbackStateArea + onClicked: control.mediaLoaded ? control.playPauseActivated() : control.loadActivated(); - anchors.fill: parent - hoverEnabled: true - onClicked: { - control.mediaLoaded ? control.playPauseActivated(mouseX, mouseY) : control.loadActivated(mouseX, mouseY); - } + } + + ImageButton { + Layout.alignment: Qt.AlignLeft + id: volumeButton + + 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"; } - } - VolumeControl { - Layout.alignment: Qt.AlignLeft - id: volumeSlider - orientation: Qt.Horizontal - Layout.rightMargin: 5 - Layout.preferredHeight: control.controlHeight + onClicked: control.muted = !control.muted + + } + + NhekoSlider { + state: "" + + 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 + Layout.alignment: Qt.AlignLeft + Layout.preferredWidth: 0 + opacity: 0 + id: volumeSlider + orientation: Qt.Horizontal + property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, QtMultimedia.LogarithmicVolumeScale, QtMultimedia.LinearVolumeScale) + value: 1 - text: (!control.mediaLoaded) ? "-/-" : (durationToString(control.positionValue) + "/" + durationToString(control.duration)) - color: Nheko.colors.text + onDesiredVolumeChanged: { + control.muted = !(desiredVolume > 0); } - Item { - Layout.fillWidth: true - } + 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 + } + } + } + + } + ] + } + + Label { + Layout.alignment: Qt.AlignRight + + text: (!control.mediaLoaded) ? "-- / --" : (durationToString(control.positionValue) + " / " + durationToString(control.duration)) + color: Nheko.colors.text + } + + Item { + Layout.fillWidth: true } } + // Fade controls in/out Behavior on opacity { OpacityAnimator { diff --git a/resources/qml/ui/media/VolumeControl.qml b/resources/qml/ui/media/VolumeControl.qml deleted file mode 100644 index e87550ac..00000000 --- a/resources/qml/ui/media/VolumeControl.qml +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -import "../" - -import QtMultimedia 5.15 -import QtQuick 2.15 -import QtQuick.Controls 2.15 - -import im.nheko 1.0 - -// Volume slider activator -Image { - // TODO: add icons for different volume levels - id: volumeImage - - property alias desiredVolume: volumeSlider.desiredVolume - property alias orientation: volumeSlider.orientation - property alias controlsVisible: volumeSliderRect.visible - property bool muted: false - property color controlColor: (volumeImageArea.containsMouse) ? Nheko.colors.highlight : Nheko.colors.text - width: sourceSize.width + volumeSliderRect.implicitWidth - - source: (desiredVolume > 0 && !muted) ? "image://colorimage/:/icons/icons/ui/volume-up.png?" + controlColor : "image://colorimage/:/icons/icons/ui/volume-off-indicator.png?" + controlColor - fillMode: Image.PreserveAspectFit - - MouseArea { - id: volumeImageArea - - anchors.fill: parent - hoverEnabled: true - onExited: volumeSliderHideTimer.start() - onPositionChanged: volumeSliderHideTimer.start() - onClicked: volumeImage.muted = !volumeImage.muted - - // For hiding volume slider after a while - Timer { - id: volumeSliderHideTimer - - interval: 1500 - repeat: false - running: false - } - - } - - Rectangle { - id: volumeSliderRect - - opacity: (visible) ? 1 : 0 - anchors.bottom: volumeSlider.orientation == Qt.Vertical ? volumeImage.top : undefined - anchors.left: volumeSlider.orientation == Qt.Vertical ? undefined : volumeImage.right - anchors.horizontalCenter: volumeSlider.orientation == Qt.Vertical ? volumeImage.horizontalCenter : undefined - anchors.verticalCenter: volumeSlider.orientation == Qt.Vertical ? undefined : volumeImage.verticalCenter - color: { - if (volumeSlider.orientation == Qt.Vertical) { - var wc = Nheko.colors.window; - return Qt.rgba(wc.r, wc.g, wc.b, 0.5); - } else { - return "transparent"; - } - } - /* TODO: base width on the slider width (some issue with it not having a geometry - when using the width here?) */ - width: volumeSlider.orientation == Qt.Vertical ? volumeImage.width * 0.7 : 100 - radius: volumeSlider.width / 2 - height: volumeSlider.orientation == Qt.Vertical ? 100 : volumeImage.height * 0.7 - visible: volumeImageArea.containsMouse || volumeSliderHideTimer.running || volumeSliderRectMouseArea.containsMouse - - NhekoSlider { - // TODO: the slider is slightly off-center on the left for some reason... - id: volumeSlider - - sliderWidth: 8 - sliderHeight: 8 - // Desired value to avoid loop onMoved -> media.volume -> value -> onMoved... - property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, QtMultimedia.LogarithmicVolumeScale, QtMultimedia.LinearVolumeScale) - - value: 1 - anchors.fill: volumeSliderRect - anchors.horizontalCenter: orientation == Qt.Vertical ? volumeSliderRect.horizontalCenter : undefined - anchors.verticalCenter: orientation == Qt.Vertical ? undefined : volumeSliderRect.verticalCenter - orientation: Qt.Vertical - onDesiredVolumeChanged: { - volumeImage.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(); - } - } - - Behavior on opacity { - OpacityAnimator { - duration: 100 - } - - } - - } - -} diff --git a/resources/res.qrc b/resources/res.qrc index 4e243251..a60f4ab0 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -185,7 +185,6 @@ qml/ui/Spinner.qml qml/ui/animations/BlinkAnimation.qml qml/ui/media/MediaControls.qml - qml/ui/media/VolumeControl.qml qml/voip/ActiveCallBar.qml qml/voip/CallDevices.qml qml/voip/CallInvite.qml From 435047b1baeb9a6fa33034f855393c0064f69b2c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 11 Nov 2021 19:56:51 +0100 Subject: [PATCH 12/13] Make it possible to unhide the controls on mobile --- .../qml/delegates/PlayableMediaMessage.qml | 6 ++++ resources/qml/ui/media/MediaControls.qml | 34 ++++++++----------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 67214dd7..99c82a9b 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -48,6 +48,12 @@ Item { width: parent.width height: parent.height - fileInfoLabel.height + + TapHandler { + onTapped: mediaControls.showControls(); + } + + Image { anchors.fill: parent diff --git a/resources/qml/ui/media/MediaControls.qml b/resources/qml/ui/media/MediaControls.qml index f5321dca..00ccdd26 100644 --- a/resources/qml/ui/media/MediaControls.qml +++ b/resources/qml/ui/media/MediaControls.qml @@ -21,16 +21,21 @@ Rectangle { property var duration property var positionValue: 0 property var position - property bool shouldShowControls: !playingVideo || playerMouseArea.shouldShowControls || volumeSlider.controlsVisible + property bool shouldShowControls: !playingVideo || playerMouseArea.shouldShowControls || volumeSlider.state == "shown" 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 signal playPauseActivated() signal loadActivated() + function showControls() { + controlHideTimer.restart(); + } + function durationToString(duration) { function maybeZeroPrepend(time) { return (time < 10) ? "0" + time.toString() : time.toString(); @@ -50,26 +55,18 @@ Rectangle { return hh + ":" + mm + ":" + ss; } - MouseArea { + HoverHandler { id: playerMouseArea - property bool shouldShowControls: (containsMouse && controlHideTimer.running) || (control.mediaState != MediaPlayer.PlayingState) || controlLayout.contains(mapToItem(controlLayout, mouseX, mouseY)) + property bool shouldShowControls: hovered || controlHideTimer.running || control.mediaState != MediaPlayer.PlayingState - onClicked: { - control.mediaLoaded ? control.playPauseActivated() : control.loadActivated(); - } - hoverEnabled: true - onPositionChanged: controlHideTimer.start() - onExited: controlHideTimer.start() - onEntered: controlHideTimer.start() - anchors.fill: control - propagateComposedEvents: true + onHoveredChanged: showControls(); } ColumnLayout { id: controlLayout - opacity: control.shouldShowControls ? 1 : 0 + enabled: control.shouldShowControls spacing: 0 anchors.bottom: control.bottom @@ -219,13 +216,12 @@ Rectangle { } + } - // Fade controls in/out - Behavior on opacity { - OpacityAnimator { - duration: 100 - } - + // Fade controls in/out + Behavior on opacity { + OpacityAnimator { + duration: 100 } } From b7b4fd0e9bd7c6a6ec0312014102c479264b3f0f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 11 Nov 2021 21:32:38 +0100 Subject: [PATCH 13/13] Format qml --- resources/qml/CommunitiesList.qml | 13 +-- .../qml/delegates/PlayableMediaMessage.qml | 54 ++++----- resources/qml/dialogs/ReadReceipts.qml | 7 +- resources/qml/ui/NhekoSlider.qml | 22 ++-- resources/qml/ui/media/MediaControls.qml | 107 ++++++++++-------- 5 files changed, 103 insertions(+), 100 deletions(-) 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 99c82a9b..c738e5b4 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -21,14 +21,13 @@ Item { 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 - 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 + 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 @@ -44,16 +43,14 @@ Item { Rectangle { id: videoContainer + color: type == MtxEvent.VideoMessage ? Nheko.colors.window : "transparent" width: parent.width height: parent.height - fileInfoLabel.height - - TapHandler { - onTapped: mediaControls.showControls(); - } - - + TapHandler { + onTapped: mediaControls.showControls() + } Image { anchors.fill: parent @@ -72,34 +69,31 @@ Item { flushMode: VideoOutput.FirstFrame } - } } - MediaControls { - id: mediaControls + 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 - } + 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 + } // information about file name and file size Label { id: fileInfoLabel anchors.bottom: content.bottom - text: body + " [" + filesize + "]" textFormat: Text.PlainText elide: Text.ElideRight 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 index 6cf1fd2d..23e22f51 100644 --- a/resources/qml/ui/NhekoSlider.qml +++ b/resources/qml/ui/NhekoSlider.qml @@ -8,23 +8,23 @@ import im.nheko 1.0 Slider { id: control - value: 0 - property color progressColor: Nheko.colors.highlight + property color progressColor: Nheko.colors.highlight property bool alwaysShowSlider: true property int sliderRadius: 16 - implicitHeight: sliderRadius - padding: 0 + value: 0 + implicitHeight: sliderRadius + padding: 0 background: Rectangle { - x: control.leftPadding + handle.width/2 + x: control.leftPadding + handle.width / 2 y: control.topPadding + control.availableHeight / 2 - height / 2 implicitWidth: 200 - implicitHeight: control.sliderRadius/4 + implicitHeight: control.sliderRadius / 4 width: control.availableWidth - handle.width height: implicitHeight - radius: height/2 + radius: height / 2 color: Nheko.colors.buttonText Rectangle { @@ -33,6 +33,7 @@ Slider { color: control.progressColor radius: 2 } + } handle: Rectangle { @@ -40,9 +41,10 @@ Slider { 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 + 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 index 00ccdd26..7216e552 100644 --- a/resources/qml/ui/media/MediaControls.qml +++ b/resources/qml/ui/media/MediaControls.qml @@ -22,12 +22,6 @@ Rectangle { property var positionValue: 0 property var position property bool shouldShowControls: !playingVideo || playerMouseArea.shouldShowControls || volumeSlider.state == "shown" - 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 signal playPauseActivated() signal loadActivated() @@ -55,19 +49,25 @@ Rectangle { 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(); + onHoveredChanged: showControls() } ColumnLayout { - id: controlLayout - enabled: control.shouldShowControls + enabled: control.shouldShowControls spacing: 0 anchors.bottom: control.bottom anchors.left: control.left @@ -77,9 +77,7 @@ Rectangle { Layout.fillWidth: true Layout.leftMargin: Nheko.paddingSmall Layout.rightMargin: Nheko.paddingSmall - enabled: control.mediaLoaded - value: control.positionValue onMoved: control.position = value from: 0 @@ -87,7 +85,6 @@ Rectangle { alwaysShowSlider: false } - RowLayout { Layout.margins: Nheko.paddingSmall spacing: Nheko.paddingSmall @@ -95,95 +92,87 @@ Rectangle { // Cache/Play/pause button ImageButton { - Layout.alignment: Qt.AlignLeft 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"; + return ":/icons/icons/ui/pause-symbol.png"; else - return ":/icons/icons/ui/play-sign.png"; + return ":/icons/icons/ui/play-sign.png"; } else { return ":/icons/icons/ui/arrow-pointing-down.png"; } } - - onClicked: control.mediaLoaded ? control.playPauseActivated() : control.loadActivated(); - + onClicked: control.mediaLoaded ? control.playPauseActivated() : control.loadActivated() } ImageButton { - Layout.alignment: Qt.AlignLeft id: volumeButton + Layout.alignment: Qt.AlignLeft buttonTextColor: Nheko.colors.text Layout.preferredHeight: 24 Layout.preferredWidth: 24 - image: { - if (control.muted || control.desiredVolume <= 0) { + if (control.muted || control.desiredVolume <= 0) return ":/icons/icons/ui/volume-off-indicator.png"; - } else { + 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: "" - - states: State { - name: "shown" - when: Settings.mobileMode || volumeButton.hovered || volumeSlider.hovered || volumeSlider.pressed - PropertyChanges {target: volumeSlider; Layout.preferredWidth: 100} - PropertyChanges {target: volumeSlider; opacity: 1} - } - Layout.alignment: Qt.AlignLeft Layout.preferredWidth: 0 opacity: 0 - id: volumeSlider orientation: Qt.Horizontal - property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, QtMultimedia.LogarithmicVolumeScale, QtMultimedia.LinearVolumeScale) value: 1 - onDesiredVolumeChanged: { control.muted = !(desiredVolume > 0); } - transitions: [ Transition { from: "" to: "shown" SequentialAnimation { - PauseAnimation { duration: 50 } + PauseAnimation { + duration: 50 + } + NumberAnimation { duration: 100 properties: "opacity" - easing.type: Easing.InQuad + easing.type: Easing.InQuad } + } NumberAnimation { properties: "Layout.preferredWidth" duration: 150 } + }, Transition { from: "shown" to: "" SequentialAnimation { - PauseAnimation { duration: 100 } + PauseAnimation { + duration: 100 + } ParallelAnimation { NumberAnimation { @@ -196,16 +185,34 @@ Rectangle { 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 } @@ -218,14 +225,6 @@ Rectangle { } - // Fade controls in/out - Behavior on opacity { - OpacityAnimator { - duration: 100 - } - - } - // For hiding controls on stationary cursor Timer { id: controlHideTimer @@ -234,4 +233,12 @@ Rectangle { repeat: false } + // Fade controls in/out + Behavior on opacity { + OpacityAnimator { + duration: 100 + } + + } + }