Warning, /multimedia/kasts/src/qml/Mobile/MobilePlayerControls.qml is written in an unsupported language. File is not indexed.

0001 /**
0002  * SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
0003  * SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006  */
0007 
0008 import QtQuick
0009 import QtQuick.Controls as Controls
0010 import QtQuick.Layouts
0011 import QtQuick.Effects
0012 
0013 import org.kde.kirigami as Kirigami
0014 import org.kde.kmediasession
0015 
0016 import org.kde.kasts
0017 import org.kde.kasts.settings
0018 
0019 import ".."
0020 
0021 Kirigami.Page {
0022     id: playerControls
0023 
0024     title: AudioManager.entry ? AudioManager.entry.title : i18n("No Track Loaded")
0025     clip: true
0026     Layout.margins: 0
0027 
0028     leftPadding: 0
0029     rightPadding: 0
0030     topPadding: 0
0031     bottomPadding: Kirigami.Units.gridUnit
0032 
0033     Kirigami.Theme.colorSet: Kirigami.Theme.View
0034     Kirigami.Theme.inherit: false
0035 
0036     Component {
0037         id: slider
0038         ChapterSlider {
0039             model: chapterModel
0040         }
0041     }
0042 
0043     background: MultiEffect {
0044         source: backgroundImage
0045         anchors.fill: parent
0046         opacity: 0.2
0047 
0048         brightness: 0.3
0049         saturation: 2
0050         contrast: -0.7
0051         blurMax: 64
0052         blur: 1.0
0053         blurEnabled: true
0054         autoPaddingEnabled: false
0055 
0056         Image {
0057             id: backgroundImage
0058             source: AudioManager.entry.cachedImage
0059             asynchronous: true
0060             visible: false
0061             anchors.fill: parent
0062             fillMode: Image.PreserveAspectCrop
0063         }
0064     }
0065 
0066     ColumnLayout {
0067         id: playerControlsColumn
0068         anchors.fill: parent
0069         anchors.topMargin: Kirigami.Units.largeSpacing * 2
0070         anchors.bottomMargin: Kirigami.Units.largeSpacing
0071         spacing: 0
0072 
0073         Controls.Button {
0074             id: swipeUpButton
0075             property int swipeUpButtonSize: Kirigami.Units.iconSizes.smallMedium
0076             icon.name: "arrow-down"
0077             icon.height: swipeUpButtonSize
0078             icon.width: swipeUpButtonSize
0079             flat: true
0080             Layout.alignment: Qt.AlignHCenter
0081             Layout.topMargin: 0
0082             Layout.bottomMargin: Kirigami.Units.largeSpacing
0083             onClicked: toClose.restart()
0084         }
0085 
0086         Controls.SwipeView {
0087             id: swipeView
0088 
0089             currentIndex: 0
0090             Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
0091             Layout.preferredWidth: parent.width
0092             Layout.preferredHeight: parent.height - media.height - swipeUpButton.height
0093             Layout.margins: 0
0094 
0095             // we are unable to use Controls.Control here to set padding since it seems to eat touch events on the parent flickable
0096             Item {
0097                 Item {
0098                     property int textMargin: Kirigami.Units.largeSpacing // margin above the text below the image
0099                     anchors.fill: parent
0100                     anchors.leftMargin: Kirigami.Units.largeSpacing * 2
0101                     anchors.rightMargin: Kirigami.Units.largeSpacing * 2
0102 
0103                     Item {
0104                         id: coverImage
0105                         anchors {
0106                             top: parent.top
0107                             bottom: kastsMainWindow.isWidescreen ? parent.bottom : undefined
0108                             left: parent.left
0109                             right: kastsMainWindow.isWidescreen ? undefined : parent.right
0110                             margins: 0
0111                             topMargin: kastsMainWindow.isWidescreen ? 0 : (parent.height - Math.min(height, width) - imageLabels.implicitHeight - 2 * parent.textMargin) / 2
0112                         }
0113                         height: Math.min(parent.height - (kastsMainWindow.isWidescreen ? 0 : imageLabels.implicitHeight + 2 * parent.textMargin), parent.width)
0114                         width: kastsMainWindow.isWidescreen ? Math.min(parent.height, parent.width / 2) : Math.min(parent.width, height)
0115 
0116                         ImageWithFallback {
0117                             imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image"
0118                             imageFillMode: Image.PreserveAspectCrop
0119                             anchors.centerIn: parent
0120                             anchors.margins: 0
0121                             width: Math.min(parent.width, parent.height)
0122                             height: Math.min(parent.height, parent.width)
0123                             fractionalRadius: 1 / 20
0124                         }
0125                     }
0126 
0127                     Item {
0128                         anchors {
0129                             top: kastsMainWindow.isWidescreen ? parent.top : coverImage.bottom
0130                             bottom: parent.bottom
0131                             left: kastsMainWindow.isWidescreen ? coverImage.right : parent.left
0132                             right: parent.right
0133                             leftMargin: kastsMainWindow.isWidescreen ? parent.textMargin : 0
0134                             topMargin: kastsMainWindow.isWidescreen ? 0 : parent.textMargin
0135                             bottomMargin: 0
0136                         }
0137 
0138                         ColumnLayout {
0139                             id: imageLabels
0140                             anchors.verticalCenter: parent.verticalCenter
0141                             anchors.left: parent.left
0142                             anchors.right: parent.right
0143                             anchors.margins: 0
0144                             Controls.Label {
0145                                 text: AudioManager.entry ? AudioManager.entry.title : i18n("No Title")
0146                                 elide: Text.ElideRight
0147                                 Layout.alignment: Qt.AlignHCenter
0148                                 Layout.maximumWidth: parent.width
0149                                 font.weight: Font.Medium
0150                             }
0151 
0152                             Controls.Label {
0153                                 text: AudioManager.entry ? AudioManager.entry.feed.name : i18n("No podcast title")
0154                                 elide: Text.ElideRight
0155                                 Layout.alignment: Qt.AlignHCenter
0156                                 Layout.maximumWidth: parent.width
0157                                 opacity: 0.6
0158                             }
0159                         }
0160                     }
0161                 }
0162             }
0163 
0164             Item {
0165                 Flickable {
0166                     anchors.fill: parent
0167                     anchors.leftMargin: Kirigami.Units.largeSpacing * 2 + playerControlsColumn.anchors.margins
0168                     anchors.rightMargin: Kirigami.Units.largeSpacing * 2
0169                     clip: true
0170                     contentHeight: description.height
0171                     ColumnLayout {
0172                         id: description
0173                         width: parent.width
0174                         Kirigami.Heading {
0175                             text: AudioManager.entry ? AudioManager.entry.title : i18n("No Track Title")
0176                             level: 3
0177                             wrapMode: Text.WordWrap
0178                             Layout.fillWidth: true
0179                             Layout.bottomMargin: Kirigami.Units.largeSpacing
0180                         }
0181 
0182                         Controls.Label {
0183                             id: text
0184                             Layout.fillWidth: true
0185                             text: AudioManager.entry ? AudioManager.entry.adjustedContent(width, font.pixelSize) : i18n("No track loaded")
0186                             verticalAlignment: Text.AlignTop
0187                             baseUrl: AudioManager.entry ? AudioManager.entry.baseUrl : ""
0188                             textFormat: Text.RichText
0189                             wrapMode: Text.WordWrap
0190                             onLinkHovered: {
0191                                 cursorShape: Qt.PointingHandCursor;
0192                             }
0193                             onLinkActivated: (link) => {
0194                                 if (link.split("://")[0] === "timestamp") {
0195                                     if (AudioManager.entry && AudioManager.entry.enclosure) {
0196                                         AudioManager.seek(link.split("://")[1]);
0197                                     }
0198                                 } else {
0199                                     Qt.openUrlExternally(link);
0200                                 }
0201                             }
0202                         }
0203                     }
0204                 }
0205             }
0206 
0207             Item {
0208                 Item {
0209                     anchors.fill: parent
0210                     anchors.leftMargin: Kirigami.Units.largeSpacing * 2
0211                     anchors.rightMargin: Kirigami.Units.largeSpacing * 2
0212 
0213                     Kirigami.PlaceholderMessage {
0214                         visible: chapterList.count === 0
0215 
0216                         width: parent.width
0217                         anchors.centerIn: parent
0218 
0219                         text: i18n("No chapters found")
0220                     }
0221 
0222                     ListView {
0223                         id: chapterList
0224                         model: ChapterModel {
0225                             id: chapterModel
0226                             entry: AudioManager.entry ? AudioManager.entry : null
0227                             duration: AudioManager.duration / 1000
0228                         }
0229                         clip: true
0230                         visible: chapterList.count !== 0
0231                         anchors.fill: parent
0232                         delegate: ChapterListDelegate {
0233                             entry: AudioManager.entry ? AudioManager.entry : null
0234                         }
0235                     }
0236                 }
0237             }
0238         }
0239 
0240         Item {
0241             id: media
0242 
0243             implicitHeight: mediaControls.height
0244             Layout.leftMargin: Kirigami.Units.largeSpacing * 2
0245             Layout.rightMargin: Kirigami.Units.largeSpacing * 2
0246             Layout.bottomMargin: 0
0247             Layout.topMargin: 0
0248             Layout.fillWidth: true
0249 
0250             ColumnLayout {
0251                 id: mediaControls
0252 
0253                 anchors.left: parent.left
0254                 anchors.right: parent.right
0255                 anchors.bottom: parent.bottom
0256                 anchors.margins: 0
0257                 spacing: 0
0258 
0259                 RowLayout {
0260                     id: contextButtons
0261                     Layout.preferredHeight: Kirigami.Units.gridUnit * 3
0262                     Layout.leftMargin: Kirigami.Units.largeSpacing
0263                     Layout.rightMargin: Kirigami.Units.largeSpacing
0264                     Layout.topMargin: Kirigami.Units.smallSpacing
0265                     Layout.bottomMargin: Kirigami.Units.smallSpacing
0266                     Layout.fillWidth: true
0267                     property int iconSize: Math.floor(Kirigami.Units.gridUnit * 1.3)
0268                     property int buttonSize: bottomRow.buttonSize
0269 
0270                     Controls.ToolButton {
0271                         visible: AudioManager.entry
0272                         checked: swipeView.currentIndex === 0
0273                         Layout.maximumHeight: parent.height
0274                         Layout.preferredHeight: contextButtons.buttonSize
0275                         Layout.maximumWidth: height
0276                         Layout.preferredWidth: height
0277                         icon.name: "viewimage"
0278                         icon.width: contextButtons.iconSize
0279                         icon.height: contextButtons.iconSize
0280                         onClicked: {
0281                             swipeView.currentIndex = 0;
0282                         }
0283                     }
0284 
0285                     Controls.ToolButton {
0286                         visible: AudioManager.entry
0287                         checked: swipeView.currentIndex === 1
0288                         Layout.maximumHeight: parent.height
0289                         Layout.preferredHeight: contextButtons.buttonSize
0290                         Layout.maximumWidth: height
0291                         Layout.preferredWidth: height
0292                         icon.name: "documentinfo"
0293                         icon.width: contextButtons.iconSize
0294                         icon.height: contextButtons.iconSize
0295                         onClicked: {
0296                             swipeView.currentIndex = 1;
0297                         }
0298                     }
0299 
0300                     Controls.ToolButton {
0301                         visible: AudioManager.entry && chapterList.count !== 0
0302                         checked: swipeView.currentIndex === 2
0303                         Layout.maximumHeight: parent.height
0304                         Layout.preferredHeight: contextButtons.buttonSize
0305                         Layout.maximumWidth: height
0306                         Layout.preferredWidth: height
0307                         icon.name: "view-media-playlist"
0308                         icon.width: contextButtons.iconSize
0309                         icon.height: contextButtons.iconSize
0310                         onClicked: {
0311                             swipeView.currentIndex = 2;
0312                         }
0313                     }
0314 
0315                     Item {
0316                         Layout.fillWidth: true
0317                     }
0318 
0319                     Controls.ToolButton {
0320                         checkable: true
0321                         checked: AudioManager.remainingSleepTime > 0
0322                         Layout.maximumHeight: parent.height
0323                         Layout.preferredHeight: contextButtons.buttonSize
0324                         Layout.maximumWidth: height
0325                         Layout.preferredWidth: height
0326                         icon.name: "clock"
0327                         icon.width: contextButtons.iconSize
0328                         icon.height: contextButtons.iconSize
0329                         onClicked: {
0330                             toggle(); // only set the on/off state based on sleep timer state
0331                             sleepTimerDialog.open()
0332                         }
0333                     }
0334                     Controls.ToolButton {
0335                         id: volumeButton
0336                         Layout.maximumHeight: parent.height
0337                         Layout.preferredHeight: contextButtons.buttonSize
0338                         Layout.maximumWidth: height
0339                         Layout.preferredWidth: height
0340                         icon.name: AudioManager.muted ? "player-volume-muted" : "player-volume"
0341                         icon.width: contextButtons.iconSize
0342                         icon.height: contextButtons.iconSize
0343                         enabled: AudioManager.PlaybackState != AudioManager.StoppedState && AudioManager.canPlay
0344                         checked: volumePopup.visible
0345 
0346                         Controls.ToolTip.visible: hovered
0347                         Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
0348                         Controls.ToolTip.text: i18nc("@action:button", "Open volume settings")
0349 
0350                         onClicked: {
0351                             if (volumePopup.visible) {
0352                                 volumePopup.close();
0353                             } else {
0354                                 volumePopup.open();
0355                             }
0356                         }
0357 
0358                         Controls.Popup {
0359                             id: volumePopup
0360                             x: -padding
0361                             y: -volumePopup.height
0362 
0363                             focus: true
0364                             padding: Kirigami.Units.smallSpacing
0365                             contentWidth: muteButton.implicitWidth
0366 
0367                             contentItem: ColumnLayout {
0368                                 id: popupContent
0369 
0370                                 Controls.ToolButton {
0371                                     id: muteButton
0372                                     enabled: AudioManager.PlaybackState != AudioManager.StoppedState && AudioManager.canPlay
0373                                     icon.name: AudioManager.muted ? "player-volume-muted" : "player-volume"
0374                                     onClicked: AudioManager.muted = !AudioManager.muted
0375 
0376                                     Controls.ToolTip.visible: hovered
0377                                     Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
0378                                     Controls.ToolTip.text: i18nc("@action:button", "Toggle mute")
0379 
0380                                 }
0381 
0382                                 VolumeSlider {
0383                                     id: volumeSlider
0384                                 }
0385                             }
0386                         }
0387                     }
0388                 }
0389 
0390                 Loader {
0391                     active: !kastsMainWindow.isWidescreen
0392                     visible: !kastsMainWindow.isWidescreen
0393                     sourceComponent: slider
0394                     Layout.fillWidth: true
0395                     Layout.leftMargin: Kirigami.Units.largeSpacing
0396                     Layout.rightMargin: Kirigami.Units.largeSpacing
0397                 }
0398 
0399                 RowLayout {
0400                     id: controls
0401                     Layout.leftMargin: Kirigami.Units.largeSpacing
0402                     Layout.rightMargin: Kirigami.Units.largeSpacing
0403                     Layout.fillWidth: true
0404                     Controls.Label {
0405                         Layout.alignment: Qt.AlignLeft
0406                         padding: Kirigami.Units.largeSpacing
0407                         text: AudioManager.formattedPosition
0408                         font: Kirigami.Theme.smallFont
0409                     }
0410                     Loader {
0411                         active: kastsMainWindow.isWidescreen
0412                         sourceComponent: slider
0413                         Layout.fillWidth: true
0414 
0415                     }
0416                     Item {
0417                         Layout.alignment: Qt.AlignRight
0418                         Layout.preferredHeight: endLabel.implicitHeight
0419                         Layout.preferredWidth: endLabel.implicitWidth
0420                         Controls.Label {
0421                             id: endLabel
0422                             padding: Kirigami.Units.largeSpacing
0423                             anchors.right: parent.right
0424                             anchors.verticalCenter: parent.verticalCenter
0425                             text: (SettingsManager.toggleRemainingTime) ?
0426                                     "-" + AudioManager.formattedLeftDuration
0427                                     : AudioManager.formattedDuration
0428                             font: Kirigami.Theme.smallFont
0429 
0430                         }
0431                         MouseArea {
0432                             anchors.fill: parent
0433                             hoverEnabled: true
0434                             onClicked: {
0435                                 SettingsManager.toggleRemainingTime = !SettingsManager.toggleRemainingTime;
0436                                 SettingsManager.save();
0437                             }
0438                         }
0439                     }
0440                 }
0441 
0442                 RowLayout {
0443                     id: bottomRow
0444                     Layout.leftMargin: Kirigami.Units.largeSpacing
0445                     Layout.rightMargin: Kirigami.Units.largeSpacing
0446                     Layout.maximumWidth: Number.POSITIVE_INFINITY //TODO ?
0447                     Layout.fillWidth: true
0448                     Layout.bottomMargin: 0
0449                     Layout.topMargin: 0
0450 
0451                     // Make button width scale properly on narrow windows instead of overflowing
0452                     property int buttonSize: Math.min(playButton.implicitWidth, ((playerControlsColumn.width - 4 * spacing) / 5 - playButton.leftPadding - playButton.rightPadding))
0453                     property int iconSize: Kirigami.Units.gridUnit * 1.5
0454 
0455                     // left section
0456                     Controls.Button {
0457                         id: playbackRateButton
0458                         text: AudioManager.playbackRate.toFixed(2) + "x"
0459                         checkable: true
0460                         checked: playbackRateMenu.visible
0461                         onClicked: {
0462                             if (playbackRateMenu.visible) {
0463                                 playbackRateMenu.dismiss();
0464                             } else {
0465                                 playbackRateMenu.popup(playbackRateButton, 0, -playbackRateMenu.height);
0466                             }
0467                         }
0468                         flat: true
0469                         padding: 0
0470                         implicitWidth: playButton.width * 1.5
0471                         implicitHeight: playButton.height
0472                     }
0473 
0474                     // middle section
0475                     RowLayout {
0476                         spacing: Kirigami.Units.largeSpacing
0477                         Layout.alignment: Qt.AlignHCenter
0478                         Controls.Button {
0479                             icon.name: "media-seek-backward"
0480                             icon.height: bottomRow.iconSize
0481                             icon.width: bottomRow.iconSize
0482                             flat: true
0483                             Layout.alignment: Qt.AlignRight
0484                             Layout.preferredWidth: bottomRow.buttonSize
0485                             onClicked: AudioManager.skipBackward()
0486                             enabled: AudioManager.canSkipBackward
0487                         }
0488                         Controls.Button {
0489                             id: playButton
0490                             icon.name: AudioManager.playbackState === KMediaSession.PlayingState ? "media-playback-pause" : "media-playback-start"
0491                             icon.height: bottomRow.iconSize
0492                             icon.width: bottomRow.iconSize
0493                             flat: true
0494                             Layout.alignment: Qt.AlignHCenter
0495                             Layout.preferredWidth: bottomRow.buttonSize
0496                             onClicked: AudioManager.playbackState === KMediaSession.PlayingState ? AudioManager.pause() : AudioManager.play()
0497                             enabled: AudioManager.canPlay
0498                         }
0499                         Controls.Button {
0500                             icon.name: "media-seek-forward"
0501                             icon.height: bottomRow.iconSize
0502                             icon.width: bottomRow.iconSize
0503                             flat: true
0504                             Layout.alignment: Qt.AlignLeft
0505                             Layout.preferredWidth: bottomRow.buttonSize
0506                             onClicked: AudioManager.skipForward()
0507                             enabled: AudioManager.canSkipForward
0508                         }
0509                     }
0510 
0511                     // right section
0512                     Controls.Button {
0513                         icon.name: "media-skip-forward"
0514                         icon.height: bottomRow.iconSize
0515                         icon.width: bottomRow.iconSize
0516                         flat: true
0517                         Layout.alignment: Qt.AlignRight
0518                         Layout.preferredWidth: bottomRow.buttonSize
0519                         onClicked: AudioManager.next()
0520                         enabled: AudioManager.canGoNext
0521                     }
0522                 }
0523             }
0524         }
0525     }
0526 
0527     PlaybackRateMenu {
0528         id: playbackRateMenu
0529         parentButton: playbackRateButton
0530     }
0531 }