Warning, /multimedia/plasmatube/src/ui/videoplayer/VideoPlayer.qml is written in an unsupported language. File is not indexed.
0001 // SPDX-FileCopyrightText: 2019 Linus Jahn <lnj@kaidan.im>
0002 // SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
0003 // SPDX-License-Identifier: GPL-3.0-or-later
0004
0005 import QtQuick
0006 import QtQuick.Layouts
0007 import QtQuick.Controls as QQC2
0008 import org.kde.kirigami as Kirigami
0009 import Qt5Compat.GraphicalEffects
0010
0011 import org.kde.plasmatube
0012 import "../components/utils.js" as Utils
0013 import "../"
0014 import "../components"
0015
0016 Kirigami.ScrollablePage {
0017 id: root
0018
0019 flickable.boundsBehavior: Flickable.StopAtBounds
0020
0021 title: videoName
0022 leftPadding: 0
0023 rightPadding: 0
0024 topPadding: 0
0025 bottomPadding: 0
0026
0027 Kirigami.Theme.colorSet: Kirigami.Theme.View
0028
0029 readonly property var video: PlasmaTube.videoController.currentVideo
0030
0031 property var previewSource: renderer
0032
0033 property string currentVideoId
0034 property int currentVideoIndex
0035 property string currentVideoTitle
0036 property string currentChannelName
0037 property string currentChannelId
0038
0039 readonly property string videoName: video.title
0040 readonly property string channelName: video.author
0041 readonly property bool isPlaying: !renderer.paused
0042
0043 readonly property bool widescreen: root.width > 1200
0044 property bool inFullScreen: false
0045
0046 signal requestClosePlayer()
0047
0048 function goToChannel() {
0049 const author = video.author;
0050 const authorId = video.authorId;
0051 pageStack.pop();
0052 pageStack.push(Qt.createComponent("org.kde.plasmatube", "ChannelPage"), {author, authorId});
0053 root.requestClosePlayer();
0054 }
0055
0056 function toggleFullscreen() {
0057 if (root.inFullScreen) {
0058 root.exitFullScreen();
0059 } else {
0060 root.openFullScreen();
0061 }
0062 }
0063
0064 function openFullScreen() {
0065 videoContainer.parent = QQC2.Overlay.overlay;
0066 videoContainer.anchors.fill = QQC2.Overlay.overlay;
0067 root.inFullScreen = true;
0068 applicationWindow().globalDrawer.close();
0069 applicationWindow().showFullScreen();
0070 }
0071
0072 function exitFullScreen() {
0073 videoContainer.parent = inlineVideoContainer;
0074 videoContainer.anchors.fill = inlineVideoContainer;
0075 root.inFullScreen = false;
0076 if (!applicationWindow().globalDrawer.modal) {
0077 applicationWindow().globalDrawer.open();
0078 }
0079 applicationWindow().showNormal();
0080 }
0081
0082 header: Kirigami.AbstractApplicationHeader{
0083 contentItem:
0084 RowLayout{
0085 QQC2.ToolButton {
0086 id: closeButton
0087 icon.name: "go-previous-view"
0088
0089 width: Kirigami.Units.gridUnit * 2
0090 height: Kirigami.Units.gridUnit * 2
0091 onClicked: root.requestClosePlayer();
0092
0093 }
0094 Kirigami.Heading{
0095 text: video.title
0096 }
0097 }
0098 }
0099
0100 Keys.onLeftPressed: (event) => {
0101 PlasmaTube.videoController.currentPlayer.seek(-5);
0102 event.accepted = true;
0103 }
0104
0105 Keys.onRightPressed: (event) => {
0106 PlasmaTube.videoController.currentPlayer.seek(5);
0107 event.accepted = true;
0108 }
0109
0110 Keys.onEscapePressed: (event) => {
0111 if (root.inFullScreen) {
0112 root.exitFullScreen();
0113 }
0114 }
0115
0116 GridLayout {
0117 columns: root.widescreen ? 2 : 1
0118 rowSpacing: 0
0119 columnSpacing: 0
0120
0121 ColumnLayout {
0122 id: parentColumn
0123 spacing: 0
0124 Layout.alignment: Qt.AlignTop
0125 Layout.fillWidth: true
0126
0127 Item {
0128 Layout.margins: widescreen? Kirigami.Units.largeSpacing * 2 : 0
0129 id: inlineVideoContainer
0130 Layout.fillWidth: true
0131 Layout.preferredHeight: width / 16.0 * 9.0
0132 Layout.maximumHeight: root.height
0133 layer.enabled: true
0134 layer.effect: OpacityMask {
0135 maskSource: playerMask
0136 }
0137 MouseArea {
0138 id: videoContainer
0139 anchors.fill: parent
0140
0141 Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
0142 Kirigami.Theme.inherit: false
0143
0144 property bool showControls: false
0145 onEntered: {
0146 videoContainer.showControls = true;
0147 controlTimer.restart();
0148 }
0149 onPositionChanged: {
0150 videoContainer.showControls = true;
0151 controlTimer.restart();
0152 }
0153 onExited: {
0154 controlTimer.stop();
0155 videoContainer.showControls = false;
0156 }
0157 onDoubleClicked: root.toggleFullscreen()
0158
0159 hoverEnabled: !Kirigami.Settings.tabletMode
0160
0161 Timer {
0162 id: controlTimer
0163 interval: 2000
0164 onTriggered: videoContainer.showControls = false
0165 }
0166
0167 MpvObject {
0168 id: renderer
0169 anchors.fill: parent
0170
0171 visible: !stopped
0172 }
0173 Rectangle {
0174 anchors.fill: renderer
0175
0176 color: "black"
0177 visible: renderer.stopped
0178
0179 Image {
0180 id: thumbnailImage
0181
0182 anchors.fill: parent
0183 source: video.thumbnailUrl("high")
0184 fillMode: Image.PreserveAspectCrop
0185 }
0186
0187 QQC2.BusyIndicator {
0188 anchors.centerIn: parent
0189 }
0190 }
0191 Rectangle {
0192 id: playerMask
0193 radius: widescreen ? 7 : 0
0194 anchors.fill: renderer
0195 visible: false
0196 }
0197 VideoData {
0198 title: video.title
0199 visible: opacity > 0
0200 opacity: videoContainer.showControls ? 1 : 0
0201 Behavior on opacity {
0202 NumberAnimation { duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic }
0203 }
0204 }
0205
0206 VideoControls {
0207 inFullScreen: root.inFullScreen
0208 anchors.fill: parent
0209
0210 onRequestFullScreen: root.toggleFullscreen()
0211
0212 visible: opacity > 0
0213 opacity: videoContainer.showControls ? 1 : 0
0214 Behavior on opacity {
0215 NumberAnimation { duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic }
0216 }
0217
0218 Keys.forwardTo: [root]
0219 }
0220
0221 // TODO: this whole thing could probably be a taphandler...
0222 TapHandler {
0223 acceptedDevices: Qt.TouchScreen
0224 onTapped: {
0225 videoContainer.showControls = true;
0226 controlTimer.restart();
0227 }
0228 }
0229 }
0230 }
0231
0232
0233 // extra layout to make all details invisible while loading
0234 ColumnLayout {
0235 Layout.topMargin: root.widescreen ? 0 : Kirigami.Units.gridUnit
0236 Layout.leftMargin: Kirigami.Units.gridUnit
0237 Layout.rightMargin: Kirigami.Units.gridUnit
0238 Layout.fillWidth: true
0239 spacing: 0
0240 visible: !PlasmaTube.videoController.videoModel.isLoading && video.isLoaded
0241 enabled: !PlasmaTube.videoController.videoModel.isLoading && video.isLoaded
0242
0243 // title
0244 Kirigami.Heading {
0245 Layout.fillWidth: true
0246 text: video.title
0247 wrapMode: Text.Wrap
0248 font.weight: Font.Bold
0249 }
0250
0251 // author info and like statistics
0252 RowLayout {
0253 Layout.topMargin: Kirigami.Units.gridUnit
0254 Layout.fillWidth: true
0255 spacing: Kirigami.Units.largeSpacing
0256
0257 Image {
0258 id: chanelThumb
0259 Layout.preferredHeight: 50
0260 Layout.preferredWidth: 50
0261 fillMode: Image.PreserveAspectFit
0262 source: video.authorThumbnail(100)
0263 layer.enabled: true
0264 layer.effect: OpacityMask {
0265 maskSource: mask
0266 }
0267 Rectangle {
0268 id: mask
0269 radius: chanelThumb.height/2
0270 anchors.fill: chanelThumb
0271 visible: false
0272 }
0273 MouseArea {
0274 anchors.fill: parent
0275 cursorShape: Qt.PointingHandCursor
0276 onClicked: root.goToChannel()
0277 }
0278 }
0279
0280 ColumnLayout {
0281 id: column
0282 spacing: 0
0283
0284 QQC2.Label {
0285 text: video.author
0286 font.weight: Font.Bold
0287
0288 MouseArea {
0289 anchors.fill: parent
0290 cursorShape: Qt.PointingHandCursor
0291 onClicked: root.goToChannel()
0292 }
0293 }
0294
0295 SubscriptionButton {
0296 id: subscribeButton
0297
0298 channelId: video.authorId
0299 subCountText: video.subCountText
0300 }
0301 }
0302
0303 Item {
0304 Layout.fillWidth: true
0305 }
0306
0307 QQC2.Button {
0308 text: i18n("Share")
0309 icon.name: "emblem-shared-symbolic"
0310
0311 Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
0312
0313 onClicked: shareMenu.popup()
0314
0315 ShareMenu {
0316 id: shareMenu
0317
0318 url: "https://youtube.com/watch?=" + video.videoId
0319 shareTitle: video.title
0320 }
0321 }
0322 }
0323
0324 RowLayout {
0325 Layout.topMargin: Kirigami.Units.gridUnit
0326 spacing: Kirigami.Units.largeSpacing
0327
0328 Kirigami.Chip {
0329 closable: false
0330 enabled: false
0331 labelItem.color: Kirigami.Theme.disabledTextColor
0332 labelItem.font.weight: Font.Bold
0333 text: video.publishedText
0334 }
0335
0336 Kirigami.Chip {
0337 closable: false
0338 enabled: false
0339 labelItem.color: Kirigami.Theme.disabledTextColor
0340 labelItem.font.weight: Font.Bold
0341 text: i18n("%1 views", Utils.formatCount(video.viewCount))
0342 }
0343
0344 Kirigami.Chip {
0345 closable: false
0346 enabled: false
0347 labelItem.color: Kirigami.Theme.disabledTextColor
0348 labelItem.font.weight: Font.Bold
0349 text: i18n("%1 Likes", Utils.formatCount(video.likeCount))
0350 visible: video.likeCount > 0 // hide like count when we don't know/there is none
0351 }
0352 }
0353
0354 // video description
0355 QQC2.TextArea {
0356 readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\\?\:\)\(]+(\(.*?\))*(\?(?=[a-z])[^\s\\\)]+|$)?)/g
0357
0358 Layout.preferredHeight: video.description.length > 0 ? implicitHeight : 0
0359
0360 text: video.description.replace(linkRegex, function() {
0361 if (arguments[1]) {
0362 return arguments[0];
0363 }
0364 const l = arguments[2];
0365 if ([".", ","].includes(l[l.length-1])) {
0366 const link = l.substring(0, l.length-1);
0367 const leftover = l[l.length-1];
0368 return `<a href="${link}">${link}</a>${leftover}`;
0369 }
0370 return `<a href="${l}">${l}</a>`;
0371 }).replace(/(?:\r\n|\r|\n)/g, '<br>')
0372 textFormat: TextEdit.RichText
0373 background: null
0374 readOnly: true
0375 padding: 0
0376
0377 Layout.topMargin: Kirigami.Units.gridUnit
0378 Layout.bottomMargin: Kirigami.Units.largeSpacing
0379 Layout.fillWidth: true
0380
0381 onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
0382 applicationWindow().hoverLinkIndicator.text = hoveredLink;
0383 } else {
0384 applicationWindow().hoverLinkIndicator.text = "";
0385 }
0386
0387 onLinkActivated: (link) => Qt.openUrlExternally(link)
0388
0389 HoverHandler {
0390 cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
0391 }
0392
0393 selectByMouse: !Kirigami.Settings.isMobile
0394 }
0395
0396 Kirigami.Heading {
0397 text: i18n("Comments")
0398 }
0399
0400 Comments {
0401 id: comments
0402 Layout.fillWidth: true
0403 Layout.fillHeight: true
0404 }
0405 }
0406
0407 QQC2.BusyIndicator {
0408 Layout.alignment: Qt.AlignCenter
0409 visible: PlasmaTube.videoController.videoModel.isLoading
0410 }
0411 }
0412
0413 ColumnLayout {
0414 Layout.alignment: Qt.AlignTop
0415 Layout.margins: Kirigami.Units.largeSpacing * 2
0416 Layout.fillWidth: !root.widescreen
0417 Layout.preferredWidth: Kirigami.Units.gridUnit * 20
0418 spacing: Kirigami.Units.largeSpacing
0419
0420 Kirigami.Heading {
0421 text: i18n("Queue")
0422 visible: PlasmaTube.videoController.videoQueue.shouldBeVisible
0423 }
0424
0425 VideoQueueView {
0426 Layout.fillWidth: true
0427 Layout.preferredHeight: Kirigami.Units.gridUnit * 15
0428 visible: PlasmaTube.videoController.videoQueue.shouldBeVisible
0429 }
0430
0431 Kirigami.Heading {
0432 text: i18n("Recommended")
0433 }
0434
0435 Repeater {
0436 model: video.recommendedVideosModel()
0437 delegate: VideoListItem {
0438 id: videoDelegate
0439 Layout.fillWidth: true
0440 Layout.maximumWidth: parentColumn.width
0441 vid: model.id
0442 thumbnail: model.thumbnail
0443 liveNow: model.liveNow
0444 length: model.length
0445 title: model.title
0446 author: model.author
0447 authorId: model.authorId
0448 description: model.description
0449 viewCount: model.viewCount
0450 publishedText: model.publishedText
0451 watched: model.watched
0452
0453 onClicked: {
0454 video.recommendedVideosModel().markAsWatched(index);
0455 PlasmaTube.videoController.play(vid);
0456 }
0457
0458 onContextMenuRequested: {
0459 currentVideoId = vid;
0460 currentVideoIndex = index;
0461 currentVideoTitle = title;
0462 currentChannelName = author;
0463 currentChannelId = authorId;
0464 videoMenu.isWatched = watched;
0465 videoMenu.popup();
0466 }
0467 }
0468 }
0469 }
0470 }
0471
0472 VideoMenu {
0473 id: videoMenu
0474
0475 videoId: currentVideoId
0476 channelName: currentChannelName
0477 channelId: currentChannelId
0478
0479 onMarkWatched: video.recommendedVideosModel().markAsWatched(currentVideoIndex)
0480 onMarkUnwatched: video.recommendedVideosModel().markAsUnwatched(currentVideoIndex)
0481 }
0482
0483 Connections {
0484 target: PlasmaTube.videoController
0485
0486 function onCurrentVideoChanged() {
0487 if (PlasmaTube.videoController.currentVideo !== null) {
0488 comments.loadComments(PlasmaTube.videoController.currentVideo.videoId);
0489 }
0490 }
0491 }
0492 }