Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • nheko-reborn/nheko
1 result
Show changes
Commits on Source (20)
resources/icons/ui/volume-up.png

617 B

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