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