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 }