1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
|
// 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 QtQuick.Layouts 1.2
import im.nheko 1.0
ColumnLayout {
required property double proportionalHeight
required property int type
required property int originalWidth
required property string thumbnailUrl
required property string eventId
required property string url
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
}
id: content
Layout.maximumWidth: parent? parent.width: undefined
MxcMedia {
id: mxcmedia
// TODO: Show error in overlay or so?
onError: console.log(error)
roomm: room
onMediaStatusChanged: {
if (status == MxcMedia.LoadedMedia) {
progress.updatePositionTexts();
}
}
}
Rectangle {
id: videoContainer
visible: type == MtxEvent.VideoMessage
//property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : /////model.data.width)
// property double tempWidth: (model.data.width < 1) ? 400 : model.data.width
// property double tempHeight: tempWidth * model.data.proportionalHeight
//property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 400 : originalWidth)
property double tempWidth: Math.min(parent ? parent.width: undefined, originalWidth < 1 ? 400 : originalWidth)
property double tempHeight: tempWidth * proportionalHeight
property double divisor: isReply ? 4 : 2
property bool tooHigh: tempHeight > timelineRoot.height / divisor
Layout.maximumWidth: Layout.preferredWidth
Layout.preferredHeight: tooHigh ? timelineRoot.height / divisor : tempHeight
Layout.preferredWidth: tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth
Image {
anchors.fill: parent
source: thumbnailUrl.replace("mxc://", "image://MxcImage/")
asynchronous: true
fillMode: Image.PreserveAspectFit
// Button and window colored overlay to cache media
Rectangle {
// Display over video controls
z: videoOutput.z + 1
visible: !mxcmedia.loaded
anchors.fill: parent
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
clip: true
anchors.fill: parent
fillMode: VideoOutput.PreserveAspectFit
source: mxcmedia
flushMode: VideoOutput.FirstFrame
// TODO: once we can use Qt 5.12, use HoverHandler
MouseArea {
id: playerMouseArea
// Toggle play state on clicks
onClicked: {
if (controlRect.shouldShowControls &&
!controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) {
(mxcmedia.state == MediaPlayer.PlayingState) ?
mxcmedia.pause() :
mxcmedia.play()
}
}
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
}
(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
}
}
|