Support video events, new media player
|
@ -14,6 +14,8 @@ Qt 5.12+, including:
|
|||
- qt5-qmake
|
||||
- qt5-devel
|
||||
|
||||
- qtav
|
||||
|
||||
- python3
|
||||
- python3-devel
|
||||
- olm-python3 >= 3.1
|
||||
|
|
7
TODO.md
|
@ -1,5 +1,7 @@
|
|||
- Media
|
||||
- Caching
|
||||
- What effect will it have on GIFs? Can we set `cache:false` on them or get
|
||||
the frame count once they're cached?
|
||||
- Downloading
|
||||
- Bottom/top bar
|
||||
- Uploading (+local echo)
|
||||
|
@ -11,6 +13,11 @@
|
|||
- GIF thumbnails: load the real animated image
|
||||
- Copy thumbnail URL in context menu?
|
||||
|
||||
- GIFs can use the video player
|
||||
- Video bug: when media is done playing, clicking on progress slider always
|
||||
bring back to the beginning no matter where
|
||||
- Video: missing buttons and small size problems
|
||||
|
||||
- Refactor EventContent
|
||||
- No background/padding around medias
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# widgets: Make native file dialogs available to QML (must use QApplication)
|
||||
QT = quick widgets
|
||||
QT = quick widgets av
|
||||
DEFINES += QT_DEPRECATED_WARNINGS
|
||||
CONFIG += warn_off c++11 release
|
||||
TEMPLATE = app
|
||||
|
|
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 334 B |
3
src/icons/thin/player-fullscreen-exit.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m15 2h2v5h7v2h-9zm9 13v2h-7v5h-2v-7zm-15 7h-2v-5h-7v-2h9zm-9-13v-2h7v-5h2v7z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 184 B |
3
src/icons/thin/player-fullscreen.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m24 9h-2v-4h-4v-2h6zm-6 12v-2h4v-4h2v6zm-18-6h2v4h4v2h-6zm6-12v2h-4v4h-2v-6z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 184 B |
3
src/icons/thin/player-loop.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18 13v5h-5l1.607-1.608c-3.404-2.824-5.642-8.392-9.179-8.392-2.113 0-3.479 1.578-3.479 4s1.365 4 3.479 4c1.664 0 2.86-1.068 4.015-2.392l1.244 1.561c-1.499 1.531-3.05 2.831-5.259 2.831-3.197 0-5.428-2.455-5.428-6s2.231-6 5.428-6c4.839 0 7.34 6.449 10.591 8.981zm.57-7c-2.211 0-3.762 1.301-5.261 2.833l1.244 1.561c1.156-1.325 2.352-2.394 4.017-2.394 2.114 0 3.48 1.578 3.48 4 0 1.819-.771 3.162-2.051 3.718v2.099c2.412-.623 4-2.829 4-5.816.001-3.546-2.231-6.001-5.429-6.001z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 580 B |
6
src/icons/thin/player-pause.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke-width=".948329">
|
||||
<path d="m8.928091 20.993272h-4v-17.9865437h4z"/>
|
||||
<path d="m19.035955 3.0067283h-4v17.9865437h4z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 247 B |
3
src/icons/thin/player-play.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m3 22v-20l18 10z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 124 B |
3
src/icons/thin/player-restart.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m13.5 2c-5.629 0-10.212 4.436-10.475 10h-3.025l4.537 5.917 4.463-5.917h-2.975c.26-3.902 3.508-7 7.475-7 4.136 0 7.5 3.364 7.5 7.5s-3.364 7.5-7.5 7.5c-2.381 0-4.502-1.119-5.876-2.854l-1.847 2.449c1.919 2.088 4.664 3.405 7.723 3.405 5.798 0 10.5-4.702 10.5-10.5s-4.702-10.5-10.5-10.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 390 B |
3
src/icons/thin/player-speed.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m15.91 13.34 2.636-4.026-.454-.406-3.673 3.099c-.675-.138-1.402.068-1.894.618-.736.823-.665 2.088.159 2.824s2.088.665 2.824-.159c.492-.55.615-1.295.402-1.95zm-3.91-10.646v-2.694h4v2.694c-1.439-.243-2.592-.238-4 0zm8.851 2.064 1.407-1.407 1.414 1.414-1.321 1.321c-.462-.484-.964-.927-1.5-1.328zm-18.851 4.242h8v2h-8zm-2 4h8v2h-8zm3 4h7v2h-7zm21-3c0 5.523-4.477 10-10 10-2.79 0-5.3-1.155-7.111-3h3.28c1.138.631 2.439 1 3.831 1 4.411 0 8-3.589 8-8s-3.589-8-8-8c-1.392 0-2.693.369-3.831 1h-3.28c1.811-1.845 4.321-3 7.111-3 5.523 0 10 4.477 10 10z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 650 B |
3
src/icons/thin/player-track-audio.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m13 0h-2v15.676c-3.379-.667-7 1.915-7 4.731 0 2.367 1.881 3.593 3.919 3.593 2.423 0 5.077-1.728 5.081-5.24v-12.76c3.009 2.223 5.623 3.243 5.059 7 1.431-1.727 1.941-2.817 1.941-4.051 0-4.446-7-5.915-7-8.949z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 314 B |
3
src/icons/thin/player-track-subtitle.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m0 1v16.981h4v5.019l7-5.019h13v-16.981zm13 12h-8v-1h8zm6-3h-14v-1h14zm0-3h-14v-1h14z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 192 B |
3
src/icons/thin/player-track-video.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m22 1v2h-2v-2h-2v4h-12v-4h-2v2h-2v-2h-2v22h2v-2h2v2h2v-4h12v4h2v-2h2v2h2v-22zm-18 18h-2v-2h2zm0-4h-2v-2h2zm0-4h-2v-2h2zm0-4h-2v-2h2zm14 9h-12v-8h12zm4 3h-2v-2h2zm0-4h-2v-2h2zm0-4h-2v-2h2zm0-4h-2v-2h2z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 308 B |
9
src/icons/thin/player-volume-high.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14.509208 8.343398c1.202865.9346638 1.945334 2.226588 1.94209 3.651856-.0032 1.425268-.748954 2.714677-1.953439 3.648082l.978322 1.538485c1.713516-1.324631 2.775344-3.158736 2.780206-5.184051.0032-2.0265727-1.05048-3.8594193-2.759132-5.1890826z" stroke-width="1.428038"/>
|
||||
<path d="m18.318701 4.9920248c2.307024 1.7957761 3.72921 4.2712232 3.723838 7.0065862-.0054 2.735361-1.440099 5.212198-3.754288 7.003804l.980846 1.74296c2.887361-2.234991 4.68032-5.328952 4.685693-8.743986.009-3.4150319-1.766089-6.5062134-4.648077-8.7467639z" stroke-width="1.57784"/>
|
||||
<g transform="matrix(.6875 0 0 1.01875 0 -.225)">
|
||||
<path d="m16 2-13.2653508 5v10l13.2653508 5z" stroke-width="1.236723"/>
|
||||
<path d="m2.7346492 7h-2.7346492v10h2.7346492z" stroke-width=".739547"/>
|
||||
<path d="m7 7c-4.7095572 2.7190641-5.0350591 2.9069927 0 0z" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 973 B |
8
src/icons/thin/player-volume-low.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14.509208 8.343398c1.202865.9346638 1.945334 2.226588 1.94209 3.651856-.0032 1.425268-.748954 2.714677-1.953439 3.648082l.978322 1.538485c1.713516-1.324631 2.775344-3.158736 2.780206-5.184051.0032-2.0265727-1.05048-3.8594193-2.759132-5.1890826z" stroke-width="1.428038"/>
|
||||
<g transform="matrix(.6875 0 0 1.01875 0 -.225)">
|
||||
<path d="m16 2-13.2653508 5v10l13.2653508 5z" stroke-width="1.236723"/>
|
||||
<path d="m2.7346492 7h-2.7346492v10h2.7346492z" stroke-width=".739547"/>
|
||||
<path d="m7 7c-4.7095572 2.7190641-5.0350591 2.9069927 0 0z" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 684 B |
11
src/icons/thin/player-volume-mute.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(.6875 0 0 1.01875 0 -.225)">
|
||||
<path d="m16 2-13.7653508 5v10l13.7653508 5z" stroke-width="1.236723"/>
|
||||
<path d="m2.7346492 7h-2.7346492v10h2.7346492z" stroke-width=".739547"/>
|
||||
<path d="m7 7c-4.7095572 2.7190641-5.0350591 2.9069927 0 0z" fill="none"/>
|
||||
</g>
|
||||
<g fill="none" stroke="#000" stroke-width="1.767194">
|
||||
<path d="m14.900494 16.233449c7.7076-7.7076018 8.474709-8.4747094 8.474709-8.4747094"/>
|
||||
<path d="m23.375202 16.233449c-7.707601-7.7076013-8.474709-8.4747098-8.474709-8.4747098"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 660 B |
|
@ -7,8 +7,6 @@ from typing import Any, Dict
|
|||
|
||||
import aiofiles
|
||||
|
||||
import pyotherside
|
||||
|
||||
from .backend import Backend
|
||||
from .theme_parser import convert_to_qml
|
||||
from .utils import dict_update_recursive
|
||||
|
@ -106,11 +104,19 @@ class UISettings(JSONConfigFile):
|
|||
async def default_data(self) -> JsonData:
|
||||
return {
|
||||
"alertOnMessageForMsec": 4000,
|
||||
"autoPlayGIF": True,
|
||||
"clearRoomFilterOnEnter": True,
|
||||
"clearRoomFilterOnEscape": True,
|
||||
"theme": "Default.qpl",
|
||||
"writeAliases": {},
|
||||
"media": {
|
||||
"autoLoad": True,
|
||||
"autoPlay": False,
|
||||
"autoPlayGIF": True,
|
||||
"autoHideOSDAfterMsec": 3000,
|
||||
"defaultVolume": 100,
|
||||
"startMuted": False,
|
||||
"hoverPreviewHeight": 192,
|
||||
},
|
||||
"keys": {
|
||||
"startPythonDebugger": "Alt+Shift+D",
|
||||
"toggleDebugConsole": "Alt+Shift+C",
|
||||
|
|
|
@ -34,7 +34,7 @@ Image {
|
|||
cache: true // Needed to allow GIFs to loop
|
||||
paused: ! visible || window.hidden || userPaused
|
||||
|
||||
property bool userPaused: ! window.settings.autoPlayGIF
|
||||
property bool userPaused: ! window.settings.media.autoPlayGIF
|
||||
|
||||
TapHandler {
|
||||
onTapped: parent.userPaused = ! parent.userPaused
|
||||
|
@ -42,7 +42,7 @@ Image {
|
|||
|
||||
HIcon {
|
||||
anchors.centerIn: parent
|
||||
svgName: "play"
|
||||
svgName: "play-overlay"
|
||||
colorize: "transparent"
|
||||
dimension: Math.min(
|
||||
parent.width - theme.spacing * 2,
|
||||
|
|
|
@ -4,10 +4,13 @@ import QtQuick.Controls 2.12
|
|||
ProgressBar {
|
||||
id: bar
|
||||
|
||||
property color backgroundColor: theme.controls.progressBar.background
|
||||
property color foregroundColor: theme.controls.progressBar.foreground
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 200
|
||||
implicitHeight: theme.controls.progressBar.height
|
||||
color: theme.controls.progressBar.background
|
||||
color: backgroundColor
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
|
@ -17,7 +20,7 @@ ProgressBar {
|
|||
Rectangle {
|
||||
width: bar.visualPosition * parent.width
|
||||
height: parent.height
|
||||
color: theme.controls.progressBar.foreground
|
||||
color: foregroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
72
src/qml/Base/HSlider.qml
Normal file
|
@ -0,0 +1,72 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Slider {
|
||||
id: slider
|
||||
leftPadding: 0
|
||||
rightPadding: leftPadding
|
||||
topPadding: 0
|
||||
bottomPadding: topPadding
|
||||
|
||||
property bool enableRadius: true
|
||||
property bool fullHeight: false
|
||||
property color backgroundColor: theme.controls.slider.background
|
||||
property color foregroundColor: theme.controls.slider.foreground
|
||||
|
||||
property alias toolTip: toolTip
|
||||
property alias mouseArea: mouseArea
|
||||
|
||||
background: Rectangle {
|
||||
color: backgroundColor
|
||||
x: slider.leftPadding
|
||||
y: slider.topPadding + slider.availableHeight / 2 - height / 2
|
||||
|
||||
implicitWidth: 200
|
||||
implicitHeight: theme.controls.slider.height
|
||||
width: slider.availableWidth
|
||||
height: fullHeight ? slider.height : implicitHeight
|
||||
radius: enableRadius ? theme.controls.slider.radius : 0
|
||||
|
||||
Rectangle {
|
||||
width: slider.visualPosition * parent.width
|
||||
height: parent.height
|
||||
color: foregroundColor
|
||||
radius: parent.radius
|
||||
}
|
||||
}
|
||||
|
||||
handle: Rectangle {
|
||||
x: slider.leftPadding + slider.visualPosition *
|
||||
(slider.availableWidth - width)
|
||||
y: slider.topPadding + slider.availableHeight / 2 - height / 2
|
||||
|
||||
implicitWidth: theme.controls.slider.handle.size
|
||||
implicitHeight: implicitWidth
|
||||
radius: implicitWidth / 2
|
||||
|
||||
color: slider.pressed ?
|
||||
theme.controls.slider.handle.pressedInside :
|
||||
theme.controls.slider.handle.inside
|
||||
|
||||
border.color: slider.pressed ?
|
||||
theme.controls.slider.handle.pressedBorder :
|
||||
theme.controls.slider.handle.border
|
||||
|
||||
Behavior on color { HColorAnimation {} }
|
||||
Behavior on border.color { HColorAnimation {} }
|
||||
}
|
||||
|
||||
HToolTip {
|
||||
id: toolTip
|
||||
parent: slider.handle
|
||||
visible: slider.pressed && text
|
||||
delay: 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: slider.hovered ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ ToolTip {
|
|||
bottomPadding: topPadding
|
||||
|
||||
Layout.maximumWidth: Math.min(
|
||||
mainUI.width / 1.25, theme.fontSize.normal * 0.5 * 75,
|
||||
window.width / 1.25, theme.fontSize.normal * 0.5 * 75,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ ToolTip {
|
|||
HNumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
|
||||
}
|
||||
exit: Transition {
|
||||
HNumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
|
||||
HNumberAnimation { property: "opacity"; to: 0.0 }
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
|
|
268
src/qml/Base/MediaPlayer/OSD.qml
Normal file
|
@ -0,0 +1,268 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtAV 1.7
|
||||
import "../../Base"
|
||||
import "../../utils.js" as Utils
|
||||
|
||||
HColumnLayout {
|
||||
id: osd
|
||||
visible: osdScaleTransform.yScale > 0
|
||||
|
||||
transform: Scale {
|
||||
id: osdScaleTransform
|
||||
yScale: osdHover.hovered ||
|
||||
media.playbackState !== MediaPlayer.PlayingState ||
|
||||
osd.showup ?
|
||||
1 : 0
|
||||
origin.y: osd.height
|
||||
|
||||
Behavior on yScale { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
|
||||
property Item media: parent // QtAV.Video or QtAV.Audio
|
||||
property bool showup: false
|
||||
property bool enableFullScreen: false
|
||||
property bool fullScreen: false
|
||||
|
||||
property int savedDuration: 0
|
||||
readonly property int duration: media.duration
|
||||
readonly property int boundPosition:
|
||||
Math.min(media.position, savedDuration)
|
||||
|
||||
|
||||
onShowupChanged: if (showup) osdHideTimer.restart()
|
||||
onDurationChanged: if (duration) savedDuration = duration
|
||||
|
||||
|
||||
function togglePlay() {
|
||||
media.playbackState === MediaPlayer.PlayingState ?
|
||||
media.pause() : media.play()
|
||||
}
|
||||
|
||||
function seekToPosition(pos) { // pos: 0.0 to 1.0
|
||||
if (media.playbackState === MediaPlayer.StoppedState) media.play()
|
||||
if (media.seekable) media.seek(pos * savedDuration)
|
||||
}
|
||||
|
||||
|
||||
HoverHandler { id: osdHover }
|
||||
|
||||
Timer {
|
||||
id: osdHideTimer
|
||||
interval: window.settings.media.autoHideOSDAfterMsec
|
||||
onTriggered: osd.showup = false
|
||||
}
|
||||
|
||||
HSlider {
|
||||
id: timeSlider
|
||||
topPadding: 5
|
||||
z: 1
|
||||
to: savedDuration || boundPosition
|
||||
backgroundColor: theme.mediaPlayer.progress.background
|
||||
enableRadius: false
|
||||
fullHeight: true
|
||||
mouseArea.hoverEnabled: true
|
||||
|
||||
onMoved: seekToPosition(timeSlider.position)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: theme.mediaPlayer.progress.height
|
||||
|
||||
HToolTip {
|
||||
id: previewToolTip
|
||||
x: timeSlider.mouseArea.mouseX - width / 2
|
||||
visible: preview.implicitWidth >=
|
||||
previewLabel.implicitWidth + previewLabel.padding &&
|
||||
|
||||
preview.implicitHeight >=
|
||||
previewLabel.implicitHeight + previewLabel.padding &&
|
||||
|
||||
! timeSlider.pressed && timeSlider.mouseArea.containsMouse
|
||||
|
||||
readonly property int wantTimestamp:
|
||||
visible ?
|
||||
savedDuration *
|
||||
(timeSlider.mouseArea.mouseX / timeSlider.mouseArea.width) :
|
||||
-1
|
||||
|
||||
Timer {
|
||||
interval: 300
|
||||
running: previewToolTip.visible
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: preview.timestamp = previewToolTip.wantTimestamp
|
||||
}
|
||||
|
||||
contentItem: VideoPreview {
|
||||
id: preview
|
||||
implicitHeight: Math.min(
|
||||
window.settings.media.hoverPreviewHeight,
|
||||
media.height - osd.height - theme.spacing
|
||||
)
|
||||
implicitWidth: Math.min(
|
||||
implicitHeight * media.savedAspectRatio,
|
||||
media.width - theme.spacing,
|
||||
)
|
||||
file: media.source
|
||||
|
||||
HLabel {
|
||||
id: previewLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: padding / 4
|
||||
text: Utils.formatDuration(previewToolTip.wantTimestamp)
|
||||
padding: theme.spacing / 2
|
||||
opacity: previewToolTip.wantTimestamp === -1 ? 0 : 1
|
||||
|
||||
background: Rectangle {
|
||||
color: theme.mediaPlayer.controls.background
|
||||
radius: theme.radius
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: timeSlider
|
||||
property: "value"
|
||||
value: boundPosition
|
||||
when: ! timeSlider.pressed
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: theme.mediaPlayer.controls.background
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
HRowLayout {
|
||||
width: parent.width
|
||||
|
||||
OSDButton {
|
||||
readonly property string mode:
|
||||
media.playbackState === MediaPlayer.StoppedState &&
|
||||
savedDuration &&
|
||||
boundPosition >= savedDuration - 500 ?
|
||||
"restart" :
|
||||
|
||||
media.playbackState == MediaPlayer.PlayingState ? "pause" :
|
||||
|
||||
"play"
|
||||
|
||||
icon.name: "player-" + mode
|
||||
toolTip.text: qsTr(
|
||||
mode === "play" ? "Play" :
|
||||
mode === "pause" ? "Pause" :
|
||||
"Restart"
|
||||
)
|
||||
onClicked: togglePlay()
|
||||
}
|
||||
|
||||
// OSDButton {
|
||||
// icon.name: "player-loop"
|
||||
// visible: false
|
||||
// }
|
||||
|
||||
OSDButton {
|
||||
id: volumeButton
|
||||
icon.name: "player-volume-" + (
|
||||
media.muted ? "mute" : media.volume > 0.5 ? "high" : "low"
|
||||
)
|
||||
text: media.muted ? "" : Math.round(media.volume * 100)
|
||||
toolTip.text: media.muted ? qsTr("Unmute") : qsTr("Mute")
|
||||
onClicked: media.muted = ! media.muted
|
||||
}
|
||||
|
||||
HSlider {
|
||||
value: media.volume
|
||||
onMoved: media.volume = value
|
||||
|
||||
visible: Layout.preferredWidth > 0
|
||||
Layout.preferredWidth:
|
||||
! media.muted &&
|
||||
(hovered || pressed || volumeButton.hovered) ?
|
||||
theme.mediaPlayer.controls.volumeSliderWidth : 0
|
||||
Layout.fillHeight: true
|
||||
|
||||
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
OSDButton {
|
||||
id: speedButton
|
||||
icon.name: "player-speed"
|
||||
text: qsTr("%1x").arg(Utils.round(media.playbackRate))
|
||||
toolTip.text: qsTr("Reset speed")
|
||||
onClicked: media.playbackRate = 1
|
||||
}
|
||||
|
||||
HSlider {
|
||||
id: speedSlider
|
||||
from: 0.2
|
||||
to: 4
|
||||
value: media.playbackRate
|
||||
stepSize: 0.2
|
||||
snapMode: HSlider.SnapAlways
|
||||
|
||||
onMoved: media.playbackRate = value
|
||||
|
||||
visible: Layout.preferredWidth > 0
|
||||
Layout.preferredWidth:
|
||||
(hovered || pressed || speedButton.hovered) ?
|
||||
theme.mediaPlayer.controls.speedSliderWidth : 0
|
||||
Layout.fillHeight: true
|
||||
|
||||
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
OSDLabel {
|
||||
text: boundPosition && savedDuration ?
|
||||
|
||||
qsTr("%1 / %2")
|
||||
.arg(Utils.formatDuration(boundPosition))
|
||||
.arg(Utils.formatDuration(savedDuration)) :
|
||||
|
||||
boundPosition || savedDuration ?
|
||||
Utils.formatDuration(boundPosition || savedDuration) :
|
||||
|
||||
""
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
OSDLabel {
|
||||
text: boundPosition && savedDuration ?
|
||||
qsTr("-%1").arg(
|
||||
Utils.formatDuration(savedDuration - boundPosition)
|
||||
) : ""
|
||||
}
|
||||
|
||||
// OSDButton {
|
||||
// icon.name: "player-track-video"
|
||||
// }
|
||||
|
||||
// OSDButton {
|
||||
// icon.name: "player-track-audio"
|
||||
// }
|
||||
|
||||
// OSDButton {
|
||||
// icon.name: "player-track-subtitle"
|
||||
// }
|
||||
|
||||
OSDButton {
|
||||
icon.name: "download"
|
||||
toolTip.text: qsTr("Download")
|
||||
onClicked: Qt.openUrlExternally(media.source)
|
||||
}
|
||||
|
||||
OSDButton {
|
||||
id: fullScreenButton
|
||||
icon.name: "player-fullscreen" + (fullScreen ? "-exit" : "")
|
||||
toolTip.text: fullScreen ?
|
||||
qsTr("Exit fullscreen") : qsTr("Fullscreen")
|
||||
onClicked: fullScreen = ! fullScreen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
src/qml/Base/MediaPlayer/OSDButton.qml
Normal file
|
@ -0,0 +1,8 @@
|
|||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
|
||||
|
||||
HButton {
|
||||
backgroundColor: "transparent"
|
||||
iconItem.dimension: theme.mediaPlayer.controls.iconHeight
|
||||
}
|
9
src/qml/Base/MediaPlayer/OSDLabel.qml
Normal file
|
@ -0,0 +1,9 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../Base"
|
||||
|
||||
|
||||
HLabel {
|
||||
Layout.leftMargin: theme.spacing / 2
|
||||
Layout.rightMargin: Layout.leftMargin
|
||||
}
|
74
src/qml/Base/MediaPlayer/VideoPlayer.qml
Normal file
|
@ -0,0 +1,74 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Window 2.12
|
||||
import QtAV 1.7
|
||||
|
||||
Video {
|
||||
id: video
|
||||
autoLoad: window.settings.media.autoLoad
|
||||
autoPlay: window.settings.media.autoPlay
|
||||
volume: window.settings.media.defaultVolume / 100
|
||||
muted: window.settings.media.startMuted
|
||||
implicitWidth: fullScreen ? window.width : 640
|
||||
implicitHeight: fullScreen ? window.height : (width / savedAspectRatio)
|
||||
|
||||
|
||||
property bool hovered: false
|
||||
property alias fullScreen: osd.fullScreen
|
||||
|
||||
property int oldVisibility: Window.Windowed
|
||||
property QtObject oldParent: video.parent
|
||||
|
||||
property real savedAspectRatio: 16 / 9
|
||||
|
||||
|
||||
onSourceAspectRatioChanged:
|
||||
if (sourceAspectRatio) savedAspectRatio = sourceAspectRatio
|
||||
|
||||
onFullScreenChanged: {
|
||||
if (fullScreen) {
|
||||
oldVisibility = window.visibility
|
||||
window.visibility = Window.FullScreen
|
||||
|
||||
oldParent = video.parent
|
||||
video.parent = mainUI.fullScreenPopup.contentItem
|
||||
|
||||
mainUI.fullScreenPopup.open()
|
||||
|
||||
} else {
|
||||
window.visibility = oldVisibility
|
||||
mainUI.fullScreenPopup.close()
|
||||
|
||||
video.parent = oldParent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
target: mainUI.fullScreenPopup
|
||||
onClosed: fullScreen = false
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: osd.togglePlay()
|
||||
onDoubleTapped: video.fullScreen = ! video.fullScreen
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
width: parent.width
|
||||
height: parent.height - (osd.visible ? osd.height : 0)
|
||||
acceptedButtons: Qt.NoButton
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
|
||||
onContainsMouseChanged: video.hovered = containsMouse
|
||||
onMouseXChanged: osd.showup = true
|
||||
onMouseYChanged: osd.showup = true
|
||||
}
|
||||
|
||||
OSD {
|
||||
id: osd
|
||||
width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
enableFullScreen: true
|
||||
}
|
||||
}
|
|
@ -130,12 +130,6 @@ Column {
|
|||
contextMenu.media[0] === EventDelegate.Media.Image ?
|
||||
qsTr("Copy image address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Video ?
|
||||
qsTr("Copy video address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Audio ?
|
||||
qsTr("Copy audio address") :
|
||||
|
||||
qsTr("Copy media address")
|
||||
|
||||
visible: Boolean(text)
|
||||
|
|
|
@ -65,6 +65,10 @@ HLoader {
|
|||
fileSize: info.media_size,
|
||||
}
|
||||
|
||||
} else if (type === EventDelegate.Media.Video) {
|
||||
var file = "EventVideo.qml"
|
||||
var props = { source: mediaUrl }
|
||||
|
||||
} else { return }
|
||||
|
||||
loader.setSource(file, props)
|
||||
|
|
19
src/qml/Chat/Timeline/EventVideo.qml
Normal file
|
@ -0,0 +1,19 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtAV 1.7
|
||||
import "../../Base"
|
||||
import "../../Base/MediaPlayer"
|
||||
import "../../utils.js" as Utils
|
||||
|
||||
VideoPlayer {
|
||||
id: video
|
||||
width: fullScreen ? implicitWidth : Math.min(
|
||||
mainColumn.width - eventContent.spacing * 2,
|
||||
theme.chat.message.videoWidth,
|
||||
)
|
||||
|
||||
onHoveredChanged: {
|
||||
eventDelegate.hoveredMediaTypeUrl =
|
||||
hovered ? [EventDelegate.Media.Video, video.source] : []
|
||||
}
|
||||
}
|
|
@ -12,11 +12,12 @@ Item {
|
|||
Component.onCompleted: window.mainUI = mainUI
|
||||
Keys.forwardTo: [shortcuts]
|
||||
|
||||
property alias shortcuts: shortcuts
|
||||
property alias sidePane: sidePane
|
||||
property alias pageLoader: pageLoader
|
||||
property alias pressAnimation: pressAnimation
|
||||
property alias debugConsole: debugConsoleLoader.item
|
||||
readonly property alias shortcuts: shortcuts
|
||||
readonly property alias sidePane: sidePane
|
||||
readonly property alias pageLoader: pageLoader
|
||||
readonly property alias pressAnimation: pressAnimation
|
||||
readonly property alias debugConsole: debugConsoleLoader.item
|
||||
readonly property alias fullScreenPopup: fullScreenPopup
|
||||
|
||||
SequentialAnimation {
|
||||
id: pressAnimation
|
||||
|
@ -145,4 +146,11 @@ Item {
|
|||
id: debugConsoleLoader
|
||||
source: debugMode ? "DebugConsole.qml" : ""
|
||||
}
|
||||
|
||||
HPopup {
|
||||
id: fullScreenPopup
|
||||
dim: false
|
||||
width: window.width
|
||||
height: window.height
|
||||
}
|
||||
}
|
||||
|
|
|
@ -204,6 +204,26 @@ function formatTime(time, seconds=true) {
|
|||
}
|
||||
|
||||
|
||||
function formatDuration(milliseconds) {
|
||||
let totalSeconds = milliseconds / 1000
|
||||
|
||||
let hours = Math.floor(totalSeconds / 3600)
|
||||
let minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
let seconds = Math.floor(totalSeconds % 60)
|
||||
|
||||
if (seconds < 10) seconds = "0" + seconds
|
||||
if (hours < 1) return minutes + ":" + seconds
|
||||
|
||||
if (minutes < 10) minutes = "0" + minutes
|
||||
return hours + ":" + minutes + ":" + seconds
|
||||
}
|
||||
|
||||
|
||||
function round(float) {
|
||||
return parseFloat(float.toFixed(2))
|
||||
}
|
||||
|
||||
|
||||
function getItem(array, mainKey, value) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i][mainKey] === value) { return array[i] }
|
||||
|
|
|
@ -176,6 +176,19 @@ controls:
|
|||
color background: colors.inputBackground
|
||||
color foreground: colors.accentBackground
|
||||
|
||||
slider:
|
||||
int radius: 2
|
||||
int height: controls.progressBar.height
|
||||
color background: controls.progressBar.background
|
||||
color foreground: controls.progressBar.foreground
|
||||
|
||||
handle:
|
||||
int size: 20
|
||||
color inside: hsluv(0, 0, 90)
|
||||
color pressedInside: "white"
|
||||
color border: "black"
|
||||
color pressedBorder: colors.strongAccentBackground
|
||||
|
||||
image:
|
||||
int maxPauseIndicatorSize: 64
|
||||
|
||||
|
@ -320,6 +333,7 @@ chat:
|
|||
'<style type"text/css">\n' + styleSheet + '\n</style>\n'
|
||||
|
||||
int thumbnailWidth: 256
|
||||
int videoWidth: 512
|
||||
|
||||
daybreak:
|
||||
color background: colors.strongBackground
|
||||
|
@ -342,3 +356,17 @@ chat:
|
|||
|
||||
composer:
|
||||
color background: colors.strongBackground
|
||||
|
||||
|
||||
mediaPlayer:
|
||||
progress:
|
||||
int height: 8
|
||||
color background: hsluv(0, 0, 0, 0.5)
|
||||
|
||||
controls:
|
||||
int iconHeight: 20
|
||||
int volumeSliderWidth: 100
|
||||
int speedSliderWidth: 100
|
||||
color background: hsluv(
|
||||
colors.hue, colors.saturation * 1.25, colors.intensity * 2, 0.85,
|
||||
)
|
||||
|
|