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 }